-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use CDK for ec2 sample app setup (#74)
*Issue #, if available:* *Description of changes:* We use CDK to replace the bash script to set up the EC2 sample app. In addition, the following changes are made: * Create a separate VPC for hosting the sample app to improve network isolation * Place EC2 instances and RDS databases into private subnets and restrict outbound internet traffic through Network Address Translation (NAT) * Use EC2 user data for application deployment * Use system manager instead of ssh to allow developer to log into ec2 instances for debugging purposes (the microservices are still running in tmux session. To see them, you can log into the ec2 instances through ec2 console and then run `sudo -iu ec2-user bash`) * Create private dns records for the config server and discovery server so that other microservice can refer to a static dns name rather than dynamic ip addresses * Modify the cloudwatch agent config to replace the Dependencies with proper service names By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Ping Xiang <>
- Loading branch information
Showing
31 changed files
with
6,268 additions
and
678 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
*.js | ||
!jest.config.js | ||
*.d.ts | ||
node_modules | ||
|
||
# CDK asset staging directory | ||
.cdk.staging | ||
cdk.out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
*.ts | ||
!*.d.ts | ||
|
||
# CDK asset staging directory | ||
.cdk.staging | ||
cdk.out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
#!/usr/bin/env node | ||
import 'source-map-support/register'; | ||
import * as cdk from 'aws-cdk-lib'; | ||
import { NetworkStack } from '../lib/network-stack'; | ||
import { IAMStack } from '../lib/iam-stack'; | ||
import { DatabaseStack } from '../lib/database-stack'; | ||
import { ComputeStack } from '../lib/compute-stack'; | ||
import { LoadBalancerStack } from '../lib/load-balancer-stack'; | ||
|
||
const app = new cdk.App(); | ||
|
||
const networkStack = new NetworkStack(app, 'AppSignalsEC2NetworkStack'); | ||
// Pass the VPC and RDS security group to the DatabaseStack | ||
const databaseStack = new DatabaseStack(app, 'AppSignalsEC2DatabaseStack', { | ||
vpc: networkStack.vpc, | ||
rdsSecurityGroup: networkStack.rdsSecurityGroup, | ||
}); | ||
const iamStack = new IAMStack(app, 'AppSignalsEC2IAMStack'); | ||
// IAM stack is strangely depend on the database stack because of the secrets generated by secret manager | ||
iamStack.addDependency(databaseStack); | ||
|
||
const computeStack = new ComputeStack(app, 'AppSignalsEC2ComputeStack', { | ||
vpc: networkStack.vpc, | ||
ec2SecurityGroup: networkStack.ec2SecurityGroup, | ||
ec2InstanceRole: iamStack.ec2InstanceRole, | ||
hostedZone: networkStack.hostedZone, | ||
dbSecretArn: databaseStack.dbSecret.secretArn, | ||
}); | ||
|
||
// Create the LoadBalancerStack | ||
const loadBalancerStack = new LoadBalancerStack(app, 'AppSignalsEC2LoadBalancerStack', { | ||
vpc: networkStack.vpc, | ||
albSecurityGroup: networkStack.albSecurityGroup, | ||
frontendInstance: computeStack.frontendInstance, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
{ | ||
"app": "npx ts-node --prefer-ts-exts bin/ec2.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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
#!/bin/bash | ||
|
||
# Script to synthesize, deploy, or destroy AWS CDK stacks with stack dependencies | ||
# Usage: ./cdk-deploy.sh <action> | ||
# Example for deploy: ./cdk-deploy.sh deploy | ||
# Example for destroy: ./cdk-deploy.sh destroy | ||
# Example to only synth: ./cdk-deploy.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 | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module.exports = { | ||
testEnvironment: 'node', | ||
roots: ['<rootDir>/test'], | ||
testMatch: ['**/*.test.ts'], | ||
transform: { | ||
'^.+\\.tsx?$': 'ts-jest' | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import * as crypto from 'crypto'; | ||
import * as cdk from 'aws-cdk-lib'; | ||
import { Construct } from 'constructs'; | ||
import { | ||
Vpc, | ||
InstanceType, | ||
InstanceClass, | ||
InstanceSize, | ||
MachineImage, | ||
SecurityGroup, | ||
SubnetType, | ||
Instance, | ||
UserData, | ||
LaunchTemplate | ||
} from 'aws-cdk-lib/aws-ec2'; | ||
import { Role } from 'aws-cdk-lib/aws-iam'; | ||
import { PrivateHostedZone, ARecord, RecordTarget } from 'aws-cdk-lib/aws-route53'; | ||
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; | ||
import { readFileSync } from 'fs'; | ||
import { join } from 'path'; | ||
import { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; | ||
|
||
interface ComputeStackProps extends cdk.StackProps { | ||
vpc: Vpc; | ||
ec2SecurityGroup: SecurityGroup; | ||
ec2InstanceRole: Role; | ||
hostedZone: PrivateHostedZone; | ||
dbSecretArn: string; | ||
} | ||
|
||
export class ComputeStack extends cdk.Stack { | ||
public frontendInstance: Instance; | ||
public visitsAsg: AutoScalingGroup; | ||
constructor(scope: Construct, id: string, props: ComputeStackProps) { | ||
super(scope, id, props); | ||
|
||
const { vpc, ec2SecurityGroup, ec2InstanceRole, hostedZone, dbSecretArn } = props; | ||
|
||
// Import the database secret | ||
const dbSecret = Secret.fromSecretCompleteArn(this, 'DBSecret', dbSecretArn); | ||
|
||
// Grant the EC2 instance role permission to read the secret | ||
dbSecret.grantRead(ec2InstanceRole); | ||
|
||
// Define common properties for all instances | ||
const instanceType = InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM); | ||
const machineImage = MachineImage.latestAmazonLinux2023({}); | ||
const vpcSubnets = { subnetType: SubnetType.PRIVATE_WITH_NAT }; | ||
|
||
// List of services to deploy | ||
const services = [ | ||
{ name: 'setup', script: 'setup-user-data.sh', useAsg: false }, | ||
{ name: 'pet-clinic-frontend', script: 'pet-clinic-frontend-user-data.sh', useAsg: false }, | ||
{ name: 'vets', script: 'vets-user-data.sh', useAsg: false }, | ||
{ name: 'customers', script: 'customers-user-data.sh', useAsg: false }, | ||
{ name: 'insurances', script: 'insurances-user-data.sh', useAsg: false }, | ||
{ name: 'billings', script: 'billings-user-data.sh', useAsg: false }, | ||
{ name: 'payments', script: 'payments-user-data.sh', useAsg: false }, | ||
{ name: 'visits', script: 'visits-user-data.sh', useAsg: true }, | ||
]; | ||
|
||
services.forEach((service) => { | ||
// Read the user data script from file | ||
let userDataScript = readFileSync(join(__dirname, 'user-data', service.script), 'utf8'); | ||
|
||
// Compute a hash of the User Data | ||
const userDataHash = crypto.createHash('sha256').update(userDataScript).digest('hex'); | ||
|
||
const userData = UserData.forLinux(); | ||
userData.addCommands(userDataScript); | ||
if (service.useAsg) { | ||
// Create a Launch Template for the visits service | ||
const launchTemplate = new LaunchTemplate(this, `${service.name}LaunchTemplate`, { | ||
launchTemplateName: `${service.name}-launch-template`, | ||
machineImage, | ||
instanceType, | ||
securityGroup: ec2SecurityGroup, | ||
role: ec2InstanceRole, | ||
userData, | ||
}); | ||
|
||
// Create an Auto Scaling Group for the visits service | ||
const asg = new AutoScalingGroup(this, `${service.name}ASG`, { | ||
autoScalingGroupName: `${service.name}-asg`, | ||
vpc, | ||
vpcSubnets, | ||
minCapacity: 1, | ||
maxCapacity: 1, | ||
desiredCapacity: 1, | ||
launchTemplate, | ||
}); | ||
|
||
|
||
// Save the visits ASG for potential use in other stacks | ||
if (service.name === 'visits') { | ||
this.visitsAsg = asg; | ||
} | ||
|
||
// Output the ASG Name | ||
new cdk.CfnOutput(this, `${service.name}ASGName`, { | ||
value: asg.autoScalingGroupName, | ||
description: `Auto Scaling Group Name for the ${service.name} service`, | ||
}); | ||
|
||
} else { | ||
|
||
// Create the EC2 instance | ||
const instance = new Instance(this, `${service.name}Instance-${userDataHash.substring(0, 8)}`, { | ||
instanceName: `${service.name}-instance`, | ||
vpc, | ||
instanceType, | ||
machineImage, | ||
securityGroup: ec2SecurityGroup, | ||
role: ec2InstanceRole, | ||
userData, | ||
vpcSubnets, | ||
}); | ||
|
||
if (service.name === 'pet-clinic-frontend') { | ||
this.frontendInstance = instance; | ||
} | ||
|
||
// Create Route 53 A Record | ||
new ARecord(this, `${service.name}DNSRecord`, { | ||
zone: hostedZone, | ||
recordName: `${service.name}`, | ||
target: RecordTarget.fromIpAddresses(instance.instancePrivateIp), | ||
}); | ||
|
||
// Output the instance ID and DNS name | ||
new cdk.CfnOutput(this, `${service.name}InstanceID`, { | ||
value: instance.instanceId, | ||
description: `Instance ID of the ${service.name} service`, | ||
}); | ||
|
||
new cdk.CfnOutput(this, `${service.name}PrivateIP`, { | ||
value: instance.instancePrivateIp, | ||
description: `Private IP of the ${service.name} service`, | ||
}); | ||
} | ||
}); | ||
} | ||
} |
Oops, something went wrong.