宮下(mizzy)です。
Amazon SNSからSlackに通知する仕組みを導入した話(概要編)でご紹介した仕組みの実装面について解説します。
TerraformによるAWS上のリソース設定
SNS TopicとLambda Functionの関連づけ
Slack通知用SNS Topicの作成と、実際に通知を行うLambda Functionの関連づけは、Terraformコードでこのように記述しています。
ただし、Lambda FunctionはTerraformではなくecspressoで管理しているため、Lambda Functionの情報はData Sourceで読み込んでいます。
data "aws_lambda_function" "sns_to_slack" { function_name = "sns-to-slack" } resource "aws_sns_topic" "slack_notification" { name = "slack-notification" } resource "aws_sns_topic_subscription" "slack_notification" { endpoint = data.aws_lambda_function.sns_to_slack.arn protocol = "lambda" topic_arn = aws_sns_topic.slack_notification.arn } resource "aws_lambda_permission" "slack_notification" { action = "lambda:InvokeFunction" function_name = data.aws_lambda_function.sns_to_slack.function_name principal = "sns.amazonaws.com" source_arn = aws_sns_topic.slack_notification.arn }
Bot User OAuth Tokenの格納場所作成とアクセス権の設定
Lambda FunctionからSlackへの接続時に必要な、Bot User OAuth Tokenを格納するためのSSMパラメータを作成し、Lambda Functionから値を読み取れるよう権限設定しています。
Bot User OAuth Tokenの値はTerraformでは設定しないため、ダミーの値を設定し、ignore_changes
で変更を無視するようにしています。
resource "aws_ssm_parameter" "talentio_ruboty_bot_user_oauth_token" { name = "/slack/bot-user-oauth-token/TalentioRuboty" type = "SecureString" value = "dummy" lifecycle { ignore_changes = [value] } } resource "aws_iam_role" "sns_to_slack" { name = "sns-to-slack" assume_role_policy = data.aws_iam_policy_document.sns_to_slack_assume_role.json inline_policy { name = "inline" policy = data.aws_iam_policy_document.sns_to_slack.json } } data "aws_iam_policy_document" "sns_to_slack_assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { identifiers = ["lambda.amazonaws.com"] type = "Service" } } } data "aws_iam_policy_document" "sns_to_slack" { statement { effect = "Allow" actions = ["ssm:GetParameter"] resources = [ aws_ssm_parameter.talentio_ruboty_bot_user_oauth_token.arn, ] } }
SNSにPublishする際、以下のようにSlackApp
でSlack App名を指定するようにしていますが、ここで指定された値によって、Tokenを読み取るSSMパラメータを変更することで、複数のSlack Appに対応できるようにしています。
aws sns publish \ --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxx:slack-notification \ --subject 'Message from CLI' \ --message '{ "SlackApp": "TalentioRuboty", "SlackChannel": "mizzy-test" }'
通知用Lambda Function
Slackへ通知を行うLambda Function本体は、以下のような125行のRubyコードです。
require 'json' require 'aws-sdk-ssm' require 'uri' require 'net/http' require 'slack-ruby-client' def handler(event:, context:) event = event['Records'][0] subject = event['Sns']['Subject'] message = JSON.load(event['Sns']['Message']) if message['Status'] == 'Success' title = ":white_check_mark: #{subject}" elsif message['Status'] == 'Fail' title = ":x: #{subject}" elsif message['Status'] == 'Warning' title = ":warning: #{subject}" else title = ":information_source: #{subject}" end blocks = [{ type: 'header', text: { type: 'plain_text', text: title, }, }] if !message['User'].nil? blocks.append( { type: 'section', text: { type: 'mrkdwn', text: "<@#{message['User']}>", }, } ) end fields = [] if !message['Parameters'].nil? message['Parameters'].each do |key, value| fields.append({ type: 'mrkdwn', text: "*#{key}:*\n #{value}", }) end end if !message['StartTime'].nil? start_time = Time.parse(message['StartTime']) end_time = Time.now fields.append({ type: 'mrkdwn', text: "*Elapsed time:*\n #{((end_time - start_time) / 60).round(1)} minutes", }) end # fieldsが空の場合Slackがinvalid_blocksエラーを返すので空白文字で埋める if fields.size == 0 fields.append({ type: 'mrkdwn', text: ' ', }) end blocks.append({ type: 'section', fields: fields, }) if !message['Arn'].nil? link = arn_to_url(message['Arn']) blocks.append( { type: 'section', text: { type: 'mrkdwn', text: "*Link:*\n<#{link}|#{link}>", }, } ) end if !message['Url'].nil? blocks.append( { type: 'section', text: { type: 'mrkdwn', text: "*Link:*\n<#{message['Url']}|#{message['Url']}>", }, } ) end ssm = Aws::SSM::Client.new req = { name: "/slack/bot-user-oauth-token/#{message['SlackApp']}", with_decryption: true, } res = ssm.get_parameter(req) Slack.configure do |config| config.token = res.parameter.value end client = Slack::Web::Client.new client.auth_test client.chat_postMessage(channel: message['SlackChannel'], text: title, blocks: JSON.dump(blocks), as_user: true) return end def arn_to_url(arn) elements = arn.split(':') region = elements[3] "https://console.aws.amazon.com/go/view?arn=#{arn}" end
おわりに
Amazon SNSからSlackに通知する仕組みは、各社さんそれぞれ実装していると思いますし、検索すると色々と記事が出てくるのですが、実運用レベルでどのように実装しているか、といった記事はあまりなさそうでしたので、実例として紹介してみました。
通知にはLambda FunctionではなくAWS Chatbotの利用も考えましたが、通知内容のカスタマイズを柔軟に行うことができなさそうだったので、Lambda Functionを利用する形になりました。
Amazon SNSからSlackに通知する仕組みを導入した話(概要編)でも書きましたが、元々、このような仕組みを導入したのは、AWS Step Functionsから簡単に通知できるようにしたかったからです。
Step Functionsを実際の運用でどのように利用して、どのように通知しているか、という具体例については、また後日ご紹介できればと思います。