Let's write β

プログラミング中にできたことか、思ったこととか

クロスアカウントでAWS CodePipelineの通知を集約する設定をterraform & serverless-frameworkで実施した

背景

今までEC2で管理し、踏み台サーバーからデプロイしていたサービスを拡大にともなってコンテナ化を開始しはじめたので、 デプロイをCodePipelineに乗りかえました。 それにともなって、デプロイの経過をSlackで確認できるようにAWS Chatbotとの連携を実施しましたが、 AWS Organization環境下ではSNS関係の設定等が必要だったので記録として残しておきます。

環境

弊社では、コスト管理や権限の管理の容易さ、ミスの防止等々の観点からアプリケーションの環境(プロダクションやステージング等)毎に、 AWS Organizationに所属するアカウントを発行して環境を容易しています。

やった事

f:id:Pocket7878_dev:20210624202630p:plain
Slackへの通知ワークフロー

図のように、各環境内の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でそのように設定したらエラーが発生しました...

コンソールからはできなそうだったので、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に流せるようになりました。

f:id:Pocket7878_dev:20210624202035p:plain

参考文献

docs.aws.amazon.com

qiita.com

www.beex-inc.com