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

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

### やりたいことのイメージ１
![cicd (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219595/30e64ea7-7298-03fd-5211-7cf04f509007.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

### やりたいことのイメージ２
![cicd (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219595/d4e4753d-8283-d38d-b563-1f76d0ad9724.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アカウントからの参照を許可する。↓例


```json
{
    "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"を書く


### パッケージする

```sh
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テンプレートファイルを作る

https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/solutions/StackSetsResource/Templates/stack-set-template.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
      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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219595/dd9acf11-81de-61b7-840a-a7369b711ff1.png)