こんにちは、SRE の中村です。
この記事はユニファアドベントカレンダーの17日目の記事です!
Unifa Advent Calendar 2024 - Adventar
はじめに
今日は、年末年始のお休み期間でもスマホ一つで障害対応ができるようになりました、というお話をしていきます。(絶対にしたくはない)
先日、SRE が勤務開始する前に障害が発生し、対応権限のあるメンバーがおらず初動が遅れてしまったことがありました。
そこで、Bot 的なもので AWS リソースの操作ができないかという話になり、Slack から AWS CLI を実行できる環境を構築することになりました。
以下のような構成になります。
リソースと矢印が多く少し見づらくなってしまったのですが、ざっくり以下のようになっています。
- Slack から AWS CLI を実行
- AWS Chatbot(Slack app) が AWS Chatbot(AWS リソース) に CLI コマンドを渡す
- AWS Chatbot が Step Functions を実行
- Lambda を実行し Slack に承認可否を求める
- 承認結果を API Gateway を介して Lambda に渡し、承認された場合は継続、拒否された場合はそこで終了
- 承認された場合は該当リソースをスケーリング
- スケーリング結果を Slack に返す
では具体的にどうなっているのかというお話は、要点をかいつまみながら以下に続けます。
Slack から Step Functions を実行するまでの動き
そもそも Slack から AWS CLI が実行できるというだけで感動だったのですが、設定は思った以上に簡単です。
まず Slack チャンネルに AWS Chatbot というアプリを入れ、あとは AWS サービスの AWS Chatbot と Slack を連携させるのみです。
以下の記事が参考になるかと思います。
Slack と AWS Chatbot で ChatOps をやってみよう - builders.flash☆ - 変化を求めるデベロッパーを応援するウェブマガジン | AWS
この記事では初めに SNS が出てきますが、これは Slack に通知するために使用しているもので、今回とはデータの流れが逆になっています。
SNS が無くても CLI は実行できます。
記事の最後にもキャプチャが載っていましたが、実際に Slack から AWS CLI を実行するときは以下のようになります。
非常に簡単ですね!
ただし一点気をつけなければならないのは、 実行できる CLI はAWS Chatbot の権限に依存するため、
仮に admin 権限を付与していた場合、誰でも好き放題できてしまうことになります。
この辺りは Chatbot の権限 + ガードレールのポリシーで必ず制限をかけておくことをお勧めします。↓
Understanding AWS Chatbot permissions - AWS Chatbot
さて、これで Slack から AWS リソースが操作できるようになりました。
なので、必要なパラメータを持たせて EC2 や ECS をスケーリングできる Lambda を実行すれば今回の目的は達成できます。
ただ、Step Functions やら API Gateway やら色々と組み合わせているのは、Slack 上で承認フローを挟む必要があったからです。
いくら Chatbot の権限を制限しても、自由に EC2/ECS のスケーリングができるのはちょっと安全とは言い難いです。
これは悪さをする人がいるのではないか、ということではなく誤操作を恐れてのことです。誰にでもミスはあります。
そこで登場するのが Step Functions と API Gateway です。
ではどのように承認フローを実現するか見ていきます。
Step Functions, API Gateway, Lambda で承認フローを構築する
CloudFormation で構築しているので、 template.yaml
に基づいてお話しします。
WaitForPermissionScalingAbsoluteValueECS: Type: Task Resource: arn:aws:states:::lambda:invoke.waitForTaskToken Parameters: FunctionName: !Ref PostScalingRequestToSlackLambda Payload: TaskToken.$: $$.Task.Token AWSAccountID.$: $.aws_account_id ECSClusterName.$: $.ecs_cluster_name ECSServiceName.$: $.ecs_service_name Min.$: $.min Max.$: $.max ResultPath: $.payload TimeoutSeconds: 600 Catch: - ErrorEquals: - RejectedScalingRequest Next: Rejected - ErrorEquals: - States.ALL Next: Error Next: ExecScalingEC2OrECS
ステートマシンを待機させる
ここで Resource
に指定している arn:aws:states:::lambda:invoke.waitForTaskToken
は、コールバックタスクというものです。
これによって Slack 上で承認可否が確定されるまでステートマシンを待機させることができます。
Discover service integration patterns in Step Functions - AWS Step Functions
詳細は上記ドキュメントを確認していただきたいのですが、待機させている間に Parameters
の FunctionName
で指定した Lambda を実行し、
最終的に SendTaskSuccess
or SendTaskFailure
API のパラメータとして task_token
を返すことでステートマシンを再開させます。
「最終的に」と表現したのは、その Lambda 自身が返す必要はなく、誰からだろうと task_token
さえ返せば問題ないためです。
実際今回の場合、この Lambda はスケーリングの承認リクエストを Slack に送信するのみで、 task_token
は別の Lambda が返しています。
では task_token
を返す Lambda はどのように実行しているかというと、Slack での承認 -> API Gateway -> Lambda という流れになっています。
承認結果を受け取らないと SendTaskSuccess
or SendTaskFailure
API どちらを実行するか決められないので、Step Functions からは外れて Slack での承認がトリガーになっています。
ちなみに Slack には以下のようなメッセージを送信しています。
ここで「Approve」「Reject」どちらかのボタンを押すと最終確認画面を挟み確定されるようにしています。
Slack Payload はこちら
payload = { "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": text } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Approve" }, "confirm": { "title": { "type": "plain_text", "text": "承認してよろしいですか?" }, "text": { "type": "mrkdwn", "text": text }, "confirm": { "type": "plain_text", "text": "OK" }, "deny": { "type": "plain_text", "text": "Cancel" } }, "style": "primary", "value": task_token }, { "type": "button", "text": { "type": "plain_text", "text": "Reject" }, "confirm": { "title": { "type": "plain_text", "text": "却下してよろしいですか?" }, "text": { "type": "mrkdwn", "text": text }, "confirm": { "type": "plain_text", "text": "OK" }, "deny": { "type": "plain_text", "text": "Cancel" } }, "style": "danger", "value": task_token } ] } ] }
※ Slack Payload の作成時は以下利用
Reference: blocks | Slack
Slack から API Gateway にリクエストする
続いて Slack から API Gateway へのリクエストですが、Slack app 側でボタン押下時のリクエスト先を設定しています。
以下の部分で指定できます。
ここで指定した API Gateway に対し、上述した Slack Payload の value
で task_token
を渡しているため、後続の Lambda で受け取ることができるようになります。
あとは長旅から帰ってきた task_token
と共に SendTaskSuccess
or SendTaskFailure
API を実行し、承認結果によって処理を分岐させ、「Approve」なら対象リソースをスケーリング、「Reject」なら終了させるだけです。
この辺りは Lambda でお好きなように実装していただければ良さそうです。
尚、ここまで必要最小限の機能のお話をしてきましたが、弊社ではいくつか工夫を施してあり、以下の機能も追加してあります。
- 現在の AutoScaling 設定(そもそも今何台動いているのかなど)が分からない場合でも安全にスケーリングできる
- 絶対値と相対値どちらを指定してもスケーリングできる
- 絶対値であれば最小何台、最大何台という風に決め打ちする指定法で、相対値であれば現在の台数にプラス何台という指定法
- Slack 上で承認可否を求めるメッセージには現在の AutoScaling 設定を添える
- 絶対値と相対値どちらを指定してもスケーリングできる
- 「Approve」or「Reject」ボタンが押されたらメッセージを修正
- ボタン部分を消して、ボタンを押した Slack ユーザー名と承認結果で上書きすることで重複して押せない & 誰が押したのか分かるようにする
- スケーリング結果やエラー発生時には Slack に通知
- 「Reject」ボタンが押された場合はエラー扱いにはしない
おわりに
これで年末年始のお休み期間でもスマホ一つで障害対応ができるようになりました。(絶対にしたくはない)
とはいえ、障害発生時の対応フローを整備するのも大事ですが、そもそも障害が発生しない構成を作り、ご利用いただいている方々に少しでも良いサービスを提供できるよう取り組んでいこうと思います。
最後に、ユニファでは一緒に働いてくれる仲間を募集中です!
少しでもご興味をお持ちいただけましたら以下をご確認ください。