Skip to content

Commit

Permalink
Use CDK for ec2 sample app setup (#74)
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 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
pxaws authored Dec 3, 2024
1 parent e2f8dc9 commit 9c200fb
Show file tree
Hide file tree
Showing 31 changed files with 6,268 additions and 678 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This code for sample application is intended for demonstration purposes only. It
* kubectl is installed - https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html
* eksctl is installed - https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html
* jq is installed - https://jqlang.github.io/jq/download/
* AWS CDK is installed - https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install
* [Optional] If you plan to install the infrastructure resources using Terraform, terraform cli is required. https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli
* [Optional] If you want to try out AWS Bedrock/GenAI support with Application Signals, enable Amazon Titian, Anthropic Claude, Meta Llama foundation models by following the instructions in https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html
# EKS demo
Expand Down
8 changes: 8 additions & 0 deletions cdk/ec2/.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/ec2/.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
35 changes: 35 additions & 0 deletions cdk/ec2/bin/ec2.ts
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,
});
80 changes: 80 additions & 0 deletions cdk/ec2/cdk.json
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
}
}
54 changes: 54 additions & 0 deletions cdk/ec2/ec2-cdk.sh
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
8 changes: 8 additions & 0 deletions cdk/ec2/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'
}
};
143 changes: 143 additions & 0 deletions cdk/ec2/lib/compute-stack.ts
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`,
});
}
});
}
}
Loading

0 comments on commit 9c200fb

Please sign in to comment.