diff --git a/cdk/bin/app.ts b/cdk/bin/app.ts index a91cf9f1..f5dc080d 100644 --- a/cdk/bin/app.ts +++ b/cdk/bin/app.ts @@ -4,6 +4,7 @@ import * as cdk from 'aws-cdk-lib'; import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; import { MainStack } from '../lib/main-stack'; +import { DrStack } from '../lib/dr-stack'; import { AppStack } from '../lib/app-stack'; import type { AppProps, Environment } from '../lib/types'; @@ -42,10 +43,28 @@ if (!prod) { const app = new cdk.App(); +const prodEnvironment = environments.find((env) => env.env === 'prod'); +if (!prodEnvironment) { + throw new Error('No prod environment found'); +} +const drStack = new DrStack( + app, + `${prodEnvironment.appName}-drstack-${prodEnvironment.env}`, + { ...prodEnvironment, region: 'ap-southeast-4' }, + { + env: { account: prodEnvironment.account, region: 'ap-southeast-4' }, + }, +); + environments.forEach((environment) => { - const mainStack = new MainStack(app, `${environment.appName}-stack-${environment.env}`, environment, { - env: { account: environment.account, region: environment.region }, - }); + const mainStack = new MainStack( + app, + `${environment.appName}-stack-${environment.env}`, + { drBucket: environment.env === 'prod' ? drStack.drBucket : undefined, ...environment }, + { + env: { account: environment.account, region: environment.region }, + }, + ); const props: AppProps = { ...environment, diff --git a/cdk/lib/main-stack.ts b/cdk/lib/main-stack.ts index 60050efb..aceb123f 100644 --- a/cdk/lib/main-stack.ts +++ b/cdk/lib/main-stack.ts @@ -2,12 +2,14 @@ import * as cdk from 'aws-cdk-lib'; import type { Construct } from 'constructs'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; +import * as iam from 'aws-cdk-lib/aws-iam'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import type { Environment } from './types'; import { NagSuppressions } from 'cdk-nag'; +import { CfnBucket } from 'aws-cdk-lib/aws-s3'; export class MainStack extends cdk.Stack { public catalogBucket: s3.IBucket; @@ -26,6 +28,7 @@ export class MainStack extends cdk.Stack { // account, // region, // railsEnv, + drBucket, env, acmeValue, zoneName, @@ -97,6 +100,7 @@ export class MainStack extends cdk.Stack { // intelligentTieringConfigurations: [ ], // TODO: Decide on lifecycle rules lifecycleRules: [{ abortIncompleteMultipartUploadAfter: cdk.Duration.days(7) }], + versioned: env === 'prod', inventories: [ { destination: { @@ -128,6 +132,80 @@ export class MainStack extends cdk.Stack { serverAccessLogsPrefix: `s3-access-logs/${appName}-catalog-${env}`, }); + if (env === 'prod') { + if (!drBucket) { + throw new Error('DR bucket is required in prod environment'); + } + + const replicationRole = new iam.Role(this, 'CatalogBucketReplicationRole', { + roleName: 'catalog-bucket-replication-role', + assumedBy: new iam.ServicePrincipal('s3.amazonaws.com'), + }); + + replicationRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetReplicationConfiguration', 's3:ListBucket'], + resources: [this.catalogBucket.bucketArn], + }), + ); + + replicationRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObjectVersionForReplication', + 's3:GetObjectVersionAcl', + 's3:GetObjectVersionTagging', + 's3:GetObjectVersion', + 's3:GetObjectLegalHold', + 's3:GetObjectRetention', + ], + resources: [this.catalogBucket.arnForObjects('*')], + }), + ); + + NagSuppressions.addResourceSuppressions( + replicationRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'All wildcard permission are on purpose for replication', + }, + ], + true, + ); + + replicationRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:GetObjectVersionTagging'], + resources: [`${drBucket.bucketArn}/*`], + }), + ); + + const cfnBucket = this.catalogBucket.node.defaultChild as CfnBucket; + cfnBucket.replicationConfiguration = { + role: replicationRole.roleArn, + rules: [ + { + id: drBucket.bucketArn, + status: 'Enabled', + priority: 1, + filter: {}, + destination: { + bucket: drBucket.bucketArn, + storageClass: s3.StorageClass.GLACIER_INSTANT_RETRIEVAL.toString(), + }, + + deleteMarkerReplication: { + status: 'Enabled', + }, + }, + ], + }; + } + cdk.Tags.of(this).add('uni:billing:application', 'para'); } } diff --git a/cdk/lib/types.ts b/cdk/lib/types.ts index 31018cc5..a1c03fd6 100644 --- a/cdk/lib/types.ts +++ b/cdk/lib/types.ts @@ -12,6 +12,7 @@ export type Environment = { readonly zoneName: string, readonly acmeValue: string, readonly cloudflare: string, + readonly drBucket?: IBucket, }; export type AppProps = Environment & {