LoginSignup
6
9

More than 3 years have passed since last update.

複数AWSアカウントにまたがるCI/CD方法

Last updated at Posted at 2019-09-20

前置き

 AWSアカウント単位でのスケールアウトが必要になった場合「開発用AWSアカウント」「本番用AWSアカウント1」「本番用AWSアカウント2」のような使い分けをすることがあります。
 その場合のCI/CDが意外と面倒だったので、覚書をしておきます。
 なお、サンプルのアプリはlambdaとします。

やりたいことのイメージ1

cicd (1).png

解決方法

そもそも何が問題か

 lambdaをターゲットとするので、AWSの構築とアプリのデプロイはCloudFormationで可能です。
 また、CloudFormationのテンプレートをCloudFormation Stacksetsで複数AWSアカウントに配布することも可能です。
 ですが、CloudFormation StackSetsは、CodePipelineに対応していないので、ソースやCloudFormationのテンプレートファイルの変更を検知して、CI/CDすることがそのままではできません。

どのように解決するか

 CloudFormation StackSetsをCloudFormationで作成し、そのCloudFormationをCodePipelineで実行することで解決します。
 ただし、CloudFormationでは、CloudFormation StackSetsの作成をサポートしていないので、CloudFormationのカスタムリソースの機能を利用して、CloudFormation StackSetsを作成します。

参考)
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources.html

やりたいことのイメージ2

cicd (2).png

実際の手順

カスタムリソースのバックエンドのlambdaを作る

このlambdaはAWSの人たちが作って公開しているので
https://github.com/awslabs/aws-cloudformation-templates/tree/master/aws/solutions/StackSetsResource
のリポジトリにあるのでダウンロードします。

build.sh修正

build.sh
#! /bin/sh

# Figure out AWS profile to use
[[ -z "${AWS_PROFILE}" ]] && profile='default' || profile="${AWS_PROFILE}"
[[ -z "${2}" ]] && profile=$profile || profile="${2}"


# Figure out install region
[[ -z "${AWS_DEFAULT_REGION}" ]] && region='us-east-1' || region="${AWS_DEFAULT_REGION}"
〜略〜

※プロファイル(default)とリージョン(us-east-1)を、自分の環境に合わせて変更します。

 その後build.shを実行すると、カスタムリソースのバックエンドのlambdaを作るためのCloudFormationのスタックが作成され
StackSetCustomResource-StackSetResourceFunction-XXXXXXXXXXXというlambdaがデプロイされます。
 これがカスタムリソースのバックエンドのlambdaです。

各AWSアカウントに配布したいCloudFormationテンプレートファイルを作る

app.yaml作成

app.yaml
Resources:
  StackSetsSampleLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code: functions/
      Handler: main.lambda_handler
      Runtime: python3.6
      Role: !GetAtt StackSetsSampleIamRole.Arn
      Timeout: 30
  StackSetsSampleIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        -
          PolicyName: "stacksets_sample_lambda_policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              -
                Effect: "Allow"
                Action: "cloudwatch:*"
                Resource: "*"

functions/main.py作成

functions/main.py
def lambda_handler(event, context):
    return "hello"

各AWSアカウントに配布するソースを置くS3バケットの作成

適当にバケットを作り、バケットポリシーで、配布先のAWSアカウントからの参照を許可する。↓例

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "GetObject",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{配布先AWSID}:root"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{作成したバケット名}/*"
        },
        {
            "Sid": "ListBucket",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{配布先AWSID}:root"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::{作成したバケット名}"
        }
    ]
}

デプロイする環境の数だけ"Principal"を書く

パッケージする

aws cloudformation package --template-file app.yaml --s3-bucket {作成したバケット名} --output-template-file packaged-app.yaml

パッケージした後のyamlファイルを確認して、S3にアップロードする

packaged-app.yaml確認

packaged-app.yaml
Resources:
  StackSetsSampleLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: {作成したバケット名}
        S3Key: {自動的にキーが入っています}
      Handler: main.lambda_handler
      Runtime: python3.6
      Role:
        Fn::GetAtt:
        - StackSetsSampleIamRole
        - Arn
      Timeout: 30
  StackSetsSampleIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
      Policies:
      - PolicyName: stacksets_sample_lambda_policy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action: cloudwatch:*
            Resource: '*'

先程作ったS3のバケットにアップロードしておきます。

