別のAWSアカウントにあるCodeCommit RepositoryをソースとするCodePipelineをCloudFormationで構築してみた

2020.07.07

中山です

今日はタイトルの通り、別のAWSアカウントにあるCodeCommit RepositoryをソースとするCodePipelineをCloudFormationで構築してみたので、その内容をまとめてみました。

背景

まず、構築してみた経緯を少し述べます。

マルチアカウント戦略

AWSを利用する際、どのようにAWSアカウントを利用するか事前に戦略を定めて運用を開始することが多いのではないかと思います。 個人的な印象では、環境毎(Production/Staging/Develop,etc)にAWSアカウントを作成して運用するケースが多いです。 実際、AWSからもLanding ZoneというソリューションやControl Towerというサービスといった形でAWSアカウントの分割と統制の方法が述べられています。

ここ数年は多くのサービスがOrganizationsと連携したりResource Access Manager/Transit Gateway/PrivateLink/AWS SSOなどのサービスが登場したことでアカウント分割のデメリットは緩和されています。 アカウントを分割することによるデメリットであるネットワークトポロジーの複雑さ上昇や管理系サービスの設定が煩雑になることが、以前に比べると気にならなくなってきています。 もちろん、今でもそれなりに面倒ではあるのですが。

アカウント戦略の具体的な例は以下の記事によくまとまっていると思いますので、是非ご覧ください。

AWSアカウントとVPC、分ける? 分けない?: 分割パターンのメリット・デメリット

環境ではなくサービスに紐付くソースコードリポジトリ

あらゆるリソースを環境単位できれいに分けることができればいいのですが、それが難しいものもあります。 ソースコードリポジトリ(AWSリソースとしては、CodeCommitのGitリポジトリ)はそのひとつです。 デプロイ先の環境やデプロイのためのパイプラインは環境毎に作成すればいいですが、リポジトリを環境毎に分割するのは現実的ではありません。

そうなると、この記事のタイトルのようにCodePipelineから他のAWSアカウントにあるCodeCommitのリポジトリを参照する必要性が生じます。 (そこの貴方!「GitHubとか使えばいいやろ」とか、思いましたね?私もそう思います。)

やってみた

ということで、実際にやってみました。 なお、今回は再利用性が高そうなネタだったのでCloudFormationテンプレートを作ってみました。

参考情報

以下の記事を参考にしています。 大まかな流れはこの記事とほぼ同じです。

CodePipelineでアカウントをまたいだパイプラインを作成してみる

全体像

先に、今回構築する環境の図を示します。

構成時のポイント

今回の構成で気をつけるべきポイントを先に解説します。

Source StageでAction Roleを指定

パイプライン上のSource Stageでアクションを定義する際にRoleを指定することができます。 CodeCommitのGit Repositoryは別のアカウントに存在するため、リポジトリのあるアカウントにCodeCommitを操作できるIAM Roleを作成しておく必要があります。 また、そのRoleはCodePipelineのService RoleからAssumeRoleが可能である必要もあります。

AWS::CodePipeline::Pipeline ActionDeclaration

また、Source StageのAction Roleはパイプラインで指定するArtifact用のストレージにアクセスできる必要もあります(こちらについては次のポイントで少し補足)。 Artifact用のストレージは別のアカウントに存在するため、IAM Roleだけでなくリソースポリシー(今回の場合はBucket Policy)でもアクセスを許可する必要があります。

クロスアカウントポリシー評価論理

ArtifactLocation(S3)でKMSを利用する必要がある(CodePipelineの仕様)

CodePipelineでは、ステージ間でアーティファクトをやりとりするためにS3 Bucketを利用します。 パイプラインを作成する際、アーティファクトを暗号化するためにAWSマネージドのCMK / カスタマーマネージドのCMKのいずれかを選択する必要があります。 暗号化しないという選択肢はありません。

CodePipeline でパイプラインを作成する

そこで意識する必要があることが、Source Stageで利用するRoleにCMKによる暗号化・復号の権限を付与する必要があるという点です。 RoleにもKey Policyにもアクセス許可設定を実施する必要があります。

CloudFormationテンプレートの構成

今回、CloudFormationテンプレートを3つに分けました。

  • 1.事前準備
    • CodePipeline Service Role
    • CodeDeploy Service Role
    • S3 Bucket / Bucket Policy
    • Customer Master Key
  • 2.Git Repository
    • CodeCommit Repository
    • CodePipeline (Source Stage Action Role)
  • 3.CI/CD Pipeline and Environment
    • EC2 Instance / Security Group
    • CodeDeploy Application / Deployment Group
    • CodePipeline (Pipeline)

