From 4db32d43f8cef5223010bf61e421cb11d5765feb Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 22 Jan 2025 22:58:21 -0800 Subject: [PATCH 1/2] Add rds to eks cdk --- cdk/eks/bin/eks.ts | 13 +++- .../billing-service-deployment.yaml | 4 +- .../insurance-service-deployment.yaml | 4 +- cdk/eks/lib/stacks/eks-stack.ts | 28 ++++++-- cdk/eks/lib/stacks/iam-stack.ts | 1 + cdk/eks/lib/stacks/network-stack.ts | 12 +++- cdk/eks/lib/stacks/rds-stack.ts | 70 +++++++++++++++++++ cdk/eks/lib/utils/utils.ts | 6 +- cdk/eks/package-lock.json | 3 + 9 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 cdk/eks/lib/stacks/rds-stack.ts diff --git a/cdk/eks/bin/eks.ts b/cdk/eks/bin/eks.ts index 77176b3..e51a361 100644 --- a/cdk/eks/bin/eks.ts +++ b/cdk/eks/bin/eks.ts @@ -5,6 +5,7 @@ import { NetworkStack } from '../lib/stacks/network-stack'; import { IAMStack } from '../lib/stacks/iam-stack'; import { EksStack } from '../lib/stacks/eks-stack'; import { SloStack } from '../lib/stacks/slo-stack'; +import { RdsStack } from '../lib/stacks/rds-stack'; import { SyntheticCanaryStack } from '../lib/stacks/canary-stack'; const app = new App(); @@ -14,6 +15,13 @@ const enableSlo = app.node.tryGetContext('enableSlo') || false; const networkStack = new NetworkStack(app, 'AppSignalsEksNetworkStack'); const iamStack = new IAMStack(app, 'AppSignalsEksIamStack'); +const rdsStack = new RdsStack(app, 'AppSignalsEksRdsStack', { + vpc: networkStack.vpc, + rdsSecurityGroup: networkStack.rdsSecurityGroup, +}) + +rdsStack.addDependency(networkStack); + const eksStack = new EksStack(app, 'AppSignalsEksClusterStack', { vpc: networkStack.vpc, eksClusterRoleProp: iamStack.eksClusterRoleProp, @@ -21,10 +29,13 @@ const eksStack = new EksStack(app, 'AppSignalsEksClusterStack', { ebsCsiAddonRoleProp: iamStack.ebsCsiAddonRoleProp, sampleAppRoleProp: iamStack.sampleAppRoleProp, cloudwatchAddonRoleProp: iamStack.cloudwatchAddonRoleProp, + rdsClusterEndpoint: rdsStack.clusterEndpoint, + rdsSecurityGroupId: networkStack.rdsSecurityGroupId, }); eksStack.addDependency(networkStack); eksStack.addDependency(iamStack); +eksStack.addDependency(rdsStack); const syntheticCanaryStack = new SyntheticCanaryStack(app, 'AppSignalsSyntheticCanaryStack', { vpc: networkStack.vpc, @@ -32,7 +43,7 @@ const syntheticCanaryStack = new SyntheticCanaryStack(app, 'AppSignalsSyntheticC syntheticCanaryRoleProp: iamStack.syntheticCanaryRoleProp, }) -syntheticCanaryStack.addDependency(eksStack); +syntheticCanaryStack.addDependency(rdsStack); // After AppSignal is enabled, it takes up to 10 minutes for the SLO metrics to become available. If this is deployed before the SLO metrics // are available, it will fail. diff --git a/cdk/eks/lib/manifests/sample-app/billing-service-deployment.yaml b/cdk/eks/lib/manifests/sample-app/billing-service-deployment.yaml index b340aef..3fc9560 100644 --- a/cdk/eks/lib/manifests/sample-app/billing-service-deployment.yaml +++ b/cdk/eks/lib/manifests/sample-app/billing-service-deployment.yaml @@ -33,14 +33,12 @@ spec: value: 'postgres' - name: DB_USER value: 'djangouser' - - name: DB_USER_PASSWORD - value: 'asdfqwer' - name: DATABASE_PROFILE value: postgresql - name: REGION value: ${REGION} - name: DB_SERVICE_HOST - value: db.pet-clinic.svc.cluster.local + value: - name: DB_SERVICE_PORT value: '5432' # command: ["sh", "-c"] diff --git a/cdk/eks/lib/manifests/sample-app/insurance-service-deployment.yaml b/cdk/eks/lib/manifests/sample-app/insurance-service-deployment.yaml index e78db75..e326a6a 100644 --- a/cdk/eks/lib/manifests/sample-app/insurance-service-deployment.yaml +++ b/cdk/eks/lib/manifests/sample-app/insurance-service-deployment.yaml @@ -33,12 +33,10 @@ spec: value: 'postgres' - name: DB_USER value: 'djangouser' - - name: DB_USER_PASSWORD - value: 'asdfqwer' - name: DATABASE_PROFILE value: postgresql - name: DB_SERVICE_HOST - value: db.pet-clinic.svc.cluster.local + value: - name: DB_SERVICE_PORT value: '5432' # command: ["sh", "-c"] diff --git a/cdk/eks/lib/stacks/eks-stack.ts b/cdk/eks/lib/stacks/eks-stack.ts index 153b49e..b69ea2d 100644 --- a/cdk/eks/lib/stacks/eks-stack.ts +++ b/cdk/eks/lib/stacks/eks-stack.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { Construct } from 'constructs'; -import { StackProps, Stack, CfnJson, CfnWaitConditionHandle, CfnWaitCondition } from 'aws-cdk-lib'; -import { Vpc, InstanceType } from 'aws-cdk-lib/aws-ec2'; +import { StackProps, Stack, CfnJson, Fn, CfnWaitConditionHandle, CfnWaitCondition } from 'aws-cdk-lib'; +import { Vpc, InstanceType, ISecurityGroup,SecurityGroup, Port } from 'aws-cdk-lib/aws-ec2'; import { Role, RoleProps, PolicyStatement, FederatedPrincipal, Effect } from 'aws-cdk-lib/aws-iam'; import { CfnAddon, Cluster, KubernetesManifest, KubernetesVersion, ServiceAccount, KubernetesObjectValue, Nodegroup } from 'aws-cdk-lib/aws-eks'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -15,7 +15,9 @@ interface EksStackProps extends StackProps { eksNodeGroupRoleProp: RoleProps, ebsCsiAddonRoleProp: RoleProps, sampleAppRoleProp: RoleProps, - cloudwatchAddonRoleProp: RoleProps + cloudwatchAddonRoleProp: RoleProps, + rdsClusterEndpoint: string, + rdsSecurityGroupId: string, } export class EksStack extends Stack { @@ -52,12 +54,20 @@ export class EksStack extends Stack { // Ingress External Ip public readonly ingressExternalIp: KubernetesObjectValue; + private readonly rdsSecurityGroup: ISecurityGroup; + private readonly rdsClusterEndpoint: string; constructor(scope: Construct, id: string, props: EksStackProps) { super(scope, id, props); - const { vpc, eksClusterRoleProp, eksNodeGroupRoleProp, ebsCsiAddonRoleProp, sampleAppRoleProp, cloudwatchAddonRoleProp } = props; + const { vpc, eksClusterRoleProp, eksNodeGroupRoleProp, ebsCsiAddonRoleProp, sampleAppRoleProp, cloudwatchAddonRoleProp, rdsClusterEndpoint, rdsSecurityGroupId } = props; this.vpc = vpc; + this.rdsClusterEndpoint = rdsClusterEndpoint; + this.rdsSecurityGroup = SecurityGroup.fromSecurityGroupId( + this, + 'ImportedRdsSecurityGroup', + rdsSecurityGroupId + ); // The IAM roles must be created in the EKS stack because some of the roles need to be given federated principals, and this cannot be done if the role is imported this.eksClusterRole = new Role(this, 'EksClusterRole', eksClusterRoleProp); @@ -66,7 +76,6 @@ export class EksStack extends Stack { this.sampleAppRole = new Role(this, 'SampleAppRole', sampleAppRoleProp); this.cloudwatchAddonRole = new Role(this, 'CloduwatchAddonRole', cloudwatchAddonRoleProp); - // Create EKS Cluster this.cluster = this.createEksCluster(); // Add the Cloduwatch Addon @@ -121,7 +130,12 @@ export class EksStack extends Stack { maxSize: 5, releaseVersion: nodeGroupAmiReleaseVersion, }); - + + this.rdsSecurityGroup.addIngressRule( + cluster.connections.securityGroups[0], + Port.tcp(5432), + 'Allow EKS to connect to RDS' + ) return cluster; } @@ -172,7 +186,7 @@ export class EksStack extends Stack { manifestFiles.forEach((file) => { const filePath = path.join(manifestPath, file); const yamlFile = readYamlFile(filePath); - const transformedYamlFile = transformYaml(yamlFile, this.account, this.region, this.SAMPLE_APP_NAMESPACE, this.ingressExternalIp?.value); + const transformedYamlFile = transformYaml(yamlFile, this.account, this.region, this.SAMPLE_APP_NAMESPACE, this.ingressExternalIp?.value, this.rdsClusterEndpoint); const manifest = this.cluster.addManifest(transformNameToId(file), ...transformedYamlFile); dependencies.forEach((dependnecy) => { diff --git a/cdk/eks/lib/stacks/iam-stack.ts b/cdk/eks/lib/stacks/iam-stack.ts index 3f0f4a8..36216c1 100644 --- a/cdk/eks/lib/stacks/iam-stack.ts +++ b/cdk/eks/lib/stacks/iam-stack.ts @@ -69,6 +69,7 @@ export class IAMStack extends Stack { ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'), ManagedPolicy.fromAwsManagedPolicyName('AmazonBedrockFullAccess'), ManagedPolicy.fromAwsManagedPolicyName('AWSXrayFullAccess'), + ManagedPolicy.fromAwsManagedPolicyName('SecretsManagerReadWrite'), ], }; diff --git a/cdk/eks/lib/stacks/network-stack.ts b/cdk/eks/lib/stacks/network-stack.ts index fab4cc3..293ce7c 100644 --- a/cdk/eks/lib/stacks/network-stack.ts +++ b/cdk/eks/lib/stacks/network-stack.ts @@ -1,11 +1,12 @@ import { Construct } from 'constructs'; import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; -import { Vpc, SubnetType } from 'aws-cdk-lib/aws-ec2'; -import { PrivateHostedZone } from 'aws-cdk-lib/aws-route53'; +import { Vpc, SubnetType, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; export class NetworkStack extends Stack { // Expose properties for use in other stacks public readonly vpc: Vpc; + public readonly rdsSecurityGroup: SecurityGroup; + public readonly rdsSecurityGroupId: string; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); @@ -29,6 +30,13 @@ export class NetworkStack extends Stack { natGateways: 2, }); + this.rdsSecurityGroup = new SecurityGroup(this, 'EksRdsSecurityGroup', { + vpc: this.vpc, + description: 'Allow traffic from EKS', + }); + + this.rdsSecurityGroupId = this.rdsSecurityGroup.securityGroupId; + // Output the VPC and subnet IDs new CfnOutput(this, 'PetClinicEksVPCID', { value: this.vpc.vpcId, diff --git a/cdk/eks/lib/stacks/rds-stack.ts b/cdk/eks/lib/stacks/rds-stack.ts new file mode 100644 index 0000000..745e100 --- /dev/null +++ b/cdk/eks/lib/stacks/rds-stack.ts @@ -0,0 +1,70 @@ +import { RemovalPolicy, Fn, Duration, CfnOutput } from 'aws-cdk-lib'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { SubnetGroup, DatabaseCluster, DatabaseClusterEngine, AuroraPostgresEngineVersion, Credentials, PerformanceInsightRetention } from 'aws-cdk-lib/aws-rds'; +import { InstanceType, InstanceClass, InstanceSize} from 'aws-cdk-lib/aws-ec2'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Vpc, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; + +interface RdsStackProps extends StackProps { + vpc: Vpc; + rdsSecurityGroup: SecurityGroup +} + +export class RdsStack extends Stack { + public readonly clusterEndpoint: string; + private readonly securityGroup: SecurityGroup; + private readonly rdsSubnetGroupName = 'eks-rds-subnet-group'; + private readonly dbClusterIdentifier = 'eks-petclinic-python'; + private readonly masterUsername = 'djangouser'; + private readonly secretName = 'petclinic-python-dbsecret'; + constructor(scope: Construct, id: string, props: RdsStackProps) { + super(scope, id, props); + + const { vpc, rdsSecurityGroup } = props; + this.securityGroup = rdsSecurityGroup; + + // Create a database subnet group + const dbSubnetGroup = new SubnetGroup(this, 'EksDbSubnetGroup', { + vpc: vpc, + subnetGroupName: this.rdsSubnetGroupName, + description: 'Subnet group for RDS', + removalPolicy: RemovalPolicy.DESTROY, + }); + + const dbSecret = new Secret(this, 'EksDbSecret', { + secretName: this.secretName, + description: `Randomly generated password for ${this.dbClusterIdentifier} database`, + generateSecretString: { + passwordLength: 10, + }, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Create an Aurora PostgreSQL cluster + const dbCluster = new DatabaseCluster(this, 'EksRdsCluster', { + engine: DatabaseClusterEngine.auroraPostgres({ + version: AuroraPostgresEngineVersion.VER_16_4, + }), + credentials: Credentials.fromUsername(this.masterUsername, { + password: dbSecret.secretValue, + }), + instances: 1, + instanceProps: { + instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM), + vpc, + }, + subnetGroup: dbSubnetGroup, + backup: { + retention: Duration.days(1), + }, + + performanceInsightRetention: PerformanceInsightRetention.LONG_TERM, + removalPolicy: RemovalPolicy.DESTROY, + clusterIdentifier: this.dbClusterIdentifier, + securityGroups: [this.securityGroup], + }); + + this.clusterEndpoint = dbCluster.clusterEndpoint.socketAddress.split(':')[0]; + } +} \ No newline at end of file diff --git a/cdk/eks/lib/utils/utils.ts b/cdk/eks/lib/utils/utils.ts index 9a15412..5142d55 100644 --- a/cdk/eks/lib/utils/utils.ts +++ b/cdk/eks/lib/utils/utils.ts @@ -25,7 +25,7 @@ export function readYamlFile(filePath: string): Record[] { } // Function that replaces the namespace and account id placeholder with actual values -export function transformYaml(obj: any, accountId: string, region: string, namespace: string, ingressExternalIp: string): any { +export function transformYaml(obj: any, accountId: string, region: string, namespace: string, ingressExternalIp: string, rdsClusterEndpoint: string): any { if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (typeof obj[key] === 'string') { @@ -37,9 +37,11 @@ export function transformYaml(obj: any, accountId: string, region: string, names obj[key] = `http://${ingressExternalIp}`; } else if (obj[key].includes('${REGION}')) { obj[key] = obj[key].replace('${REGION}', region); + } else if (obj[key].includes('')) { + obj[key] = obj[key].replace('', rdsClusterEndpoint); } } else if (typeof obj[key] === 'object') { - transformYaml(obj[key], accountId, region, namespace, ingressExternalIp); + transformYaml(obj[key], accountId, region, namespace, ingressExternalIp, rdsClusterEndpoint); } } } diff --git a/cdk/eks/package-lock.json b/cdk/eks/package-lock.json index a588db5..09a1070 100644 --- a/cdk/eks/package-lock.json +++ b/cdk/eks/package-lock.json @@ -26,6 +26,9 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "~5.6.3" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@ampproject/remapping": { From 6bebe22eb91d727aa40601bb4e5cd84a732918e4 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 29 Jan 2025 02:07:33 -0800 Subject: [PATCH 2/2] Downgrade RDS engine version to match script --- cdk/eks/lib/stacks/rds-stack.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cdk/eks/lib/stacks/rds-stack.ts b/cdk/eks/lib/stacks/rds-stack.ts index 745e100..90e03d3 100644 --- a/cdk/eks/lib/stacks/rds-stack.ts +++ b/cdk/eks/lib/stacks/rds-stack.ts @@ -37,6 +37,7 @@ export class RdsStack extends Stack { description: `Randomly generated password for ${this.dbClusterIdentifier} database`, generateSecretString: { passwordLength: 10, + excludeCharacters: '/@" ', }, removalPolicy: RemovalPolicy.DESTROY, }); @@ -44,14 +45,14 @@ export class RdsStack extends Stack { // Create an Aurora PostgreSQL cluster const dbCluster = new DatabaseCluster(this, 'EksRdsCluster', { engine: DatabaseClusterEngine.auroraPostgres({ - version: AuroraPostgresEngineVersion.VER_16_4, + version: AuroraPostgresEngineVersion.VER_15_4, }), credentials: Credentials.fromUsername(this.masterUsername, { password: dbSecret.secretValue, }), instances: 1, instanceProps: { - instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM), + instanceType: InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MEDIUM), vpc, }, subnetGroup: dbSubnetGroup,