How do you use an AWS CDK codepipeline to deploy a stack to another AWS account?

0

We have a project that uses yaml templates to create a changeset and execute the changeset to deploy a frontend stack to two different AWS accounts. We're trying to create a codepipeline that builds and deploys a simple frontend module to the AWS account the codepipeline is in and a second account (test and prod accounts). The former we've accomplished, but when it comes to deploying across AWS accounts we're running into trouble with access and permission, and which roles to use where.

We have two service roles set up. The first role (TestCodepipelineBaseServiceRole) looks like this:

    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "codepipeline:StartPipelineExecution"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "iam:*Role",
                "iam:*Policy*",
                "iam:*Group",
                "iam:*InstanceProfile"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ec2:Describe*",
                "ec2:DescribeSecurityGroups",
                "ec2:*Address",
                "ec2:*SecurityGroup*",
                "ec2:*Tags*",
                "autoscaling:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "route53:GetHostedZone",
                "route53:ChangeResourceRecordSets",
                "route53:GetChange",
                "route53:ListResourceRecordSets"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codecommit:UploadArchive",
                "codecommit:CancelUploadArchive",
                "codecommit:GetBranch",
                "codecommit:GetCommit",
                "codecommit:GetUploadArchiveStatus"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudfront:CreateInvalidation",
                "cloudfront:GetInvalidation"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "kms:CreateKey",
                "kms:*Key*",
                "kms:*Alias*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "kinesis:*",
                "lambda:*",
                "sqs:*",
                "events:*",
                "cognito-idp:*",
                "dynamodb:*",
                "cloudformation:*",
                "s3:*",
                "sns:*",
                "apigateway:*",
                "elasticbeanstalk:*",
                "elasticloadbalancing:*",
                "cloudwatch:*",
                "codedeploy:*",
                "logs:*",
                "cloudfront:*",
                "glue:*",
                "ssm:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": "arn:aws:iam::{account2}:role/codepipeline-base-CodePipelineCrossAccountServiceRole",
            "Effect": "Allow"
        }
    ]
}

The second role (CodePipelineCrossAccountServiceRole) looks like this:

    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "iam:*Role",
                "iam:*Policy*",
                "iam:*Group",
                "iam:*InstanceProfile"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ec2:Describe*",
                "ec2:DescribeSecurityGroups",
                "ec2:*Address",
                "ec2:*SecurityGroup*",
                "ec2:*Tags*",
                "autoscaling:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "route53:GetHostedZone",
                "route53:ChangeResourceRecordSets",
                "route53:GetChange",
                "route53:ListResourceRecordSets"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "kms:CreateKey",
                "kms:*Key*",
                "kms:*Alias*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudfront:CreateInvalidation",
                "cloudfront:GetInvalidation"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "kinesis:*",
                "lambda:*",
                "sqs:*",
                "events:*",
                "cognito-idp:*",
                "dynamodb:*",
                "cloudformation:*",
                "s3:*",
                "sns:*",
                "apigateway:*",
                "elasticbeanstalk:*",
                "elasticloadbalancing:*",
                "cloudwatch:*",
                "logs:*",
                "cloudfront:*",
                "glue:*",
                "ecr:GetAuthorizationToken",
                "ecs:*",
                "states:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "kms:DescribeKey",
                "kms:GenerateDataKey*",
                "kms:Encrypt",
                "kms:ReEncrypt*",
                "kms:Decrypt"
            ],
            "Resource": "arn:aws:kms:{region}:{account1}:key/{UUID}",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ecr:*"
            ],
            "Resource": "arn:aws:ecr:*:{account2}:repository/myRepo/jobs",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage"
            ],
            "Resource": "arn:aws:ecr:*:{account2}:repository/amazoncorretto11",
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject*"
            ],
            "Resource": "arn:aws:s3:::codepipeline-base-{region}-{account1}-artifacts/*",
            "Effect": "Allow"
        }
    ]
}


Trust relationships:

    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{account1}:root"
            },
            "Action": "sts:AssumeRole"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudformation.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}


We also have a role we're creating in the codepipeline that we are passing to each action that are CodeBuildAction.

This is the action we're adding to deploy the frontend module:

using Amazon.CDK.AWS.CodeBuild;
using Amazon.CDK.AWS.CodePipeline;
using Amazon.CDK.AWS.CodePipeline.Actions;
using Amazon.CDK.AWS.IAM;
using Common;

namespace CdkCodepipeline.StageDeployModules.Action.DeployActionsUiMainVue3;

