Skip to content

Commit

Permalink
Use CDK for ecs sample app setup (#77)
Browse files Browse the repository at this point in the history
*Issue #, if available:*

*Description of changes:*
We use CDK to replace the bash script to set up the ECS sample app. In
addition, the following changes are made:
1. Create a separate VPC for hosting the sample app to improve network
isolation.
2. Place ECS services into a private subnet with egress.
3. Place RDS database into a private isolated subnet and use Secrets
Manager to manage the access to the database.
4. Modify the cloudwatch agent config to replace the Dependencies with
proper service names.
5. Use private ECR instead of public ECR to obtain images (README is
updated accordingly).
6. Fix issues caused by regions not being specified in customers and
visits services.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
  • Loading branch information
blairhyy-amazon authored Dec 4, 2024
1 parent 9c200fb commit 5044467
Show file tree
Hide file tree
Showing 37 changed files with 5,660 additions and 1,797 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,13 @@ The following instructions set up an kubernetes cluster on 2 EC2 instances (one
# ECS Demo
The following instructions set up an ECS cluster with all services running in Fargate. You can run these steps in your personal AWS account to follow along (Not recommended for production usage).
1. Build container images and push them to public ECR repo
1. Build container images and push them to private ECR repo. Replace `region-name` with the region you choose.
```shell
export ACCOUNT=`aws sts get-caller-identity | jq .Account -r`
export REGION=region-name
```
``` shell
./mvnw clean install -P buildDocker && ./push-public-ecr.sh
./mvnw clean install -P buildDocker && ./push-ecr.sh
```

2. Set up a ECS cluster and deploy sample app. Replace `region-name` with the region you choose.
Expand Down
8 changes: 8 additions & 0 deletions cdk/ecs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions cdk/ecs/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
3 changes: 3 additions & 0 deletions cdk/ecs/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage
75 changes: 75 additions & 0 deletions cdk/ecs/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"app": "npx ts-node --prefer-ts-exts lib/app.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true
}
}
56 changes: 56 additions & 0 deletions cdk/ecs/ecs-cdk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash

# Script to synthesize, deploy, or destroy AWS CDK stacks with stack dependencies
# Usage: ./ecs-cdk.sh <action>
# Example for deploy: ./ecs-cdk.sh deploy
# Example for destroy: ./ecs-cdk.sh destroy
# Example to only synth: ./ecs-cdk.sh synth

ACTION=$1

# Check for action parameter
if [[ -z "$ACTION" ]]; then
echo "Usage: $0 <action>"
echo "action can be 'synth', 'deploy', or 'destroy'"
exit 1
fi


# Run CDK synth once for all stacks
if [[ "$ACTION" == "synth" || "$ACTION" == "deploy" ]]; then
echo "Running npm install"
npm install

echo "Running CDK bootstrap"
cdk bootstrap

rm -rf cdk.out
echo "Running CDK synth for all stacks..."
if cdk synth; then
echo "CDK synth successful!"
if [[ "$ACTION" == "synth" ]]; then
exit 0
fi
else
echo "CDK synth failed. Exiting."
exit 1
fi
fi

# Deploy or destroy all stacks in the app
if [[ "$ACTION" == "deploy" ]]; then
echo "Starting CDK deployment for all stacks in the app"
if cdk deploy --all --require-approval never; then
echo "Deployment successful for all stacks in the app"
else
echo "Deployment failed. Attempting to clean up resources by destroying all stacks..."
cdk destroy --all --force --verbose
fi
elif [[ "$ACTION" == "destroy" ]]; then
echo "Starting CDK destroy for all stacks in the app"
cdk destroy --all --force --verbose
echo "Destroy complete for all stacks in the app"
else
echo "Invalid action: $ACTION. Please use 'synth', 'deploy', or 'destroy'."
exit 1
fi
8 changes: 8 additions & 0 deletions cdk/ecs/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
};
73 changes: 73 additions & 0 deletions cdk/ecs/lib/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as assert from 'assert';
import { App } from 'aws-cdk-lib';

import { getLatestAdotJavaTag, getLatestAdotPythonTag } from './utils';
import { EcsClusterStack } from './stacks/ecsStack';
import { IamRolesStack } from './stacks/iamRolesStack';
import { PetClinicNetworkStack } from './stacks/petClinicNetworkStack';
import { ServiceDiscoveryStack } from './stacks/servicediscoveryStack';
import { LogStack } from './stacks/logStack';
import { LoadBalancerStack } from './stacks/loadbalancerStack';
import { RdsDatabaseStack } from './stacks/databaseStack';

