Multi-account codepipeline

Contents

Intro

In this note I will put some light on the basics of multi-account CI/CD pipelines setup in AWS. The constraints are the following:

  • The dev teams should work independently and have the full control of their own code
  • The enterprise has the defined guardrails and policies for IAM management
  • The neterprise has the library of ready to use modules for infra deployment In this note I will only touch the aspecs of passing the artifacts and pipeline setting-up. CD tools and practices will probably come in future.

Design

The source repo for app resides in SRC Account, CI tools are deployed in CI Account, deployment targets are DEV and PROD accounts. The CodePipeline will have Source step that takes the code from SRC Account, the CI stage that will run build / test procedures and publish stage that will deploy hte artifacts to CodeArtifact repo.

sequenceDiagram
    participant CI Account
    participant Source Account
    participant DEV Account
    participant PROD Account
    CI Account ->> Source Account: Get App SRC
    Note over CI Account: Get DevOps SRC
    Note over CI Account: Build BOM 
    Note over CI Account: Run syntetic tests 
    Note over CI Account: Publish artifacts
    CI Account ->> DEV Account: Deploy DEV
    CI Account ->> DEV Account: Run Functional Tests
    Note over CI Account: Promote Artifacts
    Note over Source Account: Merge Pull Request
    CI Account ->> PROD Account: Deploy PROD
    CI Account ->> Source Account: Tag Commit

Multi account problem statement

AWS CodePipeline can doesn't have easy way to build multi-account steps. This is possible, but requires some not evident preparation steps. In general, you will need to:

  • Create S3 bucket that will be used for codepipeline artifacts storage
  • Assign proper cross-account policy to the bucket, to allow other accounts use the bucket
  • Share the KMS key to encrypt / decrypt operations during pipeline run and artifacts storing
  • Create the set of IAM roles with proper policies

General CodePipeline Flow

Codepipepline has defined permissions flow, where pipeleline and actions can assume IAM Roles.

