Give permission to invoke lambda from ECS by cdk - amazon-web-services

When Invoking lambda from ECS,Permission error comes.
botocore.exceptions.ClientError: An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:sts::678100228XXX:assumed-role/vw-dev-fargate-stack-TaskDefAdminTaskRoleA25A3679-1K9EPRKUW9TNV/21bdeb6c10b14db4b1515986d946959a is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-northeast-1:678100228XXX:function:vw-dev-lambda because no identity-based policy allows the lambda:InvokeFunction action
So, I want to add the permission to ECS for accessing lambda.
I set ecs in ecs.ts and lambda in lambda.ts
My current idea is to give the permission to ecs in lambda.ts
in my ecs.ts
const ecsAdminService = new ecs.FargateService(this, "AdminService", {
cluster,
taskDefinition:taskDefinitionAdmin,
desiredCount: 2,
vpcSubnets: {subnetType: ec2.SubnetType.PUBLIC },
assignPublicIp: true,
securityGroups:[adminServiceSg],
enableExecuteCommand:true,
serviceName: "sw-ecs-my-dx-tokyo-jxc-91"
});
in my lambda.ts
const myLambda = new lambda.DockerImageFunction(this, "myLambda", {
functionName: `vw-${targetEnv}-lambda`,
vpc:vpc,
vpcSubnets: {subnetType: ec2.SubnetType.PRIVATE_WITH_NAT },
timeout: cdk.Duration.minutes(1),
code: lambda.DockerImageCode.fromEcr(myEcrRepo),
environment:{
}
});
# I am making here below.
const ecs = "somehow get the ecs here"
myLambda.grantInvoke(ecs) # Something like this.
Am I correct??
I am stuck with two problems.
How can I get the ecs defined in another file?
How can I give the ecs permission to invoke?
Or ,am I basically wrong?
Any help appreciated. thank you very much.

This is easily done by passing variables between stacks
For example in some-app
// bin/some-app.ts
import * as cdk from 'aws-cdk-lib';
import { SomeEcsStack } from '../lib/ecs';
import { SomeLambdaStack} from '../lib/lambda'
const app = new cdk.App();
const lmb = new SomeLambdaStack(app, 'SomeLambdaStack');
new SomeEcsStack(app, 'SomeEcsStack', {
lambdaFunc: lmb.lambdaFunc
});
Make your lambda function public
// lib/lambda.ts
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class SomeLambdaStack extends Stack {
public readonly lambdaFunc: lambda.Function; // <-- making it available
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const myLambda = new lambda.DockerImageFunction(this, "myLambda", {
functionName: `vw-${targetEnv}-lambda`,
vpc:vpc,
vpcSubnets: {subnetType: ec2.SubnetType.PRIVATE_WITH_NAT },
timeout: cdk.Duration.minutes(1),
code: lambda.DockerImageCode.fromEcr(myEcrRepo),
});
this.lambdaFunc = myLambda; // <-- making it available
}
Grant the ecs task definition role permissions to invoke
// lib/ecs.ts
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface SomeEcsStackProps extends StackProps {
readonly lambdaFunc: lambda.Function; // <-- expect lambda to be passed
}
export class SomeEcsStack extends Stack {
constructor(scope: Construct, id: string, props?: SomeEcsStackProps) {
super(scope, id, props);
const ecsAdminService = new ecs.FargateService(this, "AdminService", {
cluster,
taskDefinition:taskDefinitionAdmin,
desiredCount: 2,
vpcSubnets: {subnetType: ec2.SubnetType.PUBLIC },
assignPublicIp: true,
securityGroups:[adminServiceSg],
enableExecuteCommand:true,
serviceName: "sw-ecs-my-dx-tokyo-jxc-91"
});
props.lambdaFunc.grantInvoke(taskDefinitionAdmin.taskRole) // <-- Grant permission to task role
}

Related

CDKpipeline - Cannot set lambda layer in a stack called from multiple stages in a pipeline

I want to set multiple stages with the same stack in a cdk pipeline. But I am getting the following error when bootstrapping my cdk project
C:\dev\aws-cdk\node_modules\aws-cdk-lib\aws-lambda\lib\code.ts:185
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
^
Error: Asset is already associated with another stack 'msm-customer'. Create a new Code instance for every stack.
at AssetCode.bind (C:\dev\aws-cdk\node_modules\aws-cdk-lib\aws-lambda\lib\code.ts:185:13)
at new LayerVersion (C:\dev\aws-cdk\node_modules\aws-cdk-lib\aws-lambda\lib\layers.ts:124:29)
at new CustomerStack (C:\dev\aws-cdk\lib\CustomerStack.ts:22:17)
After debugging the code I found out that it is the layer declaration in the "CustomerStack" that is causing the issue. If I comment the layer section or if I keep only one stage in my pipeline then the bootstrap cmd works successfully. .
Pipelinestack.ts
// Creates a CodeCommit repository called 'CodeRepo'
const repo = new codecommit.Repository(this, 'CodeRepo', {
repositoryName: "CodeRepo"
});
const pipeline = new CodePipeline(this, 'Pipeline-dev', {
pipelineName: 'Pipeline-dev',
synth: new CodeBuildStep('SynthStep-dev', {
//role: role,
input: CodePipelineSource.codeCommit(repo, 'master'),
installCommands: [
'npm install -g aws-cdk'
],
commands: [
'npm ci',
'npm run build',
'npx cdk synth'
],
})
});
pipeline.addStage(new PipelineStage(this, 'dev'));
pipeline.addStage(new PipelineStage(this, 'uat'));
pipeline.addStage(new PipelineStage(this, 'prod'));
PipelineStage.ts
export class PipelineStage extends Stage {
constructor(scope: Construct, id: string, props?: StageProps) {
super(scope, id, props);
new CustomerStack(this, 'msm-customer-' + id, {
stackName: 'msm-customer'
env: {
account: process.env.ACCOUNT,
region: process.env.REGION,
},
});
}
}
CustomerStack.ts
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import 'dotenv/config';
export class CustomerStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
//define existing role
const role = iam.Role.fromRoleArn(this, 'Role',
`arn:aws:iam::${Stack.of(this).account}:role/` + process.env.IAM_ROLE,
{ mutable: false },
);
//define layer
const layer = new lambda.LayerVersion(this, 'msm-layer', {
code: lambda.Code.fromAsset('resources/layer/customer'),
description: 'Frontend common resources',
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
removalPolicy: cdk.RemovalPolicy.DESTROY
});
const lambdaDefault = new lambda.Function(this, 'default', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('resources/lambda/customer/default'),
handler: 'index.handler',
role: role,
timeout: Duration.seconds(20),
memorySize: 256,
layers: [layer],
allowPublicSubnet: true,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }
});
//rest of the code
}
}

