背景
今までEC2で管理し、踏み台サーバーからデプロイしていたサービスを拡大にともなってコンテナ化を開始しはじめたので、 デプロイをCodePipelineに乗りかえました。 それにともなって、デプロイの経過をSlackで確認できるようにAWS Chatbotとの連携を実施しましたが、 AWS Organization環境下ではSNS関係の設定等が必要だったので記録として残しておきます。
環境
弊社では、コスト管理や権限の管理の容易さ、ミスの防止等々の観点からアプリケーションの環境(プロダクションやステージング等)毎に、 AWS Organizationに所属するアカウントを発行して環境を容易しています。
やった事
図のように、各環境内のSNSからAWS Lambdaを利用して、Slackへの通知を集約した環境のSNSに通知を転送するようにしました。
なぜこのようにしたか
AWS Chatbotを環境毎に連携を増やすのはメンテナンス観点から避けたい
各環境からAWS ChatbotをSlackにインストールするというのも考えたのですが、 現状通知先を得に環境毎に振りわけたいという事もないため、同じチャンネルに投稿するためのAWS Chatbotを各環境でサブスクライブする事になりそうです。 そうするとAWS BotがSlackに何個も登録される事になりBotがどの環境のものなのかわからなってしまう不安を感じたので一元管理する事にしました。
AWS CodeStarの通知先のSNSに設定できるのは同一のAWSアカウント内のトピックだけだった
AW CodeStarのSNSから直接目的のSNSに通知を飛ばせればよかったのですが、terraformでそのように設定したらエラーが発生しました...
The target SNS topic account ID and the user account ID do not match. SNS topics can only be added if they are in the same AWS account as the notification rule.
— Pocket7878 (@Pocket7878) 2021年6月23日
あー、直接codestarにはterraformでも指定できないかー
コンソールからはできなそうだったので、terraformだったらワンチャンとおもったのでしたが無理でした。 SNSからSNSへの転送もサポートされておらず、AWS LambdaからはSNSから通知を受信して、他の環境のSNSにそもままPublishするという事ができたので、そのようにする事にしました。
やった事
各環境のCodeStarの通知用SNSをセットアップ
まずは、CodePipelineと接続して、SNSに通知するCodeStarのルールを設定しました。
/* * Codestar */ data "aws_caller_identity" "current" {} data "aws_iam_policy_document" "sns_topic_policy" { policy_id = "__default_policy_ID" statement { sid = "__default_statement_ID" effect = "Allow" principals { type = "AWS" identifiers = ["*"] } actions = [ "SNS:Publish", "SNS:RemovePermission", "SNS:SetTopicAttributes", "SNS:DeleteTopic", "SNS:ListSubscriptionsByTopic", "SNS:GetTopicAttributes", "SNS:Receive", "SNS:AddPermission", "SNS:Subscribe", ] resources = [ aws_sns_topic.default.arn, ] condition { test = "StringEquals" variable = "AWS:SourceOwner" values = [data.aws_caller_identity.current.account_id] } } statement { actions = ["sns:Publish"] principals { type = "Service" identifiers = ["codestar-notifications.amazonaws.com"] } resources = [aws_sns_topic.default.arn] } } resource "aws_sns_topic" "default" { name = "sample-codestar-notification-sns-topic" } resource "aws_sns_topic_policy" "default" { arn = aws_sns_topic.default.arn policy = data.aws_iam_policy_document.sns_topic_policy.json } resource "aws_codestarnotifications_notification_rule" "default" { detail_type = "BASIC" event_type_ids = [ // Pipeline events "codepipeline-pipeline-pipeline-execution-started", "codepipeline-pipeline-pipeline-execution-succeeded", "codepipeline-pipeline-pipeline-execution-failed", "codepipeline-pipeline-pipeline-execution-canceled", // Approval events "codepipeline-pipeline-manual-approval-needed", ] name = "sample-codepipeline-notification-rule" resource = aws_codepipeline.default.arn target { address = aws_sns_topic.default.arn } }
転送先のSNSの通知トピックを設定
resource "aws_sns_topic" "default" { name = "sample-codestar-notification-sns-topic" } resource "aws_sns_topic_policy" "default" { arn = aws_sns_topic.default.arn policy = data.aws_iam_policy_document.sns_topic_policy.json } data "aws_caller_identity" "current" {} data "aws_iam_policy_document" "sns_topic_policy" { policy_id = "__default_policy_ID" statement { sid = "__default_statement_ID" effect = "Allow" principals { type = "AWS" identifiers = ["*"] } actions = [ "SNS:Publish", "SNS:RemovePermission", "SNS:SetTopicAttributes", "SNS:DeleteTopic", "SNS:ListSubscriptionsByTopic", "SNS:GetTopicAttributes", "SNS:Receive", "SNS:AddPermission", "SNS:Subscribe", ] resources = [ aws_sns_topic.default.arn, ] condition { test = "StringEquals" variable = "AWS:SourceOwner" values = [data.aws_caller_identity.current.account_id] } } statement { sid = "__console_pub_0" effect = "Allow" principals { type = "AWS" identifiers = var.publish_account_ids } actions = [ "SNS:Publish", ] resources = [ aws_sns_topic.default.arn, ] } }
各環境のSNSトピックと似ていますが、
statement { sid = "__console_pub_0" effect = "Allow" principals { type = "AWS" identifiers = var.publish_account_ids } actions = [ "SNS:Publish", ] resources = [ aws_sns_topic.default.arn, ] }
ここで、このトピックにPublishする事を許可する外部のAWSアカウントIDを指定している所がミソです。
各環境のSNSからSNSを転送するAWS Lambda
SNSを転送するようの、AWS Lambdaをserverless-frameworkで配置しました。
from __future__ import print_function import json import boto3 import os import logging logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context): os.environ["AWS_DATA_PATH"] = os.environ["LAMBDA_TASK_ROOT"] client = boto3.client('sns') try: for r in event['Records']: message = r["Sns"]["Message"] client.publish( TopicArn=os.environ["DESTINATION_SNS_TOPIC_ARN"], Message=message ) return {"statusCode": 200} except Exception as e: logger.error("Failed to forward sns message: {}".format(e)) return { "statusCode": 422, "body": "Invoked from anything but SNS" }
service: crew-exp frameworkVersion: '2' provider: name: aws runtime: python3.8 lambdaHashingVersion: 20201221 region: ap-northeast-1 plugins: - serverless-python-requirements package: individually: true stage: ${opt:stage, self:custom.defaultStage} functions: forward-codestar-sns: handler: forward-codestar-sns/handler.lambda_handler memorySize: 256 maximumRetryAttempts: 1 events: - sns: arn: ${self:custom.sns.codestar.${env:APP_ENV}} role: ForwardCodestarSNSIAMRole environment: DESTINATION_SNS_TOPIC_ARN: ${self:custom.sns.codestar.tool} resources: Resources: ForwardCodestarSNSIAMRole: Type: AWS::IAM::Role Properties: RoleName: sample-forward-codestar-sns-lambda-iam-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: sample-forward-codestar-sns-lambda-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - 'Fn::Join': - ':' - - 'arn:aws:logs' - Ref: 'AWS::Region' - Ref: 'AWS::AccountId' - 'log-group:/aws/lambda/*:*:*' - Effect: Allow Action: - SNS:Subscribe Resource: ${self:custom.sns.codestar.${env:APP_ENV}} - Effect: Allow Action: - SNS:Publish Resource: ${self:custom.sns.codestar.tool} custom: defaultStage: dev pythonRequirements: dockerizePip: true sns: codestar: sandbox: arn:aws:sns:ap-northeast-1:xxxxxx:sample-codestar-notification-sns-topic tool: arn:aws:sns:ap-northeast-1:yyyyy:sample-codestar-notification-sns-topic
転送先の環境でAWS ChatbotをSNSに接続する
最後に、AWS Chatbotから通知が転送されてくるSNSを購読してSlackに連携させるようにする事で、無事各環境からの通知をSlackに流せるようになりました。