Edited at

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のウォームアップとかその辺周りっぽい