public abstract class ActionDeployStackUiMainVue3
{
    public static void Add(
        AwsResourceUtil awsResourceUtil,
        CdkCodepipelineStack stackCodepipeline,
        Artifact_ artifactUiMainVue3Build,
        IStage stageDeployModules,
        IRole roleCodepipeline,
        IRole roleCodeBuild,
        CdkConfig config)
    {
        var customNameDeployStackUiMainVue3 = "deploy-stack-ui-main-vue3-" + (config.IsProd ? "prod" : "test");

        // Creates an id with prod/test and branch name
        var projectDeployStackUiMainVue3Id = awsResourceUtil
            .NameBuilder("project", customNameDeployStackUiMainVue3)
            .Build();

        stageDeployModules.AddAction(new CodeBuildAction(new CodeBuildActionProps
        {
            ActionName = "DeployStackUiMainVue3Action",
            Role = roleCodeBuild,
            Input = artifactUiMainVue3Build,
            RunOrder = 1,

            Project = AwsResourceUtil.OverrideLogicalId(
                projectDeployStackUiMainVue3Id,
                new PipelineProject(
                    stackCodepipeline,
                    projectDeployStackUiMainVue3Id,
                    new PipelineProjectProps
                    {
                        ProjectName = projectDeployStackUiMainVue3Id,
                        Role = roleCodepipeline,
                        Environment = GlobalSettings.BuildEnvironment,
                        BuildSpec = BuildSpec.FromObject(new Dictionary<string, object>
                        {
                            { "version", "0.2" },
                            {
                                "phases", new Dictionary<string, object>
                                {
                                    {
                                        "install", new Dictionary<string, object>
                                        {
                                            {
                                                "runtime-versions", GlobalSettings.RuntimeVersions
                                            },
                                            {
                                                "commands", new[]
                                                {
                                                    "npm install -g aws-cdk"
                                                }
                                            }
                                        }
                                    },
                                    {
                                        "build", new Dictionary<string, object>
                                        {
                                            {
                                                "commands", new[]
                                                {
                                                    "cd src",
                                                    "cd Cdk",
                                                    $"cdk deploy stack-ui-main-vue3-{awsResourceUtil.BranchInfo.ShortName} --context branch={awsResourceUtil.BranchInfo.Name} --context accountId={config.AccountId} --context region={config.Region} --require-approval=never"
                                                }
                                            }
                                        }
                                    }
                                }
                            },
                            {
                                "artifacts", new Dictionary<string, object>
                                {
                                    { "base-directory", "./" },
                                    { "files", new[] { "**/*" } }
                                }
                            }
                        })
                    }))
        }));
    }
}

When we try to pass the test role as a property in the PipelineProject we get this error: Could not assume role in target account using current credentials (which are for account {account1}) User: arn:aws:sts::{account1}:assumed-role/codepipeline-base-CodePipelineServiceRole/AWSCodeBuild{UUID} is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::{account2}:role/cdk-hnb659fds-file-publishing-role-{account2}-{region} . Please make sure that this role exists in the account. If it doesn't exist, (re)-bootstrap the environment with the right '--trust', using the latest version of the CDK CLI.

We have tried adding the following to TestCodepipelineBaseServiceRole policy which then gives the error below:

            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
"arn:aws:iam::{account2}:role/cdk-hnb659fds-deploy-role-{account2}-{region}",
"arn:aws:iam::{account2}:role/cdk-hnb659fds-file-publishing-role-{account2}-{region}"
]
            "Effect": "Allow"
        }


Error: Could not assume role in target account using current credentials (which are for account {account1}) User: arn:aws:sts::{account1}:assumed-role/codepipeline-base-CodePipelineServiceRole/AWSCodeBuild-{UUID} is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::{account2}:role/cdk-hnb659fds-file-publishing-role-{account2}-{region}

When we try to pass the prod role as a property in the PipelineProject we get this error: Failed to call UpdateProject, reason: Invalid service role: Service role account ID does not match caller's account

2 Answers
3

Test Role Permissions: In the TestCodepipelineBaseServiceRole policy, you've added permissions for assuming roles in the second account. However, the error suggests that the CodeBuild service role in account 1 doesn't have permission to assume roles in account 2.Ensure that the trust relationship of the TestCodepipelineBaseServiceRole allows the CodeBuild service role in account 1 to assume roles in account 2. Modify the trust relationship of TestCodepipelineBaseServiceRole to include the CodeBuild service role ARN in account 1. ** Prod Role Permissions:** The error suggests that the service role you're trying to use for the prod environment does not have the correct permissions. Ensure that the service role specified in roleCodepipeline in your CodeBuildAction has the necessary permissions to perform actions in the target AWS account. ** Trust Relationships:** Verify the trust relationships of all roles involved. The trust relationship of the roles in account 2 should explicitly allow the roles from account 1 to assume them.

IAM Policy Conditions: Ensure that there are no conditions on the IAM policies that might restrict the assumption of roles across accounts, such as specifying specific IP ranges or requiring MFA. ** Error Resolution:** When you receive an error message like "Failed to call UpdateProject, reason: Invalid service role: Service role account ID does not match caller's account," it typically means that the service role specified does not exist or does not have the correct permissions. Double-check the ARN of the service role and verify its permissions.

Testing and Debugging: During debugging, you can temporarily grant broad permissions to ensure that IAM roles are not the root cause of the issue. Once the pipeline is working as expected, you can fine-tune the permissions based on the principle of least privilege.

EXPERT
answered 5 months ago
0
Accepted Answer
                "sts:AssumeRole"
            ],
            "Resource": [
"arn:aws:iam::{account2}:role/cdk-hnb659fds-deploy-role-{account2}-{region}",
"arn:aws:iam::{account2}:role/cdk-hnb659fds-file-publishing-role-{account2}-{region}"
]
            "Effect": "Allow"
        }

We ended up creating our own role and not using the service roles. We did have to add the policy statement above, but we also went into the file-publishing-role in account2 and added a trust relationship to account1 like this:

            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{account1]:root"
            },
            "Action": "sts:AssumeRole"
        }

answered 5 months ago
profile picture
EXPERT
reviewed 3 months ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions