SlackのSlash Commandを使ったデプロイの実現方法は色々ありますが、今回はAWS Lambda functionとEC2 Run Commandを使ったやり方を紹介します。
仕組み
この方式のメリットは以下になります。
- 仕組みが簡単
- 要はSlackとLamda経由でEC2上のデプロイコマンドを叩くだけなので、わかりやすい
- AWSだけで完結する
- Slack以外、外部サービスに依存しない(GitHubとか使ってなくてもOK)
- ローコスト
- bot用にEC2インスタンスを用意する必要がなく、必要なときにLambda functionを呼び出すだけ
- デプロイ可能なユーザの管理をSlackだけで制限できる
- 特定チャンネルのみでコマンド実行可能にできるので、プライベートチャンネルを作ってそこにデプロイできるユーザを集めれば、擬似的なアクセス管理が簡単に行える
**「今まで毎回手動でSSHログインしてデプロイしていたのを、Slack経由で行えるようにしたい」**という場合におすすめです。
1. Slash Commandを追加
まず、SlackにSlash Commandを追加します。
https:/{ワークスペース名}/apps/A0F82E8CA-slash-commands
にアクセスし、適当な名前でslash-commandを作成してください。ここでは/deploy
としました。
URLにはこの時点では適当にダミーを入れておけばOKです。
発行されたトークンを利用しますので、控えておいてください。
2. AWS KMSの設定
1.で作成したSlash Commandのtokenがバレると好き放題通知を叩かれてしまうので1、tokenを暗号化するためにAWS KMSキーを作成します。
キーの作成
「東京リージョン」を指定して「キーの作成」を行います。2
動作確認
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を同時に作成します。
AWS Lambda > 関数 > 「関数の作成」 から、slack-echo-command
BluePrintを選択します。
個人的にはPython(slack-echo-command-python)がよかったんですが、LambdaのWISYWIGエディタがよくわかんないインデントエラーを出してこけたのでnodejs(slack-echo-command)にしました。
続いて、以下のように入力します。
- ロールは自動作成
- 後でIAM設定を追加しますので、ロール名を覚えておいてください
- 新規APIの作成
- 対応するAPI Gatewayを自動作成
- Slackから叩かれるのでセキュリティは「オープン」
- API名とかは適当に
- kmsEncryptTokenの値を置換
- KMSキーの動作確認で利用した、暗号化後のtokenを指定
疎通確認
ここまで出来たら、slack経由で実際にLambda functionが呼び出せるか確認しておきます。
「API Gateway」を選択すると対応するAPIエンドポイントが表示されるので、このURLを1.で作成したSlash Commandに登録します。
あとは実際にSlackから動作確認を行い、以下のようにecho出力されていればOKです。
4. EC2 Run Commandの動作確認
続いてEC2上のデプロイコマンドを、直接叩けるようにします。方法は色々ありますが、ここではLambdaから呼び出すことを想定し、EC2 Run Commandを利用します。
EC2 Run Commandを使うにはSSM権限が必要なので、EC2インスタンスに付与するIAM Roleを作成します。
AWS IAM > ロール > 「ロールの作成」
「SSM」で検索し、AmazonEC2RoleforSSM
を付与。
![スクリーンショット 2018-08-10 14.02.05.png]
(https://qiita-image-store.s3.amazonaws.com/0/50184/60bec268-71f1-54b6-c3e2-98561347dd06.png)
作成したIAM Roleを、実行対象のEC2に割当てます。
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 > コマンドの実行 > 「コマンドを実行」
結果は以下のようにして確認します。
2500文字しか出力されないということなので、場合によっては以下のようにS3に吐き出させることになるかもしれません。
なお、コマンドは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への権限を与える必要があります。
Lambda Function作成時に作成したロール(SampleSlackDeployRole
)を選択し、以下のようなインラインポリシーを追加します。
AWS IAM > ロール > SampleSlackDeployRole
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand"
],
"Resource": "*"
}
]
}
AWS SDKによるEC2 Run Commandの処理を追加
ここまで出来たら、こんな感じでLambda Functionを書き換えればOKです。
(略)
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です。お疲れ様でした!
ハマりどころ
Slash Commandの3秒制限
Slash Commandの仕様として、指定したurlにPOSTしたあと、3秒以内にレスポンスが帰ってこないとタイムアウトになります。
Lambda functionのデフォルト設定メモリ128MBだと、初回起動時だけ4、このタイムアウトに引っかかることがあるので、256MBにしたらとりあえず大丈夫になりました。Lambda functionのタイムアウトの値もこの制限に合わせて3秒になっている感じ。
通常、デプロイコマンドの実行には3秒以上かかると思うので、Lambda functionではデプロイコマンドの呼び出しだけを責務とし、処理結果の通知等を行う場合はLambda functionではなくデプロイコマンド側で行う必要があります。ご注意ください。(ssm list-command-invocations相当の処理をLambda上でやると簡単にタイムアウトになります)
参考:
- 【新機能】AWS LambdaにSlack連携のBluePrintが登場。ChatOpsがより手軽に | Developers.IO
- 東京リージョンでEC2 Run Command (Linux)を使ってみました | AWSやシステム・アプリ開発の最新情報をお届け|クロスパワーブログ