1. 事前準備

Pipeline側のAWSアカウントにPipelineで利用するいくつかのリソースを事前に作成します。

29-33行目では、CodePipelineのService Roleに対してSource StageのAction RoleにAssumeRoleするための許可を追加しています。 なお、Action Roleの信頼ポリシーにはPipeline側のAWSアカウントを信頼する設定を別途追加します。

200-216行目では、Artifact用のS3 BucketのBucket PolicyにRepository側のAWSアカウントからアーティファクトを保存する権限を追加します。 なお、Source StageのAction Roleにもアーティファクトを保存する権限を別途追加します。

256,271行目では、アーティファクトを保存する際に暗号化するための権限をCMKのKey Policyに追加します。 なお、Source StageのAction Roleにもアーティファクトを暗号化のための権限を別途追加します。

AWSTemplateFormatVersion: 2010-09-09
Description: Step 1, Pre-requirements (in Production Account)
Parameters:
  RepositoryAccountId:
    Description: Repository Account ID
    MaxLength: 12
    MinLength: 12
    Type: String

Resources:
  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - codepipeline.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: Pipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - sts:AssumeRole
                Resource:
                  - !Sub arn:aws:iam::${RepositoryAccountId}:role/* # Cross Account Access
              - Effect: Allow
                Action: 
                  - iam:PassRole
                Resource:
                  - "*"
                Condition: 
                  StringEqualsIfExists: 
                    iam:PassedToService: 
                      - cloudformation.amazonaws.com
                      - elasticbeanstalk.amazonaws.com
                      - ec2.amazonaws.com
                      - ecs-tasks.amazonaws.com
              - Effect: Allow
                Action: 
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - elasticbeanstalk:*
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - s3:*
                  - sns:*
                  - cloudformation:*
                  - rds:*
                  - sqs:*
                  - ecs:*
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - lambda:InvokeFunction
                  - lambda:ListFunctions
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - opsworks:CreateDeployment
                  - opsworks:DescribeApps
                  - opsworks:DescribeCommands
                  - opsworks:DescribeDeployments
                  - opsworks:DescribeInstances
                  - opsworks:DescribeStacks
                  - opsworks:UpdateApp
                  - opsworks:UpdateStack
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - cloudformation:CreateStack
                  - cloudformation:DeleteStack
                  - cloudformation:DescribeStacks
                  - cloudformation:UpdateStack
                  - cloudformation:CreateChangeSet
                  - cloudformation:DeleteChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:SetStackPolicy
                  - cloudformation:ValidateTemplate
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - devicefarm:ListProjects
                  - devicefarm:ListDevicePools
                  - devicefarm:GetRun
                  - devicefarm:GetUpload
                  - devicefarm:CreateUpload
                  - devicefarm:ScheduleRun
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - servicecatalog:ListProvisioningArtifacts
                  - servicecatalog:CreateProvisioningArtifact
                  - servicecatalog:DescribeProvisioningArtifact
                  - servicecatalog:DeleteProvisioningArtifact
                  - servicecatalog:UpdateProduct
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - cloudformation:ValidateTemplate
                Resource:
                  - "*"
              - Effect: Allow
                Action: 
                  - ecr:DescribeImages
                Resource:
                  - "*"
  DeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - codedeploy.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
  S3Bucket:
    # DeletionPolicy: Retain
    Description: Creating Amazon S3 bucket for AWS CodePipeline artifacts
    Properties:
      BucketName: !Join
      - "-"
      - - artifacts
        - !Ref AWS::Region
        - !Ref AWS::AccountId
        - "pipeline"
      VersioningConfiguration:
        Status: Enabled
    Type: AWS::S3::Bucket
  S3ArtifactBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Description: Setting Amazon S3 bucket policy for AWS CodePipeline access
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
        - Action:
          - s3:PutObject
          Effect: Deny
          Principal: "*"
          Resource:
          - !Sub arn:aws:s3:::${S3Bucket}/*
          Condition: 
            StringNotEquals: 
              s3:x-amz-server-side-encryption: aws:kms
        - Action:
          - s3:*
          Effect: Deny
          Principal: "*"
          Resource:
          - !Sub arn:aws:s3:::${S3Bucket}/*
          Condition: 
            Bool: 
              aws:SecureTransport: false
        - Action:
          - s3:Get*
          - s3:Put*
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${RepositoryAccountId}:root
          Resource:
          - !Sub arn:aws:s3:::${S3Bucket}/*
        - Action:
          - s3:ListBucket
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${RepositoryAccountId}:root
          Resource:
          - !Sub arn:aws:s3:::${S3Bucket}
        Version: 2012-10-17
  Key:
    Type: AWS::KMS::Key
    Properties:
      Description: An example symmetric CMK
      KeyPolicy:
        Version: 2012-10-17
        Id: key-default-1
        Statement:
        - Sid: Enable IAM User Permissions
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${AWS::AccountId}:root
          Action: kms:*
          Resource: "*"
        - Sid: Allow administration of the key
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${AWS::AccountId}:user/cm-nakayama.nobuhiro
          Action:
          - kms:Create*
          - kms:Describe*
          - kms:Enable*
          - kms:List*
          - kms:Put*
          - kms:Update*
          - kms:Revoke*
          - kms:Disable*
          - kms:Get*
          - kms:Delete*
          - kms:ScheduleKeyDeletion
          - kms:CancelKeyDeletion
          Resource: "*"
        - Sid: Allow use of the key
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${RepositoryAccountId}:root
            - !GetAtt PipelineRole.Arn
            - !GetAtt DeployRole.Arn
          Action:
          - kms:DescribeKey
          - kms:Encrypt
          - kms:Decrypt
          - kms:ReEncrypt*
          - kms:GenerateDataKey
          - kms:GenerateDataKeyWithoutPlaintext
          Resource: "*"
        - Sid: Allow attachment of persistent resources
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${RepositoryAccountId}:root
            - !GetAtt PipelineRole.Arn
            - !GetAtt DeployRole.Arn
          Action:
          - kms:CreateGrant
          - kms:ListGrants
          - kms:RevokeGrant
          Resource: "*"
          Condition:
            Bool: 
              kms:GrantIsForAWSResource: true
  Alias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/CodePipelineArtifact
      TargetKeyId:
        Ref: Key

2. Git Repository

Repository側のAWSアカウントにCodeCommit Repositoryとそれらを操作するIAM Roleを作成します。

29-31行目では、CodePipelineのService RoleによるAssumeRoleを許可する権限を追加しています。

40-45行目では、Source StageのAction Roleに対してArtifactをS3に保存するための権限を追加しています。

46-54行目では、Source StageのAction Roleに対してArtifactを暗号化して保存するための権限を追加しています。

AWSTemplateFormatVersion: 2010-09-09
Description: Step 2, CodeCommit (in Repository Account)
Parameters:
  ProductionAccountId:
    Description: Production Account ID
    MaxLength: 12
    MinLength: 12
    Type: String
  S3BucketARN:
    Description: Production Account S3 Bucket ARN for Artifact (Created by 01-requirement.yml)
    Type: String
  CmkArn:
    Description: Production Account CMK ARN (Created by 01-requirement.yml)
    Type: String

Resources:
  Repo:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Sub SampleRepository-${AWS::AccountId}
      RepositoryDescription: "Sample Repository"
  Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
              - !Sub arn:aws:iam::${ProductionAccountId}:root
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: source
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - s3:PutObject
                  - s3:PutObjectAcl
                Resource:
                  - !Sub ${S3BucketARN}/*
              - Effect: Allow
                Action: 
                  - kms:DescribeKey
                  - kms:GenerateDataKey*
                  - kms:Encrypt
                  - kms:ReEncrypt*
                  - kms:Decrypt
                Resource:
                  - !Ref CmkArn
              - Effect: Allow
                Action: 
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:UploadArchive
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:CancelUploadArchive
                Resource:
                  - !GetAtt Repo.Arn

3. CI/CD Pipeline and Environment

Pipeline側のAWSアカウントにデプロイ先となるAWSアカウントとPipelineに関連するリソース一式(CodeDeployのリソースを含む)を作成します。

132行目では、Source StageのAction RoleとしてRepository側のAWSアカウントで作成したIAM Roleを指定しています。

AWSTemplateFormatVersion: 2010-09-09
Description: Step 3, Environment and Pipeline (in Production Account)
Parameters:
  SubnetId:
    Description: Public Subnet (Internet routable)
    Type: String
  VpcId:
    Type: String
  ArtifactBucketName:
    Description: Production Account S3 Bucket Name for Artifact (Created by 01-requirement.yml)
    Type: String
  CmkArn:
    Description: Production Account CMK ARN (Created by 01-requirement.yml)
    Type: String
  CodePipelineRoleArn:
    Description: Production Account CodePipeline Service Role ARN (Created by 01-requirement.yml)
    Type: String
  CodeDeployRoleArn:
    Description: Production Account CodeDeploy Service Role ARN (Created by 01-requirement.yml)
    Type: String
  CodeCommitRoleArn:
    Description: Repository Account CodeCommit Action Role ARN (Created by 02-codecommit.yml)
    Type: String
  RepositoryName:
    Description: Repository Account CodeCommit Repository Name (Created by 02-codecommit.yml)
    Type: String

Resources:
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        GroupDescription: Allow http to client host
        VpcId:
          Ref: VpcId
        SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
  Ec2Instance: 
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: ami-0ee1410f0644c1cac # Tokyo Region
      IamInstanceProfile: !Ref InstanceProfile
      InstanceType: t3.micro
      SecurityGroupIds: 
        - !Ref InstanceSecurityGroup
      SubnetId: !Ref SubnetId
      Tags: 
        - Key: "Name"
          Value: "CodePipelineApp"
      UserData: !Base64 |
        #!/bin/bash
        yum -y update
        yum install -y ruby
        yum install -y aws-cli
        cd /home/ec2-user
        aws s3 cp s3://aws-codedeploy-ap-northeast-1/latest/install . --region ap-northeast-1
        chmod +x ./install
        ./install auto
  InstanceProfile: 
    Type: "AWS::IAM::InstanceProfile"
    Properties: 
      Path: "/"
      Roles: 
        - 
          Ref: "Role"
  Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforAWSCodeDeploy
      Policies:
        - PolicyName: kms
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - kms:DescribeKey
                  - kms:GenerateDataKey*
                  - kms:Encrypt
                  - kms:ReEncrypt*
                  - kms:Decrypt
                Resource:
                  - !Ref CmkArn
  CodeDeployApplication:
    Type: AWS::CodeDeploy::Application
    Properties:
      ComputePlatform: Server
  DeploymentGroup: 
    Type: AWS::CodeDeploy::DeploymentGroup
    Properties: 
      ApplicationName: !Ref CodeDeployApplication
      Ec2TagFilters: 
        - Type: KEY_AND_VALUE
          Key: Name
          Value: CodePipelineApp
      ServiceRoleArn: !Ref CodeDeployRoleArn
  Pipeline: 
    Type: AWS::CodePipeline::Pipeline 
    Properties: 
      RoleArn: !Ref CodePipelineRoleArn
      Stages: 
        - 
          Name: Source
          Actions:
            - Name: Source
              InputArtifacts: []
              OutputArtifacts:
                - Name: SourceOutput
              Configuration:
                RepositoryName: !Ref RepositoryName
                BranchName: master
                PollForSourceChanges: false
              RunOrder: 1
              ActionTypeId:
                Version: 1
                Provider: CodeCommit
                Category: Source
                Owner: AWS
              RoleArn: !Ref CodeCommitRoleArn
        - 
          Name: Release 
          Actions: 
            - 
              Name: ReleaseAction
              InputArtifacts: 
                - 
                  Name: SourceOutput 
              ActionTypeId: 
                Category: Deploy 
                Owner: AWS 
                Version: 1
                Provider: CodeDeploy 
              Configuration: 
                ApplicationName: !Ref CodeDeployApplication
                DeploymentGroupName: !Ref DeploymentGroup
              RunOrder: 1 
      ArtifactStore: 
        Type: S3 
        Location: !Ref ArtifactBucketName
        EncryptionKey:
          Id: !Ref CmkArn
          Type: KMS

構築手順

こんな感じのテンプレートを使って以下の手順で構築を実施します。

  • テンプレート1でスタック作成
  • テンプレート2でスタック作成
  • サンプルアプリケーションをリポジトリにプッシュ
  • テンプレート3でスタック作成
  • パイプラインを実行(変更をリリース)

サンプルアプリケーションは、公式ドキュメントで紹介されているものを利用しました。

例 1: AWS CloudFormation を使用して AWS CodeCommit パイプラインを作成する

SampleApp_Linux.zip

また、2つ目以降のCloudFormation Stackを作成する際に指定するパラメーターには、前の手順で作成したリソース名やARNを設定します。

動作確認

パイプラインを使ってリリースします。 こんな感じになれば無事成功です。

デプロイ先にアクセスすると、以下の画面が表示されるはずです。

まとめ

今回のポイントは、クロスアカウントであるが故にプリンシパル(IAM Roleなど、操作の主体)にもリソースポリシーにもアクセスを許可する設定を実施する必要がある点ではないかと思います。 それ以外にはいくつかの仕様をおさえておけば、そんなに難しくないかと思います。

現場からは以上です。