How to deploy application stack for a cross accounts pipeline

I'm setting up a CI pipeline for AWS Rds only. The pipeline is going to deploy RDS stack across Alpha/Gamma/Prod AWS account in order.
I have an application stack defined as:
export class ApplicationStack extends CDK.Stack {
constructor(scope: Construct, id: string, props: ApplicationStackProps) {
super(scope, id, props);
const coreVpc: Ec2.IVpc = Ec2.Vpc.fromLookup(this, "CoreVpc", {
vpcName: "CoreVpc",
});
const rdsStack = new RdsStack(scope, `eCommerceDatabaseRdsStack-${props.stageName}`, {
vpc: coreVpc,
description: `The stack defines eCommerceDatabase at ${props.stageName}.`,
});
}
}
Now I'm trying to deploy above application stack to a pipeline that's across multiple AWS accounts.
Here is how I tried to create the pipeline:
/**
* This is where we define the whole pipeline.
*/
export class PipelineStack extends Cdk.Stack {
constructor(scope: Cdk.App, id: string, props: PipelineStackProps) {
super(scope, id, props);
// Initialize the pipeline
const pipeline = new codepipeline.Pipeline(this, "Pipeline", {
pipelineName: "eCommerceDatabasePipeline",
restartExecutionOnUpdate: true,
});
// Add stages to this pipeline.
pipelineStageInfoList.forEach((pipelineStage: PipelineStageInfo) => {
const applicationStack = new ApplicationStack(
this,
`eCommerceDatabaseStack-${pipelineStage.stageName}`,
{
stageName: pipelineStage.stageName,
}
);
const stage: Cdk.aws_codepipeline.IStage = pipeline.addStage({
stageName: pipelineStage.stageName,
});
// This is where I'm having trouble.
//It complains that applicationStack.templateFile is just a string,
//not assignable to ArtifactPath type.
stage.addAction(
new codepipeline_actions.CloudFormationCreateUpdateStackAction({
actionName: `eCommerceDatabase-${pipelineStage.stageName}-Deploy`,
templatePath: applicationStack.templateFile,
stackName: `eCommerceDatabase-${pipelineStage.stageName}`,
region: pipelineStage.awsRegion,
adminPermissions: true,
})
);
});
}
}
As commented above, I don't know how to pass the application cloudformation stack to the pipeline stage. I don't know whether I'm using the right approach either.
I got this work. So share my pipeline stack here. I referred https://cdkworkshop.com/20-typescript/70-advanced-topics/200-pipelines/1000-setting-up.html to setup the CDK package. My RDS clusters are in the private subnets of VPC(which is controlled by a different CDK package https://github.com/yangliunewyork/AwsStandardVpcTemplate).
import * as Cdk from "aws-cdk-lib";
import * as pipelines from "aws-cdk-lib/pipelines";
import {
PipelineStageInfo,
pipelineStageInfoList,
} from "../config/pipeline-config";
import { PipelineStage } from "./pipeline-stage";
export interface PipelineStackProps extends Cdk.StackProps {
readonly pipelineName: string;
readonly description: string;
}
export class PipelineStack extends Cdk.Stack {
constructor(scope: Cdk.App, id: string, props: PipelineStackProps) {
super(scope, id, props);
// Initialize the pipeline
const pipeline = new pipelines.CodePipeline(this, "Pipeline", {
pipelineName: "rdsDatabasePipeline",
// Create KMS keys for the artifact buckets,
// allowing cross-account deployments
crossAccountKeys: true,
// allow the pipeline to reconfigure itself when assets or stages
// are being added to it
selfMutation: true,
// synth is expected to produce the CDK Cloud Assembly as its output
synth: new pipelines.ShellStep("Synth", {
input: pipelines.CodePipelineSource.gitHub(
"yang/RdsDatabaseCdk",
"main",
{
authentication: Cdk.SecretValue.secretsManager('github-token')
}
),
// Install dependencies, build and run cdk synth
commands: [
'npm ci',
'npm run build',
'npx cdk synth'
],
}),
});
// Add stages to this pipeline.
pipelineStageInfoList.forEach((pipelineStage: PipelineStageInfo) => {
pipeline.addStage(
new PipelineStage(this, pipelineStage.stageName, {
stageName: pipelineStage.stageName,
pipelineName: props.pipelineName,
env: {
account: pipelineStage.awsAccount,
region: pipelineStage.awsRegion,
},
})
);
});
}
}