class ApplicationSignalsECSDemo {
private readonly app: App;

constructor() {
this.app = new App();
this.runApp();
}

public async runApp(): Promise<void> {
const [adotJavaImageTag, adotPythonImageTag] = await Promise.all([
getLatestAdotJavaTag(),
getLatestAdotPythonTag(),
]);

assert(adotJavaImageTag !== '', 'ADOT Java Image Tag is empty');
assert(adotPythonImageTag !== '', 'ADOT Python Image Tag is empty');

const petClinicNetworkStack = new PetClinicNetworkStack(this.app, 'PetClinicNetworkStack');

const logStack = new LogStack(this.app, 'LogStack');

const loadbalancerStack = new LoadBalancerStack(this.app, 'LoadBalancerStack', {
vpc: petClinicNetworkStack.vpc,
securityGroup: petClinicNetworkStack.albSecurityGroup,
});

const rdsDatabaseStack = new RdsDatabaseStack(this.app, 'RdsDatabaseStack', {
vpc: petClinicNetworkStack.vpc,
rdsSecurityGroup: petClinicNetworkStack.rdsSecurityGroup,
});

const iamRolesStack = new IamRolesStack(this.app, 'IamRolesStack');

// Grant ecsTaskRole access to database
rdsDatabaseStack.dbSecret.grantRead(iamRolesStack.ecsTaskRole);
rdsDatabaseStack.dbSecret.grantWrite(iamRolesStack.ecsTaskRole);

const serviceDiscoveryStack = new ServiceDiscoveryStack(this.app, 'ServiceDiscoveryStack', {
vpc: petClinicNetworkStack.vpc,
});

new EcsClusterStack(this.app, 'EcsClusterStack', {
vpc: petClinicNetworkStack.vpc,
securityGroups: [petClinicNetworkStack.ecsSecurityGroup],
ecsTaskRole: iamRolesStack.ecsTaskRole,
ecsTaskExecutionRole: iamRolesStack.ecsTaskExecutionRole,
serviceDiscoveryStack: serviceDiscoveryStack,
logStack: logStack,
adotPythonImageTag: adotPythonImageTag,
adotJavaImageTag: adotJavaImageTag,
dbSecret: rdsDatabaseStack.dbSecret,
dbInstanceEndpointAddress: rdsDatabaseStack.rdsInstance.dbInstanceEndpointAddress,
loadBalancerTargetGroup: loadbalancerStack.targetGroup,
loadBalancerDnsName: loadbalancerStack.loadBalancer.loadBalancerDnsName,
});

this.app.synth();
}
}

new ApplicationSignalsECSDemo();
84 changes: 84 additions & 0 deletions cdk/ecs/lib/stacks/databaseStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Construct } from 'constructs';
import {
DatabaseInstance,
SubnetGroup,
Credentials,
DatabaseInstanceEngine,
PostgresEngineVersion,
StorageType,
} from 'aws-cdk-lib/aws-rds';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { StackProps, Stack, CfnOutput, Duration, RemovalPolicy, Tags } from 'aws-cdk-lib';
import { Vpc, SecurityGroup, SubnetType, InstanceType, InstanceClass, InstanceSize } from 'aws-cdk-lib/aws-ec2';

interface RdsDatabaseStackProps extends StackProps {
readonly vpc: Vpc;
readonly rdsSecurityGroup: SecurityGroup;
}

export class RdsDatabaseStack extends Stack {
private readonly vpc: Vpc;
private readonly DB_INSTANCE_IDENTIFIER: string = 'petclinic-python';
public readonly rdsInstance: DatabaseInstance;
public readonly dbSecret: Secret;

constructor(scope: Construct, id: string, props: RdsDatabaseStackProps) {
super(scope, id, props);

this.vpc = props.vpc;

// Create DB Subnet Group
const dbSubnetGroup = new SubnetGroup(this, 'MyDBSubnetGroup', {
vpc: this.vpc,
description: 'Subnet group for RDS',
subnetGroupName: 'my-db-subnet-group',
vpcSubnets: {
subnetType: SubnetType.PRIVATE_ISOLATED, // Ensure private subnets with NAT are used
},
removalPolicy: RemovalPolicy.DESTROY,
});

// Create a Secret for the database credentials
this.dbSecret = new Secret(this, 'DBSecret', {
secretName: 'PetClinicDBCredentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'root' }),
generateStringKey: 'password',
excludePunctuation: true,
includeSpace: false,
},
});

// Create database instance
this.rdsInstance = new DatabaseInstance(this, 'MyDatabase', {
vpc: this.vpc,
credentials: Credentials.fromSecret(this.dbSecret),
vpcSubnets: {
subnetType: SubnetType.PRIVATE_ISOLATED, // Ensure private subnets with NAT are used
},
publiclyAccessible: false,
instanceIdentifier: this.DB_INSTANCE_IDENTIFIER,
instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MICRO), // db.t3.micro
engine: DatabaseInstanceEngine.postgres({
version: PostgresEngineVersion.VER_14,
}),
allocatedStorage: 20, // 20 GB allocated storage
maxAllocatedStorage: 25,
storageType: StorageType.GP2,
subnetGroup: dbSubnetGroup,
securityGroups: [props.rdsSecurityGroup],
multiAz: false, // Disable Multi-AZ
backupRetention: Duration.days(0), // 0 days backup retention
removalPolicy: RemovalPolicy.DESTROY, // For dev/testing environments
deletionProtection: false, // Disable deletion protection
deleteAutomatedBackups: true,
});

Tags.of(this.rdsInstance).add('Name', this.DB_INSTANCE_IDENTIFIER);

// Output the subnet group name
new CfnOutput(this, 'DBSubnetGroupName', {
value: dbSubnetGroup.subnetGroupName,
});
}
}
Loading

0 comments on commit 5044467

Please sign in to comment.