-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(virus-scanner): use Lambda as runtime
- Port constructs from cdklabs/cdk-ecr-deployment to copy Docker images from Docker Hub to private ECR - Use bootstrap code hosted on public S3 bucket for assoc lambda fn - Replace ECS-based virus scanner with Lambda-based one, - grant invoke rights to FormSG - ensure Docker image in ECR before creating associated lambda - Use Cloudwatch Event to keep virus-scanner warm every 3 min - Take the chance to fix CORS permissions in S3 - Remove default value for `emailHost` param
- Loading branch information
Showing
8 changed files
with
291 additions
and
376 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import * as path from 'path' | ||
import { aws_ec2 as ec2, aws_iam as iam, aws_lambda as lambda, Duration, CustomResource, Token, RemovalPolicy } from 'aws-cdk-lib' | ||
import { PolicyStatement, AddToPrincipalPolicyResult } from 'aws-cdk-lib/aws-iam' | ||
import { RuntimeFamily } from 'aws-cdk-lib/aws-lambda' | ||
import { Construct } from 'constructs' | ||
|
||
export interface ECRDeploymentProps { | ||
|
||
/** | ||
* Lambda code to use for the Golang lambda for custom resource. | ||
* | ||
* In most cases, this should not be specified. Instead, the code for the lambda | ||
* should be obtained from {@link getCode}, which would build it, or load it from local disk. | ||
* | ||
* Specify this only when your deployment environment prevents you from doing either of | ||
* these things, eg, when you are using this in a CloudFormation template synthesized from CDK. | ||
* | ||
* @default - code for the lambda is built using {@link buildImage} | ||
*/ | ||
readonly code?: lambda.Code | ||
|
||
/** | ||
* Image to use to build Golang lambda for custom resource, if download fails or is not wanted. | ||
* | ||
* Might be needed for local build if all images need to come from own registry. | ||
* | ||
* Note that image should use yum as a package manager and have golang available. | ||
* | ||
* If {@link code} is specified, this property is ignored. | ||
* | ||
* @default - public.ecr.aws/sam/build-go1.x:latest | ||
*/ | ||
readonly buildImage?: string | ||
/** | ||
* The source of the docker image. | ||
*/ | ||
readonly src: IImageName | ||
|
||
/** | ||
* The destination of the docker image. | ||
*/ | ||
readonly dest: IImageName | ||
|
||
/** | ||
* The amount of memory (in MiB) to allocate to the AWS Lambda function which | ||
* replicates the files from the CDK bucket to the destination bucket. | ||
* | ||
* If you are deploying large files, you will need to increase this number | ||
* accordingly. | ||
* | ||
* @default - 512 | ||
*/ | ||
readonly memoryLimit?: number | ||
|
||
/** | ||
* Execution role associated with this function | ||
* | ||
* @default - A role is automatically created | ||
*/ | ||
readonly role?: iam.IRole | ||
|
||
/** | ||
* The VPC network to place the deployment lambda handler in. | ||
* | ||
* @default - None | ||
*/ | ||
readonly vpc?: ec2.IVpc | ||
|
||
/** | ||
* Where in the VPC to place the deployment lambda handler. | ||
* Only used if 'vpc' is supplied. | ||
* | ||
* @default - the Vpc default strategy if not specified | ||
*/ | ||
readonly vpcSubnets?: ec2.SubnetSelection | ||
|
||
/** | ||
* The list of security groups to associate with the Lambda's network interfaces. | ||
* | ||
* Only used if 'vpc' is supplied. | ||
* | ||
* @default - If the function is placed within a VPC and a security group is | ||
* not specified, either by this or securityGroup prop, a dedicated security | ||
* group will be created for this function. | ||
*/ | ||
readonly securityGroups?: ec2.SecurityGroup[] | ||
|
||
/** | ||
* The lambda function runtime environment. | ||
* | ||
* @default - lambda.Runtime.PROVIDED_AL2023 | ||
*/ | ||
readonly lambdaRuntime?: lambda.Runtime | ||
|
||
/** | ||
* The name of the lambda handler. | ||
* | ||
* @default - bootstrap | ||
*/ | ||
readonly lambdaHandler?: string | ||
|
||
/** | ||
* The environment variable to set | ||
*/ | ||
readonly environment?: { [key: string]: string } | ||
} | ||
|
||
export interface IImageName { | ||
/** | ||
* The uri of the docker image. | ||
* | ||
* The uri spec follows https://github.com/containers/skopeo | ||
*/ | ||
readonly uri: string | ||
|
||
/** | ||
* The credentials of the docker image. Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name` | ||
*/ | ||
creds?: string | ||
} | ||
|
||
function getCode(buildImage: string): lambda.AssetCode { | ||
return lambda.Code.fromDockerBuild(path.join(__dirname, '../lambda'), { | ||
buildArgs: { | ||
buildImage, | ||
}, | ||
}) | ||
} | ||
|
||
export class DockerImageName implements IImageName { | ||
public constructor(private name: string, public creds?: string) { } | ||
public get uri(): string { return `docker://${this.name}` } | ||
} | ||
|
||
export class S3ArchiveName implements IImageName { | ||
private name: string | ||
public constructor(p: string, ref?: string, public creds?: string) { | ||
this.name = p | ||
if (ref) { | ||
this.name += ':' + ref | ||
} | ||
} | ||
public get uri(): string { return `s3://${this.name}` } | ||
} | ||
|
||
export class ECRDeployment extends Construct { | ||
private handler: lambda.SingletonFunction | ||
readonly customResource: CustomResource | ||
|
||
constructor(scope: Construct, id: string, props: ECRDeploymentProps) { | ||
super(scope, id) | ||
const memoryLimit = props.memoryLimit ?? 512 | ||
this.handler = new lambda.SingletonFunction(this, 'CustomResourceHandler', { | ||
uuid: this.renderSingletonUuid(memoryLimit), | ||
code: props.code ?? getCode(props.buildImage ?? 'public.ecr.aws/docker/library/golang:1'), | ||
runtime: props.lambdaRuntime ?? new lambda.Runtime('provided.al2023', RuntimeFamily.OTHER), // not using Runtime.PROVIDED_AL2023 to support older CDK versions (< 2.105.0) | ||
handler: props.lambdaHandler ?? 'bootstrap', | ||
environment: props.environment, | ||
lambdaPurpose: 'Custom::CDKECRDeployment', | ||
timeout: Duration.minutes(15), | ||
role: props.role, | ||
memorySize: memoryLimit, | ||
vpc: props.vpc, | ||
vpcSubnets: props.vpcSubnets, | ||
securityGroups: props.securityGroups, | ||
}) | ||
|
||
const handlerRole = this.handler.role | ||
if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role') } | ||
|
||
handlerRole.addToPrincipalPolicy( | ||
new iam.PolicyStatement({ | ||
effect: iam.Effect.ALLOW, | ||
actions: [ | ||
'ecr:GetAuthorizationToken', | ||
'ecr:BatchCheckLayerAvailability', | ||
'ecr:GetDownloadUrlForLayer', | ||
'ecr:GetRepositoryPolicy', | ||
'ecr:DescribeRepositories', | ||
'ecr:ListImages', | ||
'ecr:DescribeImages', | ||
'ecr:BatchGetImage', | ||
'ecr:ListTagsForResource', | ||
'ecr:DescribeImageScanFindings', | ||
'ecr:InitiateLayerUpload', | ||
'ecr:UploadLayerPart', | ||
'ecr:CompleteLayerUpload', | ||
'ecr:PutImage', | ||
], | ||
resources: ['*'], | ||
})) | ||
handlerRole.addToPrincipalPolicy(new iam.PolicyStatement({ | ||
effect: iam.Effect.ALLOW, | ||
actions: [ | ||
's3:GetObject', | ||
], | ||
resources: ['*'], | ||
})) | ||
|
||
this.customResource = new CustomResource(this, 'CustomResource', { | ||
serviceToken: this.handler.functionArn, | ||
resourceType: 'Custom::CDKBucketDeployment', | ||
properties: { | ||
SrcImage: props.src.uri, | ||
SrcCreds: props.src.creds, | ||
DestImage: props.dest.uri, | ||
DestCreds: props.dest.creds, | ||
}, | ||
}) | ||
} | ||
|
||
public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { | ||
const handlerRole = this.handler.role | ||
if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role') } | ||
|
||
return handlerRole.addToPrincipalPolicy(statement) | ||
} | ||
|
||
private renderSingletonUuid(memoryLimit?: number) { | ||
let uuid = 'bd07c930-edb9-4112-a20f-03f096f53666' | ||
|
||
// if user specify a custom memory limit, define another singleton handler | ||
// with this configuration. otherwise, it won't be possible to use multiple | ||
// configurations since we have a singleton. | ||
if (memoryLimit) { | ||
if (Token.isUnresolved(memoryLimit)) { | ||
throw new Error('Can\'t use tokens when specifying "memoryLimit" since we use it to identify the singleton custom resource handler') | ||
} | ||
|
||
uuid += `-${memoryLimit.toString()}MiB` | ||
} | ||
|
||
return uuid | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.