Configで検知した異常をSNSとLambdaを使ってSlackへ通知してみた
AWSでサービスを動かしているとSlackと連携して通知をする機会がしばしばあると思う.社内で,AWSのConfigで検知した異常をSlackに通知してほしいと言われたので,Simple Notification Service(以降SNS)とLambdaを使って実現した.今回はその方法を書いていく.
はじめに
弊社は自社サービスが多く,アカウントはサービス単位で発行され,開発エンジニアはそれぞれのアカウントで環境を構築している.今回は,各アカウントで設定されたConfigからSNSとLambdaを経由して会社共有のSlackに通知する.SNSとLambdaは各アカウントごとに作成するのはコストの面でよくないので,インフラチームのアカウントで1つだけ作成する方針にしている.ということで今回は以下のような設計で構築した.
なお,Slackへ通知する際に必要なIncoming Webhookの設定はこの記事では割愛する.公式を参考にしていただくとスムーズに設定できるかと思う.
手順
Configを作成
Configとは,AWSリソースの設定を評価,監査,審査できるサービスである.Configではリソースを評価するためのルールを設定する.今回設定するルールは,インバウンドのsshを設定しているセキュリティグループでIPをフルオープンにしていないかチェックするrestricted-ssh
にする.ルールは自由に選んでよいが,通知テストをする上では一時的にルールを破る必要があるのでそれに適したルールを設定するのがよいと思う.また,設定項目にAmazon SNS トピック
があるが,SNSトピックの設定をした後でないとarnの入力ができないため一旦このままにしておく.その他はデフォルトのまま.
SNSを作成
SNSとは簡単に言うと「何らかの通知を送信したい人(またはシステム)から、受信したい人(またはシステム)へ通知を送る仕組み」を提供するマネージドサービスである.SNSでは「送信したい人」を発行者と呼び,「受信したい人」を購読者と呼ぶ.そして,双方を繋げる中間役をトピックという.SNSを利用する際はまずトピックを作成し,そのトピックと通信できる発行者・購読者を設定することで,アクセスを制御することができる.今回の場合,発行者は各サービスのConfigで,購読者はLambdaとなるように設定する.トピック作成時に出てくるアクセスポリシーの設定では異なるアカウントからの発行を許可するよう設定しておく.
Configに通知先としてSNSを設定
トピックの設定が終わるとarnが発行されるので,それをConfigに設定する.ConfigとSNSは別アカウントにあるため別のアカウントからトピックを選択
を選び,トピックのarnを入力.
Config用のロールを作成
Configにアタッチされているポリシーにもトピックのarnを設定しなければならない.Configを作成する際ロールを新規で作成すると,AWSServiceRoleForConfig
というロールが割り当てられる.そのロールにはSNSへの発行(Publish)を許可するポリシーが設定されていないので,SNSに発行(Publish)可能なロールを新たに作成してConfigにアタッチする必要がある.まずはロールを作成.
次に,以下のようなポリシーを作成し上記のロールにアタッチ.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": "sns:Publish", "Resource": "arn:aws:sns:hogehoge" } ] }
このロールをConfigから選択する. これでConfigからSNSまでの通知の設定は完了.
Lambdaを作成
SNSから通知を受け取り,Slackへ通知するためのLambdaを作成する.Python3.7で以下のように記述した.
import json import os import urllib.request SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL'] def lambda_handler(event, context): # cloudwatchにログを吐くため用のデバッグ文 print(event['Records'][0]['Sns']['Message']) message = json.loads(event['Records'][0]['Sns']['Message']) title = message['configRuleName'] detail = message['awsAccountId'] + 'アカウントで' + message['configRuleName'] + 'の異常が検知されました' post_slack(title, detail) def post_slack(title: str, detail: str) -> None: payload = { 'attachments': [ { 'color': '#D50200', 'pretext': title, 'text': detail } ] } request = urllib.request.Request( SLACK_WEBHOOK_URL, data=json.dumps(payload).encode('utf-8'), method="POST" ) try: with urllib.request.urlopen(request) as response: print(response.read()) except urllib.error.HTTPError as err: print(err.code)
やっていることは単純で,ConfigからSNSを経由して受け取った情報から,Slackで表示する文字列を生成してSlackへ投稿するだけである.SLACK_WEBHOOK_URL
の部分はIncoming Webhookの設定で吐き出されるチャンネルのWebhook先のURLである.冒頭でも言及した通り,SlackのIncoming Webhookの設定方法はここでは割愛する.さらに,SNSからの通知をトリガーにLambdaを動作させたいのでトリガーに先ほど作成したSNSを設定する.
SNSの購読者にLambdaを設定
Lambdaを作成した際にarnが発行されるので,それを使いSNSにLambdaを購読者として設定する.Lambdaでトリガーを設定するとここは自動で設定されているかもしれない.これでSNSに発行されたConfigの情報をLambdaで受け取ることができる.
テスト
ここまでで通知の設定は完了した.試しにConfigが設定してあるセキュリティグループのインバウンドを任意の場所からsshできるよう設定してみると,ちょっと経ってからSlackに通知が届く.正直どのタイミングでConfigがSNSに通知するのかよく分かっていない. また以下のようにコンソールから,ConfigとSNS間,SNSとLambda間,LambdaとSlack間で結合テストができる.
はまったところ
SNSへ発行可能なConfig用のロールを作成しなければならないことに気づかずはまってしまった.ConfigからSNSへの通知テストは通ったので,ConfigとSNS間はあまり疑っていなかった.しかし,それはコンソールにログインしているユーザのロールにSNSへの発行が許可されていて,コンソール上からは通知テストがうまくいっているというだけだった.ここで学んだことは,AWSだとどのサービスが動いていて,どのロールが適用されているかを理解し,アクセス先はもとよりアクセス元のACLもしっかり考慮しないといけないこと.