Help us understand the problem. What is going on with this article?

AWS Lambda functionと EC2 Run Commandで、かんたんSlash Commandデプロイ

More than 1 year has passed since last update.

SlackのSlash Commandを使ったデプロイの実現方法は色々ありますが、今回はAWS Lambda functionとEC2 Run Commandを使ったやり方を紹介します。

仕組み

スクリーンショット 2018-08-13 15.09.37.png

この方式のメリットは以下になります。

  • 仕組みが簡単
    • 要はSlackとLamda経由でEC2上のデプロイコマンドを叩くだけなので、わかりやすい
  • AWSだけで完結する
    • Slack以外、外部サービスに依存しない(GitHubとか使ってなくてもOK)
  • ローコスト
    • bot用にEC2インスタンスを用意する必要がなく、必要なときにLambda functionを呼び出すだけ
  • デプロイ可能なユーザの管理をSlackだけで制限できる
    • 特定チャンネルのみでコマンド実行可能にできるので、プライベートチャンネルを作ってそこにデプロイできるユーザを集めれば、擬似的なアクセス管理が簡単に行える

「今まで毎回手動でSSHログインしてデプロイしていたのを、Slack経由で行えるようにしたい」という場合におすすめです。

1. Slash Commandを追加

まず、SlackにSlash Commandを追加します。

スクリーンショット 2018-08-13 14.22.40.png

https:/{ワークスペース名}/apps/A0F82E8CA-slash-commandsにアクセスし、適当な名前でslash-commandを作成してください。ここでは/deployとしました。

スクリーンショット 2018-08-10 13.18.11.png
スクリーンショット 2018-08-10 13.18.23.png

URLにはこの時点では適当にダミーを入れておけばOKです。
発行されたトークンを利用しますので、控えておいてください。

2. AWS KMSの設定

1.で作成したSlash Commandのtokenがバレると好き放題通知を叩かれてしまうので1、tokenを暗号化するためにAWS KMSキーを作成します。

スクリーンショット 2018-08-13 14.23.22.png

キーの作成

「東京リージョン」を指定して「キーの作成」を行います。2

スクリーンショット 2018-08-10 13.30.09.png
スクリーンショット 2018-08-10 13.30.33.png

動作確認

awscliを使い、作成したキーでtokenをきちんと暗号化/復号化できることを確認します。

$ SLASH_COMMAND_TOKEN=cT0Uwi1eltqho4itj5h2QyXy
$ AWS_KMS_KEY=SampleSlackDeployKey
$ AWS_REGION=ap-northeast-1

// 暗号化
$ echo $SLASH_COMMAND_TOKEN
cT0Uwi1eltqho4itj5h2QyXy

$ ENCRYPT=`aws kms encrypt --region ${AWS_REGION} --key-id alias/${AWS_KMS_KEY} --plaintext ${SLASH_COMMAND_TOKEN} --query CiphertextBlob --output text`
$ echo $ENCRYPT
AQICAHj1B6d60FLb+iijclctcD7cMERl79RaxvmJ61C2WHitoQHfo1YdhI08K1qJhMfjjq/QAAAAdjB0BgkqhkiG9w0BBwagZzBlAgEAMGAGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQM3PoBE7CUolclOAqYAgEQgDMdPzkI1RGGBK64zdYTTEOrXIOk99Jwq4X8fE4ZljcB8g0npv/xmCSlF/yBSvY7WdWTCo4=

// 復号化
$ DECRYPT=`aws kms decrypt --region ${AWS_REGION} --ciphertext-blob fileb://<(echo ${ENCRYPT}|base64 --decode) | jq .Plaintext --raw-output |base64 --decode`
$ echo $DECRYPT
cT0Uwi1eltqho4itj5h2QyXy

3. Lambda Function、API Gatewayの作成

BluePrint3をベースにLambda FunctionとAPI Gatewayを同時に作成します。

スクリーンショット 2018-08-13 14.30.18.png

AWS Lambda > 関数 > 「関数の作成」 から、slack-echo-commandBluePrintを選択します。

スクリーンショット 2018-08-10 13.46.53.png

個人的にはPython(slack-echo-command-python)がよかったんですが、LambdaのWISYWIGエディタがよくわかんないインデントエラーを出してこけたのでnodejs(slack-echo-command)にしました。

続いて、以下のように入力します。