Remove unnecessary AWS resources, VPC + NAT gateway

I recently set up an application on AWS via CDK. The application consists of a Dockerized nodejs application, which connects to an RDS instance, and has a Redis caching layer as well. After having the application deployed for a few days, the costs are much higher than I had anticipated, even with minimal traffic. After looking through the cost explorer, it looks like half of the cost is coming from the NAT gateways.
In my current setup, I have created two VPCs. One is used for the application stack, and the other is for the CodePipeline. I needed to add one for the pipeline because without it I was hitting rate limits when trying to pull Docker images during the CodeBuildAction steps.
I'm not very comfortable with the networking bits, but I feel like there are extra resources involved. The pipeline VPC has three NAT gateways and three EIPs. These end up just sitting there waiting for the next deployment, which seems like a huge waste. It seems like a new gateway + EIP is allocated for each construct the VPC is attached to in CDK. Can I just make it reuse the same one? Is there an alternative to adding a VPC at all and not getting rate limited by Docker?
I also find it very surprising (I might just be naive) that the NAT gateway is so far equally as expensive as my current Fargate task costs. Is there an alternative that would serve my purposes, but come at a little lower cost?
Anyways, here are my two stacks:
// pipeline-stack.ts
import { SecretValue, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Artifact, IStage, Pipeline } from "aws-cdk-lib/aws-codepipeline";
import {
CloudFormationCreateUpdateStackAction,
CodeBuildAction,
CodeBuildActionType,
GitHubSourceAction,
} from "aws-cdk-lib/aws-codepipeline-actions";
import {
BuildEnvironmentVariableType,
BuildSpec,
LinuxBuildImage,
PipelineProject,
} from "aws-cdk-lib/aws-codebuild";
import { SnsTopic } from "aws-cdk-lib/aws-events-targets";
import { Topic } from "aws-cdk-lib/aws-sns";
import { EventField, RuleTargetInput } from "aws-cdk-lib/aws-events";
import { EmailSubscription, SmsSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
import ApiStack from "./stacks/api-stack";
import { ManagedPolicy, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";
import { Secret } from "aws-cdk-lib/aws-ecs";
import { BuildEnvironmentVariable } from "aws-cdk-lib/aws-codebuild/lib/project";
import * as SecretsManager from "aws-cdk-lib/aws-secretsmanager";
import { getApplicationEnvironment, getApplicationSecrets } from "./secrets-helper";
const capFirst = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
interface PipelineStackProps extends StackProps {
environment: string;
emailAddress: string;
phoneNumber: string;
branch: string;
secrets: {
arn: string;
};
repo: {
uri: string;
name: string;
};
}
export class PipelineStack extends Stack {
private readonly envName: string;
private readonly pipeline: Pipeline;
// source outputs
private cdkSourceOutput: Artifact;
private applicationSourceOutput: Artifact;
// code source actions
private cdkSourceAction: GitHubSourceAction;
private applicationSourceAction: GitHubSourceAction;
// build outputs
private cdkBuildOutput: Artifact;
private applicationBuildOutput: Artifact;
// notifications
private pipelineNotificationsTopic: Topic;
private readonly codeBuildVpc: Vpc;
private readonly codeBuildSecurityGroup: SecurityGroup;
private readonly secrets: SecretsManager.ISecret;
private readonly ecrCodeBuildRole: Role;
// stages
private sourceStage: IStage;
private selfMutateStage: IStage;
private buildStage: IStage;
private apiTestsStage: IStage;
constructor(scope: Construct, id: string, props: PipelineStackProps) {
super(scope, id, props);
this.envName = props.environment;
this.addNotifications(props);
this.ecrCodeBuildRole = new Role(this, "application-build-project-role", {
assumedBy: new ServicePrincipal("codebuild.amazonaws.com"),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ContainerRegistryPowerUser"),
],
});
this.codeBuildVpc = new Vpc(this, "codebuild-vpc", {
vpcName: "codebuild-vpc",
enableDnsSupport: true,
});
this.codeBuildSecurityGroup = new SecurityGroup(this, "codebuild-vpc-security-group", {
vpc: this.codeBuildVpc,
allowAllOutbound: true,
});
this.secrets = SecretsManager.Secret.fromSecretCompleteArn(this, "secrets", props.secrets.arn);
this.pipeline = new Pipeline(this, "pipeline", {
pipelineName: `${capFirst(this.envName)}Pipeline`,
crossAccountKeys: false,
restartExecutionOnUpdate: true,
});
// STAGE 1 - Source Stage
this.addSourceStage(props);
// STAGE 2 - Build Stage
this.addBuildStage(props);
// STAGE 3: SelfMutate Stage
this.addSelfMutateStage();
// STAGE 4: Testing
this.addTestStage();
}
addNotifications(props: PipelineStackProps) {
this.pipelineNotificationsTopic = new Topic(this, "pipeline-notifications-topic", {
topicName: `PipelineNotifications${capFirst(props.environment)}`,
});
this.pipelineNotificationsTopic.addSubscription(new EmailSubscription(props.emailAddress));
this.pipelineNotificationsTopic.addSubscription(new SmsSubscription(props.phoneNumber));
}
/**
* Stage 1
*/
addSourceStage(props: PipelineStackProps) {
this.cdkSourceOutput = new Artifact("cdk-source-output");
this.cdkSourceAction = new GitHubSourceAction({
actionName: "CdkSource",
owner: "my-org",
repo: "my-cdk-repo",
branch: "main",
oauthToken: SecretValue.secretsManager("/connections/github/access-token"),
output: this.cdkSourceOutput,
});
this.applicationSourceOutput = new Artifact("ApplicationSourceOutput");
this.applicationSourceAction = new GitHubSourceAction({
actionName: "ApplicationSource",
owner: "my-org",
repo: "my-application-repo",
branch: props.branch,
oauthToken: SecretValue.secretsManager("/connections/github/access-token"),
output: this.applicationSourceOutput,
});
this.sourceStage = this.pipeline.addStage({
stageName: "Source",
actions: [this.cdkSourceAction, this.applicationSourceAction],
});
}
/**
* stage 2
*/
addBuildStage(props: PipelineStackProps) {
const cdkBuildAction = this.createCdkBuildAction();
const applicationBuildAction = this.createApplicationBuildAction(props);
this.buildStage = this.pipeline.addStage({
stageName: "Build",
actions: [cdkBuildAction, applicationBuildAction],
});
}
/**
* stage 3
*/
addSelfMutateStage() {
this.selfMutateStage = this.pipeline.addStage({
stageName: "PipelineUpdate",
actions: [
new CloudFormationCreateUpdateStackAction({
actionName: "PipelineCreateUpdateStackAction",
stackName: this.stackName,
templatePath: this.cdkBuildOutput.atPath(`${this.stackName}.template.json`),
adminPermissions: true,
}),
],
});
}
/**
* stage 4
*/
addTestStage() {
const testAction = new CodeBuildAction({
actionName: "RunApiTests",
type: CodeBuildActionType.TEST,
input: this.applicationSourceOutput,
project: new PipelineProject(this, "api-tests-project", {
vpc: this.codeBuildVpc,
securityGroups: [this.codeBuildSecurityGroup],
environment: {
buildImage: LinuxBuildImage.STANDARD_5_0,
privileged: true,
},
buildSpec: BuildSpec.fromObject({
version: "0.2",
phases: {
install: {
commands: ["cp .env.testing .env"],
},
build: {
commands: [
"ls",
"docker-compose -f docker-compose.staging.yml run -e NODE_ENV=testing --rm api node ace test",
],
},
},
}),
}),
runOrder: 1,
});
this.apiTestsStage = this.pipeline.addStage({
stageName: "RunApiTests",
actions: [testAction],
});
}
createCdkBuildAction() {
this.cdkBuildOutput = new Artifact("CdkBuildOutput");
return new CodeBuildAction({
actionName: "CdkBuildAction",
input: this.cdkSourceOutput,
outputs: [this.cdkBuildOutput],
project: new PipelineProject(this, "cdk-build-project", {
environment: {
buildImage: LinuxBuildImage.STANDARD_5_0,
},
buildSpec: BuildSpec.fromSourceFilename("build-specs/cdk-build-spec.yml"),
}),
});
}
createApplicationBuildAction(props: PipelineStackProps) {
this.applicationBuildOutput = new Artifact("ApplicationBuildOutput");
const project = new PipelineProject(this, "application-build-project", {
vpc: this.codeBuildVpc,
securityGroups: [this.codeBuildSecurityGroup],
environment: {
buildImage: LinuxBuildImage.STANDARD_5_0,
privileged: true,
},
environmentVariables: {
ENV: {
value: this.envName,
},
ECR_REPO_URI: {
value: props.repo.uri,
},
ECR_REPO_NAME: {
value: props.repo.name,
},
AWS_REGION: {
value: props.env!.region,
},
},
buildSpec: BuildSpec.fromObject({
version: "0.2",
phases: {
pre_build: {
commands: [
"echo 'Logging into Amazon ECR...'",
"aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REPO_URI",
'COMMIT_HASH=$(echo "$CODEBUILD_RESOLVED_SOURCE_VERSION" | head -c 8)',
],
},
build: {
commands: ["docker build -t $ECR_REPO_NAME:latest ."],
},
post_build: {
commands: [
"docker tag $ECR_REPO_NAME:latest $ECR_REPO_URI/$ECR_REPO_NAME:latest",
"docker tag $ECR_REPO_NAME:latest $ECR_REPO_URI/$ECR_REPO_NAME:$ENV-$COMMIT_HASH",
"docker push $ECR_REPO_URI/$ECR_REPO_NAME:latest",
"docker push $ECR_REPO_URI/$ECR_REPO_NAME:$ENV-$COMMIT_HASH",
],
},
},
}),
role: this.ecrCodeBuildRole,
});
return new CodeBuildAction({
actionName: "ApplicationBuildAction",
input: this.applicationSourceOutput,
outputs: [this.applicationBuildOutput],
project: project,
});
}
public addDatabaseMigrationStage(apiStack: ApiStack, stageName: string): IStage {
let buildEnv: { [name: string]: BuildEnvironmentVariable } = {
ENV: {
value: this.envName,
},
ECR_REPO_URI: {
type: BuildEnvironmentVariableType.PLAINTEXT,
value: apiStack.repoUri,
},
ECR_REPO_NAME: {
type: BuildEnvironmentVariableType.PLAINTEXT,
value: apiStack.repoName,
},
AWS_REGION: {
type: BuildEnvironmentVariableType.PLAINTEXT,
value: this.region,
},
};
buildEnv = this.getBuildEnvAppSecrets(getApplicationSecrets(this.secrets), buildEnv);
buildEnv = this.getBuildEnvAppEnvVars(
getApplicationEnvironment({
REDIS_HOST: apiStack.redisHost.importValue,
REDIS_PORT: apiStack.redisPort.importValue,
}),
buildEnv,
);
let envVarNames = Object.keys(buildEnv);
const envFileCommand = `printenv | grep '${envVarNames.join("\\|")}' >> .env`;
return this.pipeline.addStage({
stageName: stageName,
actions: [
new CodeBuildAction({
actionName: "DatabaseMigrations",
input: this.applicationSourceOutput,
project: new PipelineProject(this, "database-migrations-project", {
description: "Run database migrations against RDS database",
environment: {
buildImage: LinuxBuildImage.STANDARD_5_0,
privileged: true,
},
environmentVariables: buildEnv,
buildSpec: BuildSpec.fromObject({
version: "0.2",
phases: {
pre_build: {
commands: [
"echo 'Logging into Amazon ECR...'",
"aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REPO_URI",
'COMMIT_HASH=$(echo "$CODEBUILD_RESOLVED_SOURCE_VERSION" | head -c 8)',
envFileCommand,
"cat .env",
],
},
build: {
commands: [
`docker run --env-file .env --name api $ECR_REPO_URI/$ECR_REPO_NAME:$ENV-$COMMIT_HASH node ace migration:run --force`,
": > .env",
],
},
},
}),
role: this.ecrCodeBuildRole,
}),
}),
],
});
}
private getBuildEnvAppSecrets(
secrets: { [key: string]: Secret },
buildEnv: { [name: string]: BuildEnvironmentVariable },
): { [name: string]: BuildEnvironmentVariable } {
for (let key in secrets) {
buildEnv[key] = {
type: BuildEnvironmentVariableType.SECRETS_MANAGER,
value: `${this.secrets.secretArn}:${key}`,
};
}
return buildEnv;
}
private getBuildEnvAppEnvVars(
vars: { [key: string]: string },
buildEnv: { [name: string]: BuildEnvironmentVariable },
): { [name: string]: BuildEnvironmentVariable } {
for (let key in vars) {
buildEnv[key] = {
value: vars[key],
};
}
return buildEnv;
}
public addApplicationStage(apiStack: ApiStack, stageName: string): IStage {
return this.pipeline.addStage({
stageName: stageName,
actions: [
new CloudFormationCreateUpdateStackAction({
actionName: "ApplicationUpdate",
stackName: apiStack.stackName,
templatePath: this.cdkBuildOutput.atPath(`${apiStack.stackName}.template.json`),
adminPermissions: true,
}),
],
});
}
}
// api-stack.ts
import { CfnOutput, CfnResource, Lazy, Stack, StackProps } from "aws-cdk-lib";
import * as EC2 from "aws-cdk-lib/aws-ec2";
import { ISubnet } from "aws-cdk-lib/aws-ec2";
import * as ECS from "aws-cdk-lib/aws-ecs";
import { DeploymentControllerType, ScalableTaskCount } from "aws-cdk-lib/aws-ecs";
import * as EcsPatterns from "aws-cdk-lib/aws-ecs-patterns";
import * as RDS from "aws-cdk-lib/aws-rds";
import { Credentials } from "aws-cdk-lib/aws-rds";
import * as Route53 from "aws-cdk-lib/aws-route53";
import * as Route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as ECR from "aws-cdk-lib/aws-ecr";
import * as CertificateManager from "aws-cdk-lib/aws-certificatemanager";
import * as SecretsManager from "aws-cdk-lib/aws-secretsmanager";
import * as ElasticCache from "aws-cdk-lib/aws-elasticache";
import { Construct } from "constructs";
import { getApplicationEnvironment, getApplicationSecrets } from "../secrets-helper";
export type ApiStackProps = StackProps & {
environment: string;
hostedZone: {
id: string;
name: string;
};
domainName: string;
scaling: {
desiredCount: number;
maxCount: number;
cpuPercentage: number;
memoryPercentage: number;
};
repository: {
uri: string;
arn: string;
name: string;
};
secrets: { arn: string };
};
export default class ApiStack extends Stack {
vpc: EC2.Vpc;
cluster: ECS.Cluster;
ecsService: EcsPatterns.ApplicationLoadBalancedFargateService;
certificate: CertificateManager.ICertificate;
repository: ECR.IRepository;
database: RDS.IDatabaseInstance;
databaseCredentials: Credentials;
hostedZone: Route53.IHostedZone;
aliasRecord: Route53.ARecord;
redis: ElasticCache.CfnReplicationGroup;
repoUri: string;
repoName: string;
applicationEnvVariables: {
[key: string]: string;
};
redisHost: CfnOutput;
redisPort: CfnOutput;
gatewayUrl: CfnOutput;
constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
this.repoUri = props.repository.uri;
this.repoName = props.repository.name;
this.setUpVpc(props);
this.setUpRedisCluster(props);
this.setUpDatabase(props);
this.setUpCluster(props);
this.setUpHostedZone(props);
this.setUpCertificate(props);
this.setUpRepository(props);
this.setUpEcsService(props);
this.setUpAliasRecord(props);
}
private resourceName(props: ApiStackProps, resourceType: string): string {
return `twibs-api-${resourceType}-${props.environment}`;
}
private setUpVpc(props: ApiStackProps) {
this.vpc = new EC2.Vpc(this, this.resourceName(props, "vpc"), {
maxAzs: 3, // Default is all AZs in region
});
}
private setUpRedisCluster(props: ApiStackProps) {
const subnetGroup = new ElasticCache.CfnSubnetGroup(this, "cache-subnet-group", {
cacheSubnetGroupName: "redis-cache-subnet-group",
subnetIds: this.vpc.privateSubnets.map((subnet: ISubnet) => subnet.subnetId),
description: "Subnet group for Redis Cache cluster",
});
const securityGroup = new EC2.SecurityGroup(this, "redis-security-group", {
vpc: this.vpc,
description: `SecurityGroup associated with RedisDB Cluster - ${props.environment}`,
allowAllOutbound: false,
});
securityGroup.addIngressRule(
EC2.Peer.ipv4(this.vpc.vpcCidrBlock),
EC2.Port.tcp(6379),
"Allow from VPC on port 6379",
);
this.redis = new ElasticCache.CfnReplicationGroup(this, "redis", {
numNodeGroups: 1,
cacheNodeType: "cache.t2.small",
engine: "redis",
multiAzEnabled: false,
autoMinorVersionUpgrade: false,
cacheParameterGroupName: "default.redis6.x.cluster.on",
engineVersion: "6.x",
cacheSubnetGroupName: subnetGroup.ref,
securityGroupIds: [securityGroup.securityGroupId],
replicationGroupDescription: "RedisDB setup by CDK",
replicasPerNodeGroup: 0,
port: 6379,
});
}
private setUpDatabase(props: ApiStackProps) {
if (["production", "staging", "develop"].includes(props.environment)) {
return;
}
this.databaseCredentials = Credentials.fromUsername("my_db_username");
this.database = new RDS.DatabaseInstance(this, "database", {
vpc: this.vpc,
engine: RDS.DatabaseInstanceEngine.postgres({
version: RDS.PostgresEngineVersion.VER_13_4,
}),
credentials: this.databaseCredentials,
databaseName: `my_app_${props.environment}`,
deletionProtection: true,
});
}
private setUpCluster(props: ApiStackProps) {
this.cluster = new ECS.Cluster(this, this.resourceName(props, "cluster"), {
vpc: this.vpc,
capacity: {
instanceType: EC2.InstanceType.of(EC2.InstanceClass.T3, EC2.InstanceSize.`SMALL`),
},
});
}
private setUpHostedZone(props: ApiStackProps) {
this.hostedZone = Route53.HostedZone.fromHostedZoneAttributes(
this,
this.resourceName(props, "hosted-zone"),
{
hostedZoneId: props.hostedZone.id,
zoneName: props.hostedZone.name,
},
);
}
private setUpCertificate(props: ApiStackProps) {
this.certificate = new CertificateManager.Certificate(this, "certificate", {
domainName: props.domainName,
validation: CertificateManager.CertificateValidation.fromDns(this.hostedZone),
});
}
private setUpRepository(props: ApiStackProps) {
this.repository = ECR.Repository.fromRepositoryAttributes(
this,
this.resourceName(props, "repository"),
{
repositoryArn: props.repository.arn,
repositoryName: props.repository.name,
},
);
}
private setUpEcsService(props: ApiStackProps) {
const secrets = SecretsManager.Secret.fromSecretCompleteArn(this, "secrets", props.secrets.arn);
this.redisHost = new CfnOutput(this, "redis-host-output", {
value: this.redis.attrConfigurationEndPointAddress,
exportName: "redis-host-output",
});
this.redisPort = new CfnOutput(this, "redis-port-output", {
value: this.redis.attrConfigurationEndPointPort,
exportName: "redis-port-output",
});
// Create a load-balanced ecs-service service and make it public
this.ecsService = new EcsPatterns.ApplicationLoadBalancedFargateService(
this,
this.resourceName(props, "ecs-service"),
{
serviceName: `${props.environment}-api-service`,
cluster: this.cluster, // Required
cpu: 256, // Default is 256
desiredCount: props.scaling.desiredCount, // Default is 1
taskImageOptions: {
image: ECS.ContainerImage.fromEcrRepository(this.repository),
environment: getApplicationEnvironment({
REDIS_HOST: this.redis.attrConfigurationEndPointAddress,
REDIS_PORT: this.redis.attrConfigurationEndPointPort,
}),
secrets: getApplicationSecrets(secrets),
},
memoryLimitMiB: 512, // Default is 512
publicLoadBalancer: true, // Default is false
domainZone: this.hostedZone,
certificate: this.certificate,
},
);
const scalableTarget = this.ecsService.service.autoScaleTaskCount({
minCapacity: props.scaling.desiredCount,
maxCapacity: props.scaling.maxCount,
});
scalableTarget.scaleOnCpuUtilization("cpu-scaling", {
targetUtilizationPercent: props.scaling.cpuPercentage,
});
scalableTarget.scaleOnMemoryUtilization("memory-scaling", {
targetUtilizationPercent: props.scaling.memoryPercentage,
});
secrets.grantRead(this.ecsService.taskDefinition.taskRole);
}
private setUpAliasRecord(props: ApiStackProps) {
this.gatewayUrl = new CfnOutput(this, "gateway-url-output", {
value: this.ecsService.loadBalancer.loadBalancerDnsName,
});
this.aliasRecord = new Route53.ARecord(this, "alias-record", {
zone: this.hostedZone,
recordName: props.domainName,
target: Route53.RecordTarget.fromAlias(
new Route53Targets.LoadBalancerTarget(this.ecsService.loadBalancer),
),
});
const shouldCreateWWW = props.domainName.split(".").length === 2;
if (shouldCreateWWW) {
new Route53.ARecord(this, "alias-record-www", {
zone: this.hostedZone,
recordName: `www.${props.domainName}`,
target: Route53.RecordTarget.fromAlias(
new Route53Targets.LoadBalancerTarget(this.ecsService.loadBalancer),
),
});
}
}
}
Any advice is greatly appreciated.
I would strongly advise moving from the Docker directory to ECR public gallery to avoid ratelimit issues: https://gallery.ecr.aws/
That said, to answer the question about the number of NATs created. As you can see in the CDK docs, what you're seeing reflects the default behavior (emphasis mine):
A VPC consists of one or more subnets that instances can be placed
into. CDK distinguishes three different subnet types:
Public (SubnetType.PUBLIC) - public subnets connect directly to the Internet using an Internet Gateway. If you want your instances to
have a public IP address and be directly reachable from the Internet,
you must place them in a public subnet.
Private with Internet Access (SubnetType.PRIVATE_WITH_NAT) - instances in private subnets are not directly routable from the
Internet, and connect out to the Internet via a NAT gateway. By
default, a NAT gateway is created in every public subnet for maximum
availability. Be aware that you will be charged for NAT gateways.
Isolated (SubnetType.PRIVATE_ISOLATED) - isolated subnets do not route from or to the Internet, and as such do not require NAT
gateways. They can only connect to or be connected to from other
instances in the same VPC. A default VPC configuration will not
include isolated subnets,
A default VPC configuration will create public and private subnets.
However, if natGateways:0 and subnetConfiguration is undefined,
default VPC configuration will create public and isolated subnets.
So a separate NAT is created for every Public subnet.
Also, the docs for the natGateways parameter mentioned above also describe the default behavior:
(default: One NAT gateway/instance per Availability Zone)
To limit the number of AZs used by the VPC, specify the maxAzs parameter. Set it to 1 to only have a single NAT per VPC.
If you're fine with making the resources in the VPC publicly reachable from the internet, you can place them in Public subnets and avoid the creation of NATs altogether.
this.vpc = new EC2.Vpc(this, this.resourceName(props, "vpc"), {
maxAzs: 1,
natGateways: 0;
});
If you do this, you have to tell your resources to use the public subnet instead of the isolated one.
However, CodeBuild projects do not support this.
They require a NAT to connect to the internet if placed into a VPC. See this question for details.
So if you want your build project to be in a VPC, you need to place it into a private subnet. This is done by default, so no additional configuration needed. Just make sure you have at least one NAT gateway.
To sum up, the real solution to the Docker Hub rate limit issue is to switch over to ECR Public gallery.

