How deploy aws lambda function in one aws account using ecr image from another aws account through cdk typescript?
Below is our cdk code in typescript in different files.
# bin/ApisecDataScienceInfra.ts file content.
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { AppDeployPipelineStack } from "../lib/pipeline/application_deployment_pipeline_stack";
import { PipelineStack } from "../lib/pipeline/pipeline_stack";
import { accountConfig } from "../aws_accounts";
const app = new cdk.App();
const ecrRepositoryName = "apisec_data_science_repo";
const infraPipelineStack = new PipelineStack(
app,
"ApisecDataScienceInfrastructurePipelineStack",
{
env: {
account: accountConfig.stages.shared.account,
region: accountConfig.stages.shared.region,
},
ecrRepositoryName: ecrRepositoryName,
}
);
const applicationPipelineStack = new AppDeployPipelineStack(
app,
"ApisecDataSciencePipeline",
{
env: {
account: accountConfig.stages.shared.account,
region: accountConfig.stages.shared.region,
},
ecrRepositoryName: ecrRepositoryName,
codeCommitRepositoryName: "ApisecDataScienceInfra",
}
);
# aws_accounts.ts file content
export const accountConfig = {
stages: {
shared: {
account: "share-xxxxx",
region: "us-east-1",
},
dev: {
account: "dev-xxxxxx",
region: "us-east-1",
},
qa: {
account: "qa-xxxxxxx",
region: "us-east-1",
},
prod: {
account: "prod-xxxxxxx",
region: "us-east-1",
},
},
};
# lib/pipeline/pipeline_stack.ts file content
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import {
CodePipeline,
CodePipelineSource,
ShellStep,
} from "aws-cdk-lib/pipelines";
import { PipelineStage } from "./pipeline_stage";
import { Role, ServicePrincipal, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
import { accountConfig } from "../../aws_accounts";
import { Repository } from "aws-cdk-lib/aws-ecr";
export interface PipelineStackProps extends StackProps {
readonly ecrRepositoryName: string;
}
export class PipelineStack extends Stack {
constructor(scope: Construct, id: string, props?: PipelineStackProps) {
super(scope, id, props);
// Reference the GitHub token stored in Secrets Manager
const githubToken = Secret.fromSecretNameV2(
this,
"GitHubToken",
"github-token"
);
const pipelineDeploymentRole = new Role(this, "PipelineDeploymentRole", {
assumedBy: new ServicePrincipal("codepipeline.amazonaws.com"),
});
pipelineDeploymentRole.addToPolicy(
new PolicyStatement({
actions: ["sts:AssumeRole"],
resources: [
`arn:aws:iam::${accountConfig.stages.dev.account}:role/cdk-hnb659fds-*`,
`arn:aws:iam::${accountConfig.stages.qa.account}:role/cdk-hnb659fds-*`,
`arn:aws:iam::${accountConfig.stages.prod.account}:role/cdk-hnb659fds-*`,
],
})
);
const deploymentEcrRepository = Repository.fromRepositoryName(
this,
`{id}-ECRRepoName`,
props?.ecrRepositoryName!
);
const codepipeline = new CodePipeline(
this,
"ApisecDataScienceInfraPipeline",
{
pipelineName: "ApisecDataScienceInfraPipeline",
synth: new ShellStep("Synth", {
input: CodePipelineSource.gitHub(
"test/testDataScienceInfra",
"build",
{
authentication: githubToken.secretValue,
}
),
commands: ["npm ci", "npm run build", "npx cdk synth"],
}),
crossAccountKeys: true,
role: pipelineDeploymentRole,
}
);
codepipeline.addStage(
new PipelineStage(this, `${id}-devStage`, {
stageName: "dev",
env: {
account: accountConfig.stages.dev.account,
region: accountConfig.stages.dev.region,
},
ecrRepository: deploymentEcrRepository,
})
);
codepipeline.addStage(
new PipelineStage(this, `${id}-qaStage`, {
stageName: "qa",
env: {
account: accountConfig.stages.qa.account,
region: accountConfig.stages.qa.region,
},
ecrRepository: deploymentEcrRepository,
})
);
codepipeline.addStage(
new PipelineStage(this, `${id}-prodStage`, {
stageName: "prod",
env: {
account: accountConfig.stages.prod.account,
region: accountConfig.stages.prod.region,
},
ecrRepository: deploymentEcrRepository,
})
);
}
}
# lib/pipeline/pipeline_stage.ts file content
import { Construct } from "constructs";
import { Stage, StageProps } from "aws-cdk-lib";
import { ApisecDataScienceInfrastructureStack } from "../stack/apisec_data_science_infra_stack";
import { IRepository } from "aws-cdk-lib/aws-ecr";
export interface PipelineStageProps extends StageProps {
readonly ecrRepository: IRepository;
}
export class PipelineStage extends Stage {
constructor(scope: Construct, id: string, props?: PipelineStageProps) {
super(scope, id, props);
const baseInfra = new ApisecDataScienceInfrastructureStack(
this,
`ApisecDataScienceInfrastructureStack-${props?.stageName}`,
{
env: {
account: props?.env?.account!,
region: props?.env?.region!,
},
ecrRepository: props?.ecrRepository!,
serviceName: "DataScienceService",
stage: props?.stageName!,
}
);
}
}
# lib/stack/apisec_data_science_infra_stack.ts file content
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { ECSFargateService } from "../contruct/ecs_fargate_service";
import { DataScienceLambdaService } from "../contruct/data_science_lambda_service";
import { IRepository } from "aws-cdk-lib/aws-ecr";
export interface ApisecDataScienceInfrastructureProps
extends cdk.StackProps {
readonly ecrRepository: IRepository;
readonly serviceName: string;
readonly stage: string;
}
export class ApisecDataScienceInfrastructureStack extends cdk.Stack {
constructor(
scope: Construct,
id: string,
props?: ApisecDataScienceInfrastructureProps
) {
super(scope, id, props);
if (!props?.ecrRepository) {
throw new Error("ECR repository must be provided!");
}
const datasciencelambdaservice = new DataScienceLambdaService(this, "DataScienceLambdaServiceConstruct", {
ecrRepository: props.ecrRepository,
stage: props?.stage
})
}
}
# lib/contruct/data_science_lambda_service.ts file content
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as logs from "aws-cdk-lib/aws-logs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import { IRepository } from "aws-cdk-lib/aws-ecr";
export interface DataScienceLambdaServiceProps {
readonly ecrRepository: IRepository;
readonly stage: string;
}
export class DataScienceLambdaService extends Construct {
constructor(scope: Construct, id: string, props: DataScienceLambdaServiceProps) {
super(scope, id);
const azs = ["us-east-2a", "us-east-2b", "us-east-2c"];
// Import Public and Private Subnet IDs
const vpcPublicSubnetIdsRef = cdk.Token.asList(
cdk.Fn.split(",", cdk.Fn.importValue("PublicSubnetIds"))
);
const vpcPrivateSubnetIdsRef = cdk.Token.asList(
cdk.Fn.split(",", cdk.Fn.importValue("PrivateSubnetIds"))
);
const privateSubnetIds: string[] = azs.map((_, index) => cdk.Fn.select(index, vpcPrivateSubnetIdsRef));
// VPC
const vpc = ec2.Vpc.fromVpcAttributes(this, `${id}-VpcImport`, {
vpcId: cdk.Fn.importValue("VPCId"),
availabilityZones: azs,
privateSubnetIds: privateSubnetIds,
});
const lambdaBucket = new s3.Bucket(this, "LambdaS3Bucket", {
bucketName: `apisec-datascience-lambda-${props?.stage}-assets`,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// IAM Role for Lambda
const lambdaRole = new iam.Role(this, "LambdaExecutionRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
});
lambdaRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"));
// Additional permissions for CloudWatch Logs
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
resources: ["arn:aws:logs:*:*:log-group:/aws/lambda/*:*"],
})
);
lambdaBucket.grantReadWrite(lambdaRole);
// Grant Lambda Role Permissions to Pull from ECR
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:DescribeImages",
"ecr:BatchCheckLayerAvailability",
"ecr:PassRole"
],
resources: ["*"],
})
);
// Allow Lambda to Authenticate with ECR
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["ecr:GetAuthorizationToken"],
resources: ["*"],
})
);
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface"
],
resources: ["*"],
})
);
// Define Lambda Function Configurations
const lambdaConfigs = [
{
functionName: "extract_endpoints_and_params_handler",
memorySize: 4096,
cmdOverride: ["src.business_flows.handlers.extract_endpoints_and_params_handler"],
timeoutDuration: 15,
},
{
functionName: "score_two_params_handler",
memorySize: 512,
cmdOverride: ["src.business_flows.handlers.score_two_params_handler"],
timeoutDuration: 1,
},
{
functionName: "get_business_flows_handler",
memorySize: 512,
cmdOverride: ["src.business_flows.handlers.get_business_flows_handler"],
timeoutDuration: 1,
},
];
lambdaConfigs.forEach((config) => {
// Create CloudWatch Log Group for each Lambda function
const logGroup = new logs.LogGroup(this, `${config.functionName}LogGroup`, {
logGroupName: `/aws/lambda/${config.functionName}`,
retention: logs.RetentionDays.THREE_MONTHS,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Create Lambda Function
const lambdaFunction = new lambda.DockerImageFunction(this, config.functionName, {
functionName: config.functionName,
code: lambda.DockerImageCode.fromEcr(props.ecrRepository, {
tag: "latest",
cmd: config.cmdOverride,
}),
role: lambdaRole,
memorySize: config.memorySize,
environment: {
S3_BUCKET_NAME: lambdaBucket.bucketName,
LOG_GROUP_NAME: logGroup.logGroupName,
OPENAI_API_KEY: "testapikey"
},
timeout: cdk.Duration.minutes(config.timeoutDuration),
//vpc: vpc,
//vpcSubnets: {
//subnets: privateSubnetIds.map((id, index) =>
// ec2.Subnet.fromSubnetId(this, `${config.functionName}-Subnet-${index}`, id)
//),
//},
});
// Grant Lambda function write access to CloudWatch Log Group
logGroup.grantWrite(lambdaFunction);
// Export Lambda Function Name
new cdk.CfnOutput(this, `${config.functionName}Name`, {
exportName: `${props?.stage}-${config.functionName.replace(/_/g,"-")}-Name`,
value: lambdaFunction.functionName,
});
// Export Lambda Function ARN
new cdk.CfnOutput(this, `${config.functionName}Arn`, {
exportName: `${props?.stage}-${config.functionName.replace(/_/g, "-")}-Arn`,
value: lambdaFunction.functionArn,
});
});
}
}
# lib/pipeline/application_deployment_pipeline_stack.ts file contents
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as codebuild from "aws-cdk-lib/aws-codebuild";
import * as codepipeline from "aws-cdk-lib/aws-codepipeline";
import * as codepipeline_actions from "aws-cdk-lib/aws-codepipeline-actions";
import { Construct } from "constructs";
import { Repository } from "aws-cdk-lib/aws-codecommit";
import { accountConfig } from "../../aws_accounts";
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
export interface AppDeployPipelineStackProps extends cdk.StackProps {
readonly ecrRepositoryName: string;
readonly codeCommitRepositoryName: string;
}
export class AppDeployPipelineStack extends cdk.Stack {
constructor(
scope: Construct,
id: string,
props?: AppDeployPipelineStackProps
) {
super(scope, id, props);
const stages = ["dev", "qa", "prod"];
const functionNames = [
"extract_endpoints_and_params_handler",
"score_two_params_handler",
"get_business_flows_handler",
];
// ECR repository
const ecrRepository = new ecr.Repository(this, `${id}ECRRepo`, {
repositoryName: props?.ecrRepositoryName,
});
ecrRepository.addToResourcePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [
new iam.AccountPrincipal(accountConfig.stages.dev.account),
new iam.AccountPrincipal(accountConfig.stages.qa.account),
new iam.AccountPrincipal(accountConfig.stages.prod.account),
],
actions: ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability", "ecr:GetAuthorizationToken"],
})
);
// IAM Policy for Lambda update permissions
const lambdaUpdatePolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["lambda:UpdateFunctionCode", "lambda:GetFunction"],
resources: ["arn:aws:lambda:*:*:function:*"]
});
// CodeBuild project
const project = new codebuild.Project(this, `{id}CodeBuild`, {
projectName: `${this.stackName}`,
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
privileged: true,
},
environmentVariables: {
REPOSITORY_URI: {
value: `${ecrRepository.repositoryUri}`,
},
},
buildSpec: codebuild.BuildSpec.fromObject({
version: "0.2",
phases: {
install: {
"runtime-versions": {
python: "3.11",
},
},
pre_build: {
commands: [
"apt-get update",
"apt-get install uuid-runtime",
"echo Logging in to Amazon ECR",
"aws --version",
"ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -f5 -d ':')",
"aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com",
"IMAGE_TAG=$(uuidgen)",
],
},
build: {
commands: [
"echo Build started on `date`",
"echo Building Docker Image",
"docker build -t apisecdatascienceservice:latest .",
"docker tag apisecdatascienceservice:latest $REPOSITORY_URI:latest",
"docker tag apisecdatascienceservice:latest $REPOSITORY_URI:$IMAGE_TAG",
],
},
post_build: {
commands: [
"echo Build completed on `date`",
"echo Pushing the Docker images...",
"docker push $REPOSITORY_URI:latest",
"docker push $REPOSITORY_URI:$IMAGE_TAG",
"echo Writing image definitions file...",
"echo $IMAGE_TAG > imageTag.txt",
"pwd; ls -al; cat imageTag.txt",
],
},
},
artifacts: {
files: ["imageTag.txt"],
},
}),
});
project.addToRolePolicy(lambdaUpdatePolicy);
// ***pipeline actions***
const sourceOutput = new codepipeline.Artifact();
const buildOutput = new codepipeline.Artifact();
const githubToken = Secret.fromSecretNameV2(
this,
"GitHubToken",
"github-token"
);
const sourceAction = new codepipeline_actions.GitHubSourceAction({
actionName: "GitHub_Source",
owner: "test",
repo: "test-datascience",
branch: "main",
oauthToken: githubToken.secretValue,
output: sourceOutput,
});
const buildAction = new codepipeline_actions.CodeBuildAction({
actionName: "codebuild",
project: project,
input: sourceOutput,
outputs: [buildOutput],
});
const pipeline = new codepipeline.Pipeline(this, id, {
pipelineName: id,
stages: [
{
stageName: "source",
actions: [sourceAction],
},
{
stageName: "build",
actions: [buildAction],
},
],
crossAccountKeys: true,
});
// Add Lambda update deployment stage
stages.forEach((stage) => {
const DeployAction = new codepipeline_actions.CodeBuildAction({
actionName: `DeployLambda-${stage}`,
project: new codebuild.PipelineProject(this, `DeployLambdaProject-${stage}`, {
buildSpec: codebuild.BuildSpec.fromObject({
version: "0.2",
phases: {
build: {
commands: [
"IMAGE_TAG=$(cat imageTag.txt)",
...functionNames.map(
(functionName) =>
`aws lambda update-function-code --function-name ${functionName} --image-uri $REPOSITORY_URI:$IMAGE_TAG --region $AWS_DEFAULT_REGION`
),
],
},
},
}),
environmentVariables: {
REPOSITORY_URI: { value: `${ecrRepository.repositoryUri}` },
},
}),
input: buildOutput,
});
pipeline.addStage({ stageName: `Deploy-${stage}`, actions: [DeployAction] });
});
ecrRepository.grantPullPush(project.role!);
new cdk.CfnOutput(this, "image", {
value: ecrRepository.repositoryUri + ":latest",
});
}
}
Given sample code. Now while creating lambda-function with ecr image in other aws account we are getting below error messages and lambda-function creation is failing with below error message
This AWS::Lambda::Function resource is in a CREATE_FAILED state.
Resource handler returned message: "Lambda does not have permission to access the ECR image. Check the ECR permissions. (Service: Lambda, Status Code: 403, Request ID: 703ba847-ccad-46ef-8092-4331db8a74ac) (SDK Attempt Count: 1)" (RequestToken: 0d10a127-84b9-4553-8c04-ec5cc5aa09dd, HandlerErrorCode: AccessDenied)
So based on the current cdk code what changes are required to fix this issue?
It's all described here.
Basically you need to add these statements to your ECR resource policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountPermission",
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
}
},
{
"Sid": "LambdaECRImageCrossAccountRetrievalPolicy",
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Condition": {
"StringLike": {
"aws:sourceARN": "arn:aws:lambda:us-east-1:123456789012:function:*"
}
}
}
]
}
For your CDK code that probably looks something like:
ecrRepository.addToResourcePolicy(
new iam.PolicyStatement({
sid: "LambdaECRImageCrossAccountRetrievalPolicy",
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('lambda.amazonaws.com')],
actions: ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"],
conditions: {
StringLike: {
'aws:sourceArn': [
`arn:aws:lambda:eu-west-2:${accountConfig.stages.dev.account}:function:*`,
`arn:aws:lambda:eu-west-2:${accountConfig.stages.qa.account}:function:*`,
`arn:aws:lambda:eu-west-2:${accountConfig.stages.prod.account}:function:*`
]
},
},
})
);
And you should be able to remove "ecr:BatchCheckLayerAvailability" and "ecr:GetAuthorizationToken" from your cross account permissions statement - they're not needed for the Lambda service principal to pull an image.
Also note that the role for your Lambda function does not need permissions to pull the image - that's all done by the Lambda service principal. So you should review those permissions and remove the ones not needed - in particular iam:PassRole on '*'.