スクリーンショット 2018-08-10 13.49.33.png
スクリーンショット 2018-08-10 13.50.29.png

  • ロールは自動作成
    • 後でIAM設定を追加しますので、ロール名を覚えておいてください
  • 新規APIの作成
    • 対応するAPI Gatewayを自動作成
    • Slackから叩かれるのでセキュリティは「オープン」
    • API名とかは適当に
  • kmsEncryptTokenの値を置換
    • KMSキーの動作確認で利用した、暗号化後のtokenを指定

疎通確認

ここまで出来たら、slack経由で実際にLambda functionが呼び出せるか確認しておきます。
スクリーンショット 2018-08-13 14.32.28.png

「API Gateway」を選択すると対応するAPIエンドポイントが表示されるので、このURLを1.で作成したSlash Commandに登録します。

スクリーンショット 2018-08-10 13.51.44.png
スクリーンショット 2018-08-10 13.53.18.png

あとは実際にSlackから動作確認を行い、以下のようにecho出力されていればOKです。

スクリーンショット 2018-08-10 14.58.28.png

4. EC2 Run Commandの動作確認

続いてEC2上のデプロイコマンドを、直接叩けるようにします。方法は色々ありますが、ここではLambdaから呼び出すことを想定し、EC2 Run Commandを利用します。

スクリーンショット 2018-08-13 14.34.13.png

EC2 Run Commandを使うにはSSM権限が必要なので、EC2インスタンスに付与するIAM Roleを作成します。

AWS IAM > ロール > 「ロールの作成」

スクリーンショット 2018-08-10 14.01.39.png

「SSM」で検索し、AmazonEC2RoleforSSMを付与。

スクリーンショット 2018-08-10 14.02.05.png

スクリーンショット 2018-08-10 14.03.08.png

作成したIAM Roleを、実行対象のEC2に割当てます。

スクリーンショット 2018-08-10 14.04.08.png

Amazon Linux AMIならamazon-ssm-agentがデフォルトで起動していますが、付与したRoleを有効化するにはssm-agentの再起動が必要です。

以下のように手動でssm-agentを再起動するか、EC2インスタンスごと再起動してください。

[ec2-user@ip-172-31-3-110 ~]$ sudo restart amazon-ssm-agent
amazon-ssm-agent start/running, process 28419

動作確認

EC2コンソールから動作確認を行います。ここではhostnameコマンドを実行させてみます。

AWS EC2 > コマンドの実行 > 「コマンドを実行」

スクリーンショット 2018-08-10 14.10.45.png

結果は以下のようにして確認します。

スクリーンショット 2018-08-10 14.13.35.png
スクリーンショット 2018-08-10 14.13.54.png

2500文字しか出力されないということなので、場合によっては以下のようにS3に吐き出させることになるかもしれません。

スクリーンショット 2018-08-10 14.16.27.png
スクリーンショット 2018-08-10 14.17.11.png

なお、コマンドはrootユーザで実行されるのでご注意ください。デプロイ対象によっては適宜/sbin/runuser -l $user等を使う必要がありそうです。

このように試行錯誤する場合、awscliでやった方が楽かもしれません。お好みで。

// whoamiコマンドをec2-userで実行する例
$ AWS_REGION=ap-northeast-1
$ EC2_INSTANCE_ID=i-002c4696d9983f352
$ PARAMETERS=$'{"commands":["/sbin/runuser -l ec2-user -c \'whoami\'"],"executionTimeout":["3600"]}'

// 実行
$ COMMAND_ID=`aws ssm send-command \
 --document-name "AWS-RunShellScript" \
 --instance-ids $EC2_INSTANCE_ID  \
 --parameters $'{"commands":["/sbin/runuser -l ec2-user -c \'whoami\'"],"executionTimeout":["3600"]}'  \
 --timeout-seconds 600 \
 --region $AWS_REGION \
  | jq -r .Command.CommandId`

$ echo $COMMAND_ID
1cf72cca-8a22-4d45-91ee-4e0c9550ef62

// 結果確認
$ aws ssm list-command-invocations --command-id $COMMAND_ID --region $AWS_REGION --details \
  | jq .CommandInvocations[0].CommandPlugins[0].Output
"ec2-user\n"

5. Lambda FunctionからEC2 Run Commandの呼び出し

Lambda FunctionへのSSM権限付与

あとはLambda Functionから上記のEC2 Run Commandを使うだけなのですが、Lambda FunctionにSSMへの権限を与える必要があります。

スクリーンショット 2018-08-13 14.35.43.png

Lambda Function作成時に作成したロール(SampleSlackDeployRole)を選択し、以下のようなインラインポリシーを追加します。

AWS IAM > ロール > SampleSlackDeployRole

policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:SendCommand"
            ],
            "Resource": "*"
        }
    ]
}

スクリーンショット 2018-08-13 15.23.39.png

AWS SDKによるEC2 Run Commandの処理を追加

ここまで出来たら、こんな感じでLambda Functionを書き換えればOKです。

lambda-function.js
()
const AWS = require('aws-sdk');
const qs = require('querystring');

const kmsEncryptedToken = process.env.kmsEncryptedToken;
let token;

// EC2 Run Commandを追加
function processEvent(event, callback) {
    const params = qs.parse(event.body);
    const requestToken = params.token;
    if (requestToken !== token) {
        console.error(`Request token (${requestToken}) does not match expected`);
        return callback('Invalid request token');
    }

    const user = params.user_name;          // slash-commandの実行ユーザ
    const command = params.command;         // slash-command名
    const channel = params.channel_name;    // slash-commandが実行されたチャンネル名
    const commandText = params.text;        // slash-commandのコマンド名以降({$stage}を想定)

    // 特定のチャンネル以外で実行された場合
    if (channel !== 'private-deploy') {
        callback(null, '「private-deploy」以外のチャンネルでは実行できません。');
    }

    // EC2 Run Commandの実行ターゲット
    const target_instance_ids = ['i-002c4696d9983f352'];

    // 実行コマンド組み立て
    const cmd = `echo "/opt/deploy.sh ${commandText}"`; // FIXME: remove echo 
    const ec2_run_command = `/sbin/runuser -l ec2-user -c \'${cmd}\'`;

    // EC2 Run Command 設定
    const ssm = new AWS.SSM();
    const ssm_send_command_params = {
        DocumentName: 'AWS-RunShellScript',
        InstanceIds: target_instance_ids,
        Parameters: {
            'commands': [
                ec2_run_command,
            ],
        },
        TimeoutSeconds: 3600    // 1 hour
    };

    ssm.sendCommand(ssm_send_command_params, function(err, data) {
        if (err) {
            console.log(err, err.stack); // an error occurred
            return callback('SSM sendCommand Failed.');
        } 
        console.log(data);           // successful response
        const url = `https://ap-northeast-1.console.aws.amazon.com/systems-manager/run-command/${data.Command.CommandId}?region=ap-northeast-1`
        callback(null, `デプロイコマンドを実行しました! url=${url}`);
    });
}

// Original Blue Template
exports.handler = (event, context, callback) => {
()

ここでは実行結果への参照用URLを生成し、それを返却しています。

あとはslackからSlash Commandを入力し、デプロイ先のサーバで指定したコマンドが呼び出されていればOKです。お疲れ様でした!

スクリーンショット 2018-08-13 14.53.13.png

スクリーンショット 2018-08-13 14.55.41.png

ハマりどころ

Slash Commandの3秒制限

Slash Commandの仕様として、指定したurlにPOSTしたあと、3秒以内にレスポンスが帰ってこないとタイムアウトになります。

Lambda functionのデフォルト設定メモリ128MBだと、初回起動時だけ4、このタイムアウトに引っかかることがあるので、256MBにしたらとりあえず大丈夫になりました。Lambda functionのタイムアウトの値もこの制限に合わせて3秒になっている感じ。

スクリーンショット 2018-08-13 14.49.08.png

通常、デプロイコマンドの実行には3秒以上かかると思うので、Lambda functionではデプロイコマンドの呼び出しだけを責務とし、処理結果の通知等を行う場合はLambda functionではなくデプロイコマンド側で行う必要があります。ご注意ください。(ssm list-command-invocations相当の処理をLambda上でやると簡単にタイムアウトになります)


参考:


  1. 絶対に必要というよりはLambdaのBluePrint Templateに合わせてる感じでしょうか。。。 

  2. デフォルトがus-east-1になっているのでご注意ください。 

  3. 設計図と和訳されてて微妙ですが… 

  4. Lambdaのウォームアップとかその辺周りっぽい 

terukizm
仕事でJavaやPHP、趣味でPythonやってたりやってなかったりする、フリーランスのプログラマっぽいいきものです。
http://acez.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away