AWS CDK - Cognito UserPool authorizer on API Gateway not working

My goal is to set up some lambda functions which are public (i.e. no authorization required to send requests) and other ones which require a User to be logged in within a Cognito UserPool.
In my CDK file below, I'm adding an Authorizer only on one of the two endpoints, but then when I launch a request both of them are unprotected, and in the function logs you can see there is no Cognito UserPool nor AuthenticationType.
Any ideas on what's missing?
Thanks!
{
"httpMethod":"GET",
"body":null,
"resource":"/private",
"requestContext":{
...,
"identity":{
"apiKey":null,
"userArn":null,
"cognitoAuthenticationType":null,
"caller":null,
"userAgent":"Custom User Agent String",
"user":null,
"cognitoIdentityPoolId":null,
"cognitoAuthenticationProvider":null,
"sourceIp":"127.0.0.1",
"accountId":null
},
...
},
...
}
CDK file:
import * as apigateway from '#aws-cdk/aws-apigateway';
import * as lambda from '#aws-cdk/aws-lambda';
import * as s3 from '#aws-cdk/aws-s3';
import { UserPool, VerificationEmailStyle, UserPoolClient } from '#aws-cdk/aws-cognito'
import { App, CfnParameter, Duration, Stack, StackProps } from '#aws-cdk/core';
export class CdkStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
new CfnParameter(this, 'AppId');
const userPool = new UserPool(this, 'dev-users', {
userPoolName: 'dev-users',
selfSignUpEnabled: true,
userVerification: {
emailSubject: 'Verify your email for our awesome app!',
emailBody: 'Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}',
emailStyle: VerificationEmailStyle.CODE,
smsMessage: 'Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}',
},
signInAliases: {
email: true
},
signInCaseSensitive: false,
standardAttributes: {
email: { required: true, mutable: false }
},
passwordPolicy: {
minLength: 6,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: false,
tempPasswordValidity: Duration.days(7),
}
})
const environment = { };
// The code will be uploaded to this location during the pipeline's build step
const artifactBucket = s3.Bucket.fromBucketName(this, 'ArtifactBucket', process.env.S3_BUCKET!);
const artifactKey = `${process.env.CODEBUILD_BUILD_ID}/function-code.zip`;
const code = lambda.Code.fromBucket(artifactBucket, artifactKey);
// This is a Lambda function config associated with the source code: get-all-items.js
const publicFunction = new lambda.Function(this, 'publicFunction', {
description: 'A simple example includes a HTTP get method accessible to everyone',
handler: 'src/handlers/public.publicHandler',
runtime: lambda.Runtime.NODEJS_10_X,
code,
environment,
timeout: Duration.seconds(60),
});
// Give Read permissions to the SampleTable
// This is a Lambda function config associated with the source code: put-item.js
const privateFunction = new lambda.Function(this, 'privateFunction', {
description: 'This functions should only be accessible to authorized users from a Cognito UserPool',
handler: 'src/handlers/private.privateHandler',
runtime: lambda.Runtime.NODEJS_10_X,
code,
timeout: Duration.seconds(60),
environment,
});
const api = new apigateway.RestApi(this, 'ServerlessRestApi', { cloudWatchRole: false });
const authorizer = new apigateway.CfnAuthorizer(this, 'cfnAuth', {
restApiId: api.restApiId,
name: 'HelloWorldAPIAuthorizer',
type: 'COGNITO_USER_POOLS',
identitySource: 'method.request.header.Authorization',
providerArns: [userPool.userPoolArn],
})
api.root.addResource('public').addMethod(
'GET',
new apigateway.LambdaIntegration(publicFunction)
);
api.root.addResource('private').addMethod(
'GET',
new apigateway.LambdaIntegration(privateFunction),
{
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizer: {
authorizerId: authorizer.ref
}
}
);
}
}
const app = new App();
new CdkStack(app, 'CognitoProtectedApi', {});
app.synth();
Try doing the following in your addMethod.
{
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizer // pass the authorizer object instead of authorizerId stuff.
}
Refer https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.CognitoUserPoolsAuthorizer.html for more details.
Following is for AWS CDK 2.20.0
You can create a CognitoUserPoolsAuthorizer and then either attach it as default authorizer for an API GW, or attach it specific route.
For adding to a specific method,
const userPool = new cognito.UserPool(this, 'UserPool');
const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'booksAuthorizer', {
cognitoUserPools: [userPool]
});
declare const books: apigateway.Resource;
books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), {
authorizer: auth,
authorizationType: apigateway.AuthorizationType.COGNITO,
})
Refer https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.CognitoUserPoolsAuthorizer.html

