どうも,エンジニアのナカノです.
今回は,Chatwork経由でのEC2インスタンスの管理の自動化の内容をご紹介致します.
過去に使ったことの無かった技術を,今回の開発で採用してみることにしました.
- 背景
- 作り方
- 所感
背景
現在,クローリングデータの運用/納品チームは,EC2インスタンスの管理を次の様に行ってます.
起動の場合:Chatworkのタスクを作成して、EC2の管理コンソールから起動を行う
停止の場合:EC2の管理コンソールから停止を行い,Chatworkタスクを完了にする
これで運用すれば,Chatworkタスクの状態を見て,EC2インスタンスのstateを判断出来ます.
しかし,マニュアルで運用しており,抜け漏れがあり得ますし,何より面倒さがあります.
そこで,この一連のプロセスを自動化させて,同チームの業務改善を進めようと考えました.
作り方
自動化において,インフラはAPI Gateway,Lambdaといったサーバレス構成を採用しました.
この構成とChatwork Webhookの連携を利用して,イベント駆動な仕組みになる様にしました.
また,EC2インスタンスの自動管理の仕組みの,Gitプロジェクトの構成は次の様になります.
LambdaのハンドラーはGoで開発しました.これは,Goを使ってみたかったからです!
ec2-serverless-manager ├── .circleci │ └── config.yml ├── .gitignore ├── README.md ├── ci-package │ ├── Dockerfile │ └── bin │ ├── deploy.sh │ └── validate.sh ├── go.mod ├── main.go ├── pkg # ローカルパッケージ │ ├── aws-ec2.go # EC2インスタンスの検索,起動,停止を行う │ ├── aws-ssm.go # SSMパラメータの値を取得する │ └── chatwork.go # Chatworkへのメッセージ送信を行う └── serverless.yml
Goのソースは,それぞれ次のようになります.ルールが少ないので実装が行い易いです.
各々のソースの実装は,次のリンク先のGitHubリポジトリの内容を参考にして行いました.
// main.go package main import ( "encoding/json" "fmt" "strings" pkg "ec2-serverless-manager/pkg" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type CwWebhookRequest struct { WebhookSettingID string `json:"webhook_setting_id"` WebhookEventType string `json:"webhook_event_type"` WebhookEventTime int `json:"webhook_event_time"` WebhookEvent struct { FromAccountID int `json:"from_account_id"` ToAccountID int `json:"to_account_id"` RoomID int `json:"room_id"` MessageID string `json:"message_id"` Body string `json:"body"` SendTime int `json:"send_time"` UpdateTime int `json:"update_time"` } `json:"webhook_event"` } func main() { lambda.Start(Handle) } func Handle(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // Lambdaで実行させたいことを実装する }
// pkg/aws-ec2.go package pkg import ( "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" ) var region = os.Getenv("AWS_REGION") var sess = session.Must(session.NewSession()) var svc = ec2.New( sess, aws.NewConfig().WithRegion(region), ) func StartInstance(instanceId string) (*ec2.StartInstancesOutput, error) { input := &ec2.StartInstancesInput{ InstanceIds: []*string{ aws.String(instanceId), }, } result, err := svc.StartInstances(input) return result, err } func StopInstance(instanceId string) (*ec2.StopInstancesOutput, error) { input := &ec2.StopInstancesInput{ InstanceIds: []*string{ aws.String(instanceId), }, } result, err := svc.StopInstances(input) return result, err } func SearchInstance(instanceNm, state string) (*ec2.Instance, error) { input := &ec2.DescribeInstancesInput{ Filters: []*ec2.Filter{ { Name: aws.String("tag:Name"), Values: []*string{aws.String(instanceNm)}, }, { Name: aws.String("instance-state-name"), Values: []*string{aws.String(state)}, }, }, } result, err := svc.DescribeInstances(input) if err != nil { return nil, err } var inst *ec2.Instance for _, reservation := range result.Reservations { for _, instance := range reservation.Instances { inst = instance } } return inst, err }
// pkg/aws-ssm.go package pkg import ( "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ssm" ) func GetSsmPrm(key string) string { svc := ssm.New(session.New(), &aws.Config{ Region: aws.String(os.Getenv("AWS_REGION")), }) response, _ := svc.GetParameter(&ssm.GetParameterInput{ Name: aws.String(key), WithDecryption: aws.Bool(true), }) return *response.Parameter.Value }
// pkg/chatwork.go package pkg import ( "bytes" "net/http" "os" ) type CwApiConfig struct { token string roomId string baseUrl string } var cwApiConfig = CwApiConfig{ token: GetSsmPrm("/xxxxxxxx/cw_api_token"), roomId: os.Getenv("CW_ROOM_ID"), baseUrl: "https://api.chatwork.com/v2/rooms/", } func PostMsg(msg string) { param := "body=" + msg apiUrl := cwApiConfig.baseUrl + cwApiConfig.roomId + "/messages" request, _ := http.NewRequest("POST", apiUrl, bytes.NewBufferString(param)) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("X-ChatWorkToken", cwApiConfig.token) response, _ := new(http.Client).Do(request) defer response.Body.Close() }
以上を利用すると,Chatworkのルームでのタスクの作成/完了時に次が行われる様に出来ます.
ChatworkのWebhookリクエストが,API Gatewayのエンドポイントに送られる
API Gatewayは,Lambdaプロキシ統合を使って,Lambdaの呼び出しを実行する
Lambdaは,リクエストの内容に基づいて,EC2インスタンスの有無を検索する
もし該当のEC2インスタンスが存在すれば,それに対する起動/停止を実行する
レスポンス用のメッセージをChatwork APIへ送り,特定のルームへ送信する
所感
Goには元々興味があったので,今回の自動化の対応でチャレンジすることが出来て良かったです!
一方で,初めて知る部分が色々とあったのもあり,次の点を何とかするのに地味に苦戦しました.
Go Modulesを利用したGoプロジェクトの構築
Webhook連携でのワークフローの無限ループの回避
最終的には何とかすることが出来て,これらの困難を乗り越えたことが良い経験になりました.
引き続き自動化の対応を進めていき,業務やシステムをより良い方向へ改善出来ればと思います.
次回は,このシステムのCI/CDの部分についてお話しようと思います.ご期待下さいませ~.