EC2 タグに repo名/ブランチ/スクリプトパスを書いておいて GitHub push に勝手に反応させる

GitHub への push イベントを SNS で受け取って、「<リポジトリ名>:<ブランチ名>:<実行スクリプト>」というタグが付いている EC2 インスタンスを探して指定スクリプトを実行する、という話です。

で、スクリプト内で "git pull" するもヨシ、追加でごにょごにょするもヨシ。

# 前置き

CircleCI とかそんなんでなく、GitHub に push したら pull してくれればそれでいいんですよ、というケースは地味に多いわけです。

これは GitHub > SNS > Lambda > Run Command でスクリプト実行すれば良いのですが、「じゃぁリポジトリ毎に Lambda ファンクション作るの？」って話で。

そこで EC2 タグに関連するリポジトリ名とブランチ名、実行スクリプトを書いておけば 1つの lambda が全て面倒みてくれるようにします。

リポジトリが増えた時、サーバが増えた時も EC2 インスタンスにタグ付けるだけなので簡単です。

# 仕組み

<img width="568" alt="DropShadow ~ 3.png" src="https://qiita-image-store.s3.amazonaws.com/0/116572/c85a9728-7b59-3c72-8bec-25c42a886ee1.png">

GitHub > SNS > Lambda はいろいろな方がやられている通り。
あとは EC2 インスタンスに

- key = GitHub-Webhook
- val = <リポジトリ名>:<ブランチ名>:<実行スクリプト>

というタグをつけておいて、lambda で対象 EC2 インスタンスを探して Rum Command でスクリプトを実行する。
で、スクリプトの中で git pull とか好きにせい、という構成。

例えば repo_1 というリポジトリの master に push したら、「key=GitHub-Webhook、val=repo_1:master:/hoge/hoge.sh」というタグが付いている EC2 インスタンスの /hoge/hoge.sh を実行します。

# 作る
## SSM エージェントをインストールする

Run Command を実行するには対象インスタンスに SSM エージェントを入れておく必要があります。

[SSMエージェントのインストール](http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/sysman-install-ssm-agent.html)

40秒で支度しましょう。

```
$ cd /tmp
$ curl https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm -o amazon-ssm-agent.rpm
$ sudo yum install -y amazon-ssm-agent.rpm
```

## EC2ロール割り当て

で、EC2 インスタンスに AmazonEC2RoleforSSM ポリシーをアタッチしておく。

<img width="567" alt="001.png" src="https://qiita-image-store.s3.amazonaws.com/0/116572/b700e225-8268-69f8-0692-089efb5a2830.png">

## SNS topic、GitHub webhook 専用 IAM ユーザの作成、GitHub の設定

「github-webhook」という SNS topic を作ります。
CLI でシュッと。

```
$ aws sns create-topic --name github-webhook
```

GitHub webhook 用に「github-webhook」という名前の IAM ユーザを作って、以下のようなインラインポリシーを付ける。
つまり github-webhook topic にパブリッシュする事しかできないユーザ。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "sns:Publish"
            ],
            "Sid": "Stmt0000000000000",
            "Resource": [
                "[さっき作った SNS の ARN]"
            ],
            "Effect": "Allow"
        }
    ]
}
```

で、GitHub 側もぱぱっと。

<img width="1020" alt="DropShadow ~ qiita_002.png" src="https://qiita-image-store.s3.amazonaws.com/0/116572/00ef5c55-e33b-e72e-75e5-fb8f06e7e557.png">

## Lambda 準備

特になんもしてない。
エラー処理もないのでやさしく使う。

```python
# -*- coding: utf-8 -*-

import json
import boto3

# key="GitHub-Webhook", val=<repoName>:<branchName>:<command>
# というタグを持つインスタンスを列挙する。
def search_target_instance(repoName, branchName):
    # 実行中の EC2 インスタンスを取得
    ec2 = boto3.client('ec2')
    response = ec2.describe_instances(
        Filters=[
            {
                'Name': 'instance-state-name',
                'Values': ['running']
            }
        ]
    )
    
    targetInstances = []
    
    for resv in response['Reservations']:
        for instance in resv['Instances']:
            for tag in instance['Tags']:
                if tag['Key'] == 'GitHub-Webhook':
                    tagValue = tag['Value']
                    
                    if tagValue.startswith(repoName + ":" + branchName + ":"):
                        target = instance['InstanceId'], tagValue.split(':')[-1]
                        targetInstances.append(target)

    return targetInstances

# Run Command で指定スクリプトを実行する。
def execute_command(instanceId, command):
    print instanceId + " : " + command
    ssm = boto3.client('ssm')
    
    ssmRes = ssm.send_command(
        InstanceIds = [instanceId],
        DocumentName = "AWS-RunShellScript",
        Parameters = {
            "commands": [ command ]
        })
    
    commandId = ssmRes['Command']['CommandId']
    return commandId

# Lambda エントリポイント
def lambda_handler(event, context):
    message = event['Records'][0]['Sns']['Message']
    gitHubJson = json.loads(message)
    
    # SNS からリポジトリ名とブランチ名を取得する。
    repoName = gitHubJson["repository"]["name"]
    branchName = gitHubJson["ref"].replace('refs/heads/', '')
    
    # デプロイ対象 EC2 インスタンスを取得
    targetInstances = search_target_instance(repoName, branchName)

    # EC2 タグで指定されているコマンドを実行する
    for instance in targetInstances:
        instanceId, command = instance
        print execute_command(instanceId, command)

    return {'status': 'ok'}
```

# EC2 タグつけて push

ココまで来たら、タグ付けて GitHub に push すればよい。
Run Command の結果は AWS コンソールの EC2 / Run Command から確認できます。

<img width="1067" alt="002.png" src="https://qiita-image-store.s3.amazonaws.com/0/116572/24ebda16-24a2-d7b3-9220-9e2bd391473b.png">

※ スクリプトは root で実行されるので注意。

# 結果はどうやって知るのよ？

本当は list_command_invocations() で Run Command の結果をみて Slack に通知とかすべきですが長くなるので割愛。

とりあえずスクリプト内から curl で slack に post しても良いかもしれないですね。

# 結論

大切なこと：「ssh ログインなんか絶対にしないぞ！」という強い意思。