
Deploy Your Web Application to staging and production with Elastic Beanstalk, AWS CDK, CloudFront, and Circleci pipelines
In this guide, I cover the following key aspects Elastic Beanstalk- Load balancers- EC2- Relational Database Service (RDS)- Route 53- CloudFront- Virtual Private Cloud (VPC)- Security Groups- SSL/TLS Certificate Manager - Secrets Manager- S3 Buckets- Namecheap Integration- NextJS/ReactJS CloudFront deployment- Papertrail monitoring & logging- CircleCI pipelines- Staging and Production Environments
- Deploy your web application by creating an Elastic Beanstalk environment and configuring its settings.
- Set up an RDS instance to host your application’s relational database with the desired engine and security settings.
- Manage your domain’s DNS records and hosted zones on Route 53 for efficient domain resolution.
- Accelerate content delivery and reduce latency by creating a CloudFront distribution, configuring origins, and associating it with your Elastic Beanstalk environment.
- Isolate and control your network environment by setting up a Virtual Private Cloud with proper subnets, route tables, and internet gateways.
- Define security groups for services like Elastic Beanstalk and RDS to control inbound and outbound traffic securely.
- Secure your domain with SSL/TLS certificates from AWS Certificate Manager, associating them with your CloudFront distribution and Elastic Beanstalk environment.
- Safely store and manage sensitive information, such as database credentials, using AWS Secrets Manager and integrate it with your Elastic Beanstalk environment.
- Create and configure S3 buckets for storing static assets, backups, and other necessary files with proper access controls.
- Automate your build and deployment workflows by configuring Circle CI with a .circleci/config.yml file and integrating it with your version control system.
- Point your domain to AWS by updating DNS records on Namecheap and associating the domain with your AWS resources.
https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/GettingStarted.CreateApp.html
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CreateDBInstance.html
Copy the account ID, navigate to security credentials, and create access keys.
~/.aws/config
file1
2
3
[aws-deploy]
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
1
yarn add cdk@2.70.0
1
npx cdk init app --language typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import * as cdk from '@aws-cdk/core'
import * as s3assets from '@aws-cdk/aws-s3-assets'
import * as elasticbeanstalk from '@aws-cdk/aws-elasticbeanstalk'
import * as iam from '@aws-cdk/aws-iam'
export interface EBEnvProps extends cdk.StackProps {
// Autoscaling group configuration
minSize?: string;
maxSize?: string;
instanceTypes?: string;
envName?: string;
}
export class ElbtestStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: EBEnvProps) {
super(scope, id, props);
// The code that defines your stack goes here
// Construct an S3 asset Zip from directory up.
const webAppZipArchive = new s3assets.Asset(this, 'WebAppZip', {
path: `${__dirname}/YOUR_SRC_DIR`,
});
// Create a ElasticBeanStalk app.
const appName = 'YOUR_DB';
const app = new elasticbeanstalk.CfnApplication(this, 'Application', {
applicationName: appName,
});
// Create an app version from the S3 asset defined earlier
const appVersionProps = new elasticbeanstalk.CfnApplicationVersion(this, 'AppVersion', {
applicationName: appName,
sourceBundle: {
s3Bucket: webAppZipArchive.s3BucketName,
s3Key: webAppZipArchive.s3ObjectKey,
},
});
// Make sure that Elastic Beanstalk app exists before creating an app version
appVersionProps.addDependsOn(app);
// Create role and instance profile
const myRole = new iam.Role(this, `${appName}-aws-elasticbeanstalk-ec2-role`, {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
});
const managedPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName('AWSElasticBeanstalkWebTier')
myRole.addManagedPolicy(managedPolicy);
const myProfileName = `${appName}-InstanceProfile`
const instanceProfile = new iam.CfnInstanceProfile(this, myProfileName, {
instanceProfileName: myProfileName,
roles: [
myRole.roleName
]
});
// Example of some options which can be configured
const optionSettingProperties: elasticbeanstalk.CfnEnvironment.OptionSettingProperty[] = [
{
namespace: 'aws:autoscaling:launchconfiguration',
optionName: 'IamInstanceProfile',
value: myProfileName,
},
{
namespace: 'aws:autoscaling:asg',
optionName: 'MinSize',
value: props?.maxSize ?? '1',
},
{
namespace: 'aws:autoscaling:asg',
optionName: 'MaxSize',
value: props?.maxSize ?? '1',
},
{
namespace: 'aws:ec2:instances',
optionName: 'InstanceTypes',
value: props?.instanceTypes ?? 't2.micro',
},
];
// Create an Elastic Beanstalk environment to run the application
const elbEnv = new elasticbeanstalk.CfnEnvironment(this, 'Environment', {
environmentName: props?.envName ?? `${appName}-env`,
applicationName: app.applicationName || appName,
solutionStackName: '64bit Amazon Linux 2023 v6.0.4 running Node.js 18',
optionSettings: optionSettingProperties,
versionLabel: appVersionProps.ref,
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2'
import * as rds from '@aws-cdk/aws-rds'
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import * as ssm from '@aws-cdk/aws-ssm'
import { ISecurityGroup, IVpc } from '@aws-cdk/aws-ec2';
import { Secret } from '@aws-cdk/aws-secretsmanager';
interface RdsStackProps extends cdk.StackProps {
myVpc: ec2.IVpc;
rdsSecurityGroup: ec2.ISecurityGroup;
}
export class RdsStack extends cdk.Stack {
readonly myRdsInstance: rds.DatabaseInstance;
readonly databaseCredentialsSecret: Secret;
constructor(scope: cdk.Construct, id: string, props?: RdsStackProps) {
super(scope, id, props);
const databaseUsername = process.env.DB_USERNAME;
const applicationName = 'APP_NAME';
this.databaseCredentialsSecret = new secretsmanager.Secret(this, 'DBCredentialsSecret', {
secretName: `${applicationName}-db-credentials`,
generateSecretString: {
secretStringTemplate: JSON.stringify({
username: databaseUsername
}),
excludePunctuation: true,
includeSpace: false,
generateStringKey: 'password',
}
});
new ssm.StringParameter(this, 'DBCredentialsArn', {
parameterName: `${applicationName}-db-credentials-arn`,
stringValue: this.databaseCredentialsSecret.secretArn,
});
this.myRdsInstance = new rds.DatabaseInstance(this, 'MyDatabaseInstance', {
publiclyAccessible: true,
engine: rds.DatabaseInstanceEngine.mysql({
version: rds.MysqlEngineVersion.VER_5_7,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL),
vpc: props?.myVpc as IVpc,
securityGroups: [props?.rdsSecurityGroup as ISecurityGroup],
allocatedStorage: 20,
databaseName: 'DB_NAME_HERE',
storageEncrypted: true,
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import { SecurityGroup, GatewayVpcEndpointAwsService } from '@aws-cdk/aws-ec2';
export class VpcStack extends cdk.Stack {
readonly myVpc: ec2.IVpc;
readonly bastionHostSecurityGroup: SecurityGroup;
readonly elbSecurityGroup: SecurityGroup;
readonly asgSecurityGroup: SecurityGroup;
readonly rdsSecurityGroup: SecurityGroup;
readonly elastiCacheSecurityGroup: SecurityGroup;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const applicationName = 'APP_NAME_HERE';
this.myVpc = new ec2.Vpc(this, `${applicationName}-vpc`, {
cidr: process.env.VPC_CIDR,
maxAzs: 4,
natGateways: 1,
vpnGateway: true,
subnetConfiguration: [
{
subnetType: ec2.SubnetType.PUBLIC,
name: 'Public',
cidrMask: 20,
},
{
subnetType: ec2.SubnetType.PRIVATE,
name: 'Application',
cidrMask: 20,
},
{
subnetType: ec2.SubnetType.ISOLATED,
name: 'Database',
cidrMask: 24,
}
]
});
this.myVpc.addGatewayEndpoint('s3-gateway', {
service: GatewayVpcEndpointAwsService.S3,
subnets: [{
subnetType: ec2.SubnetType.PRIVATE
}]
})
this.bastionHostSecurityGroup = new SecurityGroup(this, 'bastionHostSecurityGroup', {
allowAllOutbound: true,
securityGroupName: 'bastion-sg',
vpc: this.myVpc,
});
this.elbSecurityGroup = new SecurityGroup(this, 'elbSecurityGroup', {
allowAllOutbound: true,
securityGroupName: 'elb-sg',
vpc: this.myVpc,
});
this.elbSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));
this.elbSecurityGroup.addIngressRule(ec2.Peer.anyIpv6(), ec2.Port.tcp(80));
this.asgSecurityGroup = new SecurityGroup(this, 'asgSecurityGroup', {
allowAllOutbound: false,
securityGroupName: 'asg-sg',
vpc: this.myVpc,
});
this.asgSecurityGroup.connections.allowFrom(this.elbSecurityGroup, ec2.Port.tcp(80), 'Application Load Balancer Security Group');
this.asgSecurityGroup.connections.allowFrom(this.bastionHostSecurityGroup, ec2.Port.tcp(22), 'Allows connections from bastion hosts');
this.rdsSecurityGroup = new SecurityGroup(this, 'rdsSecurityGroup', {
allowAllOutbound: false,
securityGroupName: 'rds-sg',
vpc: this.myVpc,
})
this.rdsSecurityGroup.connections.allowFrom(this.asgSecurityGroup, ec2.Port.tcp(3306), 'Allow connections from eb Auto Scaling Group Security Group');
this.rdsSecurityGroup.connections.allowFrom(this.bastionHostSecurityGroup, ec2.Port.tcp(3306), 'Allow connections from bastion hosts');
this.elastiCacheSecurityGroup = new SecurityGroup(this, 'elastiCacheSecurityGroup', {
allowAllOutbound: false,
securityGroupName: 'elasti-sg',
vpc: this.myVpc,
});
this.elastiCacheSecurityGroup.connections.allowFrom(this.asgSecurityGroup, ec2.Port.tcp(6379), 'Allow connections from eb Auto Scaling Security Group');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import * as dotenv from 'dotenv';
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { ElbtestStack } from '../lib/elbtest-stack';
import { RdsStack } from '../lib/rds-infrastructure';
import { VpcStack } from '../lib/vpc-stack';
dotenv.config()
const app = new cdk.App();
const env = {
account: process.env.AWS_ACCOUNT_ID,
region: process.env.AWS_REGION,
}
const vpcStack = new VpcStack(app, 'VpcStack', { env: env });
const rdsStack = new RdsStack(app, 'RdsStack', { env: env , myVpc: vpcStack.myVpc, rdsSecurityGroup: vpcStack.rdsSecurityGroup });
const ebStack = new ElbtestStack(app, 'ElbtestStack', { env: env });
rdsStack.addDependency(vpcStack);
ebStack.addDependency(rdsStack);
app.synth();
1
yarn add @aws-cdk/aws-elasticbeanstalk @aws-cdk/aws-s3-assets dotenv
1
cdk bootstrap --profile aws-deploy
1
yarn build && cdk synth --all --profile aws-deploy
~/.aws/config
file.1
yarn build && cdk deploy --all --profile aws-deploy
_somevalue12312.domain.com.
just remove .domain.com.
Fill out the above form and choose the certificate which you just generated.
- Open the Amazon EC2 console at https://console.aws.amazon.com/ec2/.
- On the navigation pane, choose Load Balancers.
- Select the load balancer, scroll down click Manage Listeners, and add the following setting
1
HTTPS 443 HTTP 80 <name of the certificate>
next build
command to generate the production-ready build.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET_NAME_HERE/*"
},
{
"Sid": "2",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity YOUR_IDENTITY"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET_NAME_HERE/*"
}
]
}
1
2
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: 2
jobs:
deploy:
working_directory: ~/app
docker:
- image: circleci/ruby:2.4.3
steps:
- checkout
- run:
name: Installing deployment dependencies
working_directory: /
command: |
sudo apt-get -y -qq update
sudo apt-get install python3-pip python3-dev build-essential
sudo pip3 install awsebcli
- run:
name: Deploying
command: eb deploy my-app-$CIRCLE_BRANCH
workflows:
version: 2
build:
jobs:
- deploy:
filters:
branches:
only:
- staging
- production
1
2
3
4
5
6
7
8
9
10
branch-defaults:
production:
environment: my-app-production
staging:
environment: my-app-staging
global:
application_name: my-app
default_platform: 64bit Amazon Linux 2 v5.8.3 running Node.js 18
default_region: YOUR_AWS_REGION
sc: git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
version: 2.1
jobs:
build:
working_directory: ~/repo
docker:
- image: cimg/node:21.6.0
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package-lock.json" }}
- v1-dependencies-
- run:
name: Install dependencies
command: npm install
- save_cache:
key: v1-dependencies-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Build
command: |
npm run build
- run:
name: Export
no_output_timeout: 10m
command: npm run export
- run:
name: Deploy
command: |
if [ $CIRCLE_BRANCH = 'staging' ]; then
aws s3 sync build s3://my-app-staging
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/*"
fi
if [ $CIRCLE_BRANCH = 'production' ]; then
aws s3 sync build s3://my-app-production
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/*"
fi
workflows:
version: 2
build:
jobs:
- deploy:
filters:
branches:
only:
- staging
- production
1
ssh -i ~/Downloads/your-key.pem ec2-user@YOUR_EC2_domain