CodePipelineによるクロスアカウントデプロイをCloudFormationで実装してみる

# 背景
業務で、CodePipelineを使い別AWSアカウントへのS3デプロイ構成を構築することがありました。
[公式の記事](https://aws.amazon.com/jp/premiumsupport/knowledge-center/codepipeline-artifacts-s3/)を参考にしようとしたのですが、こちらだと最後の手順でAWS CLIを使用する必要あり。しかし諸事情で現在CLIを使えず…。
いっそのこと、学習も兼ねてCloudFormationで作ってみました。

# 概要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577028/6d849b19-6a47-7689-cd55-76e45fd81430.png)
アカウントAでCodePipelineを構築し、アカウントBのS3バケットにデプロイします。

**ポイント**
・KMSカスタマー管理型のキーを作成し、用いることアーティファクト格納用バケットの暗号化に用いること
　（理由）CodePipelineデフォルトの暗号化であるAWS管理型のキーは、別アカウントによる使用権限を持たせることができないため。

・アカウントBでデプロイ先S3にアクセス許可する```prods3role```を作成し、アカウントAのCodePipelineでそのロールを使用する。（厳密には、CodePipelineサービスロールが```prods3role```を引き受ける）
→```prods3role```の信頼関係で、CodePipelineサービスロールを指定して引き受け許可しておく必要あり。


**前提**
以下2点はコードで書かず、すでに作成済みという前提です。
アカウントA：CodeCommitリポジトリ（今回はリポジトリ名"my-Repository"で作成）
アカウントB：デプロイ先S3バケット（今回はバケット名"crossaccount-deploy-test"で作成）

今回一部しか載せていないコードもありますが、全体は[GitHub] (https://github.com/Makoto-Taguchi/CFn-CodePipeline-CrossAccountDeploy)で公開しているのでよければご覧ください。


# コード
###1. 【アカウントA】CodePipelineのサービスロールを作成
３つに分けてコードを展開します。なぜかというと、後述2.でprods3roleのprincipalに、既に存在するリソースを指定する必要がありCodePipelineサービスロールはあらかじめ作成する必要があるためです。

```yaml:CodePipelineServiceRole.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Create CodePipeline ServiceRole
Parameters:
  NamePrefix:
    Type: String
    Default: myproject
  DeployAccountID:
    Type: String
  CrossAccountRoleName:
    Type: String
    Default: prods3role

Resources:
  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NamePrefix}-CodePipelineServiceRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
  AssumeRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub ${NamePrefix}-AssumeProdRolePolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Resource:
              - !Sub arn:aws:iam::${DeployAccountID}:role/${CrossAccountRoleName}
      Roles:
        - !Ref CodePipelineServiceRole
  CodePipelineBasePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub ${NamePrefix}-CodePipelineBasePolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - codecommit:GetRepository
              - codecommit:ListBranches
              - codecommit:GetUploadArchiveStatus
              - codecommit:UploadArchive
              - codecommit:CancelUploadArchive
              - codebuild:StartBuild
              - codebuild:StopBuild
              - codebuild:BatchGet*s
              - codebuild:Get*
              - codebuild:List*
              - codecommit:GetBranch
              - codecommit:GetCommit
              - s3:*
              - iam:PassRole
            Resource: "*"
      Roles:
        - !Ref CodePipelineServiceRole

Outputs:
  CodePipelineServiceRoleArn:
    Value: !GetAtt CodePipelineServiceRole.Arn
```
後述の2.で作成するprods3roleをパラメータ```CrossAccountRoleName```として入力。
ポイントは以下の記述です。

```yaml
  AssumeRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub ${NamePrefix}-AssumeProdRolePolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Resource:
              - !Sub arn:aws:iam::${DeployAccountID}:role/${CrossAccountRoleName}
      Roles:
        - !Ref CodePipelineServiceRole
```
ResourceにアカウントBのprods3roleを指定します。
このポリシーをCodePipelineServiceRoleにアタッチすることで、CodePipelienデプロイステージでロールが```sts:AssumeRole```によりprods3roleを引き受けるようにします。



###2. 【アカウントB】クロスアカウントデプロイさせるprods3roleを作成
CodePipelineサービスロールが引き受ける、prods3roleを作ります。このロールが持つべき権限は以下。
・アカウントBのデプロイ先バケットへの書き込み権限
・アカウントAのKMSの使用権限（アーティファクトバケット復号用）
・アカウントBのアーティファクトバケットの読み取り権限
そして、これをCodePipelineが使用できるよう、**信頼関係**を記述します。

```yaml:CrossAccountAccessRole.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Create CrossAccountRole which Deploy Stage Use
Parameters:
  DeployBucketName:
    Type: String
    Default: crossaccount-deploy-test
  PipelineAccountID:
    Type: String
  CodePipelineServiceRoleName:
    Type: String
    Default: myproject-CodePipelineServiceRole

Resources:
  CrossAccountRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: prods3role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::${PipelineAccountID}:role/${CodePipelineServiceRoleName}
            Action: sts:AssumeRole
  DeployBucketAccessPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: outputbucketfullaccess
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          # Enable DeployBucket Access in DeployAccount
          - Effect: Allow
            Action:
              - s3:*
            Resource:
              - !Sub arn:aws:s3:::${DeployBucketName}/*
          - Effect: Allow
            Action:
              - s3:ListBucket
            Resource:
              - !Sub arn:aws:s3:::${DeployBucketName}
      Roles:
        - !Ref CrossAccountRole
  KMSAccessPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub devkmss3access
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          # Enable EncryptionKey Access for ArtifactBucket in PipelineAccount
          - Effect: Allow
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource:
              - !Sub arn:aws:kms:ap-northeast-1:${PipelineAccountID}:key/*
          # Enable ArtifactBucket Access in PipelineAccount
          - Effect: Allow
            Action:
              - s3:Get*
              - s3:ListBucket
            Resource:
              - arn:aws:s3:::*
      Roles:
        - !Ref CrossAccountRole
```

Resourceで、KMSキー名とアーティファクトバケット名を指定すべきですが、後述3.でそれらは作成するので、ここでは```*```としてます。全て構築後に指定し直すと良いです。

```yaml
(抜粋)
Principal:
  AWS: !Sub arn:aws:iam::${PipelineAccountID}:role/${CodePipelineServiceRoleName}
```

CrossAccountRoleで、**Principalに1.で作成したCodePipelineサービスロール名を指定**します。これによってサービスロールがprods3roleを使用することを許可しています。

余談ですが、信頼関係の記述でPrincipalに特定アカウント中の全てのロールという意味でワイルドカード```*```で指定するのは不可みたいです。試しに作ろうとするとエラーになりました。

```yaml
Principal:
  AWS: !Sub arn:aws:iam::${PipelineAccountID}:role/*
```

また、以下のように匿名ユーザを指定すると、どのアカウントからでもロール使用できるようになってしまうのでめちゃくちゃ危険です。絶対しないようにしましょう。

```yaml
Principal: "*"
```

[参考](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_elements_principal.html)


###3. 【アカウントA】CodePipelineを構築
全部書くと長いのでCodePipelineの記述だけ抜粋します。
詳細は [GitHub](https://github.com/Makoto-Taguchi/CFn-CodePipeline-CrossAccountDeploy) をご覧ください。

```yaml:CodePipeline.yml（抜粋）
  # CodePipeline
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${NamePrefix}-Pipeline
      RoleArn: !Ref CodePipelineServiceRoleArn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
        EncryptionKey:
          Id: !GetAtt KMSKey.Arn
          Type: KMS
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepositoryName
                BranchName: !Ref CodeCommitBranchName
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceArtifact
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: BuildArtifact
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: S3
              Configuration:
                BucketName: !Ref DeployBucketName
                Extract: true
              RunOrder: 1
              InputArtifacts:
                - Name: BuildArtifact
              RoleArn: !Sub  arn:aws:iam::${DeployAccountID}:role/${CrossAccountRoleName}
```

一番のポイントは最後の行で、CodePipelineデプロイステージで使用するロールにprod3sroleを使用することを記述しています。

（ちなみにBuildSpecは今回の主題と関係ないためかなりテキトーに書いてます。要はtest.txtがアーティファクトとして出力されます。）


# 実行確認
全部CFn構築できたら、AWSコンソール上で実行確認してみます。
まずアカウントAで、CodeCommitで適当にコミットします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577028/1aaf0460-2aaa-5110-394d-31a462f7f6a8.png)

CodePipelineの画面を見ると、パイプライン起動し無事デプロイまで成功しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577028/e7c34b25-47ba-8e12-7084-515ff4dd5695.png)

アカウントBにサインインし直し、デプロイ先S3バケットを見てみます。
test.txtがデプロイされていることが確認できました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577028/c3ecb34a-efd6-09e0-aaaa-f03be9dbd839.png)



# まとめ
AWS CLIを使えないためCloudFormationで構築してみました。CLIの代わりにAWS CloudShellで同じことができ、CFnなくても実現可能なことに構築後に気づきましたが。。
まあ、CFnで全体見ながら構築することでIAMの構造やKMSの使い方も理解できたので、良い機会になりました！