@aws-cdk/core#Duration TypeScript Examples
The following examples show how to use
@aws-cdk/core#Duration.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: webAppCloudFrontDistribution.ts From aws-boilerplate with MIT License | 6 votes |
private createDeployment(staticFilesBucket: Bucket, distribution: CloudFrontWebDistribution, props: WebAppCloudFrontDistributionProps) {
new BucketDeployment(this, 'DeployWebsite', {
distribution,
distributionPaths: ['/index.html'],
sources: props.sources,
destinationBucket: staticFilesBucket,
cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(Duration.hours(1))],
});
}
Example #2
Source File: webAppCloudFrontDistribution.ts From aws-boilerplate with MIT License | 6 votes |
private createApiProxySourceConfig(props: WebAppCloudFrontDistributionProps): SourceConfiguration | null {
if (!props.apiDomainName) {
return null;
}
return {
behaviors: [{
pathPattern: '/api/*',
allowedMethods: CloudFrontAllowedMethods.ALL,
forwardedValues: {
queryString: true,
headers: ["Host"],
cookies: {forward: 'all'},
},
defaultTtl: Duration.seconds(0),
minTtl: Duration.seconds(0),
maxTtl: Duration.seconds(0),
}],
customOriginSource: {
domainName: props.apiDomainName,
originProtocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
},
};
}
Example #3
Source File: city-stack.ts From MDDL with MIT License | 6 votes |
/**
* Create the audit log queue for the stack.
* @param kmsKey The encryption key for the log group
*/
private createDeadLetterQueue(kmsKey: IKey) {
const queue = new Queue(this, 'DeadLetterQueue', {
encryptionMasterKey: kmsKey,
retentionPeriod: Duration.days(10),
fifo: true,
})
return {
queue,
}
}
Example #4
Source File: auth-stack.ts From MDDL with MIT License | 6 votes |
private addTriggers(userPool: UserPool) {
const parameterPath = `/${this.stackName}/clientWebApps`
const customMessageHandler = new Function(this, 'CustomMessageHandler', {
code: Code.fromAsset(
pathToApiServiceLambda('emails/cognitoCustomMessageHandler'),
),
environment: {
CLIENT_WEB_APP_PARAMETER_PATH: parameterPath,
},
handler: 'index.handler',
memorySize: 256,
timeout: Duration.seconds(10),
runtime: Runtime.NODEJS_12_X,
})
customMessageHandler.addToRolePolicy(
new PolicyStatement({
actions: ['ssm:GetParameter'],
resources: [
`arn:${this.partition}:ssm:${this.region}:${this.account}:parameter${parameterPath}/*`,
],
}),
)
userPool.addTrigger(UserPoolOperation.CUSTOM_MESSAGE, customMessageHandler)
}
Example #5
Source File: city-stack.ts From MDDL with MIT License | 6 votes |
/**
* Create the email processing queue for the stack.
* @param kmsKey The encryption key for the log group
* @param deadLetterQueue The dead letter queue to user
*/
private createEmailProcessorQueue(kmsKey: IKey, deadLetterQueue: Queue) {
const queue = new Queue(this, 'EmailProcessorQueue', {
encryptionMasterKey: kmsKey,
fifo: true,
contentBasedDeduplication: true,
retentionPeriod: Duration.days(10),
visibilityTimeout: Duration.seconds(60),
deadLetterQueue: {
queue: deadLetterQueue,
maxReceiveCount: 50,
},
})
return {
queue,
}
}
Example #6
Source File: city-stack.ts From MDDL with MIT License | 6 votes |
/**
* Create the audit log queue for the stack.
* @param kmsKey The encryption key for the log group
* @param deadLetterQueue The dead letter queue to user
*/
private createAuditLogQueue(kmsKey: IKey, deadLetterQueue: Queue) {
const queue = new Queue(this, 'AuditLogQueue', {
encryptionMasterKey: kmsKey,
fifo: true,
retentionPeriod: Duration.days(10),
visibilityTimeout: Duration.seconds(60),
deadLetterQueue: {
queue: deadLetterQueue,
maxReceiveCount: 50,
},
})
return {
queue,
}
}
Example #7
Source File: applicationMultipleTargetGroupsFargateServiceBase.ts From aws-boilerplate with MIT License | 5 votes |
protected registerECSTargets(service: BaseService, container: ContainerDefinition, targets: ApplicationTargetProps[]): ApplicationTargetGroup {
targets?.forEach((targetProps, index) => {
const idSuffix = index > 0 ? index : '';
const targetGroup = new ApplicationTargetGroup(this, `TargetGroup${idSuffix}`, {
vpc: service.cluster.vpc,
port: targetProps.containerPort,
healthCheck: {
path: '/lbcheck',
protocol: ELBProtocol.HTTP,
interval: Duration.seconds(6),
timeout: Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 2,
},
deregistrationDelay: Duration.seconds(10),
targetType: TargetType.IP,
targets: [
service.loadBalancerTarget({
containerName: container.containerName,
containerPort: targetProps.containerPort,
protocol: targetProps.protocol,
}),
],
});
this.findListener(targetProps.listener).addTargetGroups(`ECSTargetGroup${idSuffix}${container.containerName}${targetProps.containerPort}`, {
hostHeader: targetProps.hostHeader,
pathPattern: targetProps.pathPattern,
priority: targetProps.priority,
targetGroups: [targetGroup],
});
this.targetGroups.push(targetGroup);
});
if (this.targetGroups.length === 0) {
throw new Error('At least one target group should be specified.');
}
return this.targetGroups[0];
}
Example #8
Source File: mainEcsCluster.ts From aws-boilerplate with MIT License | 5 votes |
private createPublicLoadBalancer(props: MainECSClusterProps): ApplicationLoadBalancer {
const securityGroup = new SecurityGroup(this, "ALBSecurityGroup", {
vpc: props.vpc,
});
securityGroup.addIngressRule(Peer.anyIpv4(), Port.allTraffic());
const publicLoadBalancer = new ApplicationLoadBalancer(this, "ALB", {
vpc: props.vpc,
internetFacing: true,
securityGroup: securityGroup,
idleTimeout: Duration.seconds(30),
vpcSubnets: {subnetType: SubnetType.PUBLIC, onePerAz: true},
});
const httpsListener = publicLoadBalancer.addListener("HttpsListener", {
protocol: ApplicationProtocol.HTTPS,
open: true,
port: 443,
defaultTargetGroups: [new ApplicationTargetGroup(this, 'DummyTargetGroup', {
vpc: props.vpc,
port: 80,
targetGroupName: `${props.envSettings.projectEnvName}-dtg`,
targetType: TargetType.IP,
})]
});
httpsListener.addCertificates("Certificate", [
ListenerCertificate.fromCertificateManager(props.certificate),
]);
new CfnOutput(this, "PublicLoadBalancerSecurityGroupIdOutput", {
exportName: MainECSCluster.getPublicLoadBalancerSecurityGroupIdOutputExportName(props.envSettings),
value: securityGroup.securityGroupId,
});
new CfnOutput(this, "PublicLoadBalancerDnsNameOutput", {
exportName: MainECSCluster.getLoadBalancerDnsNameOutput(props.envSettings),
value: publicLoadBalancer.loadBalancerDnsName,
});
new CfnOutput(this, "PublicLoadBalancerArnOutput", {
exportName: MainECSCluster.getLoadBalancerArnOutputExportName(props.envSettings),
value: publicLoadBalancer.loadBalancerArn,
});
new CfnOutput(this, "PublicLoadBalancerCanonicalHostedZoneIdOutput", {
exportName: MainECSCluster.getLoadBalancerCanonicalHostedZoneIdOutputExportName(props.envSettings),
value: publicLoadBalancer.loadBalancerCanonicalHostedZoneId,
});
new CfnOutput(this, "PublicLoadBalancerHttpsListenerArn", {
exportName: MainECSCluster.getLoadBalancerHttpsListenerArnOutputExportName(props.envSettings),
value: httpsListener.listenerArn,
})
return publicLoadBalancer;
}
Example #9
Source File: stack.ts From aws-boilerplate with MIT License | 5 votes |
constructor(scope: core.App, id: string, props: MigrationsStackProps) {
super(scope, id, props);
const {envSettings} = props;
const resources = new FargateServiceResources(this, "MigrationsResources", props);
const containerName = 'migrations';
const dbSecretArn = Fn.importValue(MainDatabase.getDatabaseSecretArnOutputExportName(envSettings));
const taskRole = this.createTaskRole(props);
const migrationsTaskDefinition = new FargateTaskDefinition(this, "MigrationsTaskDefinition", {
taskRole,
cpu: 256,
memoryLimitMiB: 512,
});
const containerDef = migrationsTaskDefinition.addContainer(containerName, {
image: ContainerImage.fromEcrRepository(resources.backendRepository, envSettings.version),
logging: this.createAWSLogDriver(this.node.id),
environment: {
"CHAMBER_SERVICE_NAME": this.getChamberServiceName(envSettings),
"CHAMBER_KMS_KEY_ALIAS": MainKmsKey.getKeyAlias(envSettings),
},
secrets: {
"DB_CONNECTION": EcsSecret.fromSecretsManager(
Secret.fromSecretArn(this, "DbSecret", dbSecretArn))
},
});
new StateMachine(this, "MigrationsStateMachine", {
stateMachineName: `${envSettings.projectEnvName}-migrations`,
definition: new Task(this, "MigrationsFargateTask", {
task: new RunEcsFargateTask({
cluster: resources.mainCluster,
taskDefinition: migrationsTaskDefinition,
assignPublicIp: true,
securityGroup: resources.fargateContainerSecurityGroup,
containerOverrides: [
{
containerDefinition: containerDef,
command: ['./scripts/run_migrations.sh'],
}
],
integrationPattern: ServiceIntegrationPattern.SYNC,
}),
}),
timeout: Duration.minutes(5),
});
}
Example #10
Source File: basestack.ts From sourcestack with MIT License | 5 votes |
frontend() {
this.bucket = new S3.Bucket(this, Config.instance.appEnv + '-hosting-bucket', {
bucketName: Config.instance.appEnv + '-hosting-bucket',
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'index.html',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
let oai = new OriginAccessIdentity(this, Config.instance.appEnv + '-oai');
let distributionProps: CloudFrontWebDistributionProps = {
comment: Config.instance.appEnv + ' distribution',
aliasConfiguration: Config.instance.domain ? {
names: [Config.instance.subdomain],
acmCertRef: Config.instance.certificateArn,
sslMethod: SSLMethod.SNI
} : undefined,
originConfigs: [{
s3OriginSource: {
s3BucketSource: this.bucket,
originAccessIdentity: oai,
},
behaviors: [{ isDefaultBehavior: true }],
}, {
customOriginSource: {
domainName: `${this.endpoint.restApiId}.execute-api.${this.region}.${this.urlSuffix}`
},
originPath: `/${this.endpoint.deploymentStage.stageName}`,
behaviors: [{
pathPattern: '/api/*',
allowedMethods: CloudFrontAllowedMethods.ALL,
defaultTtl: Duration.millis(0),
maxTtl: Duration.millis(0),
minTtl: Duration.millis(0),
forwardedValues: {
queryString: true,
cookies: {
forward: 'all'
},
// This doesn't work - bug in CDK?
// headers: ['*'],
}
}]
}]
}
this.distribution = new CloudFrontWebDistribution(this, Config.instance.appEnv + '-distribution', distributionProps);
if (Config.instance.domain){
new route53.CfnRecordSet(this, Config.instance.appEnv + '-recordset', {
hostedZoneName: Config.instance.domain + '.',
name: Config.instance.subdomain,
type: 'A',
aliasTarget: {
hostedZoneId: 'Z2FDTNDATAQYW2', // cloudfront.net
dnsName: this.distribution.domainName
}
})
}
this.bucket.addToResourcePolicy(new IAM.PolicyStatement({
effect: IAM.Effect.ALLOW,
resources: [
this.bucket.bucketArn,
this.bucket.bucketArn + '/*'
],
actions: [
's3:GetBucket*',
's3:GetObject*',
's3:List*',
],
principals: [
new IAM.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)
]
}));
}
Example #11
Source File: city-stack.ts From MDDL with MIT License | 5 votes |
/**
* Runs DB migrations using Knex
* @param dbSecret The DB secret for accessing the database
* @param mysqlLayer The mysql layer
*/
private runMigrations(dbSecret: ISecret, mysqlLayer: ILayerVersion) {
const runMigrationsFunction = new Function(this, 'RunMigrationsFunction', {
code: Code.fromAsset(
join(__dirname, '..', '..', 'api-service', 'dist', 'migrator'),
),
handler: 'index.handler',
runtime: Runtime.NODEJS_12_X,
vpc: this.lambdaVpc,
securityGroups: this.lambdaSecurityGroups,
layers: [mysqlLayer],
timeout: Duration.seconds(60),
memorySize: 512,
environment: {
DB_HOST: this.rdsEndpoint,
DB_USER: dbSecret.secretValueFromJson('username').toString(),
DB_PASSWORD: dbSecret.secretValueFromJson('password').toString(),
DB_NAME: dbSecret.secretValueFromJson('username').toString(),
},
})
// create a custom resource provider
const runMigrationsResourceProvider = new Provider(
this,
'RunMigrationsCustomResourceProvider',
{
onEventHandler: runMigrationsFunction,
logRetention: RetentionDays.ONE_DAY,
},
)
return {
resource: new CustomResource(this, 'RunMigrationsCustomResource', {
serviceToken: runMigrationsResourceProvider.serviceToken,
properties: {
// Dynamic prop to force execution each time
Execution: Math.random().toString(36).substr(2),
},
}),
}
}
Example #12
Source File: actions-stack.ts From hasura-cdk with MIT License | 5 votes |
constructor(scope: Construct, id: string, props: ActionsStackProps) {
super(scope, id, props);
const hostedZone = PublicHostedZone.fromHostedZoneAttributes(this, 'HasuraHostedZone', {
hostedZoneId: props.hostedZoneId,
zoneName: props.hostedZoneName,
});
const api = new RestApi(this, 'ActionsApi', {
domainName: {
domainName: props.actionsHostname,
certificate: props.certificates.actions,
},
restApiName: 'Actions',
description: 'Endpoint For Hasura Actions',
deployOptions: {
loggingLevel: MethodLoggingLevel.INFO,
dataTraceEnabled: true,
},
});
// API DNS record
new ARecord(this, 'ActionsApiAliasRecord', {
zone: hostedZone,
recordName: props.actionsHostname,
target: AddressRecordTarget.fromAlias(new route53_targets.ApiGateway(api)),
});
// Create a lambda layer to contain node_modules
const handlerDependenciesLayer = new RetainedLambdaLayerVersion(this, 'ActionHandlerDependencies', {
contentLocation: 'actions/dependencies-layer',
description: 'Dependencies layer',
compatibleRuntimes: [Runtime.NODEJS_12_X],
});
const actionHandler = new Function(this, 'ActionHandler', {
functionName: `${props.appName}-ActionHandler`,
handler: 'handler.handler',
memorySize: 1024,
runtime: Runtime.NODEJS_12_X,
code: Code.fromAsset(path.join(__dirname, '../../actions/dist/')),
timeout: Duration.seconds(4),
layers: [handlerDependenciesLayer],
});
const handlerResource = api.root.addResource('handler');
const actionHandlerIntegration = new LambdaIntegration(actionHandler);
handlerResource.addMethod('POST', actionHandlerIntegration);
handlerResource.addMethod('GET', actionHandlerIntegration);
}
Example #13
Source File: city-stack.ts From MDDL with MIT License | 5 votes |
private configureAlerts(
alertsSnsTopicArn: string,
api: HttpApi,
deadLetterQueue: IQueue,
processorQueues: IQueue[],
) {
const alarms = [
// TODO alarm for server errors from the API - needs some metric math to be a percentile
// api
// .metricServerError({
// statistic: 'sum',
// })
// .createAlarm(this, 'ServerErrorsAlarm', {
// evaluationPeriods: 1,
// threshold: 0.2,
// }),
// alarm for any messages on the dead letter queue
deadLetterQueue
.metricApproximateNumberOfMessagesVisible()
.createAlarm(this, 'DeadLetterQueueMessagesVisible', {
threshold: 1,
evaluationPeriods: 1,
}),
// alarm for any queue that has had a message sitting on it for 24 hours or more
...processorQueues.map((q) =>
q
.metricApproximateAgeOfOldestMessage()
.createAlarm(this, `ProcessingStalled${q.node.id}`, {
threshold: Duration.days(1).toSeconds(),
evaluationPeriods: 1,
}),
),
]
const alertTopic = Topic.fromTopicArn(this, 'AlertTopic', alertsSnsTopicArn)
alarms.forEach((a) => {
a.addAlarmAction(new SnsAction(alertTopic))
})
}
Example #14
Source File: city-stack.ts From MDDL with MIT License | 5 votes |
/**
* Create a lambda function with a standard configuration
* @param name The name for constructs
* @param path The path to the entrypoint
* @param props Extra properties for the function
*/
private createLambda(
name: string,
path: string,
props: {
handler?: string
dbSecret?: ISecret
extraEnvironmentVariables?: EnvironmentVariables[]
layers?: ILayerVersion[]
extraFunctionProps?: Partial<FunctionProps>
documentBucketPermissions?: BucketPermissions
collectionBucketPermissions?: BucketPermissions
auditLogSqsPermissions?: SqsPermissions
emailProcessorSqsPermissions?: SqsPermissions
auditLogGroupPermissions?: LogGroupPermissions
},
) {
const {
handler = 'index.handler',
dbSecret,
extraEnvironmentVariables = [],
extraFunctionProps = {},
layers,
} = props
const requiresDbConnectivity = !!dbSecret
const dbParams: { [key: string]: string } = dbSecret
? {
DB_HOST: this.rdsEndpoint,
DB_USER: dbSecret.secretValueFromJson('username').toString(),
DB_PASSWORD: dbSecret.secretValueFromJson('password').toString(),
DB_NAME: dbSecret.secretValueFromJson('username').toString(),
}
: {}
const environment: {
[key: string]: string
} = {
NODE_ENV: 'production',
...dbParams,
}
// add specified environment variables
extraEnvironmentVariables.forEach((e) => {
if (this.environmentVariables[e])
environment[e] = this.environmentVariables[e]
})
// add common environment variables
commonEnvironmentVariables.forEach((e) => {
if (this.environmentVariables[e])
environment[e] = this.environmentVariables[e]
})
const lambda = new Function(this, name, {
...extraFunctionProps,
code: Code.fromAsset(path),
handler,
environment,
memorySize: 512,
timeout: Duration.seconds(60),
layers,
runtime: Runtime.NODEJS_12_X,
vpc: requiresDbConnectivity ? this.lambdaVpc : undefined,
securityGroups: requiresDbConnectivity
? this.lambdaSecurityGroups
: undefined,
tracing: Tracing.ACTIVE,
})
const cfnLambda = lambda.node.defaultChild as CfnFunction
cfnLambda.addPropertyOverride('KmsKeyArn', this.kmsKey.keyArn)
this.addPermissionsToLambda(lambda, props)
return lambda
}
Example #15
Source File: city-stack.ts From MDDL with MIT License | 4 votes |
/**
* Adds collection specific routes to the API
* @param apiProps Common properties for API functions
*/
private addCollectionRoutes(apiProps: ApiProps) {
const { api, dbSecret, mySqlLayer, authorizer } = apiProps
// add route and lambda to list collections documents
this.addRoute(api, {
name: 'GetCollectionDocuments',
routeKey: 'GET /collections/{collectionId}/documents',
lambdaFunction: this.createLambda(
'GetCollectionDocuments',
pathToApiServiceLambda('collections/getDocumentsByCollectionId'),
{
dbSecret,
layers: [mySqlLayer],
extraEnvironmentVariables: [
...authEnvironmentVariables,
EnvironmentVariables.DOCUMENTS_BUCKET,
],
// permission needed to create presigned urls for thumbnails
documentBucketPermissions: {
includeRead: true,
},
},
),
authorizer,
throttlingSettings: ReadRouteDefaultThrottling,
})
// add route and lambda to list collections grants
this.addRoute(api, {
name: 'GetCollectionGrants',
routeKey: 'GET /collections/{collectionId}/grants',
lambdaFunction: this.createLambda(
'GetCollectionGrants',
pathToApiServiceLambda('collections/getGrantsByCollectionId'),
{
dbSecret,
layers: [mySqlLayer],
extraEnvironmentVariables: [...authEnvironmentVariables],
},
),
authorizer,
throttlingSettings: ReadRouteDefaultThrottling,
})
// add lambda to create a zip of collection documents
const createCollectionZip = this.createLambda(
'CreateCollectionZip',
pathToApiServiceLambda('collections/createCollectionZip'),
{
dbSecret,
layers: [mySqlLayer],
extraEnvironmentVariables: [EnvironmentVariables.DOCUMENTS_BUCKET],
extraFunctionProps: {
timeout: Duration.seconds(900),
},
documentBucketPermissions: {
// can read documents
includeRead: true,
},
collectionBucketPermissions: {
// can read and write collections
includeRead: true,
includeWrite: true,
includeTagging: true,
},
},
)
this.environmentVariables[
EnvironmentVariables.CREATE_COLLECTION_ZIP_FUNCTION_NAME
] = createCollectionZip.functionName
// add lambda to create a collection download
const downloadCollectionDocuments = this.createLambda(
'DownloadCollectionDocuments',
pathToApiServiceLambda('collections/downloadCollectionDocuments'),
{
dbSecret,
layers: [mySqlLayer],
extraEnvironmentVariables: [
...authEnvironmentVariables,
EnvironmentVariables.DOCUMENTS_BUCKET,
EnvironmentVariables.CREATE_COLLECTION_ZIP_FUNCTION_NAME,
EnvironmentVariables.ACTIVITY_RECORD_SQS_QUEUE_URL,
],
collectionBucketPermissions: {
// can read collections
includeRead: true,
},
auditLogSqsPermissions: {
includeWrite: true,
},
},
)
createCollectionZip.grantInvoke(downloadCollectionDocuments)
// add route to create a collection download
this.addRoute(api, {
name: 'DownloadCollectionDocuments',
routeKey: 'POST /collections/{collectionId}/documents/downloads',
lambdaFunction: downloadCollectionDocuments,
authorizer,
throttlingSettings: WriteRouteDefaultThrottling,
})
// add route and lambda to fetch a collection download
this.addRoute(api, {
name: 'GetDownloadForCollectionDocuments',
routeKey:
'GET /collections/{collectionId}/documents/downloads/{downloadId}',
lambdaFunction: this.createLambda(
'GetDownloadForCollectionDocuments',
pathToApiServiceLambda('collections/getDownloadForCollectionDocuments'),
{
dbSecret,
layers: [mySqlLayer],
extraEnvironmentVariables: [
...authEnvironmentVariables,
EnvironmentVariables.DOCUMENTS_BUCKET,
EnvironmentVariables.ACTIVITY_RECORD_SQS_QUEUE_URL,
],
collectionBucketPermissions: {
// can read collections
includeRead: true,
},
auditLogSqsPermissions: {
includeWrite: true,
},
},
),
authorizer,
throttlingSettings: ReadRouteDefaultThrottling,
})
}
Example #16
Source File: backend-stack.ts From flect-chime-sdk-demo with Apache License 2.0 | 4 votes |
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
///////////////////////////////
//// Authentication
///////////////////////////////
//// (1) Cognito User Pool
const userPool = new UserPool(this, `${id}_UserPool`, {
userPoolName: `${id}_UserPool`,
selfSignUpEnabled: true,
autoVerify: {
email: true,
},
passwordPolicy: {
minLength: 6,
requireSymbols: false,
},
signInAliases: {
email: true,
},
});
const userPoolClient = new UserPoolClient(this, id + "_UserPool_Client", {
userPoolClientName: `${id}_UserPoolClient`,
userPool: userPool,
accessTokenValidity: Duration.minutes(1440),
idTokenValidity: Duration.minutes(1440),
refreshTokenValidity: Duration.days(30),
});
//// (2) Policy Statement
const statement = new PolicyStatement({
effect: Effect.ALLOW,
});
statement.addActions(
"cognito-idp:GetUser",
"cognito-idp:AdminGetUser",
"chime:CreateMeeting",
"chime:DeleteMeeting",
"chime:GetMeeting",
"chime:ListMeetings",
"chime:BatchCreateAttendee",
"chime:CreateAttendee",
"chime:DeleteAttendee",
"chime:GetAttendee",
"chime:ListAttendees",
"chime:StartMeetingTranscription",
"chime:StopMeetingTranscription",
"execute-api:ManageConnections",
"ecs:RunTask",
"ecs:DescribeTasks",
"ecs:UpdateService",
"ecs:DescribeServices",
`ec2:DescribeNetworkInterfaces`,
"iam:PassRole"
);
statement.addResources(userPool.userPoolArn);
statement.addResources("arn:*:chime::*:meeting/*");
statement.addResources("arn:aws:execute-api:*:*:**/@connections/*");
statement.addResources("arn:aws:ecs:*");
statement.addResources("*");
statement.addResources("arn:aws:iam::*:*");
//////////////////////////////////////
//// Storage Resources (S3)
//////////////////////////////////////
const bucket = new s3.Bucket(this, "StaticSiteBucket", {
bucketName: `${id}-Bucket`.toLowerCase(),
removalPolicy: cdk.RemovalPolicy.DESTROY,
publicReadAccess: true,
});
let cdn: cloudfront.CloudFrontWebDistribution;
if (USE_CDN) {
const oai = new cloudfront.OriginAccessIdentity(this, "my-oai");
const myBucketPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["s3:GetObject"],
principals: [new iam.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)],
resources: [bucket.bucketArn + "/*"],
});
bucket.addToResourcePolicy(myBucketPolicy);
// Create CloudFront WebDistribution
cdn = new cloudfront.CloudFrontWebDistribution(this, "WebsiteDistribution", {
viewerCertificate: {
aliases: [],
props: {
cloudFrontDefaultCertificate: true,
},
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
originConfigs: [
{
s3OriginSource: {
s3BucketSource: bucket,
originAccessIdentity: oai,
},
behaviors: [
{
isDefaultBehavior: true,
minTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.days(365),
defaultTtl: cdk.Duration.days(1),
pathPattern: "my-contents/*",
},
],
},
],
errorConfigurations: [
{
errorCode: 403,
responsePagePath: "/index.html",
responseCode: 200,
errorCachingMinTtl: 0,
},
{
errorCode: 404,
responsePagePath: "/index.html",
responseCode: 200,
errorCachingMinTtl: 0,
},
],
});
}
//////////////////////////////////////
//// Storage Resources (DynamoDB)
//////////////////////////////////////
//// (1) Meeting Table
const meetingTable = new Table(this, "meetingTable", {
tableName: `${id}_MeetingTable`,
partitionKey: {
name: "MeetingName",
type: AttributeType.STRING,
},
readCapacity: 2,
writeCapacity: 2,
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
meetingTable.addGlobalSecondaryIndex({
indexName: "MeetingId",
partitionKey: {
name: "MeetingId",
type: AttributeType.STRING,
},
projectionType: ProjectionType.ALL,
readCapacity: 2,
writeCapacity: 2,
});
//// (2) Attendee Table
const attendeeTable = new Table(this, "attendeeTable", {
tableName: `${id}_AttendeeTable`,
partitionKey: {
name: "AttendeeId",
type: AttributeType.STRING,
},
readCapacity: 2,
writeCapacity: 2,
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
//// (3) Connection Table
const connectionTable = new Table(this, "connectionTable", {
tableName: `${id}_ConnectionTable`,
partitionKey: {
name: "MeetingId",
type: AttributeType.STRING,
},
sortKey: {
name: "AttendeeId",
type: AttributeType.STRING,
},
readCapacity: 2,
writeCapacity: 2,
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
//// (x) SlackFederationAccounts Table
const slackFederationAuthsTable = new Table(this, "slackFederationAuthsTable", {
tableName: `${id}_SlackFederationAuthsTable`,
partitionKey: {
name: "TeamId",
type: AttributeType.STRING,
},
readCapacity: 2,
writeCapacity: 2,
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
/////////////////////
/// Fargate
/////////////////////
const vpc = new ec2.Vpc(this, `${id}_vpc`, {
maxAzs: 2,
subnetConfiguration: [
{
cidrMask: 24,
name: `${id}_pub`,
subnetType: ec2.SubnetType.PUBLIC,
},
],
});
const cluster = new ecs.Cluster(this, `${id}_cluster`, { vpc });
const logGroup = new logs.LogGroup(this, `${id}_logGroup`, {
logGroupName: `/${id}-fargate`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// create a task definition with CloudWatch Logs
const logging = new ecs.AwsLogDriver({
logGroup: logGroup,
streamPrefix: "fargate",
});
const taskDefinition = new ecs.FargateTaskDefinition(this, `${id}_fargate_task`, {
family: this.node.tryGetContext("serviceName"),
cpu: 2048,
// memoryLimitMiB: 5120,
memoryLimitMiB: 8192,
});
if (USE_DOCKER) {
const container = taskDefinition.addContainer("DefaultContainer", {
containerName: `${id}_manager_container`,
image: ecs.ContainerImage.fromAsset("lib/manager"),
cpu: 2048,
// memoryLimitMiB: 4096,
memoryLimitMiB: 8192,
logging: logging,
});
} else {
const image = ecs.ContainerImage.fromRegistry(`dannadori/hmm:latest`);
const container = taskDefinition.addContainer("DefaultContainer", {
containerName: `${id}_manager_container`,
image: image,
cpu: 2048,
memoryLimitMiB: 8192,
// memoryLimitMiB: 5120,
logging: logging,
});
}
bucket.grantReadWrite(taskDefinition.taskRole);
const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
vpc: vpc,
});
securityGroup.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(3000));
///////////////////////////////
//// Lambda Layers
///////////////////////////////
// ( - ) Lambda Layer
const nodeModulesLayer = new lambda.LayerVersion(this, "NodeModulesLayer", {
layerVersionName: `${id}_LambdaLayer`,
code: lambda.AssetCode.fromAsset(`${__dirname}/layer`),
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
});
// ( - ) Utility
const addCommonSetting = (f: lambda.Function) => {
meetingTable.grantFullAccess(f);
attendeeTable.grantFullAccess(f);
connectionTable.grantFullAccess(f);
slackFederationAuthsTable.grantFullAccess(f);
f.addToRolePolicy(statement);
f.addEnvironment("MEETING_TABLE_NAME", meetingTable.tableName);
f.addEnvironment("ATTENDEE_TABLE_NAME", attendeeTable.tableName);
f.addEnvironment("CONNECTION_TABLE_NAME", connectionTable.tableName);
f.addEnvironment("SLACK_FEDERATION_AUTHS_TABLE_NAME", slackFederationAuthsTable.tableName);
f.addEnvironment("USER_POOL_ID", userPool.userPoolId);
f.addEnvironment("VPC_ID", vpc.vpcId);
f.addEnvironment("SUBNET_ID", vpc.publicSubnets[0].subnetId);
f.addEnvironment("CLUSTER_ARN", cluster.clusterArn);
f.addEnvironment("TASK_DIFINITION_ARN_MANAGER", taskDefinition.taskDefinitionArn);
f.addEnvironment("BUCKET_DOMAIN_NAME", bucket.bucketDomainName);
f.addEnvironment("MANAGER_CONTAINER_NAME", `${id}_manager_container`);
f.addEnvironment("BUCKET_ARN", bucket.bucketArn);
f.addEnvironment("BUCKET_NAME", bucket.bucketName);
f.addEnvironment("SECURITY_GROUP_NAME", securityGroup.securityGroupName);
f.addEnvironment("SECURITY_GROUP_ID", securityGroup.securityGroupId);
f.addEnvironment("USERPOOL_ID", userPool.userPoolId);
f.addEnvironment("USERPOOL_CLIENT_ID", userPoolClient.userPoolClientId);
// Slack-Chime Connector (slack access)
f.addEnvironment("SLACK_CLIENT_ID", SLACK_CLIENT_ID);
f.addEnvironment("SLACK_CLIENT_SECRET", SLACK_CLIENT_SECRET);
f.addEnvironment("SLACK_SIGNING_SECRET", SLACK_SIGNING_SECRET);
f.addEnvironment("SLACK_STATE_SECRET", SLACK_STATE_SECRET);
// Slack-Chime Connector (db access)
f.addEnvironment("SLACK_APP_DB_PASSWORD", SLACK_APP_DB_PASSWORD);
f.addEnvironment("SLACK_APP_DB_SALT", SLACK_APP_DB_SALT);
f.addEnvironment("SLACK_APP_DB_SECRET", SLACK_APP_DB_SECRET);
//// DEMO URL
if (USE_CDN) {
f.addEnvironment("DEMO_ENDPOINT", `https://${cdn!.distributionDomainName}`);
} else {
f.addEnvironment("DEMO_ENDPOINT", `https://${bucket.bucketDomainName}`);
}
f.addLayers(nodeModulesLayer);
};
// ( - ) auth
const lambdaFuncRestAPIAuth = new lambda.Function(this, "ChimeRESTAPIAuth", {
code: lambda.Code.fromAsset(`${__dirname}/dist`),
handler: "rest_auth.authorize",
runtime: lambda.Runtime.NODEJS_14_X,
timeout: Duration.seconds(5),
memorySize: 256,
});
addCommonSetting(lambdaFuncRestAPIAuth);
const restAPIRole = new Role(this, `ChimeRESTAPIRole`, {
assumedBy: new ServicePrincipal("apigateway.amazonaws.com"),
});
const restAPIPolicy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [lambdaFuncRestAPIAuth.functionArn],
actions: ["lambda:InvokeFunction"],
});
restAPIRole.addToPolicy(restAPIPolicy);
//// (1) Function For RestAPI
const lambdaFunctionForRestAPI: lambda.Function = new lambda.Function(this, "funcHelloWorld", {
functionName: `${id}_getRoot`,
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset(`${__dirname}/dist`),
handler: "index.handler",
memorySize: 256,
timeout: cdk.Duration.seconds(5),
});
addCommonSetting(lambdaFunctionForRestAPI);
//// (2) Function For SlackFederation API
const lambdaFunctionForSlackFederationRestAPI: lambda.Function = new lambda.Function(this, "funcSlackFederation", {
functionName: `${id}_slackFederationRoot`,
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset(`${__dirname}/dist/federation/slack`),
handler: "slack.handler",
memorySize: 256,
timeout: cdk.Duration.seconds(3),
});
addCommonSetting(lambdaFunctionForSlackFederationRestAPI);
///////////////////////////////
//// API Gateway
///////////////////////////////
//// ( - ) Utility
// https://github.com/aws/aws-cdk/issues/906
const addCorsOptions = (apiResource: IResource) => {
let origin;
if (FRONTEND_LOCAL_DEV) {
origin = "'https://localhost:3000'";
// origin = "'https://192.168.0.4:3000'";
} else {
if (USE_CDN) {
origin = `'https://${cdn!.distributionDomainName}'`;
} else {
origin = `'https://${bucket.bucketDomainName}'`;
}
}
apiResource.addMethod(
"OPTIONS",
new MockIntegration({
integrationResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,X-Flect-Access-Token'",
"method.response.header.Access-Control-Allow-Origin": origin,
"method.response.header.Access-Control-Allow-Credentials": "'true'",
"method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'",
},
},
],
passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
requestTemplates: {
"application/json": '{"statusCode": 200}',
},
}),
{
methodResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Methods": true,
"method.response.header.Access-Control-Allow-Credentials": true,
"method.response.header.Access-Control-Allow-Origin": true,
},
},
],
}
);
};
//// ( - ) Rest API
const restApi: RestApi = new RestApi(this, "ChimeAPI", {
restApiName: `${id}_restApi`,
});
//lambdaFunctionPostAttendeeOperation.addEnvironment("RESTAPI_ENDPOINT", restApi.url) ///// Exception(Circular dependency between resources) -> generate from request context in lambda.
//// ( - ) Authorizer
const authorizer2 = new CfnAuthorizer(this, "apiAuthorizerLambda", {
name: `${id}_authorizerLamda`,
type: "TOKEN",
// identitySource: "method.request.header.Authorization",
identitySource: "method.request.header.X-Flect-Access-Token",
restApiId: restApi.restApiId,
authorizerCredentials: restAPIRole.roleArn,
authorizerUri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${lambdaFuncRestAPIAuth.functionArn}/invocations`,
authorizerResultTtlInSeconds: 0,
});
//// (1) Get Root
const root = restApi.root;
addCorsOptions(root);
const addRoot = root.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_getRoot`,
// authorizationType: AuthorizationType.CUSTOM,
// authorizer: {
// authorizerId: authorizer2.ref,
// },
});
//// (2) Meeting
const apiMeetings = restApi.root.addResource("meetings");
const apiMeeting = apiMeetings.addResource("{meetingName}");
addCorsOptions(apiMeetings);
addCorsOptions(apiMeeting);
//// (2-1) Get Meetings
// apiMeetings.addMethod("GET", new LambdaIntegration(lambdaFunctionGetMeetings), {
apiMeetings.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_getMeetings`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
//// (2-2) Post Meeting
// apiMeetings.addMethod("POST", new LambdaIntegration(lambdaFunctionPostMeeting), {
apiMeetings.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_postMeeting`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
//// (2-3) Delete Meeting
// apiMeeting.addMethod("DELETE", new LambdaIntegration(lambdaFunctionDeleteMeeting), {
apiMeeting.addMethod("DELETE", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_deleteMeeting`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
//// (2-4) Get Meeting
// apiMeeting.addMethod("GET", new LambdaIntegration(lambdaFunctionGetMeeting), {
apiMeeting.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_getMeeting`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
///// (3) Attendee
const apiAttendees = apiMeeting.addResource("attendees");
const apiAttendee = apiAttendees.addResource("{attendeeId}");
addCorsOptions(apiAttendees);
addCorsOptions(apiAttendee);
//// (3-1) Get Attendee
// apiAttendee.addMethod("GET", new LambdaIntegration(lambdaFunctionGetAttendee), {
apiAttendee.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_getAttendee`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
//// (3-2) Post Attendee
// apiAttendees.addMethod("POST", new LambdaIntegration(lambdaFunctionPostAttendee), {
apiAttendees.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_postAttendee`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
//// (3-3) List Attendees
// apiAttendees.addMethod("GET", new LambdaIntegration(lambdaFunctionGetAttendees), {
apiAttendees.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_getAttendees`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
///// (4) Attendee Operations // Operation under Meeting
const apiAttendeeOperations = apiAttendee.addResource("operations");
const apiAttendeeOperation = apiAttendeeOperations.addResource("{operation}");
addCorsOptions(apiAttendeeOperations);
addCorsOptions(apiAttendeeOperation);
//// (4-1) Post Attendee Operation
// apiAttendeeOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionPostAttendeeOperation), {
apiAttendeeOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_postAttendeeOperation`,
authorizationType: AuthorizationType.CUSTOM,
authorizer: {
authorizerId: authorizer2.ref,
},
});
// lambdaFunctionPostAttendeeOperation.addEnvironment("RESTAPI_ENDPOINT", restApi.url) ///// Exception(Circular dependency between resources) -> generate from request context in lambda.
//// (5) Log
const apiLogs = restApi.root.addResource("logs");
addCorsOptions(apiLogs);
//// (5-1) Post Log
// apiLogs.addMethod("POST", new LambdaIntegration(lambdaFunctionPostLog), {
apiLogs.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_postLog`,
});
//// (a) Operation // Global Operation(before signin)
const apiOperations = restApi.root.addResource("operations");
const apiOperation = apiOperations.addResource("{operation}");
addCorsOptions(apiOperations);
addCorsOptions(apiOperation);
//// (a-1) Post Onetime Code Signin Request
// apiOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionPostOperation), {
apiOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
operationName: `${id}_postOperation`,
});
///// (b) slack federation
const apiSlack = restApi.root.addResource("slack");
const apiSlackOAuthRedirect = apiSlack.addResource("oauth_redirect");
const apiSlackInstall = apiSlack.addResource("install");
const apiSlackEvents = apiSlack.addResource("events");
const apiSlackApi = apiSlack.addResource("api");
const apiSlackOperation = apiSlackApi.addResource("{operation}");
addCorsOptions(apiSlackOAuthRedirect);
addCorsOptions(apiSlackInstall);
addCorsOptions(apiSlackEvents);
addCorsOptions(apiSlackOperation);
apiSlackOAuthRedirect.addMethod("GET", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
operationName: `${id}_slackOAuthRedirect`,
});
apiSlackInstall.addMethod("GET", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
operationName: `${id}_slackInstall`,
});
apiSlackEvents.addMethod("POST", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
operationName: `${id}_slackEvents`,
});
apiSlackOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
operationName: `${id}_slackApi`,
});
/////////
// WebSocket
// https://github.com/aws-samples/aws-cdk-examples/pull/325/files
////////
// API Gateway
const webSocketApi = new v2.CfnApi(this, "ChimeMessageAPI", {
name: "ChimeMessageAPI",
protocolType: "WEBSOCKET",
routeSelectionExpression: "$request.body.action",
});
//// Lambda Function
// (1) connect
const lambdaFuncMessageConnect = new lambda.Function(this, "ChimeMessageAPIConnect", {
code: lambda.Code.fromAsset(`${__dirname}/dist`),
handler: "message.connect",
runtime: lambda.Runtime.NODEJS_14_X,
timeout: Duration.seconds(30),
memorySize: 256,
});
addCommonSetting(lambdaFuncMessageConnect);
// (2) disconnect
const lambdaFuncMessageDisconnect = new lambda.Function(this, "ChimeMessageAPIDisconnect", {
code: lambda.Code.fromAsset(`${__dirname}/dist`),
handler: "message.disconnect",
runtime: lambda.Runtime.NODEJS_14_X,
timeout: Duration.seconds(30),
memorySize: 256,
});
addCommonSetting(lambdaFuncMessageDisconnect);
// (3) message
const lambdaFuncMessageMessage = new lambda.Function(this, "ChimeMessageAPIMessage", {
code: lambda.Code.fromAsset(`${__dirname}/dist`),
handler: "message.message",
runtime: lambda.Runtime.NODEJS_14_X,
timeout: Duration.seconds(30),
memorySize: 256,
});
addCommonSetting(lambdaFuncMessageMessage);
// (4) auth
const lambdaFuncMessageAuth = new lambda.Function(this, "ChimeMessageAPIAuth", {
code: lambda.Code.fromAsset(`${__dirname}/dist`),
handler: "message.authorize",
runtime: lambda.Runtime.NODEJS_14_X,
timeout: Duration.seconds(30),
memorySize: 256,
});
addCommonSetting(lambdaFuncMessageAuth);
const policy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [lambdaFuncMessageConnect.functionArn, lambdaFuncMessageDisconnect.functionArn, lambdaFuncMessageMessage.functionArn, lambdaFuncMessageAuth.functionArn],
actions: ["lambda:InvokeFunction"],
});
const role = new Role(this, `ChimeMessageAPIRole`, {
assumedBy: new ServicePrincipal("apigateway.amazonaws.com"),
});
role.addToPolicy(policy);
const messageAuthorizer = new v2.CfnAuthorizer(this, "messageAuthorizer", {
name: `${id}_authorizer`,
authorizerType: "REQUEST",
identitySource: [],
apiId: webSocketApi.ref,
authorizerUri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${lambdaFuncMessageAuth.functionArn}/invocations`,
authorizerCredentialsArn: role.roleArn,
});
//// Integration
const connectIntegration = new v2.CfnIntegration(this, "ChimeMessageAPIConnectIntegration", {
apiId: webSocketApi.ref,
integrationType: "AWS_PROXY",
integrationUri: "arn:aws:apigateway:" + this.region + ":lambda:path/2015-03-31/functions/" + lambdaFuncMessageConnect.functionArn + "/invocations",
credentialsArn: role.roleArn,
});
const disconnectIntegration = new v2.CfnIntegration(this, "ChimeMessageAPIDisconnectIntegration", {
apiId: webSocketApi.ref,
integrationType: "AWS_PROXY",
integrationUri: "arn:aws:apigateway:" + this.region + ":lambda:path/2015-03-31/functions/" + lambdaFuncMessageDisconnect.functionArn + "/invocations",
credentialsArn: role.roleArn,
});
const messageIntegration = new v2.CfnIntegration(this, "ChimeMessageAPIMessageIntegration", {
apiId: webSocketApi.ref,
integrationType: "AWS_PROXY",
integrationUri: "arn:aws:apigateway:" + this.region + ":lambda:path/2015-03-31/functions/" + lambdaFuncMessageMessage.functionArn + "/invocations",
credentialsArn: role.roleArn,
});
// const messageAuthorizer = new CfnAuthorizer(this, 'cfnMessageAuth', {
// name: `${id}_messageAuthorizer`,
// type: "TOKEN",
// restApiId: restApi.restApiId,
// identitySource: 'method.request.header.Authorization',
// authorizerCredentials: role.roleArn,
// authorizerUri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${lambdaFuncMessageAuth.functionArn}/invocations`,
// })
//// Route
const connectRoute = new v2.CfnRoute(this, "connectRoute", {
apiId: webSocketApi.ref,
routeKey: "$connect",
authorizationType: AuthorizationType.CUSTOM,
//authorizationType: "NONE",
target: "integrations/" + connectIntegration.ref,
authorizerId: messageAuthorizer.ref,
});
const disconnectRoute = new v2.CfnRoute(this, "disconnectRoute", {
apiId: webSocketApi.ref,
routeKey: "$disconnect",
authorizationType: "NONE",
target: "integrations/" + disconnectIntegration.ref,
});
const messageRoute = new v2.CfnRoute(this, "messageRoute", {
apiId: webSocketApi.ref,
routeKey: "sendmessage",
authorizationType: "NONE",
target: "integrations/" + messageIntegration.ref,
});
//// Deploy
const deployment = new v2.CfnDeployment(this, "ChimeMessageAPIDep", {
apiId: webSocketApi.ref,
});
const stage = new v2.CfnStage(this, `ChimeMessageAPIStage`, {
apiId: webSocketApi.ref,
autoDeploy: true,
deploymentId: deployment.ref,
stageName: "Prod",
});
const dependencies = new ConcreteDependable();
dependencies.add(connectRoute);
dependencies.add(disconnectRoute);
dependencies.add(messageRoute);
deployment.node.addDependency(dependencies);
/////////////////////////////////////////////
/// add env info after api gateway
////////////////////////////////////////////
// lambdaFunctionForSlackFederationRestAPI.addEnvironment("RESTAPI_ENDPOINT", restApi.url); /// Exception(Circular dependency between resources) -> generate from request context in lambda.
///////////////////////////////
//// Output
///////////////////////////////
new CfnOutput(this, "UserPoolId", {
description: "UserPoolId",
value: userPool.userPoolId,
});
new CfnOutput(this, "UserPoolClientId", {
description: "UserPoolClientId",
value: userPoolClient.userPoolClientId,
});
new CfnOutput(this, "Bucket", {
description: "Bucket",
value: bucket.bucketName,
});
new CfnOutput(this, "BucketWebsiteDomainName", {
description: "BucketWebsiteDomainName",
value: bucket.bucketWebsiteDomainName,
});
new CfnOutput(this, "BucketDomainName", {
description: "BucketDomainName",
value: bucket.bucketDomainName,
});
new CfnOutput(this, "RestAPIEndpoint", {
description: "RestAPIEndpoint",
value: restApi.url,
});
new CfnOutput(this, "WebSocketEndpoint", {
description: "WebSocketEndpoint",
value: webSocketApi.attrApiEndpoint,
});
if (USE_CDN) {
new CfnOutput(this, "CDNComainName", {
description: "CDNComainName",
value: cdn!.distributionDomainName,
});
}
//// DEMO URL
if (USE_CDN) {
new CfnOutput(this, "DemoEndpoint", {
description: "DemoEndpoint",
value: `https://${cdn!.distributionDomainName}`,
});
new CfnOutput(this, "DistributionId", {
description: "DistributionId",
value: cdn!.distributionId,
});
} else {
new CfnOutput(this, "DemoEndpoint", {
description: "DemoEndpoint",
value: `https://${bucket.bucketDomainName}`,
});
}
// new cdk.CfnOutput(this, "AmongLoadBalancerDNS", {
// value: lb_among.loadBalancerDnsName
// });
// new cdk.CfnOutput(this, "AmongServiceArn", {
// value: ecsService_among.serviceArn
// });
// new cdk.CfnOutput(this, "AmongServiceName", {
// value: ecsService_among.serviceName
// });
}
Example #17
Source File: data-store-stack.ts From MDDL with MIT License | 4 votes |
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props)
// read out config and set defaults
const { vpcConfig = {}, rdsConfig = {}, providedKmsKey } = props
const {
cidrBlock = '10.0.0.0/16',
maxAzs = 2,
natGatewaysCount = 2,
} = vpcConfig
const {
backupRetentionDays = 30,
minCapacity = 1,
maxCapacity = 8,
} = rdsConfig
let kmsKey: IKey
if (providedKmsKey) {
// import key
kmsKey = Key.fromKeyArn(this, 'ProvidedKey', providedKmsKey.keyArn)
} else {
// create new KMS key for the data store to use
kmsKey = new Key(this, 'Key', {
description: `KMS Key for ${this.stackName} stack`,
enableKeyRotation: true,
})
// allow RDS to use the KMS key
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.Keys.html
kmsKey.addToResourcePolicy(
new PolicyStatement({
actions: ['kms:Decrypt', 'kms:GenerateDataKey*'],
resources: ['*'],
principals: [new AnyPrincipal()],
conditions: {
StringEquals: {
'kms:ViaService': `rds.${this.region}.amazonaws.com`,
'kms:CallerAccount': this.account,
},
},
}),
)
}
// create the VPC
this.vpc = new Vpc(this, 'Vpc', {
cidr: cidrBlock,
maxAzs: maxAzs,
enableDnsHostnames: false,
enableDnsSupport: true,
natGateways: natGatewaysCount,
subnetConfiguration: [
{
cidrMask: 28,
name: 'isolated',
subnetType: SubnetType.ISOLATED,
},
{
cidrMask: 28,
name: 'private',
subnetType: SubnetType.PRIVATE,
},
{
cidrMask: 28,
name: 'public',
subnetType: SubnetType.PUBLIC,
},
],
})
// create the root DB credentials
const rdsRootCredentialsSecret = new Secret(
this,
'RdsClusterCredentialsSecret',
{
secretName: `${this.stackName}-rds-root-credentials`,
generateSecretString: {
secretStringTemplate: JSON.stringify({
username: 'root',
}),
excludePunctuation: true,
includeSpace: false,
generateStringKey: 'password',
excludeCharacters: '"@/\\',
passwordLength: 30,
},
encryptionKey: kmsKey,
},
)
// configure RDS subnet group
const rdsSubnetGroup = new CfnDBSubnetGroup(this, 'RdsSubnetGroup', {
dbSubnetGroupDescription: `Subnet group for RDS ${this.stackName}`,
subnetIds: this.vpc.isolatedSubnets.map((s) => s.subnetId),
})
// configure RDS security group
const rdsSecurityGroup = new SecurityGroup(this, 'RdsSecurityGroup', {
vpc: this.vpc,
allowAllOutbound: false,
description: `${this.stackName} security group for RDS`,
})
// create the DB cluster
const rdsPort = 3306
const databaseName = 'root'
const rdsCluster = new CfnDBCluster(this, 'RdsCluster', {
engineMode: 'serverless',
engine: 'aurora-mysql',
engineVersion: '5.7.mysql_aurora.2.07.1',
enableHttpEndpoint: true,
databaseName,
kmsKeyId: kmsKey.keyId,
masterUsername: rdsRootCredentialsSecret
.secretValueFromJson('username')
.toString(),
masterUserPassword: rdsRootCredentialsSecret
.secretValueFromJson('password')
.toString(),
backupRetentionPeriod: backupRetentionDays,
scalingConfiguration: {
maxCapacity,
minCapacity,
autoPause: false,
},
deletionProtection: true,
dbSubnetGroupName: rdsSubnetGroup.ref,
dbClusterParameterGroupName: 'default.aurora-mysql5.7',
// The following three lines cause errors on updates, so they are commented out.
// If you need custom values for them, uncomment before first deployment.
// preferredMaintenanceWindow: maintenanceWindowWeekly,
// preferredBackupWindow: backupWindowDaily,
// port: rdsPort,
storageEncrypted: true,
vpcSecurityGroupIds: [rdsSecurityGroup.securityGroupId],
})
rdsCluster.applyRemovalPolicy(RemovalPolicy.SNAPSHOT)
rdsCluster.node.addDependency(this.vpc, kmsKey)
this.rdsEndpoint = rdsCluster.attrEndpointAddress
// add database networking
this.rdsAccessSecurityGroup = new SecurityGroup(
this,
'RdsAccessSecurityGroup',
{
vpc: this.vpc,
description: `${this.stackName} security group for RDS Access`,
},
)
rdsSecurityGroup.addIngressRule(
this.rdsAccessSecurityGroup,
Port.tcp(rdsPort),
)
rdsSecurityGroup.addEgressRule(
this.rdsAccessSecurityGroup,
Port.tcp(rdsPort),
)
this.rdsAccessSecurityGroup.addIngressRule(
rdsSecurityGroup,
Port.tcp(rdsPort),
)
const { layer: mysqlLayer } = this.addMysqlLayer()
// configure function used by the city stacks to create a new DB and user within this cluster
this.createDbUserFunction = new Function(this, 'CreateDbUserFunction', {
code: Code.fromAsset(path.join('build', 'create-db-and-user.zip')),
handler: 'index.handler',
runtime: Runtime.NODEJS_12_X,
vpc: this.vpc,
timeout: Duration.seconds(60),
securityGroups: [this.rdsAccessSecurityGroup],
layers: [mysqlLayer],
environment: {
DB_HOST: rdsCluster.attrEndpointAddress,
DB_USER: rdsRootCredentialsSecret
.secretValueFromJson('username')
.toString(),
DB_PASSWORD: rdsRootCredentialsSecret
.secretValueFromJson('password')
.toString(),
DB_DEFAULT_DATABASE: databaseName,
},
})
}
Example #18
Source File: aws-serverless-wordpress-stack.ts From aws-serverless-wordpress with Apache License 2.0 | 4 votes |
constructor(scope: cdk.Construct, id: string, props: StackProps) {
super(scope, id, props);
if (!props.cloudFrontHashHeader) props.cloudFrontHashHeader = Buffer.from(`${this.stackName}.${props.domainName}`).toString('base64');
const globalTagKey = 'aws-config:cloudformation:stack-name';
const globalTagValue = Buffer.from(this.stackName).toString('base64');
const awsManagedSnsKmsKey = Alias.fromAliasName(this, 'AwsManagedSnsKmsKey', 'alias/aws/sns');
const publicHostedZone = PublicHostedZone.fromLookup(this, 'ExistingPublicHostedZone', {domainName: props.domainName});
const acmCertificate = new Certificate(this, 'Certificate', {
domainName: props.hostname,
subjectAlternativeNames: props.alternativeHostname,
validation: CertificateValidation.fromDns(publicHostedZone)
});
const staticContentBucket = new Bucket(this, 'StaticContentBucket', {
encryption: BucketEncryption.S3_MANAGED,
versioned: true,
removalPolicy: props.removalPolicy
});
const loggingBucket = new Bucket(this, 'LoggingBucket', {
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: props.removalPolicy,
lifecycleRules: [
{
enabled: true,
transitions: [
{
storageClass: StorageClass.INFREQUENT_ACCESS,
transitionAfter: Duration.days(30)
},
{
storageClass: StorageClass.DEEP_ARCHIVE,
transitionAfter: Duration.days(90)
}
]
}
]
});
loggingBucket.addToResourcePolicy(new PolicyStatement({
principals: [new ServicePrincipal('delivery.logs.amazonaws.com'),],
actions: ['s3:PutObject'],
resources: [
`${loggingBucket.bucketArn}/vpc-flow-log/AWSLogs/${this.account}/*`,
`${loggingBucket.bucketArn}/application-load-balancer/AWSLogs/${this.account}/*`,
`${loggingBucket.bucketArn}/vpn-admin-application-load-balancer/AWSLogs/${this.account}/*`
],
conditions: {
StringEquals: {
's3:x-amz-acl': 'bucket-owner-full-control'
}
}
}));
loggingBucket.addToResourcePolicy(new PolicyStatement({
principals: [new ServicePrincipal('delivery.logs.amazonaws.com'),],
actions: ['s3:GetBucketAcl'],
resources: [loggingBucket.bucketArn],
}));
loggingBucket.addToResourcePolicy(new PolicyStatement({
principals: [new ArnPrincipal(`arn:aws:iam::${props.loadBalancerAccountId}:root`)],
actions: ['s3:PutObject'],
resources: [
`${loggingBucket.bucketArn}/application-load-balancer/AWSLogs/${this.account}/*`,
`${loggingBucket.bucketArn}/vpn-admin-application-load-balancer/AWSLogs/${this.account}/*`
]
}));
loggingBucket.addToResourcePolicy(new PolicyStatement({
principals: [new AccountRootPrincipal()],
actions: ['s3:GetBucketAcl', 's3:PutBucketAcl'],
resources: [loggingBucket.bucketArn]
}));
if (props.removalPolicy === RemovalPolicy.DESTROY) {
const serviceToken = CustomResourceProvider.getOrCreate(this, 'Custom::EmptyLoggingBucket', {
codeDirectory: path.join(__dirname, 'custom-resource'),
runtime: CustomResourceProviderRuntime.NODEJS_12,
timeout: Duration.minutes(3),
policyStatements: [(new PolicyStatement({
actions: ['s3:ListBucket', 's3:DeleteObject'],
resources: [loggingBucket.bucketArn, `${loggingBucket.bucketArn}/*`]
})).toStatementJson()],
environment: {
LOGGING_BUCKET_NAME: loggingBucket.bucketName
}
});
new CustomResource(this, 'EmptyLoggingBucket', {
resourceType: 'Custom::EmptyLoggingBucket',
serviceToken
});
}
const vpc = new Vpc(this, 'Vpc', {
natGateways: 3,
maxAzs: 3,
cidr: '172.16.0.0/16',
subnetConfiguration: [
{
name: 'Public',
subnetType: SubnetType.PUBLIC
},
{
name: 'Private',
subnetType: SubnetType.PRIVATE
},
{
name: 'Isolated',
subnetType: SubnetType.ISOLATED
}
],
enableDnsHostnames: true,
enableDnsSupport: true
});
const nacl = new NetworkAcl(this, 'NetworkAcl', {vpc});
nacl.addEntry('AllowAllHttpsFromIpv4', {
ruleNumber: 100,
cidr: AclCidr.anyIpv4(),
traffic: AclTraffic.tcpPort(443),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW
});
nacl.addEntry('AllowAllHttpsFromIpv6', {
ruleNumber: 101,
cidr: AclCidr.anyIpv6(),
traffic: AclTraffic.tcpPort(443),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW
});
nacl.addEntry('AllowResponseToHttpsRequestToIpv4', {
ruleNumber: 100,
cidr: AclCidr.anyIpv4(),
traffic: AclTraffic.tcpPortRange(1024, 65535),
direction: TrafficDirection.EGRESS,
ruleAction: Action.ALLOW
});
nacl.addEntry('AllowResponseToHttpsRequestToIpv6', {
ruleNumber: 101,
cidr: AclCidr.anyIpv6(),
traffic: AclTraffic.tcpPortRange(1024, 65535),
direction: TrafficDirection.EGRESS,
ruleAction: Action.ALLOW
});
new CfnFlowLog(this, 'CfnVpcFlowLog', {
resourceId: vpc.vpcId,
resourceType: 'VPC',
trafficType: 'ALL',
logDestinationType: 's3',
logDestination: `${loggingBucket.bucketArn}/vpc-flow-log`
});
const privateHostedZone = new PrivateHostedZone(this, 'PrivateHostedZone', {
vpc,
zoneName: `${props.hostname}.private`
});
const applicationLoadBalancerSecurityGroup = new SecurityGroup(this, 'ApplicationLoadBalancerSecurityGroup', {vpc});
const vpnApplicationLoadBalancerSecurityGroup = new SecurityGroup(this, 'VpcApplicationLoadBalancer', {vpc});
const elastiCacheMemcachedSecurityGroup = new SecurityGroup(this, 'ElastiCacheMemcachedSecurityGroup', {vpc});
const rdsAuroraClusterSecurityGroup = new SecurityGroup(this, 'RdsAuroraClusterSecurityGroup', {vpc});
const ecsFargateServiceSecurityGroup = new SecurityGroup(this, 'EcsFargateServiceSecurityGroup', {vpc});
const efsFileSystemSecurityGroup = new SecurityGroup(this, 'EfsFileSystemSecurityGroup', {vpc});
const elasticsearchDomainSecurityGroup = new SecurityGroup(this, 'ElasticsearchDomainSecurityGroup', {vpc});
const bastionHostSecurityGroup = new SecurityGroup(this, 'BastionHostSecurityGroup', {vpc});
const clientVpnSecurityGroup = new SecurityGroup(this, 'ClientVpnSecurityGroup', {vpc});
applicationLoadBalancerSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(443));
vpnApplicationLoadBalancerSecurityGroup.addIngressRule(clientVpnSecurityGroup, Port.tcp(443));
ecsFargateServiceSecurityGroup.addIngressRule(applicationLoadBalancerSecurityGroup, Port.tcp(80));
ecsFargateServiceSecurityGroup.addIngressRule(vpnApplicationLoadBalancerSecurityGroup, Port.tcp(80));
elastiCacheMemcachedSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(11211));
rdsAuroraClusterSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(3306));
efsFileSystemSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(2049));
elasticsearchDomainSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(9300));
clientVpnSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.udp(1194));
efsFileSystemSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(2049));
elastiCacheMemcachedSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(11211));
rdsAuroraClusterSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(3306));
elasticsearchDomainSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(9300));
const clientVpn = new CfnClientVpnEndpoint(this, 'ClientVpn', {
clientCidrBlock: '172.16.252.0/22',
serverCertificateArn: props.certificate.server,
connectionLogOptions: {enabled: false},
transportProtocol: 'udp',
vpcId: vpc.vpcId,
vpnPort: 1194,
securityGroupIds: [clientVpnSecurityGroup.securityGroupId],
authenticationOptions: [{
type: 'certificate-authentication',
mutualAuthentication: {clientRootCertificateChainArn: props.certificate.client}
}]
});
vpc.publicSubnets.forEach((subnet, i) => {
const _vpnTargetNetworkAssociation = new CfnClientVpnTargetNetworkAssociation(this, `ClientVpnPublicTargetNetworkAssociation${i}`, {
clientVpnEndpointId: clientVpn.ref,
subnetId: subnet.subnetId
});
const _vpnPublicRoute = new CfnClientVpnRoute(this, `ClientVpnPublicRoute${i}`, {
clientVpnEndpointId: clientVpn.ref,
destinationCidrBlock: '0.0.0.0/0',
targetVpcSubnetId: subnet.subnetId
});
_vpnPublicRoute.addDependsOn(_vpnTargetNetworkAssociation);
});
new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthorizationRuleForInternetAccess', {
clientVpnEndpointId: clientVpn.ref,
authorizeAllGroups: true,
targetNetworkCidr: '0.0.0.0/0'
});
new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthorizationRuleForVpcAccess', {
clientVpnEndpointId: clientVpn.ref,
authorizeAllGroups: true,
targetNetworkCidr: vpc.vpcCidrBlock
});
const rdsAuroraClusterPasswordSecret = new Secret(this, 'RdsAuroraClusterPasswordSecret', {
removalPolicy: props.removalPolicy,
generateSecretString: {excludeCharacters: ` ;+%{}` + `@'"\`/\\#`}
});
const rdsAuroraCluster = new ServerlessCluster(this, 'RdsAuroraServerlessCluster', {
vpc,
vpcSubnets: {subnetType: SubnetType.ISOLATED},
securityGroups: [rdsAuroraClusterSecurityGroup],
engine: DatabaseClusterEngine.AURORA_MYSQL,
credentials: {
username: props.databaseCredential.username,
password: SecretValue.secretsManager(rdsAuroraClusterPasswordSecret.secretArn)
},
defaultDatabaseName: props.databaseCredential.defaultDatabaseName,
deletionProtection: props.resourceDeletionProtection,
removalPolicy: props.removalPolicy,
scaling: {
minCapacity: AuroraCapacityUnit.ACU_1,
maxCapacity: AuroraCapacityUnit.ACU_16
},
backupRetention: Duration.days(7)
})
const rdsAuroraClusterPrivateDnsRecord = new CnameRecord(this, 'RdsAuroraClusterPrivateDnsRecord', {
zone: privateHostedZone,
recordName: `database.${privateHostedZone.zoneName}`,
domainName: rdsAuroraCluster.clusterEndpoint.hostname,
ttl: Duration.hours(1)
});
const elastiCacheMemcachedCluster = new CfnCacheCluster(this, 'ElastiCacheMemcachedCluster', {
cacheNodeType: 'cache.t3.micro',
engine: 'memcached',
azMode: 'cross-az',
numCacheNodes: 3,
cacheSubnetGroupName: new CfnSubnetGroup(this, 'ElastiCacheMemcachedClusterSubnetGroup', {
description: 'ElastiCacheMemcachedClusterSubnetGroup',
subnetIds: vpc.isolatedSubnets.map(subnet => subnet.subnetId)
}).ref,
vpcSecurityGroupIds: [elastiCacheMemcachedSecurityGroup.securityGroupId]
});
const elastiCacheMemcachedClusterPrivateDnsRecord = new CnameRecord(this, 'ElastiCacheMemcachedClusterPrivateDnsRecord', {
zone: privateHostedZone,
recordName: `cache.${privateHostedZone.zoneName}`,
domainName: elastiCacheMemcachedCluster.attrConfigurationEndpointAddress,
ttl: Duration.hours(1)
});
const elasticsearchServiceLinkRole = new CfnServiceLinkedRole(this, 'CfnElasticsearchServiceLinkRole', {
awsServiceName: 'es.amazonaws.com'
});
const elasticsearchDomain = new Domain(this, 'ElasticsearchDomain', {
version: ElasticsearchVersion.V7_7,
capacity: {dataNodes: 3, dataNodeInstanceType: 't3.small.elasticsearch'},
zoneAwareness: {enabled: true, availabilityZoneCount: 3},
encryptionAtRest: {enabled: true},
nodeToNodeEncryption: true,
ebs: {volumeSize: 10},
enforceHttps: true,
vpcOptions: {
subnets: vpc.isolatedSubnets,
securityGroups: [elasticsearchDomainSecurityGroup]
}
});
(elasticsearchDomain.node.defaultChild as CfnDomain).addDependsOn(elasticsearchServiceLinkRole);
const elasticsearchDomainPrivateDnsRecord = new CnameRecord(this, 'ElasticsearchDomainPrivateDnsRecord', {
zone: privateHostedZone,
recordName: `search.${privateHostedZone.zoneName}`,
domainName: elasticsearchDomain.domainEndpoint,
ttl: Duration.hours(1)
});
const fileSystem = new FileSystem(this, 'FileSystem', {
vpc,
vpcSubnets: {
subnetType: SubnetType.ISOLATED
},
securityGroup: efsFileSystemSecurityGroup,
performanceMode: PerformanceMode.GENERAL_PURPOSE,
lifecyclePolicy: LifecyclePolicy.AFTER_30_DAYS,
throughputMode: ThroughputMode.BURSTING,
encrypted: true,
removalPolicy: props.removalPolicy
});
const fileSystemAccessPoint = fileSystem.addAccessPoint('AccessPoint');
const fileSystemEndpointPrivateDnsRecord = new CnameRecord(this, 'FileSystemEndpointPrivateDnsRecord', {
zone: privateHostedZone,
recordName: `nfs.${privateHostedZone.zoneName}`,
domainName: `${fileSystem.fileSystemId}.efs.${this.region}.amazonaws.com`,
ttl: Duration.hours(1)
});
const bastionHost = new BastionHostLinux(this, 'BastionHost', {
vpc,
securityGroup: bastionHostSecurityGroup
});
bastionHost.instance.addUserData('mkdir -p /mnt/efs');
bastionHost.instance.addUserData(`mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport ${fileSystemEndpointPrivateDnsRecord.domainName}:/ /mnt/efs `);
const ecsCluster = new Cluster(this, 'EcsCluster', {
containerInsights: true,
vpc
});
const _ecsCluster = ecsCluster.node.defaultChild as CfnCluster;
_ecsCluster.capacityProviders = ['FARGATE', 'FARGATE_SPOT'];
_ecsCluster.defaultCapacityProviderStrategy = [
{
capacityProvider: 'FARGATE',
weight: 2,
base: 3
},
{
capacityProvider: 'FARGATE_SPOT',
weight: 1
}
];
const applicationLoadBalancer = new ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', {
vpc,
deletionProtection: props.resourceDeletionProtection,
http2Enabled: true,
internetFacing: true,
securityGroup: applicationLoadBalancerSecurityGroup,
vpcSubnets: {subnetType: SubnetType.PUBLIC}
});
applicationLoadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true');
applicationLoadBalancer.setAttribute('access_logs.s3.enabled', 'true');
applicationLoadBalancer.setAttribute('access_logs.s3.bucket', loggingBucket.bucketName);
applicationLoadBalancer.setAttribute('access_logs.s3.prefix', 'application-load-balancer');
applicationLoadBalancer.addListener('HttpListener', {
port: 80,
protocol: ApplicationProtocol.HTTP,
defaultAction: ListenerAction.redirect({protocol: 'HTTPS', port: '443'})
});
const httpsListener = applicationLoadBalancer.addListener('HttpsListener', {
port: 443,
protocol: ApplicationProtocol.HTTPS,
certificates: [acmCertificate]
});
const vpnAdminApplicationLoadBalancer = new ApplicationLoadBalancer(this, 'VpnAdminApplicationLoadBalancer', {
vpc,
deletionProtection: props.resourceDeletionProtection,
http2Enabled: true,
internetFacing: false,
securityGroup: vpnApplicationLoadBalancerSecurityGroup,
vpcSubnets: {subnetType: SubnetType.PRIVATE}
});
vpnAdminApplicationLoadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true');
vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.enabled', 'true');
vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.bucket', loggingBucket.bucketName);
vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.prefix', 'vpn-admin-application-load-balancer');
vpnAdminApplicationLoadBalancer.addListener('VpnAdminHttpListener', {
port: 80,
protocol: ApplicationProtocol.HTTP,
defaultAction: ListenerAction.redirect({protocol: 'HTTPS', port: '443'})
});
const vpnAdminHttpsListener = vpnAdminApplicationLoadBalancer.addListener('VpnAdminHttpsListener', {
port: 443,
protocol: ApplicationProtocol.HTTPS,
certificates: [acmCertificate]
});
const wordPressFargateTaskExecutionRole = new Role(this, 'WordpressFargateTaskExecutionRole', {
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')]
});
const wordPressFargateTaskRole = new Role(this, 'WordpressFargateTaskRole', {
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [ManagedPolicy.fromManagedPolicyArn(this, 'XRayDaemonWriteAccess', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess')],
inlinePolicies: {
_: new PolicyDocument({
statements: [
new PolicyStatement({
actions: ['s3:GetBucketLocation'],
resources: [staticContentBucket.bucketArn]
})
]
})
}
});
staticContentBucket.grantReadWrite(wordPressFargateTaskRole);
const wordPressFargateTaskDefinition = new FargateTaskDefinition(this, 'WordpressFargateTaskDefinition', {
memoryLimitMiB: 512,
cpu: 256,
executionRole: wordPressFargateTaskExecutionRole,
taskRole: wordPressFargateTaskRole,
});
wordPressFargateTaskDefinition.addVolume({
name: 'WordPressEfsVolume',
efsVolumeConfiguration: {
fileSystemId: fileSystem.fileSystemId,
transitEncryption: 'ENABLED',
authorizationConfig: {
accessPointId: fileSystemAccessPoint.accessPointId
}
}
});
const wordPressDockerImageAsset = new DockerImageAsset(this, 'WordPressDockerImageAsset', {directory: path.join(__dirname, 'images/wordpress')});
const nginxDockerImageAsset = new DockerImageAsset(this, 'NginxDockerImageAsset', {directory: path.join(__dirname, 'images/nginx')});
const wordPressContainer = wordPressFargateTaskDefinition.addContainer('WordPress', {
image: ContainerImage.fromDockerImageAsset(wordPressDockerImageAsset),
environment: {
WORDPRESS_DB_HOST: rdsAuroraClusterPrivateDnsRecord.domainName,
WORDPRESS_DB_USER: props.databaseCredential.username,
WORDPRESS_DB_NAME: props.databaseCredential.defaultDatabaseName,
},
secrets: {
WORDPRESS_DB_PASSWORD: EcsSecret.fromSecretsManager(rdsAuroraClusterPasswordSecret)
},
logging: LogDriver.awsLogs({
streamPrefix: `${this.stackName}WordPressContainerLog`,
logRetention: RetentionDays.ONE_MONTH
})
});
wordPressContainer.addMountPoints({
readOnly: false,
containerPath: '/var/www/html',
sourceVolume: 'WordPressEfsVolume'
});
const nginxContainer = wordPressFargateTaskDefinition.addContainer('Nginx', {
image: ContainerImage.fromDockerImageAsset(nginxDockerImageAsset),
logging: LogDriver.awsLogs({
streamPrefix: `${this.stackName}NginxContainerLog`,
logRetention: RetentionDays.ONE_MONTH
}),
environment: {
SERVER_NAME: props.hostname,
MEMCACHED_HOST: elastiCacheMemcachedClusterPrivateDnsRecord.domainName,
NGINX_ENTRYPOINT_QUIET_LOGS: '1'
}
});
nginxContainer.addPortMappings({
hostPort: 80,
containerPort: 80,
protocol: Protocol.TCP
});
nginxContainer.addMountPoints({
readOnly: false,
containerPath: '/var/www/html',
sourceVolume: 'WordPressEfsVolume'
});
const xrayContainer = wordPressFargateTaskDefinition.addContainer('XRay', {
image: ContainerImage.fromRegistry('amazon/aws-xray-daemon'),
logging: LogDriver.awsLogs({
streamPrefix: `${this.stackName}XRayContainerLog`,
logRetention: RetentionDays.ONE_MONTH
}),
entryPoint: ['/usr/bin/xray', '-b', '127.0.0.1:2000', '-l', 'dev', '-o'],
user: '1337'
});
xrayContainer.addPortMappings({
containerPort: 2000,
protocol: Protocol.UDP
})
const _wordPressFargateServiceTargetGroup = new CfnTargetGroup(this, 'CfnWordPressFargateServiceTargetGroup', {
matcher: {
httpCode: '200,301,302'
},
port: 80,
protocol: 'HTTP',
targetGroupAttributes: [
{
key: 'stickiness.enabled',
value: 'true'
},
{
key: 'stickiness.type',
value: 'lb_cookie'
},
{
key: 'stickiness.lb_cookie.duration_seconds',
value: '604800'
}
],
targetType: 'ip',
vpcId: vpc.vpcId,
unhealthyThresholdCount: 5,
healthCheckTimeoutSeconds: 45,
healthCheckIntervalSeconds: 60,
});
const wordPressFargateServiceTargetGroup = ApplicationTargetGroup.fromTargetGroupAttributes(this, 'WordPressFargateServiceTargetGroup', {
loadBalancerArns: applicationLoadBalancer.loadBalancerArn,
targetGroupArn: _wordPressFargateServiceTargetGroup.ref
});
httpsListener.addTargetGroups('WordPress', {targetGroups: [wordPressFargateServiceTargetGroup]});
const _wordPressVpnAdminFargateServiceTargetGroup = new CfnTargetGroup(this, 'CfnWordPressVpnAdminFargateServiceTargetGroup', {
matcher: {
httpCode: '200,301,302'
},
port: 80,
protocol: 'HTTP',
targetGroupAttributes: [
{
key: 'stickiness.enabled',
value: 'true'
},
{
key: 'stickiness.type',
value: 'lb_cookie'
},
{
key: 'stickiness.lb_cookie.duration_seconds',
value: '604800'
}
],
targetType: 'ip',
vpcId: vpc.vpcId,
unhealthyThresholdCount: 5,
healthCheckTimeoutSeconds: 45,
healthCheckIntervalSeconds: 60,
});
const wordPressVpnAdminFargateServiceTargetGroup = ApplicationTargetGroup.fromTargetGroupAttributes(this, 'WordPressVpnAdminFargateServiceTargetGroup', {
loadBalancerArns: vpnAdminApplicationLoadBalancer.loadBalancerArn,
targetGroupArn: _wordPressVpnAdminFargateServiceTargetGroup.ref
});
vpnAdminHttpsListener.addTargetGroups('WordPressVpnAdmin', {targetGroups: [wordPressVpnAdminFargateServiceTargetGroup]});
const _wordPressFargateService = new CfnService(this, 'CfnWordPressFargateService', {
cluster: ecsCluster.clusterArn,
desiredCount: 3,
deploymentConfiguration: {
maximumPercent: 200,
minimumHealthyPercent: 50
},
deploymentController: {
type: 'ECS'
},
healthCheckGracePeriodSeconds: 60,
loadBalancers: [
{
containerName: nginxContainer.containerName,
containerPort: 80,
targetGroupArn: wordPressFargateServiceTargetGroup.targetGroupArn
},
{
containerName: nginxContainer.containerName,
containerPort: 80,
targetGroupArn: wordPressVpnAdminFargateServiceTargetGroup.targetGroupArn
}
],
networkConfiguration: {
awsvpcConfiguration: {
assignPublicIp: 'DISABLED',
securityGroups: [ecsFargateServiceSecurityGroup.securityGroupId],
subnets: vpc.privateSubnets.map(subnet => subnet.subnetId)
}
},
platformVersion: '1.4.0',
taskDefinition: wordPressFargateTaskDefinition.taskDefinitionArn
});
_wordPressFargateService.addOverride('DependsOn', [
this.getLogicalId(httpsListener.node.defaultChild as CfnListener),
this.getLogicalId(vpnAdminHttpsListener.node.defaultChild as CfnListener)
]);
const wordPressServiceScaling = new ScalableTarget(this, 'WordPressFargateServiceScaling', {
scalableDimension: 'ecs:service:DesiredCount',
minCapacity: 3,
maxCapacity: 300,
serviceNamespace: ServiceNamespace.ECS,
resourceId: `service/${ecsCluster.clusterName}/${_wordPressFargateService.attrName}`
});
wordPressServiceScaling.scaleToTrackMetric('RequestCountPerTarget', {
predefinedMetric: PredefinedMetric.ALB_REQUEST_COUNT_PER_TARGET,
resourceLabel: `${applicationLoadBalancer.loadBalancerFullName}/${_wordPressFargateServiceTargetGroup.attrTargetGroupFullName}`,
targetValue: 4096,
scaleInCooldown: Duration.minutes(5),
scaleOutCooldown: Duration.minutes(5)
});
wordPressServiceScaling.scaleToTrackMetric('TargetResponseTime', {
customMetric: applicationLoadBalancer.metricTargetResponseTime(),
targetValue: 4,
scaleInCooldown: Duration.minutes(3),
scaleOutCooldown: Duration.minutes(3)
});
const adminWhitelistIpSet = new CfnIPSet(this, 'AdminWhitelistIpSet', {
addresses: [...props.whitelistIpAddress],
scope: 'REGIONAL',
ipAddressVersion: 'IPV4'
});
const wordPressCloudFrontDistributionWafWebAcl = new CfnWebACL(this, 'WordPressCloudFrontDistributionWafWebAcl', {
defaultAction: {allow: {}},
scope: 'CLOUDFRONT',
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'CloudFrontDistributionWebAclMetric'
},
rules: [
{
name: 'RuleWithAWSManagedRulesCommonRuleSet',
priority: 0,
overrideAction: {none: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'CommonRuleSetMetric'
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
excludedRules: [{name: 'SizeRestrictions_BODY'}, {name: 'GenericRFI_BODY'}, {name: 'GenericRFI_URIPATH'}, {name: 'GenericRFI_QUERYARGUMENTS'}]
}
}
},
{
name: 'RuleWithAWSManagedRulesKnownBadInputsRuleSet',
priority: 1,
overrideAction: {none: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'KnownBadInputsRuleSetMetric'
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesKnownBadInputsRuleSet',
excludedRules: []
}
}
},
{
name: 'RuleWithAWSManagedRulesWordPressRuleSet',
priority: 2,
overrideAction: {none: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'WordPressRuleSetMetric'
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesWordPressRuleSet',
excludedRules: []
}
}
},
{
name: 'RuleWithAWSManagedRulesPHPRuleSet',
priority: 3,
overrideAction: {none: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'PHPRuleSetMetric'
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesPHPRuleSet',
excludedRules: []
}
}
},
{
name: 'RuleWithAWSManagedRulesSQLiRuleSet',
priority: 4,
overrideAction: {none: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesSQLiRuleSetMetric'
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesSQLiRuleSet',
excludedRules: []
}
}
},
{
name: 'RuleWithAWSManagedRulesAmazonIpReputationList',
priority: 5,
overrideAction: {none: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AmazonIpReputationListMetric'
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesAmazonIpReputationList',
excludedRules: []
}
}
}
]
});
const applicationLoadBalancerWebAcl = new CfnWebACL(this, 'ApplicationLoadBalancerWafWebAcl', {
defaultAction: {block: {}},
scope: 'REGIONAL',
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'ApplicationLoadBalancerWebAclMetric'
},
rules: [
{
name: 'RuleToAllowNonAdminRequest',
priority: 0,
action: {allow: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'RuleToAllowNonAdminRequestMetric'
},
statement: {
andStatement: {
statements: [
{
notStatement: {
statement: {
byteMatchStatement: {
fieldToMatch: {uriPath: {}},
positionalConstraint: "STARTS_WITH",
searchString: '/wp-admin',
textTransformations: [{type: 'NONE', priority: 0}]
}
}
}
},
{
byteMatchStatement: {
fieldToMatch: {
singleHeader: {
Name: 'X_Request_From_CloudFront'
}
},
positionalConstraint: 'EXACTLY',
searchString: props.cloudFrontHashHeader,
textTransformations: [{type: 'NONE', priority: 0}]
}
}
]
}
}
},
{
name: 'RuleToAllowRequestWhitelistedIpSourceToAdminPage',
priority: 1,
action: {allow: {}},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'RuleToAllowRequestWhitelistedIpSourceToAdminPageMetric'
},
statement: {
andStatement: {
statements: [
{
byteMatchStatement: {
fieldToMatch: {
singleHeader: {
Name: 'X_Request_From_CloudFront'
}
},
positionalConstraint: 'EXACTLY',
searchString: props.cloudFrontHashHeader,
textTransformations: [{type: 'NONE', priority: 0}]
}
},
{
byteMatchStatement: {
fieldToMatch: {uriPath: {}},
positionalConstraint: "STARTS_WITH",
searchString: '/wp-admin',
textTransformations: [{type: 'NONE', priority: 0}]
}
},
{
ipSetReferenceStatement: {
arn: adminWhitelistIpSet.attrArn,
ipSetForwardedIpConfig: {
headerName: 'X-Forwarded-For',
position: 'ANY',
fallbackBehavior: 'NO_MATCH'
}
}
}
]
}
}
}
]
});
new CfnWebACLAssociation(this, 'ApplicationLoadBalancerWafWebAclAssociation', {
resourceArn: applicationLoadBalancer.loadBalancerArn,
webAclArn: applicationLoadBalancerWebAcl.attrArn
});
const wordPressDistribution = new CloudFrontWebDistribution(this, 'WordPressDistribution', {
originConfigs: [
{
customOriginSource: {
domainName: applicationLoadBalancer.loadBalancerDnsName,
originProtocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
originReadTimeout: Duration.minutes(1),
originHeaders: {
'X_Request_From_CloudFront': props.cloudFrontHashHeader
}
},
behaviors: [
{
isDefaultBehavior: true,
forwardedValues: {
queryString: true,
cookies: {
forward: 'whitelist',
whitelistedNames: [
'comment_*',
'wordpress_*',
'wp-settings-*'
]
},
headers: [
'Host',
'CloudFront-Forwarded-Proto',
'CloudFront-Is-Mobile-Viewer',
'CloudFront-Is-Tablet-Viewer',
'CloudFront-Is-Desktop-Viewer'
]
},
cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
allowedMethods: CloudFrontAllowedMethods.ALL
},
{
pathPattern: 'wp-admin/*',
forwardedValues: {
queryString: true,
cookies: {
forward: 'all'
},
headers: ['*']
},
cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
allowedMethods: CloudFrontAllowedMethods.ALL
},
{
pathPattern: 'wp-login.php',
forwardedValues: {
queryString: true,
cookies: {
forward: 'all'
},
headers: ['*']
},
cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
allowedMethods: CloudFrontAllowedMethods.ALL
}
]
}
],
priceClass: PriceClass.PRICE_CLASS_ALL,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
httpVersion: HttpVersion.HTTP2,
defaultRootObject: '',
viewerCertificate: ViewerCertificate.fromAcmCertificate(acmCertificate, {aliases: [props.hostname]}),
webACLId: wordPressCloudFrontDistributionWafWebAcl.attrArn,
loggingConfig: {
bucket: loggingBucket,
prefix: 'wordpress-distribution'
}
});
(wordPressDistribution.node.defaultChild as CfnDistribution).addDependsOn(wordPressCloudFrontDistributionWafWebAcl);
const staticContentBucketOriginAccessIdentity = new OriginAccessIdentity(this, 'StaticContentBucketOriginAccessIdentity');
staticContentBucket.grantRead(staticContentBucketOriginAccessIdentity);
const staticContentDistribution = new CloudFrontWebDistribution(this, 'StaticContentDistribution', {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: staticContentBucket,
originAccessIdentity: staticContentBucketOriginAccessIdentity
},
behaviors: [
{
isDefaultBehavior: true,
forwardedValues: {
queryString: true,
cookies: {
forward: 'none'
}
},
cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD,
allowedMethods: CloudFrontAllowedMethods.GET_HEAD
}
]
},
],
priceClass: PriceClass.PRICE_CLASS_ALL,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
httpVersion: HttpVersion.HTTP2,
defaultRootObject: '',
viewerCertificate: ViewerCertificate.fromAcmCertificate(acmCertificate, {aliases: [`static.${props.hostname}`]}),
loggingConfig: {
bucket: loggingBucket,
prefix: 'static-content-distribution'
}
});
const backupVault = new BackupVault(this, 'BackupVault', {
backupVaultName: 'AwsServerlessWordPressBackupVault',
removalPolicy: props.removalPolicy
});
const backupPlan = BackupPlan.dailyMonthly1YearRetention(this, 'BackupPlan', backupVault);
backupPlan.addSelection('BackupPlanSelection', {
resources: [
BackupResource.fromEfsFileSystem(fileSystem),
BackupResource.fromArn(this.formatArn({
resource: 'cluster',
service: 'rds',
sep: ':',
resourceName: rdsAuroraCluster.clusterIdentifier
}))
]
});
const awsConfigOnComplianceSnsTopic = new Topic(this, 'AwsConfigOnComplianceSnsTopic', {masterKey: awsManagedSnsKmsKey});
props.snsEmailSubscription.forEach(email => awsConfigOnComplianceSnsTopic.addSubscription(new EmailSubscription(email)));
const configurationRecorderRole = new Role(this, 'ConfigurationRole', {
assumedBy: new ServicePrincipal('config.amazonaws.com')
});
configurationRecorderRole.addToPolicy(new PolicyStatement({
actions: ['s3:PutObject'],
resources: [`${loggingBucket.bucketArn}/config/AWSLogs/${this.account}/*`],
conditions: {
StringLike: {
"s3:x-amz-acl": "bucket-owner-full-control"
}
}
}));
configurationRecorderRole.addToPolicy(new PolicyStatement({
actions: ['s3:GetBucketAcl'],
resources: [loggingBucket.bucketArn]
}))
const configurationRecorder = new CfnConfigurationRecorder(this, 'ConfigurationRecorder', {
recordingGroup: {
allSupported: true,
includeGlobalResourceTypes: true
},
roleArn: configurationRecorderRole.roleArn
});
const deliveryChannel = new CfnDeliveryChannel(this, 'DeliveryChannel', {
configSnapshotDeliveryProperties: {
deliveryFrequency: 'One_Hour'
},
s3BucketName: loggingBucket.bucketName,
s3KeyPrefix: 'config'
});
const ruleScope = RuleScope.fromTag(globalTagKey, globalTagValue);
const awsConfigManagesRules = [
new ManagedRule(this, 'AwsConfigManagedRuleVpcFlowLogsEnabled', {
identifier: 'VPC_FLOW_LOGS_ENABLED',
inputParameters: {trafficType: 'ALL'},
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleVpcSgOpenOnlyToAuthorizedPorts', {
identifier: 'VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS',
inputParameters: {authorizedTcpPorts: '443'},
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleInternetGatewayAuthorizedVpcOnly', {
identifier: 'INTERNET_GATEWAY_AUTHORIZED_VPC_ONLY',
inputParameters: {AuthorizedVpcIds: vpc.vpcId},
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleAcmCertificateExpirationCheck', {
identifier: 'ACM_CERTIFICATE_EXPIRATION_CHECK',
inputParameters: {daysToExpiration: 90},
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleAutoScalingGroupElbHealthcheckRequired', {
identifier: 'AUTOSCALING_GROUP_ELB_HEALTHCHECK_REQUIRED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleIncomingSshDisabled', {
identifier: 'INCOMING_SSH_DISABLED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleSnsEncryptedKms', {
identifier: 'SNS_ENCRYPTED_KMS',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleElbDeletionProtection', {
identifier: 'ELB_DELETION_PROTECTION_ENABLED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleElbLoggingEnabled', {
identifier: 'ELB_LOGGING_ENABLED',
inputParameters: {s3BucketNames: loggingBucket.bucketName},
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleAlbHttpDropInvalidHeaderEnabled', {
identifier: 'ALB_HTTP_DROP_INVALID_HEADER_ENABLED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleAlbHttpToHttpsRedirectionCheck', {
identifier: 'ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleAlbWafEnabled', {
identifier: 'ALB_WAF_ENABLED',
inputParameters: {wafWebAclIds: applicationLoadBalancerWebAcl.attrArn},
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleCloudFrontOriginAccessIdentityEnabled', {
identifier: 'CLOUDFRONT_ORIGIN_ACCESS_IDENTITY_ENABLED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleCloudFrontViewerPolicyHttps', {
identifier: 'CLOUDFRONT_VIEWER_POLICY_HTTPS',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleEfsInBackupPlan', {
identifier: 'EFS_IN_BACKUP_PLAN',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleEfsEncryptedCheck', {
identifier: 'EFS_ENCRYPTED_CHECK',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleRdsClusterDeletionProtectionEnabled', {
identifier: 'RDS_CLUSTER_DELETION_PROTECTION_ENABLED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleEdsInBackupPlan', {
identifier: 'RDS_IN_BACKUP_PLAN',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleS3BucketPublicReadProhibited', {
identifier: 'S3_BUCKET_PUBLIC_READ_PROHIBITED',
ruleScope
}),
new ManagedRule(this, 'AwsConfigManagedRuleS3BucketPublicWriteProhibited', {
identifier: 'S3_BUCKET_PUBLIC_WRITE_PROHIBITED',
ruleScope
})
]
awsConfigManagesRules.forEach(rule => {
rule.onComplianceChange('TopicEvent', {target: new SnsTopic(awsConfigOnComplianceSnsTopic)});
(rule.node.defaultChild as CfnConfigRule).addDependsOn(configurationRecorder);
(rule.node.defaultChild as CfnConfigRule).addDependsOn(deliveryChannel);
});
const awsConfigCloudFormationStackDriftDetectionCheckRule = new CloudFormationStackDriftDetectionCheck(this, 'AwsConfigCloudFormationStackDriftDetectionCheck', {ownStackOnly: true});
awsConfigCloudFormationStackDriftDetectionCheckRule.onComplianceChange('TopicEvent', {target: new SnsTopic(awsConfigOnComplianceSnsTopic)});
(awsConfigCloudFormationStackDriftDetectionCheckRule.node.defaultChild as CfnConfigRule).addDependsOn(configurationRecorder);
(awsConfigCloudFormationStackDriftDetectionCheckRule.node.defaultChild as CfnConfigRule).addDependsOn(deliveryChannel);
new CfnGroup(this, 'ResourceGroup', {
name: 'ServerlessWordPressResourceGroup',
resourceQuery: {
type: 'TAG_FILTERS_1_0',
query: {
resourceTypeFilters: ['AWS::AllSupported'],
tagFilters: [
{
key: globalTagKey,
values: [globalTagValue]
}
]
}
}
});
const rootDnsRecord = new ARecord(this, 'RootDnsRecord', {
zone: publicHostedZone,
recordName: props.hostname,
target: RecordTarget.fromAlias(new CloudFrontTarget(wordPressDistribution))
});
const vpnAdminPublicRecord = new ARecord(this, 'VpnAdminPublicDnsRecord', {
zone: publicHostedZone,
recordName: `admin.${props.hostname}`,
target: RecordTarget.fromAlias(new LoadBalancerTarget(vpnAdminApplicationLoadBalancer))
});
const vpnAdminPrivateRecord = new ARecord(this, 'VpnAdminPrivateDnsRecord', {
zone: privateHostedZone,
recordName: `admin.${privateHostedZone.zoneName}`,
target: RecordTarget.fromAlias(new LoadBalancerTarget(vpnAdminApplicationLoadBalancer))
});
const staticContentDnsRecord = new ARecord(this, 'StaticContentDnsRecord', {
zone: publicHostedZone,
recordName: `static.${props.hostname}`,
target: RecordTarget.fromAlias(new CloudFrontTarget(staticContentDistribution))
});
new CfnOutput(this, 'RootHostname', {
value: rootDnsRecord.domainName
});
new CfnOutput(this, 'VpnAdminPublicHostname', {
value: vpnAdminPublicRecord.domainName
});
new CfnOutput(this, 'VpnAdminPrivateHostname', {
value: vpnAdminPrivateRecord.domainName
});
new CfnOutput(this, 'StaticContentHostname', {
value: staticContentDnsRecord.domainName
});
new CfnOutput(this, 'RdsAuroraServerlessClusterPrivateHostname', {
value: rdsAuroraClusterPrivateDnsRecord.domainName
});
new CfnOutput(this, 'ElastiCacheMemcachedClusterPrivateHostname', {
value: elastiCacheMemcachedClusterPrivateDnsRecord.domainName
});
new CfnOutput(this, 'ElasticsearchDomainPrivateHostname', {
value: elasticsearchDomainPrivateDnsRecord.domainName
});
new CfnOutput(this, 'EfsFileSystemPrivateHostname', {
value: fileSystemEndpointPrivateDnsRecord.domainName
});
}
Example #19
Source File: city-stack.ts From MDDL with MIT License | 4 votes |
/**
* Add hosting for a web app
* @param appName The name of the web app
* @param hostedDomainConfig The configuration for its hosting domain (optional)
* @param hostedZone The hosted zone (optional)
*/
private addHosting(
appName: string,
hostedDomainConfig?: HostedDomain,
hostedZone?: IHostedZone,
) {
//Create Certificate
let viewerCertificate: ViewerCertificate | undefined
if (hostedDomainConfig) {
const certificate = Certificate.fromCertificateArn(
this,
`${appName}Certificate`,
hostedDomainConfig.certificateArn,
)
viewerCertificate = ViewerCertificate.fromAcmCertificate(certificate, {
aliases: [hostedDomainConfig.domain],
securityPolicy: SecurityPolicyProtocol.TLS_V1_2_2019,
})
}
// Random part included for easier update if needed
const bucketName = `${this.stackName}-${appName}-AEBE24AF`.toLowerCase()
this.bucketNames[appName] = bucketName
// Create App Bucket
const bucket = new Bucket(this, `${appName}Bucket`, {
blockPublicAccess: {
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
},
bucketName,
})
// Create App Origin Access Identity
const originAccessIdentity = new OriginAccessIdentity(
this,
`${appName}OriginAccessIdentity`,
{
comment: appName,
},
)
bucket.grantRead(originAccessIdentity)
// Create App CloudFront Distribution
const cloudFrontDistribution = new CloudFrontWebDistribution(
this,
`${appName}CloudFrontWebDistribution`,
{
defaultRootObject: 'index.html',
errorConfigurations: [
{
errorCode: 403,
responseCode: 200,
responsePagePath: '/index.html',
},
{
errorCode: 404,
responseCode: 200,
responsePagePath: '/index.html',
},
],
originConfigs: [
{
s3OriginSource: {
s3BucketSource: bucket,
originAccessIdentity: originAccessIdentity,
},
behaviors: [
...['/index.html', '/sw.js'].map(
(shortCachePathPattern) =>
({
maxTtl: Duration.minutes(5),
minTtl: Duration.minutes(5),
defaultTtl: Duration.minutes(5),
pathPattern: shortCachePathPattern,
compress: true,
} as any),
),
{
isDefaultBehavior: true,
compress: true,
},
],
},
],
viewerCertificate,
},
)
cloudFrontDistribution.node.addDependency(bucket, originAccessIdentity)
// Create Domain Record
if (hostedDomainConfig && hostedZone) {
const { domain } = hostedDomainConfig
const aliasRecord = new ARecord(this, `${appName}AliasRecord`, {
zone: hostedZone,
recordName: domain,
target: RecordTarget.fromAlias(
new CloudFrontTarget(cloudFrontDistribution),
),
})
aliasRecord.node.addDependency(cloudFrontDistribution)
}
return {
domain: hostedDomainConfig
? hostedDomainConfig.domain
: cloudFrontDistribution.distributionDomainName,
}
}
Example #20
Source File: city-stack.ts From MDDL with MIT License | 4 votes |
/**
* Create the bucket for storing uploads
* @param kmsKey The encryption key for the bucket
* @param corsOrigins CORS origins for the bucket policy
*/
private createUploadsBucket(kmsKey: IKey, corsOrigins: string[]) {
const bucket = new Bucket(this, 'DocumentsBucket', {
blockPublicAccess: {
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
},
encryptionKey: kmsKey,
removalPolicy: RemovalPolicy.RETAIN,
cors: [
{
allowedMethods: [HttpMethods.POST, HttpMethods.GET],
allowedOrigins: corsOrigins,
maxAge: 3000,
allowedHeaders: [
'x-amz-*',
'content-type',
'content-disposition',
'content-length',
],
},
],
})
bucket.addLifecycleRule({
expiration: Duration.days(14),
prefix: CityStack.documentsBucketCollectionsPrefix,
})
// this will mean uploads will only be accepted over HTTPS
bucket.addToResourcePolicy(
new PolicyStatement({
sid: 'DenyRequestsOverInsecureTransport',
effect: Effect.DENY,
actions: ['s3:*'],
principals: [new AnyPrincipal()],
resources: [bucket.arnForObjects('*')],
conditions: {
Bool: {
'aws:SecureTransport': false,
},
},
}),
)
// this will mean server side encryption either:
// 1. Cannot be specified in the request (which means the default bucket encryption will be used - specified by the KMS key above)
// 2. Or the aws:kms type must be specified, and if a key is specified, it must be the KMS key for the stack
// This is to work around multipart uploads streamed from the bucket itself which maintain the same encryption headers as the object streamed
// All API PutObject actions do not explicitly set a KMS key.
bucket.addToResourcePolicy(
new PolicyStatement({
sid: 'DenySpecifiedNonKmsEncryptionHeader',
effect: Effect.DENY,
principals: [new AnyPrincipal()],
actions: ['s3:PutObject'],
resources: [bucket.arnForObjects('*')],
conditions: {
StringNotEqualsIfExists: {
's3:x-amz-server-side-encryption': 'aws:kms',
's3:x-amz-server-side-encryption-aws-kms-key-id': kmsKey.keyArn,
},
},
}),
)
// this will mean requests must be signed with Signature V4 which is the latest supported algorithm
bucket.addToResourcePolicy(
new PolicyStatement({
sid: 'DenyRequestsNotUsingSignatureV4',
effect: Effect.DENY,
principals: [new AnyPrincipal()],
actions: ['s3:*'],
resources: [bucket.arnForObjects('*')],
conditions: {
StringNotEquals: {
's3:signatureversion': 'AWS4-HMAC-SHA256',
},
},
}),
)
return {
bucket,
}
}
Example #21
Source File: index.ts From aws-cdk-dynamodb-seeder with Apache License 2.0 | 4 votes |
constructor(scope: Construct, id: string, props: Props) {
super(scope, id);
if (!props.setup || !Array.isArray(props.setup)) throw new Error('setup value must be an array of JSON objects');
this.props = props;
const destinationBucket = new Bucket(this, 'acds-bucket', {
removalPolicy: RemovalPolicy.DESTROY,
});
tmp.setGracefulCleanup();
tmp.dir((err, dir) => {
if (err) throw err;
this.writeTempFile(dir, 'setup.json', props.setup);
if (props.teardown) {
this.writeTempFile(dir, 'teardown.json', props.teardown);
}
new BucketDeployment(this, id, {
sources: [Source.asset(dir)],
destinationBucket,
retainOnDelete: false,
});
});
const fn = new Function(this, 'handler', {
runtime: Runtime.NODEJS_12_X,
handler: 'index.handler',
timeout: Duration.seconds(900),
code: Code.fromInline(`
console.log('function loaded');
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const writeTypeFromAction = (action) => {
if (action === "Put")
return "Item";
if (action === "Delete")
return "Key";
}
const run = async (filename, action) => {
console.log('reading from s3');
const data = await s3.getObject({
Bucket: "${destinationBucket.bucketName}",
Key: filename
}).promise();
console.log('finished reading from s3');
console.log('transforming seed data');
const seed = JSON.parse(data.Body.toString());
console.log('finished transforming seed data');
const documentClient = new AWS.DynamoDB.DocumentClient({
convertEmptyValues: true
});
console.log('sending data to dynamodb');
do {
const requests = [];
const batch = seed.splice(0, 25);
for (let i = 0; i < batch.length; i++) {
requests.push({
[action + "Request"]: {
[writeTypeFromAction(action)]: batch[i]
}
});
}
await documentClient.batchWrite({
RequestItems: {
'${props.table.tableName}': [...requests]
}
}).promise();
}
while (seed.length > 0);
console.log('finished sending data to dynamodb');
}
exports.handler = async (event) => {
if (event.mode === "delete")
await run("teardown.json", "Delete");
if (event.mode === "create" || event.mode === "update")
await run("setup.json", "Put");
}`),
});
destinationBucket.grantRead(fn);
props.table.grantWriteData(fn);
const onEvent = new AwsCustomResource(this, 'on-event', {
onCreate: {
...this.callLambdaOptions(),
parameters: {
FunctionName: fn.functionArn,
InvokeArgs: JSON.stringify({
mode: 'create',
}),
},
},
onDelete: props.teardown
? {
...this.callLambdaOptions(),
parameters: {
FunctionName: fn.functionArn,
InvokeArgs: JSON.stringify({
mode: 'delete',
}),
},
}
: undefined,
onUpdate: props.refreshOnUpdate
? {
...this.callLambdaOptions(),
parameters: {
FunctionName: fn.functionArn,
InvokeArgs: JSON.stringify({
mode: 'update',
}),
},
}
: undefined,
policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
});
fn.grantInvoke(onEvent);
}
Example #22
Source File: stack.ts From keycloak-on-aws with Apache License 2.0 | 4 votes |
constructor(scope: Construct, id: string, props: KeycloakStackProps = {}) {
super(scope, id, props);
const dbMsg = props.auroraServerless ? 'using aurora serverless' : 'rds mysql';
const vpcMsg = props.fromExistingVPC ? 'existing vpc' : 'new vpc';
this.setDescription(`(SO8021) - Deploy keycloak ${dbMsg} with ${vpcMsg}. template version: ${process.env.VERSION}`);
const certificateArnParam = this.makeParam('CertificateArn', {
type: 'String',
description: 'Certificate Arn for Application Load Balancer',
minLength: 5,
});
this.addGroupParam({ 'Application Load Balancer Settings': [certificateArnParam] });
this._keycloakSettings.certificateArn = certificateArnParam.valueAsString;
if (!props.auroraServerless) {
const databaseInstanceType = this.makeParam('DatabaseInstanceType', {
type: 'String',
description: 'Instance type to be used for the core instances',
allowedValues: INSTANCE_TYPES,
default: 'r5.large',
});
this.addGroupParam({ 'Database Instance Settings': [databaseInstanceType] });
this._keycloakSettings.databaseInstanceType = new ec2.InstanceType(databaseInstanceType.valueAsString);
}
if (props.fromExistingVPC) {
const vpcIdParam = this.makeParam('VpcId', {
type: 'AWS::EC2::VPC::Id',
description: 'Your VPC Id',
});
const pubSubnetsParam = this.makeParam('PubSubnets', {
type: 'List<AWS::EC2::Subnet::Id>',
description: 'Public subnets (Choose two)',
});
const privSubnetsParam = this.makeParam('PrivSubnets', {
type: 'List<AWS::EC2::Subnet::Id>',
description: 'Private subnets (Choose two)',
});
const dbSubnetsParam = this.makeParam('DBSubnets', {
type: 'List<AWS::EC2::Subnet::Id>',
description: 'Database subnets (Choose two)',
});
this.addGroupParam({ 'VPC Settings': [vpcIdParam, pubSubnetsParam, privSubnetsParam, dbSubnetsParam] });
const azs = ['a', 'b'];
const vpc = ec2.Vpc.fromVpcAttributes(this, 'VpcAttr', {
vpcId: vpcIdParam.valueAsString,
vpcCidrBlock: Aws.NO_VALUE,
availabilityZones: azs,
publicSubnetIds: azs.map((_, index) => Fn.select(index, pubSubnetsParam.valueAsList)),
privateSubnetIds: azs.map((_, index) => Fn.select(index, privSubnetsParam.valueAsList)),
isolatedSubnetIds: azs.map((_, index) => Fn.select(index, dbSubnetsParam.valueAsList)),
});
Object.assign(this._keycloakSettings, {
vpc,
publicSubnets: { subnets: vpc.publicSubnets },
privateSubnets: { subnets: vpc.privateSubnets },
databaseSubnets: { subnets: vpc.isolatedSubnets },
});
}
const minContainersParam = this.makeParam('MinContainers', {
type: 'Number',
description: 'minimum containers count',
default: 2,
minValue: 2,
});
const maxContainersParam = this.makeParam('MaxContainers', {
type: 'Number',
description: 'maximum containers count',
default: 10,
minValue: 2,
});
const targetCpuUtilizationParam = this.makeParam('AutoScalingTargetCpuUtilization', {
type: 'Number',
description: 'Auto scaling target cpu utilization',
default: 75,
minValue: 0,
});
this.addGroupParam({ 'AutoScaling Settings': [minContainersParam, maxContainersParam, targetCpuUtilizationParam] });
const javaOptsParam = this.makeParam('JavaOpts', {
type: 'String',
description: 'JAVA_OPTS environment variable',
});
this.addGroupParam({ 'Environment variable': [javaOptsParam] });
new KeyCloak(this, 'KeyCloak', {
vpc: this._keycloakSettings.vpc,
publicSubnets: this._keycloakSettings.publicSubnets,
privateSubnets: this._keycloakSettings.privateSubnets,
databaseSubnets: this._keycloakSettings.databaseSubnets,
certificateArn: this._keycloakSettings.certificateArn,
auroraServerless: props.auroraServerless,
databaseInstanceType: this._keycloakSettings.databaseInstanceType,
stickinessCookieDuration: Duration.days(7),
nodeCount: minContainersParam.valueAsNumber,
autoScaleTask: {
min: minContainersParam.valueAsNumber,
max: maxContainersParam.valueAsNumber,
targetCpuUtilization: targetCpuUtilizationParam.valueAsNumber,
},
env: {
JAVA_OPTS: javaOptsParam.valueAsString,
},
});
}
Example #23
Source File: ecs-service.ts From amazon-efs-integrations with MIT No Attribution | 4 votes |
static create(
serviceType: ServiceType,
cluster: Cluster,
fileSystem?: FileSystem,
efsAccessPoints?: EfsAccessPoints,
props?: ApplicationLoadBalancedEc2ServiceProps | ApplicationLoadBalancedFargateServiceProps,
) {
let service: ApplicationLoadBalancedEc2Service | ApplicationLoadBalancedFargateService;
const containerImage = 'coderaiser/cloudcmd:14.3.10-alpine';
if (serviceType === ServiceType.EC2) {
/*
Initial task definition with the 80:80 port mapping is needed here to ensure the ALB security
group is created properly. This is not necessary for the Fargate service.
@todo: Remove when CDK supports EFS for ECS fully.
*/
const initialEc2TaskDefinition = new Ec2TaskDefinition(cluster, 'Ec2ServiceInitialTaskDefinition', {})
const container = initialEc2TaskDefinition.addContainer('cloudcmd', {
image: ContainerImage.fromRegistry(containerImage),
logging: LogDriver.awsLogs({streamPrefix: 'ecs'}),
memoryLimitMiB: 512,
})
container.addPortMappings({
containerPort: 80,
hostPort: 80,
protocol: Protocol.TCP
})
service = new ApplicationLoadBalancedEc2Service(cluster.stack, 'Ec2Service', {
cluster,
desiredCount: 2,
memoryLimitMiB: 512,
taskDefinition: initialEc2TaskDefinition,
...props,
});
} else {
service = new ApplicationLoadBalancedFargateService(cluster.stack, 'FargateService', {
cluster,
desiredCount: 2,
platformVersion: FargatePlatformVersion.VERSION1_4,
taskImageOptions: {
containerName: 'cloudcmd',
image: ContainerImage.fromRegistry(containerImage),
},
...props
});
}
// Required to run Cloud Commander from behind the ALB
service.targetGroup.enableCookieStickiness(Duration.minutes(5));
let mountPoints: MountPoint[] = [];
let volumes: Volume[] = [];
if (efsAccessPoints) {
efsAccessPoints.forEach((ap: AwsCustomResource, name: string) => {
// Don't add the "EcsPrivate" AP on the Fargate task (and vice-versa)
if (
(serviceType === ServiceType.EC2 && name === 'FargatePrivate') ||
(serviceType === ServiceType.FARGATE && name === 'EcsPrivate')
) {
return
}
// Drop the "ecs" or "fargate" suffix if it's the "private" access point
const containerPath = '/files' + (
(name === 'FargatePrivate' || name === 'EcsPrivate') ? '/private' : ap.getResponseField('RootDirectory.Path')
);
mountPoints.push({containerPath, sourceVolume: name})
volumes.push({
name,
efsVolumeConfiguration: {
fileSystemId: fileSystem!.fileSystemId,
authorizationConfig: {
iam: 'ENABLED',
accessPointId: ap.getResponseField('AccessPointId'),
},
transitEncryption: 'ENABLED',
}
})
});
} else {
// Whether it's a "Bind Mount" or a root EFS filesystem mount, we'll be using "/files" as the mount point
mountPoints = [{
containerPath: '/files',
sourceVolume: 'files',
}];
// `efsVolumeConfiguration` if we're mounting the root EFS filesystem.
if (fileSystem) {
volumes = [{
efsVolumeConfiguration: {
fileSystemId: fileSystem.fileSystemId,
transitEncryption: 'ENABLED',
},
name: 'files',
}];
} else {
volumes = [{name: 'files'}];
}
}
/*
This JSON structure represents the final desired task definition, which includes the
EFS volume configurations. This is a stop-gap measure that will be replaced when this
capability is fully supported in CloudFormation and CDK.
*/
const customTaskDefinitionJson = {
containerDefinitions: [
{
command: [
'--no-keys-panel',
'--one-file-panel',
'--port=80',
'--root=/files',
],
essential: true,
image: containerImage,
logConfiguration: {
logDriver: service.taskDefinition.defaultContainer?.logDriverConfig?.logDriver,
options: service.taskDefinition.defaultContainer?.logDriverConfig?.options,
},
memory: 512,
mountPoints,
name: service.taskDefinition.defaultContainer?.containerName,
portMappings: [
{
containerPort: 80,
hostPort: 80,
protocol: 'tcp',
},
],
},
],
cpu: '256',
executionRoleArn: service.taskDefinition.executionRole?.roleArn,
family: service.taskDefinition.family,
memory: '1024',
networkMode: serviceType === ServiceType.EC2 ? NetworkMode.BRIDGE : NetworkMode.AWS_VPC,
requiresCompatibilities: [
serviceType.toUpperCase(),
],
taskRoleArn: service.taskDefinition.taskRole.roleArn,
volumes,
};
/*
We use `AwsCustomResource` to create a new task definition revision with EFS volume
configurations, which is available in the AWS SDK.
*/
const createOrUpdateCustomTaskDefinition = {
action: 'registerTaskDefinition',
outputPath: 'taskDefinition.taskDefinitionArn',
parameters: customTaskDefinitionJson,
physicalResourceId: PhysicalResourceId.fromResponse('taskDefinition.taskDefinitionArn'),
service: 'ECS',
};
const customTaskDefinition = new AwsCustomResource(service, 'Custom' + serviceType + 'TaskDefinition', {
onCreate: createOrUpdateCustomTaskDefinition,
onUpdate: createOrUpdateCustomTaskDefinition,
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
service.taskDefinition.executionRole?.grantPassRole(customTaskDefinition.grantPrincipal);
service.taskDefinition.taskRole.grantPassRole(customTaskDefinition.grantPrincipal);
/*
Finally, we'll update the ECS service to use the new task definition revision
that we just created above.
*/
(service.service.node.tryFindChild('Service') as CfnService)?.addPropertyOverride(
'TaskDefinition',
customTaskDefinition.getResponseField('taskDefinition.taskDefinitionArn'),
);
return service;
}
Example #24
Source File: blue-green-using-ecs-stack.ts From ecs-codepipeline-demo with Apache License 2.0 | 4 votes |
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// =============================================================================
// ECR and CodeCommit repositories for the Blue/ Green deployment
// =============================================================================
// ECR repository for the docker images
const ecrRepo = new ecr.Repository(this, 'demoAppEcrRepo', {
imageScanOnPush: true
});
// CodeCommit repository for storing the source code
const codeRepo = new codeCommit.Repository(this, "demoAppCodeRepo", {
repositoryName: BlueGreenUsingEcsStack.ECS_APP_NAME,
description: "Demo application hosted on NGINX"
});
// =============================================================================
// CODE BUILD and ECS TASK ROLES for the Blue/ Green deployment
// =============================================================================
// IAM role for the Code Build project
const codeBuildServiceRole = new iam.Role(this, "codeBuildServiceRole", {
assumedBy: new ServicePrincipal('codebuild.amazonaws.com')
});
const inlinePolicyForCodeBuild = new iam.PolicyStatement({
effect: Effect.ALLOW,
actions: [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
resources: ["*"]
});
codeBuildServiceRole.addToPolicy(inlinePolicyForCodeBuild);
// ECS task role
const ecsTaskRole = new iam.Role(this, "ecsTaskRoleForWorkshop", {
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com')
});
ecsTaskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy"));
// =============================================================================
// CODE DEPLOY APPLICATION for the Blue/ Green deployment
// =============================================================================
// Creating the code deploy application
const codeDeployApplication = new codeDeploy.EcsApplication(this, "demoAppCodeDeploy");
// Creating the code deploy service role
const codeDeployServiceRole = new iam.Role(this, "codeDeployServiceRole", {
assumedBy: new ServicePrincipal('codedeploy.amazonaws.com')
});
codeDeployServiceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AWSCodeDeployRoleForECS"));
// IAM role for custom lambda function
const customLambdaServiceRole = new iam.Role(this, "codeDeployCustomLambda", {
assumedBy: new ServicePrincipal('lambda.amazonaws.com')
});
const inlinePolicyForLambda = new iam.PolicyStatement({
effect: Effect.ALLOW,
actions: [
"iam:PassRole",
"sts:AssumeRole",
"codedeploy:List*",
"codedeploy:Get*",
"codedeploy:UpdateDeploymentGroup",
"codedeploy:CreateDeploymentGroup",
"codedeploy:DeleteDeploymentGroup"
],
resources: ["*"]
});
customLambdaServiceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'))
customLambdaServiceRole.addToPolicy(inlinePolicyForLambda);
// Custom resource to create the deployment group
const createDeploymentGroupLambda = new lambda.Function(this, 'createDeploymentGroupLambda', {
code: lambda.Code.fromAsset(
path.join(__dirname, 'custom_resources'),
{
exclude: ["**", "!create_deployment_group.py"]
}),
runtime: lambda.Runtime.PYTHON_3_8,
handler: 'create_deployment_group.handler',
role: customLambdaServiceRole,
description: "Custom resource to create deployment group",
memorySize: 128,
timeout: cdk.Duration.seconds(60)
});
// =============================================================================
// VPC, ECS Cluster, ELBs and Target groups for the Blue/ Green deployment
// =============================================================================
// Creating the VPC
const vpc = new ec2.Vpc(this, 'vpcForECSCluster');
// Creating a ECS cluster
const cluster = new ecs.Cluster(this, 'ecsClusterForWorkshop', {vpc});
// Creating an application load balancer, listener and two target groups for Blue/Green deployment
const alb = new elb.ApplicationLoadBalancer(this, "alb", {
vpc: vpc,
internetFacing: true
});
const albProdListener = alb.addListener('albProdListener', {
port: 80
});
const albTestListener = alb.addListener('albTestListener', {
port: 8080
});
albProdListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere');
albTestListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere');
// Target group 1
const blueGroup = new elb.ApplicationTargetGroup(this, "blueGroup", {
vpc: vpc,
protocol: ApplicationProtocol.HTTP,
port: 80,
targetType: TargetType.IP,
healthCheck: {
path: "/",
timeout: Duration.seconds(10),
interval: Duration.seconds(15),
healthyHttpCodes: "200,404"
}
});
// Target group 2
const greenGroup = new elb.ApplicationTargetGroup(this, "greenGroup", {
vpc: vpc,
protocol: ApplicationProtocol.HTTP,
port: 80,
targetType: TargetType.IP,
healthCheck: {
path: "/",
timeout: Duration.seconds(10),
interval: Duration.seconds(15),
healthyHttpCodes: "200,404"
}
});
// Registering the blue target group with the production listener of load balancer
albProdListener.addTargetGroups("blueTarget", {
targetGroups: [blueGroup]
});
// Registering the green target group with the test listener of load balancer
albTestListener.addTargetGroups("greenTarget", {
targetGroups: [greenGroup]
});
// ================================================================================================
// CloudWatch Alarms for 5XX errors
const blue4xxMetric = new cloudWatch.Metric({
namespace: 'AWS/ApplicationELB',
metricName: 'HTTPCode_Target_4XX_Count',
dimensions: {
TargetGroup: blueGroup.targetGroupFullName,
LoadBalancer: alb.loadBalancerFullName
},
statistic: cloudWatch.Statistic.SUM,
period: Duration.minutes(1)
});
const blueGroupAlarm = new cloudWatch.Alarm(this, "blue5xxErrors", {
alarmName: "Blue_5xx_Alarm",
alarmDescription: "CloudWatch Alarm for the 4xx errors of Blue target group",
metric: blue4xxMetric,
threshold: 1,
evaluationPeriods: 1
});
const green4xxMetric = new cloudWatch.Metric({
namespace: 'AWS/ApplicationELB',
metricName: 'HTTPCode_Target_4XX_Count',
dimensions: {
TargetGroup: greenGroup.targetGroupFullName,
LoadBalancer: alb.loadBalancerFullName
},
statistic: cloudWatch.Statistic.SUM,
period: Duration.minutes(1)
});
const greenGroupAlarm = new cloudWatch.Alarm(this, "green4xxErrors", {
alarmName: "Green_4xx_Alarm",
alarmDescription: "CloudWatch Alarm for the 4xx errors of Green target group",
metric: green4xxMetric,
threshold: 1,
evaluationPeriods: 1
});
// ================================================================================================
// ================================================================================================
// DUMMY TASK DEFINITION for the initial service creation
// This is required for the service being made available to create the CodeDeploy Deployment Group
// ================================================================================================
const sampleTaskDefinition = new ecs.FargateTaskDefinition(this, "sampleTaskDefn", {
family: BlueGreenUsingEcsStack.DUMMY_TASK_FAMILY_NAME,
cpu: 256,
memoryLimitMiB: 1024,
taskRole: ecsTaskRole,
executionRole: ecsTaskRole
});
const sampleContainerDefn = sampleTaskDefinition.addContainer("sampleAppContainer", {
image: ecs.ContainerImage.fromRegistry(BlueGreenUsingEcsStack.DUMMY_CONTAINER_IMAGE),
logging: new ecs.AwsLogDriver({
logGroup: new log.LogGroup(this, "sampleAppLogGroup", {
logGroupName: BlueGreenUsingEcsStack.DUMMY_APP_LOG_GROUP_NAME,
removalPolicy: RemovalPolicy.DESTROY
}),
streamPrefix: BlueGreenUsingEcsStack.DUMMY_APP_NAME
}),
dockerLabels: {
name: BlueGreenUsingEcsStack.DUMMY_APP_NAME
}
});
sampleContainerDefn.addPortMappings({
containerPort: 80,
protocol: Protocol.TCP
});
// ================================================================================================
// ECS task definition using ECR image
// Will be used by the CODE DEPLOY for Blue/Green deployment
// ================================================================================================
const taskDefinition = new ecs.FargateTaskDefinition(this, "appTaskDefn", {
family: BlueGreenUsingEcsStack.ECS_TASK_FAMILY_NAME,
cpu: 256,
memoryLimitMiB: 1024,
taskRole: ecsTaskRole,
executionRole: ecsTaskRole
});
const containerDefinition = taskDefinition.addContainer("demoAppContainer", {
image: ContainerImage.fromEcrRepository(ecrRepo, "latest"),
logging: new ecs.AwsLogDriver({
logGroup: new log.LogGroup(this, "demoAppLogGroup", {
logGroupName: BlueGreenUsingEcsStack.ECS_APP_LOG_GROUP_NAME,
removalPolicy: RemovalPolicy.DESTROY
}),
streamPrefix: BlueGreenUsingEcsStack.ECS_APP_NAME
}),
dockerLabels: {
name: BlueGreenUsingEcsStack.ECS_APP_NAME
}
});
containerDefinition.addPortMappings({
containerPort: 80,
protocol: Protocol.TCP
});
// =============================================================================
// ECS SERVICE for the Blue/ Green deployment
// =============================================================================
const demoAppService = new ecs.FargateService(this, "demoAppService", {
cluster: cluster,
taskDefinition: sampleTaskDefinition,
healthCheckGracePeriod: Duration.seconds(10),
desiredCount: 3,
deploymentController: {
type: DeploymentControllerType.CODE_DEPLOY
},
serviceName: BlueGreenUsingEcsStack.ECS_APP_NAME
});
demoAppService.connections.allowFrom(alb, Port.tcp(80))
demoAppService.connections.allowFrom(alb, Port.tcp(8080))
demoAppService.attachToApplicationTargetGroup(blueGroup);
// =============================================================================
// CODE DEPLOY - Deployment Group CUSTOM RESOURCE for the Blue/ Green deployment
// =============================================================================
new CustomResource(this, 'customEcsDeploymentGroup', {
serviceToken: createDeploymentGroupLambda.functionArn,
properties: {
ApplicationName: codeDeployApplication.applicationName,
DeploymentGroupName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_GROUP_NAME,
DeploymentConfigName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_CONFIG_NAME,
ServiceRoleArn: codeDeployServiceRole.roleArn,
BlueTargetGroup: blueGroup.targetGroupName,
GreenTargetGroup: greenGroup.targetGroupName,
ProdListenerArn: albProdListener.listenerArn,
TestListenerArn: albTestListener.listenerArn,
EcsClusterName: cluster.clusterName,
EcsServiceName: demoAppService.serviceName,
TerminationWaitTime: BlueGreenUsingEcsStack.ECS_TASKSET_TERMINATION_WAIT_TIME,
BlueGroupAlarm: blueGroupAlarm.alarmName,
GreenGroupAlarm: greenGroupAlarm.alarmName,
}
});
const ecsDeploymentGroup = codeDeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(this, "ecsDeploymentGroup", {
application: codeDeployApplication,
deploymentGroupName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_GROUP_NAME,
deploymentConfig: EcsDeploymentConfig.fromEcsDeploymentConfigName(this, "ecsDeploymentConfig", BlueGreenUsingEcsStack.ECS_DEPLOYMENT_CONFIG_NAME)
});
// =============================================================================
// CODE BUILD PROJECT for the Blue/ Green deployment
// =============================================================================
// Creating the code build project
const demoAppCodeBuild = new codeBuild.Project(this, "demoAppCodeBuild", {
role: codeBuildServiceRole,
description: "Code build project for the demo application",
environment: {
buildImage: codeBuild.LinuxBuildImage.STANDARD_4_0,
computeType: ComputeType.SMALL,
privileged: true,
environmentVariables: {
REPOSITORY_URI: {
value: ecrRepo.repositoryUri,
type: BuildEnvironmentVariableType.PLAINTEXT
},
TASK_EXECUTION_ARN: {
value: ecsTaskRole.roleArn,
type: BuildEnvironmentVariableType.PLAINTEXT
},
TASK_FAMILY: {
value: BlueGreenUsingEcsStack.ECS_TASK_FAMILY_NAME,
type: BuildEnvironmentVariableType.PLAINTEXT
}
}
},
source: codeBuild.Source.codeCommit({
repository: codeRepo
})
});
// =============================================================================
// CODE PIPELINE for Blue/Green ECS deployment
// =============================================================================
const codePipelineServiceRole = new iam.Role(this, "codePipelineServiceRole", {
assumedBy: new ServicePrincipal('codepipeline.amazonaws.com')
});
const inlinePolicyForCodePipeline = new iam.PolicyStatement({
effect: Effect.ALLOW,
actions: [
"iam:PassRole",
"sts:AssumeRole",
"codecommit:Get*",
"codecommit:List*",
"codecommit:GitPull",
"codecommit:UploadArchive",
"codecommit:CancelUploadArchive",
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
"codedeploy:CreateDeployment",
"codedeploy:Get*",
"codedeploy:RegisterApplicationRevision",
"s3:Get*",
"s3:List*",
"s3:PutObject"
],
resources: ["*"]
});
codePipelineServiceRole.addToPolicy(inlinePolicyForCodePipeline);
const sourceArtifact = new codePipeline.Artifact('sourceArtifact');
const buildArtifact = new codePipeline.Artifact('buildArtifact');
// S3 bucket for storing the code pipeline artifacts
const demoAppArtifactsBucket = new s3.Bucket(this, "demoAppArtifactsBucket", {
encryption: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL
});
// S3 bucket policy for the code pipeline artifacts
const denyUnEncryptedObjectUploads = new iam.PolicyStatement({
effect: Effect.DENY,
actions: ["s3:PutObject"],
principals: [new AnyPrincipal()],
resources: [demoAppArtifactsBucket.bucketArn.concat("/*")],
conditions: {
StringNotEquals: {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
});
const denyInsecureConnections = new iam.PolicyStatement({
effect: Effect.DENY,
actions: ["s3:*"],
principals: [new AnyPrincipal()],
resources: [demoAppArtifactsBucket.bucketArn.concat("/*")],
conditions: {
Bool: {
"aws:SecureTransport": "false"
}
}
});
demoAppArtifactsBucket.addToResourcePolicy(denyUnEncryptedObjectUploads);
demoAppArtifactsBucket.addToResourcePolicy(denyInsecureConnections);
// Code Pipeline - CloudWatch trigger event is created by CDK
new codePipeline.Pipeline(this, "ecsBlueGreen", {
role: codePipelineServiceRole,
artifactBucket: demoAppArtifactsBucket,
stages: [
{
stageName: 'Source',
actions: [
new codePipelineActions.CodeCommitSourceAction({
actionName: 'Source',
repository: codeRepo,
output: sourceArtifact,
}),
]
},
{
stageName: 'Build',
actions: [
new codePipelineActions.CodeBuildAction({
actionName: 'Build',
project: demoAppCodeBuild,
input: sourceArtifact,
outputs: [buildArtifact]
})
]
},
{
stageName: 'Deploy',
actions: [
new codePipelineActions.CodeDeployEcsDeployAction({
actionName: 'Deploy',
deploymentGroup: ecsDeploymentGroup,
appSpecTemplateInput: buildArtifact,
taskDefinitionTemplateInput: buildArtifact,
})
]
}
]
});
// =============================================================================
// Export the outputs
// =============================================================================
new CfnOutput(this, "ecsBlueGreenCodeRepo", {
description: "Demo app code commit repository",
exportName: "ecsBlueGreenDemoAppRepo",
value: codeRepo.repositoryCloneUrlHttp
});
new CfnOutput(this, "ecsBlueGreenLBDns", {
description: "Load balancer DNS",
exportName: "ecsBlueGreenLBDns",
value: alb.loadBalancerDnsName
});
}