Goalist Developers Blog

ChatworkからのEC2インスタンスの管理の自動化

どうも,エンジニアのナカノです.

今回は,Chatwork経由でのEC2インスタンスの管理の自動化の内容をご紹介致します.

過去に使ったことの無かった技術を,今回の開発で採用してみることにしました.

目 次
  • 背景
  • 作り方
  • 所感



背景

現在,クローリングデータの運用/納品チームは,EC2インスタンスの管理を次の様に行ってます.

  • 起動の場合:Chatworkのタスクを作成して、EC2の管理コンソールから起動を行う

  • 停止の場合:EC2の管理コンソールから停止を行い,Chatworkタスクを完了にする

これで運用すれば,Chatworkタスクの状態を見て,EC2インスタンスのstateを判断出来ます.

しかし,マニュアルで運用しており,抜け漏れがあり得ますし,何より面倒さがあります.

そこで,この一連のプロセスを自動化させて,同チームの業務改善を進めようと考えました.



作り方

自動化において,インフラはAPI GatewayLambdaといったサーバレス構成を採用しました.

この構成とChatwork Webhookの連携を利用して,イベント駆動な仕組みになる様にしました.

developer.chatwork.com

f:id:r-nakano:20200831160438j:plain


また,EC2インスタンスの自動管理の仕組みの,Gitプロジェクトの構成は次の様になります.

LambdaのハンドラーはGoで開発しました.これは,Goを使ってみたかったからです!

golang.org

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リポジトリの内容を参考にして行いました.

github.com

github.com

github.com

// 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のルームでのタスクの作成/完了時に次が行われる様に出来ます.

  1. ChatworkのWebhookリクエストが,API Gatewayのエンドポイントに送られる

  2. API Gatewayは,Lambdaプロキシ統合を使って,Lambdaの呼び出しを実行する

  3. Lambdaは,リクエストの内容に基づいて,EC2インスタンスの有無を検索する

  4. もし該当のEC2インスタンスが存在すれば,それに対する起動/停止を実行する

  5. レスポンス用のメッセージをChatwork APIへ送り,特定のルームへ送信する



所感

Goには元々興味があったので,今回の自動化の対応でチャレンジすることが出来て良かったです!

一方で,初めて知る部分が色々とあったのもあり,次の点を何とかするのに地味に苦戦しました.

  • Go Modulesを利用したGoプロジェクトの構築

  • Webhook連携でのワークフローの無限ループの回避

最終的には何とかすることが出来て,これらの困難を乗り越えたことが良い経験になりました.

引き続き自動化の対応を進めていき,業務やシステムをより良い方向へ改善出来ればと思います.

次回は,このシステムのCI/CDの部分についてお話しようと思います.ご期待下さいませ~.