この記事は スタディストアドベントカレンダー 2019 9日目の記事です。
二枠連続で投稿する機会を得ましたので、業務の中のとある取り組みについて、前後編に分けてご紹介したいと思います。
さて前編ではキャッキャウフフ会の発足の経緯と最終的なゴールの解説をしました。
後編となる今回は実装上の詰まりポイントや、キャッキャウフフ会がなぜうまく回っているのかについて、メンバーで振り返りを実施した結果についてまとめます!
と、その前にまずはゴールの再掲から。
会の活動実績を改めて振り返ったところ、上記の流れを一通り実装しきるまでの活動としては全 3 回 開催されていました(その他にもいろいろなテーマで活動しているので、機会があったら改めてご紹介します!)
というわけで各回のゴール設定とつまずきポイントについてご説明〜
第 1 回: Hello AWS World !
まずは導入編です。
基本的にドライバーはWebチームの二人のどちらかが務め、ぼくはナビゲーターを務めました。
マネジメントコンソールへのログイン方法や AWS CLI の設定から始まり、「このリソースはこういう役割」をハンズオンで動かしながら紹介していきます。
なにはなくとも Hello World ! ですよ。というわけで初回の目標は以下のように設定しました。
(ちなみにSREチームでは The Serverless Appilication Framework というOSSを使用していますが、今回は勉強のためあえて AWS CLI 経由で直接操作しています)
初回は全体像の解説をメインに、API Gateway と Lambda を構築し、 curl
コマンドで Hello World !
が返ることを確認しました。
初期セットアップとしての認証情報の設定 ( $ aws configure --profile=staging
) や API Gateway の設定の複雑さに呑まれつつも、特に詰まりどころらしい詰まりどころもなく好調な滑り出しでした 👏
振り返り会では 横文字禁止 というレギュレーションが設定されたため、とてもカオスなことになりました。
- 密林提供網
- 弾力性計算雲
- 門
それぞれなんのことかわかるでしょうか??
第 2 回: Hello AWS Dark World ...
さて第 1 回で API Gateway -> Lambda の呼び出しは成功したので、次は Lambda から EC2 へのアクセス部分です。
とはいえそれぞれのリソースの役割も Lambda の操作方法も第 1 回で学んだのでまあまあそんな、詰まりどころもないだろう…と思っていたの ですが !
主に以下2点のつまずきポイントが待っていました…
1. IAM Role の存在…
最終的なゴールの図から完全に抜け落ちていた存在、そう権限まわりです。
「IAM には グループ・ユーザー・ロール・ポリシー という概念があって、いま CLI 経由でコマンドを実行しているのはユーザの権限だけど、Lambda には別途セッションマネージャーを実行するためのロールをアサインしてあげる必要があって、そのためにはポリシーのアタッチが…」
AWS を本格的に使いはじめて1年とちょっと。ぼく自身は今でこそ IAM の各概念についてわりと体系的な理解を得ることはできたものの、それをキレイに整理し、適当な権限を付与できるよう理解 する / してもらう ことにはやはりかなりのエネルギーを要しました。
よくわからないけど動かなかった、そんなときはとりあえず権限まわりに原因があったような気がします。。。
最終的には以下のように Lambda と EC2 へのロールのアタッチが必要でした。
特に詰まったのはセッションマネージャーでの接続先の EC2 内の ssm-agent
へのアタッチです。
ロールの変更を適用するには、以下のコマンドを実行してサービスの再起動が必要というポイントでした。
sudo systemctl restart snap.amazon-ssm-agent.amazon-ssm-agent.service
なかなかの初見殺し。
2. 非同期処理 in Lambda の扱い…
セッションマネージャー経由で ps
コマンドを実施したとして、その結果を受け取る前に Lambda が死んでしまったら意味がないのです。
はじめは以下のように sendCommand
で EC2 内で echo
コマンドを実行し、その結果が Lambda のログに現れることを期待していました。
// コマンド送信const { Command } = await ssm.sendCommand({DocumentName: 'AWS-RunShellScript',InstanceIds: [instanceId],Parameters: {commands: ['echo "Hello World !"',],},TimeoutSeconds: 3600 // 1 hour}).promise()// コマンド実行結果確認const { StandardOutputContent } = await ssm.getCommandInvocation({CommandId: Command.CommandId,InstanceId: instanceId,}).promise()console.log(StandardOutputContent)
ところが全然出力されない。ログを確認すると、StatusDetails
が In Progress
となっていました。
これはつまり getCommandInvocation
の 終了自体は待つものの、コマンドの中身が実行中というステータスであっても終了判定がなされてしまう ということです。
というわけで以下のように sleep
処理を追加して EC2 内でのコマンドの実行完了を待ち、さらに Lambda 側の実行タイムアウト時間を伸ばすことで無事ログ出力されることが確認できました。
// コマンド送信const { Command } = await ssm.sendCommand({DocumentName: 'AWS-RunShellScript',InstanceIds: [instanceId],Parameters: {commands: ['echo "Hello World !"',],},TimeoutSeconds: 3600 // 1 hour}).promise()// コマンド実行結果確認await sleep(5000) <- ★ ココを追加const { StandardOutputContent } = await ssm.getCommandInvocation({CommandId: Command.CommandId,InstanceId: instanceId,}).promise()console.log(StandardOutputContent)
ちなみに sleep
関数の実装は以下。
function sleep(delay) {return new Promise(resolve => {setTimeout(resolve, delay)})}
sleep
5秒というのは 「まあ5秒も待てば充分やろ」 くらいの肌感です。
全体的に、ハマりそうなポイントで無事ハマったという感じですね 👼
こういうのも一人で作業してるとイヤになりますが、みんなで「おいおいなんでだよ!?」とか「いや〜これは初見殺しですわ〜〜〜」とかやいのやいの言うことで楽しく切り抜けることができました 💮
第 3 回: Hello Future !
と、これまでの 2 回で実装自体は終わっているので、あとはそれらをつなぎこむだけ!
そんなふうに考えていた時期がぼくたちにもありました…
実は 全体を通じて最も苦戦したのはそのつなぎ込みのところ だったのです。最終回では主に以下2点のつまずきポイントが待っていました…
1. Slack の3秒ルールとの闘い…
Slack のスラッシュコマンドには3秒ルールというシビアなルールがあります。
コマンド実行後、3秒以内にレスポンスが返らないとエラーとなってしまうというヤツです。
さてこれまでの作業では上述の通り、「まあ5秒も待てば充分やろ」 という設定になっていたため、どうあがいても絶望 という感じでした。
そこで Lambda 関数を
- とりあえず即レスする君 (Dispatch)
- 実際の処理を行う君 (Command execute)
のふたりに分離することに。ので、最終的な構成図は以下のようになりました。
Dispatch
の責務は Command execute
を呼び出し、Slack にとりあえず「待っててね」というレスポンスを返すことです。
こうすることで Slack としては正常応答が返ったという扱いになるためエラーとはならず、実際の処理は別関数で実行させるという責務の分離をキレイに行うことができました。
2. なんで Lambda すぐ死んでしまうん?問題…
こちらが最大の詰まりどころでした。
処理を時系列で理解するために、シーケンス図に起こしてみたのでご覧ください。
上述のとおり Dispatch
の責務は Command execute
を呼び出すだけ。Slack は Dispatch
からのレスポンスを受け取り、一時回答として「待っててね」というメッセージを投稿します。
その裏では非同期処理として Command execute
がセッションマネージャー経由で ps
コマンドを発行、5秒待った後にその結果を取得して Slack に POST するという流れです。うん、わかりやすい。
ところがどっこいしょ
またもや ログを見る限りでは正常終了しているものの 待てど暮らせど Slack に投稿が来ないという問題と直面してしまいました。。。
悪戦苦闘の果て、これは Slack へのリクエストを投げた直後に Lambda が終了していたため、実際は 送信完了まで至っていなかった ことが原因らしいと判明しました。
具体的な実装レベルでいうと
const req = https.request(url, options, (res) => {let data = ''res.setEncoding('utf8')res.on('data', (chunk) => { data += chunk.toString() })})req.write(body)req.end()
Lambda が async handler
であり、かつ上記の req.end()
直後に 終了していたため、Slack への 送信完了さえ待っていなかった ということになります!
ので、最終的には Slack へのPOST完了を await で待つことで、無事全体の流れを通すことができました!!
なお結果論ですが、はじめから Lambda を sync handler
にしておけばここで詰まることはなかったかもしれません…
(上記、Lambda における非同期処理の挙動を理解する上で、下記のサイトを大いに参考にさせていただきました)
というわけでついに!!!
Kudos to us 🎉🎉🎉
最大級の拍手で感動を共有しました 👏👏👏
完成はしたものの…
てな感じでとりあえず正常系の動作は一通り、最低限の要求を満たすところまではたどりつきました!
ですがこれはお遊び用の Sandbox 環境上での操作で、IAM Role の付与の仕方がガバガバ(とりあえず必要なリソースに対して FullAccess)だったり、特定インスタンスへのコマンド実行にしか対応していなかったり(検証環境では複数インスタンスが起動しているので、どこに投げるか選択できるようにしたい)、手動で作った部分を Terraform の管理化においたり etc, etc...
やりたいことはまだまだたっくさんあるのです!
俺たちの戦いはこれからだ!
うまくいった理由
最後に本記事の投稿のため、なぜキャッキャウフフ会がうまくいったのか、全 3 回を通じての振り返りを実施しました。
そのなかで出てきた意見をまとめます。
・ドライバーとナビゲーターがうまく分離でき、円滑に作業を進められた
・各回ごとの目標設定が絶妙で、ちょうど良い達成感を得られていた
・終了後に毎回振り返りを実施し、楽しかったね〜という感想を共有した(in デニーズ)
・「楽しく作業し、成果を賞賛する」意識が共有されており、心理的安全性が高かった
・モニターがデカかった(50インチ)
おわりに
前後編の 2 回にわたってご紹介してきましたが、キャッキャウフフ会のキャッキャウフフ感を少しでもお伝えすることは出来たでしょうか?
ところであなたの好きな動物はなんですか?ネコちゃん?ワンちゃん?ぼくはカバとかも好きなんですよ。
というわけでキャッキャウフフ会では新しい仲間を常に募集中!ついでにスタディストにジョインしてもらえるとなお嬉しいです 😊
少しでも興味を持っていただけたら、ぜひお気軽にお声掛けいただけると大喜び!です!
一緒にデニーズ神田小川町店で振り返りしましょう!!!