Referencing the physical resource ID in a AwsCustomResource in onUpdate or onDelete

I am trying to create an AwsCustomResource to make single AWS API calls, specifically (create|update|delete)-organisational-unit. In the onCreate I can see that to return the OU's Id as the physical resource ID I can specify a physicalResourceIdPath, however for the onUpdate and onDelete I need to specify this in the API call.
I can not see how, other than to use regular custom resources with my own Lambda to gain access to the event.
Here is what I have so far, 'help?' is what I'm missing.
import * as cdk from '#aws-cdk/core';
import { AwsCustomResource } from '#aws-cdk/custom-resources'
export interface OrganisationalUnitProps {
readonly name: string;
readonly parentId: string;
}
export class OrgansationalUnit extends AwsCustomResource {
constructor(scope: cdk.Construct, id: string, props: OrganisationalUnitProps) {
super(scope, id, {
onCreate: {
service: 'Organizations',
action: 'createOrganizationalUnit',
parameters: {
Name: props.name,
ParentId: props.parentId
},
physicalResourceIdPath: 'OrganizationalUnit.Id'
},
onDelete: {
service: 'Organizations',
action: 'deleteOrganizationalUnit',
parameters: {
OrganizationalUnitId: 'help?'
}
}
});
}
}
Whilst there isn't a perfectly clean way to do it yet, here is an answer I was given in this issue that I raised on the CDK github page.
You create two custom resources. One is just a create, and one is just a delete.
const connectDirectory = new AwsCustomResource(this, 'ConnectDirectory', {
onCreate: {
service: 'DirectoryService',
action: 'connectDirectory',
parameters: { ... },
physicalResourceId: PhysicalResourceId.fromResponse('DirectoryId')
},
});
const deleteDirectory = new AwsCustomResource(this, 'DeleteDirectory', {
onDelete: {
service: 'DirectoryService',
action: 'deleteDirectory',
parameters: {
DirectoryId: connectDirectory.getResponseField('DirectoryId'),
},
},
});
That github issue has been accepted as a change request, so there may be a better answer in the future.
nowadays there is PhysicalResourceIdReference which you can use like this:
new AwsCustomResource(this, 'my-custom-resource', {
...
onCreate: {
...
physicalResourceId: PhysicalResourceId.fromResponse( ... )
},
onDelete: {
...
parameters: {
physicalResourceId: new PhysicalResourceIdReference(),
}
}
});