flowchart LR
    A("`**Pipeline**`") ==> B("`**Source** Action`")
    A ==Use Pipeline Role===> C("`Build Action`")
    B ==> D("`Assumes Role 
      in **SRC** Acount
    `")
    D ==> F("`**SRC**`")
    F==> G[["`SRC to S3 Artifact`"]]
    C ==Get Artifacts===> G
    C ==Call Build Executor===>H[["`Make build`"]]

Besides of action role and pipeline role, you would probably need to define service roles for action executors (e.g.: CodeBuild). All the roles should at least have the acccess to KMS key and pipeline artifacts bucket.

CI Account

The CI account will be the heart of the whole project. The majority of components should be deployed here. From the very beginning we will need to deploy the Pipeline IAM Role. I used the one with basic codepipeline policy. In addition, we need to add permissions to AssumeRole in Source Account. We will have several Source accounts, so you will need to put wiledcard in your policy (see the example in CF template below).

The KMS key should be used by all of the accounts in setup to share the artifacts.

The full CloudFormation template to build basic components in CI account:

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  CPipelineArtifacts:
    Type: 'AWS::S3::Bucket'
    DeletionPolicy: Retain
    Properties:
      BucketName: org-name-codepipeline-artifacts
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: False
        IgnorePublicAcls: True
        RestrictPublicBuckets: True   
  CPipelineArtifactsPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref CPipelineArtifacts
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: DenyUnEncryptedObjectUploads
            Effect: Deny
            Principal: '*'
            Action: 's3:PutObject'
            Resource: !Join 
              - ''
              - - !GetAtt 
                  - CPipelineArtifacts
                  - Arn
                - /*
            Condition:
              StringNotEquals:
                's3:x-amz-server-side-encryption': 'aws:kms'
          - Sid: DenyInsecureConnections
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource: !Join 
              - ''
              - - !GetAtt 
                  - CPipelineArtifacts
                  - Arn
                - /*
            Condition:
              Bool:
                'aws:SecureTransport': false
  CPipelineKMSKey:
    Type: 'AWS::KMS::Key'
    Properties:
      Description: KMS key for Codepipeline
      EnableKeyRotation: true
      PendingWindowInDays: 14
      KeyPolicy:
        Version: 2012-10-17
        Id: cip-codepipeline-key
        Statement:
          - Sid: Allow administration of the key
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            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: '*'
            Condition:
              ArnLike:
                "aws:PrincipalArn": !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_*"
          - Sid: Lambda access to KMS Key
            Effect: Allow
            Principal:
              AWS: !GetAtt ListenerLambdaRole.Arn
            Action:
              - kms:PutKeyPolicy
              - kms:GetKeyPolicy
            Resource: '*'
  CodePipelineServiceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: org-name-CodePipeline-Role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: AssumeRole
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'sts:AssumeRole'
                Resource: 'arn:aws:iam::*:role/org-name-ChildRole'
        - PolicyName: S3Access
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:*'
                Resource: !Join 
                  - ''
                  - - !GetAtt 
                      - CPipelineArtifacts
                      - Arn
                    - /*
        - PolicyName: org-name-CodePipeline-Policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'codecommit:CancelUploadArchive'
                  - 'codecommit:GetBranch'
                  - 'codecommit:GetCommit'
                  - 'codecommit:GetUploadArchiveStatus'
                  - 'codecommit:UploadArchive'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'codedeploy:CreateDeployment'
                  - 'codedeploy:GetApplicationRevision'
                  - 'codedeploy:GetDeployment'
                  - 'codedeploy:GetDeploymentConfig'
                  - 'codedeploy:RegisterApplicationRevision'
                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:
                  - 'lambda:InvokeFunction'
                  - 'lambda:ListFunctions'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'iam:PassRole'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'elasticbeanstalk:*'
                  - 'ec2:*'
                  - 'elasticloadbalancing:*'
                  - 'autoscaling:*'
                  - 'cloudwatch:*'
                  - 's3:*'
                  - 'sns:*'
                  - 'cloudformation:*'
                  - 'rds:*'
                  - 'sqs:*'
                  - 'ecs:*'
                Resource: '*'
              - Effect: Allow
                Action:
                  - "kms:Encrypt"
                  - "kms:Decrypt"
                  - "kms:ReEncrypt*"
                  - "kms:GenerateDataKey*"
                  - "kms:DescribeKey"
                Resource: !GetAtt CPipelineKMSKey.Arn

SRC Account

In SRC account we should have the actual code repository, and IAM role, that is allowed to make repo actions and have access to S3 bucket with artifacts.

AWSTemplateFormatVersion: "2010-09-09"
Description: Base tmeplate for CI SRC account creation.
Parameters:
  paramKMSKey:
    Type: String
    Default: ${KMS_KEY_ARN}
  pRepoList:
    Type: CommaDelimitedList
Resources:
  CIPSrcRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "ChildRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: arn:aws:iam::${CI_ACCOUNT_ID}:role/CodePipeline-Role
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: "KMSKey-Usage"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "kms:Encrypt"
                  - "kms:Decrypt"
                  - "kms:ReEncrypt*"
                  - "kms:GenerateDataKey*"
                  - "kms:DescribeKey"
                Resource: !Ref paramKMSKey
        - PolicyName: "CodeCommit-Access"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "codecommit:Get*"
                  - "codecommit:List*"
                  - "codecommit:Describe*"
                  - "codecommit:UploadArchive"
                Resource: !Ref pRepoList
        - PolicyName: "Artifact-S3-Access"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "s3:*"
                Resource: 
                  - arn:aws:s3:::codepipeline-artifacts
                  - arn:aws:s3:::codepipeline-artifacts/*

Pay attention, that in pRepoList parameter the ARNs of CodeCommit repos defined. In addition to this, make sure that you have resource policies on your artifact bucket and KMS key, that allows the access from SRC account Role. I have written the custom resource for this task, that I will share later.

DEV and PROD accounts

Actually theese are the target accounts, that will have the same roles. The setup is the same as for SRC account, the only difference here – is in permissions for ChildRole.

Pipeline Setup

Once you have all IAM roles, reource policies and everything else ready – let's configure the pipeline. There is no way to configure it through UI, so you should use API. In my case, I used CloudFormation.

Below is the part of CloudFormation template that build Pipeline with steps and Roles.

AppPipeline:
    Type: 'AWS::CodePipeline::Pipeline'
    Properties:
      Name: !Ref 'AWS::StackName'
      PipelineType: V2
      RoleArn: '{{resolve:ssm:/pipeline-params/pipeline-role-arn}}' 
      ArtifactStore:
        Type: S3
        EncryptionKey: 
          Id: '{{resolve:ssm:/pipeline-params/kms-key-arn}}'
          Type: "KMS"
        Location: '{{resolve:ssm:/pipeline-params/s3-artifact-store}}'
      Stages:
        - Name: Source
          Actions:
            - Name: AppSRC
              RoleArn: !Sub arn:aws:iam::${pSRCAcc}:role/ChildRole
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              OutputArtifacts:
                - Name: SourceOutput
              Configuration:
                BranchName: !Ref pSourcebranch
                RepositoryName: !Ref pSourceRepo
                PollForSourceChanges: true
              RunOrder: 1
            - Name: DevOpsSRC
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              OutputArtifacts:
                - Name: DevOpsCode
              Configuration:
                BranchName: main
                RepositoryName: '{{resolve:ssm:/pipeline-params/devops-repo-name}}' 
                PollForSourceChanges: false
              RunOrder: 2    
        - Name: CI
          Actions:
            - Name: CI-Process
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              InputArtifacts:
                - Name: SourceOutput
                - Name: DevOpsCode
              Configuration:
                ProjectName: !Ref CICodeBuildProject
                PrimarySource: SourceOutput
              RunOrder: 1

As you can see, we have 2 source actions here:

  • AppSRC
  • DevOpsSRC

The AppSRC gets the code from the external account, using Role in child account.

Wrapping Up

To make the things simple, you will need to build the following infra:

  1. Pipeline Role in your CI account with at least theese permissions:
    • Assume Role in each of child accounts (SRC and Destination)
    • Access to KMS Key and S3 bucket
  2. Role for each of the Actions, that are running in extrenal accounts. The following permissions required:
    • Assume Role Policy allowing this role to be assumed by Pipeline Role
    • Access to KMS Key and S3 bucket in CI Account
  3. Resource policies for Arifact S3 bucket and KMS Key allowing access for each of the roles
  4. Update the pipeline to use IAM roles in child accounts