※このテンプレートファイルは、後述のCloudFormationの設定ファイルから利用します。
httpでアクセスできれば、アップロード先はどこでも良いですが、ここでは
https://{作成したバケット名}/packaged-app.yaml
にアップロードしたものとします。

CloudFormation StackSetsを動かせるようにする

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html
を読んで、適切に権限設定をしてください。

今回の例で言うと
開発AWSアカウントが管理者アカウントなので、AWSCloudFormationStackSetAdministrationRoleを作る必要があり
疑似本番AWSアカウントや本番AWSアカウントがターゲットアカウントなので、AWSCloudFormationStackSetExecutionRoleを作る必要があります。

なお、開発用AWSアカウント自身にもデプロイしたい場合は、自分自身がターゲットアカウントになるのでAWSCloudFormationStackSetExecutionRoleを作っておく必要があります。

CloudFormation StackSetsを管理するCloudFormationテンプレートファイルを作る

これが提示されているサンプルなのですが、間違っているのと、お試しするには冗長なので、以下を使います。

stacksets.yaml作成

stacksets.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  StackSet:
    Type: Custom::StackSet
    Properties:
      ServiceToken:
        Fn::ImportValue: StackSetCustomResource
      StackSetName: StackSetsTest
      StackSetDescription: Deploy Lambda
      TemplateURL: https://{作成したバケット名}/packaged-app.yaml
      Capabilities:
        - CAPABILITY_IAM
      AdministrationRoleARN: "arn:aws:iam::{管理アカウントのAWS ID}:role/AWSCloudFormationStackSetAdministrationRole"
      ExecutionRoleName: "AWSCloudFormationStackSetExecutionRole"
      OperationPreferences: {
        "RegionOrder": [ "ap-northeast-1" ],
        "FailureToleranceCount": 0,
        "MaxConcurrentCount": 3
      }
      Tags:
        - Environment: Testing
        - Creator: Chuck
        - DeployDate: "20190918"
      StackInstances:
        # Production
        - Regions:
            - ap-northeast-1
          Accounts:
            - "{配布先AWSID1}"
            - "{配布先AWSID2}"
            - "{配布先AWSID3}"

Cloudformation StackSetsを作る

stacksets.yamlを使って、CloudFormationを実行します。

成功すると、カスタムリソースのバックエンドのlambdaが起動し、StackSetsが作られます。
StackSetsが作られたことによって、各ターゲットのAWSアカウントにCloudFormationのスタックが作成され
今回の例だと、IAMとLambdaが作成され、Lambdaにアプリがデプロイされます。

配布先AWSアカウントやソース変更時の配布

今回の構成は、いくらソースや配布するCloudFormationのテンプレートが変わっても、CloudFormation StackSetsのテンプレートファイルが変更されない限り、最初のCloudFormationが変更されなければ、配布されません。
なので、ソースなどを修正して
aws cloudformation package --template-file app.yaml 〜
を行った後は必ず

stacksets.yaml修正

stacksets.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  StackSet:
    Type: Custom::StackSet
    Properties:
      ServiceToken:
        Fn::ImportValue: StackSetCustomResource
      StackSetName: StackSetsTest
      StackSetDescription: Deploy Lambda
      TemplateURL: https://{アップロードしたpackaged-app.yamlのURL}/packaged-app.yaml
      Capabilities:
        - CAPABILITY_IAM
      AdministrationRoleARN: "arn:aws:iam::{管理アカウントのAWS ID}:role/AWSCloudFormationStackSetAdministrationRole"
      ExecutionRoleName: "AWSCloudFormationStackSetExecutionRole"
      OperationPreferences: {
        "RegionOrder": [ "ap-northeast-1" ],
        "FailureToleranceCount": 0,
        "MaxConcurrentCount": 3
      }
      Tags:
        - Environment: Testing
        - Creator: Chuck
        - DeployDate: "20190918" ← 絶対変更する
      StackInstances:
        # Production
        - Regions:
            - ap-northeast-1
          Accounts:
            - "{配布先AWSID1}"
            - "{配布先AWSID2}"
            - "{配布先AWSID3}" ← 必要に応じて変更する

して、stacksets.yamlを元にCloudFormationを動かす必要があります。
※タグだけ変えて無理やりCloudformation StackSetsに変更を検知させています。

CodePipelineとの連携について

リポジトリの各ブランチへのプッシュに合わせて
aws cloudformation package --template-file app.yaml 〜
を行い、stacksets.yamlをCodePipelineで適宜動かすだけなので割愛します。

最終イメージ

cicd (4).png

6
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9