community.aws Open in urlscan Pro
143.204.215.93  Public Scan

URL: https://community.aws/tutorials/using-ec2-userdata-to-bootstrap-python-web-app
Submission: On August 07 via api from US — Scanned from DE

Form analysis 0 forms found in the DOM

Text Content

SELECT YOUR COOKIE PREFERENCES

We use essential cookies and similar tools that are necessary to provide our
site and services. We use performance cookies to collect anonymous statistics so
we can understand how customers use our site and make improvements. Essential
cookies cannot be deactivated, but you can click “Customize cookies” to decline
performance cookies.

If you agree, AWS and approved third parties will also use cookies to provide
useful site features, remember your preferences, and display relevant content,
including relevant advertising. To continue without accepting these cookies,
click “Continue without accepting.” To make more detailed choices or learn more,
click “Customize cookies.”

Accept all cookiesContinue without acceptingCustomize cookies


CUSTOMIZE COOKIE PREFERENCES

We use cookies and similar tools (collectively, "cookies") for the following
purposes.


ESSENTIAL

Essential cookies are necessary to provide our site and services and cannot be
deactivated. They are usually set in response to your actions on the site, such
as setting your privacy preferences, signing in, or filling in forms.




PERFORMANCE

Performance cookies provide anonymous statistics about how customers navigate
our site so we can improve site experience and performance. Approved third
parties may perform analytics on our behalf, but they cannot use the data for
their own purposes.

Allow performance category
Allowed


FUNCTIONAL

Functional cookies help us provide useful site features, remember your
preferences, and display relevant content. Approved third parties may set these
cookies to provide certain site features. If you do not allow these cookies,
then some or all of these services may not function properly.

Allow functional category
Allowed


ADVERTISING

Advertising cookies may be set through our site by us or our advertising
partners and help us deliver relevant marketing content. If you do not allow
these cookies, you will experience less relevant advertising.

Allow advertising category
Allowed

Blocking some types of cookies may impact your experience of our sites. You may
review and change your choices at any time by clicking Cookie preferences in the
footer of this site. We and selected third-parties use cookies or similar
technologies as specified in the AWS Cookie Notice.

CancelSave preferences




UNABLE TO SAVE COOKIE PREFERENCES

We will only store essential cookies at this time, because we were unable to
save your cookie preferences.

If you want to change your cookie preferences, try again later using the link in
the AWS console footer, or contact support if the problem persists.

Dismiss


Search for content
Ctrl K

HomeTags
Featured Spaces
DevOps
Kubernetes
Livestreams
Generative AI
Community Programs
AWS Heroes
AWS Community Builders
AWS User Groups
AWS Cloud Clubs

--------------------------------------------------------------------------------




SITE TERMS, PRIVACY, AND MORE.




READ TIME: ~35 MINUTES


BOOTSTRAPPING AN AMAZON EC2 INSTANCE USING USER-DATA TO RUN A PYTHON WEB APP


DEPLOY A PYTHON WEB APPLICATION TO AN EC2 INSTANCE RUNNING NGINX AND UWSGI,
USING A CI/CD PIPELINE CREATED WITH AMAZON CDK.

awstutorialsec2cdkcodepipelinecodedeploycodebuildpython
Darko Mesaros
Cobus Bernard
Published Apr 21, 2023
|
Last Modified Apr 21, 2023




TABLE OF CONTENTS

Introduction

Setting up the CDK project

Create the Code for the Resource Stack

Create the EC2 Instance

Setting up GitHub

Creating the CI/CD Pipeline

Additional Files for Testing and Deploying

Bootstrap CDK

Cleaning Up Your AWS Environment

Conclusion

Manually setting up and configuring the packages required to run a Python web
app using Nginx and uWSGI on a server can be time consuming — and it's tough to
accomplish without any errors. But why do that hard work when you can automate
it? Using AWS CDK, we can set up user data scripts and an infrastructure to
preconfigure an EC2 instance - which in turn will turn a manual, time-intensive
process into a snap. In this tutorial, we will be using a combination of bash
scripts and AWS CodeDeploy to install and configure Nginx and uWSGI, set up a
systemd service for uWSGI, and copy our application using CDK. Then, we are
going to deploy our Python-based web application from a GitHub repository. We
will cover how to:

 * Create an AWS CDK stack with an Amazon EC2 instance, a CI/CD Pipeline, and
   the required resources for it to operate.
 * Install software packages on the EC2 instance's first launch by creating a
   user data asset.
 * Test, Deploy and Configure the web application using the CI/CD pipeline.

✅ AWS experience
200 - Intermediate
⏱ Time to complete
60 minutes
💰 Cost to complete
Free tier eligible
🧩 Prerequisites
- AWS account
-CDK installed: Visit Get Started with AWS CDK to learn more.
💻 Code Sample
Code sample used in tutorial on GitHub
📢 Feedback
Any feedback, issues, or just a 👍 / 👎 ?
⏰ Last Updated
2023-04-11
INTRODUCTION


To deploy this web application we will be using AWS CDK to create and deploy the
underlying infrastructure. This infrastructure will consist of an EC2 instance,
a VPC, CI/CD pipeline, and accompanying resources required for it to operate
(Security Groups and IAM permissions).

SETTING UP THE CDK PROJECT


First, let's check if our CDK version is up to date — this guide is based on v2
of the CDK. If you are still using v1, please read through the migration docs.
To check the version, run the following:

1
2
3

cdk --version

# 2.35.0 (build 85e2735)


If you see output showing 1.x.x, or you just want to ensure you are on the
latest version, run the following:

1

npm install -g aws-cdk


We will now create the skeleton CDK application using TypeScript as our language
of choice:

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

mkdir ec2-cdk
cd ec2-cdk
cdk init app --language typescript

# Output:

Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...
npm WARN deprecated w3c-hr-time@1.0.2: Use your platform's native
performance.now() and performance.timeOrigin.
npm notice
npm notice New patch version of npm available! 8.19.2 → 8.19.3
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.19.3
npm notice Run npm install -g npm@8.19.3 to update!
npm notice
✅ All done!

CREATE THE CODE FOR THE RESOURCE STACK


CDK uses the folder name for the files it generates. For this tutorial, we will
be using ec2-cdk. If you named your directory differently, please replace this
with the folder name you used. To start adding infrastructure, go to the file
lib/ec2-cdk-stack.ts. This is where we will write the code for the resource
stack you are going to create.

A resource stack is a set of cloud infrastructure resources (in your particular
case, they will be all AWS resources) that will be provisioned into a specific
account. The account and Region where these resources are provisioned can be
configured in the stack — which we will cover later on.

In this resource stack, you are going to create the following resources:

 * IAM roles: This role will be assigned to the EC2 instance to allow it to call
   other AWS services.
 * EC2 instance: The virtual machine you will use to host your web application.
 * Security group: The virtual firewall to allow inbound requests to your web
   application.
 * Secrets manager secret: This is a place where you will store your Github
   Token that we will use to authenticate the pipeline to it.
 * CI/CD Pipeline: This pipeline will consist of AWS CodePipeline, AWS
   CodeBuild, and AWS CodeDeploy.

CREATE THE EC2 INSTANCE


In this segment we will create the EC2 instance and its required resources.
During the course of this tutorial, there will be code checkpoints where we show
what the full file should look like at that point. We do recommend following
step-by-step by typing out or copying and pasting the sample code blocks to
ensure you understand what each code block does.

To start off, we will create the needed IAM role for your EC2 instance. This
role will be used to give your instance permission to interact with AWS Systems
Manager and AWS CodeDeploy. This will be important later in the tutorial. To get
started, make sure you import the following modules into your main stack.
(lib/ec2-cdk-stack.ts):

1
2
3
4
5
6

import { readFileSync } from 'fs';
import { Vpc, SubnetType, Peer, Port, AmazonLinuxGeneration,
AmazonLinuxCpuType, Instance, SecurityGroup, AmazonLinuxImage,
InstanceClass, InstanceSize, InstanceType
} from 'aws-cdk-lib/aws-ec2';
import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';


Then add the following lines to create a role and attach the needed managed IAM
Policies:

1
2
3
4
5
6
7
8
9
10
11
12

const webServerRole = new Role(this, "ec2Role", {
assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
});

// IAM policy attachment to allow access to
webServerRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
);

webServerRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonEC2RoleforAWSCodeDeploy")
);


Next step is to create a VPC where our EC2 instance will be residing. We are
creating a VPC with three Public subnets only, so there will be no NAT Gateways,
or private subnets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// This VPC has 3 public subnets, and that's it
const vpc = new Vpc(this, 'main_vpc',{
subnetConfiguration: [
{
cidrMask: 24,
name: 'pub01',
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'pub02',
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'pub03',
subnetType: SubnetType.PUBLIC,
}
]
});


We also need to be able to access our instance via http (port 80). To allow
traffic to this port, we need to set up firewall rules by creating a security
group. We will set up port 80 to allow HTTP traffic to come to the instance from
any location on the internet.

1
2
3
4
5
6
7
8
9
10
11
12

// Security Groups
// This SG will only allow HTTP traffic to the Web server
const webSg = new SecurityGroup(this, 'web_sg',{
vpc,
description: "Allows Inbound HTTP traffic to the web server.",
allowAllOutbound: true,
});

webSg.addIngressRule(
Peer.anyIpv4(),
Port.tcp(80)
);


We're now ready to create the EC2 instance using a pre-built Amazon Machine
Image (AMI - pronounced "Ay-Em-Eye") — for this tutorial, we will be using the
Amazon Linux 2 AMI for X86_64 CPU architecture. We will also pass the IAM role
and VPC created earlier, and the instance type to run on, in your case, a
t2.micro that has 1 vCPU and 1GB of memory. If you are running this tutorial in
one of the newer AWS Regions, the t2.micro type may not be available. Just use
the t3.micro one instead. To view all the different instance types, see the EC2
instance types page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// the AMI to be used for the EC2 Instance
const ami = new AmazonLinuxImage({
generation: AmazonLinuxGeneration.AMAZON_LINUX_2,
cpuType: AmazonLinuxCpuType.X86_64,
});

// The actual Web EC2 Instance for the web server
const webServer = new Instance(this, 'web_server',{
vpc,
instanceType: InstanceType.of(
InstanceClass.T2,
InstanceSize.MICRO,
),
machineImage: ami,
securityGroup: webSg,
role: webServerRole,
});


Finally we are attaching User Data and tagging the instance with specific tags.
The user data is used to bootstrap the EC2 instance and install specific
application packages on the instance's first boot. The tags are used by Systems
Manager to identify the instance later on for deployments.

Here is the user data bash script we will be attaching to the EC2 Instance. Make
sure this code sits in a file named configure_amz_linux_sample_app.sh in the
assets directory in the root of your CDK Application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

#!/bin/bash -xe
# Install OS packages
yum update -y
yum groupinstall -y "Development Tools"
amazon-linux-extras install -y nginx1
yum install -y nginx python3 python3-pip python3-devel ruby wget
pip3 install pipenv wheel
pip3 install uwsgi

# Code Deploy Agent
cd /home/ec2-user
wget https://aws-codedeploy-us-west-2.s3.us-west-2.amazonaws.com/latest/install
chmod +x ./install
./install auto


Now, use CDK to attach the user data script and add tags to the instance:

1
2
3
4
5
6

// User data - used for bootstrapping
const webSGUserData =
readFileSync('./assets/configure_amz_linux_sample_app.sh','utf-8');
webServer.addUserData(webSGUserData);
// Tag the instance
cdk.Tags.of(webServer).add('application-name','python-web')
cdk.Tags.of(webServer).add('stage','prod')


Additionally, we will configure outputs to easily track down the EC2 instance's
IP address:

1
2
3
4

// Output the public IP address of the EC2 instance
new cdk.CfnOutput(this, "IP Address", {
value: webServer.instancePublicIp,
});


We have now defined our AWS CDK stack to create an EC2 instance, a VPC, a
security group with inbound access rules, and an IAM role, attached to the EC2
instance as an IAM instance profile. On top of that, we have tagged the EC2
instance and attached a user data script to it.

> ✅ ✅ ✅ CHECKPOINT 1 ✅ ✅ ✅

Your lib/ec2-cdk-stack.ts file should now look like this:

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
95
96
97
98
99

import * as cdk from 'aws-cdk-lib';
import { readFileSync } from 'fs';
import { Construct } from 'constructs';

import { Vpc, SubnetType, Peer, Port, AmazonLinuxGeneration,
AmazonLinuxCpuType, Instance, SecurityGroup, AmazonLinuxImage,
InstanceClass, InstanceSize, InstanceType
} from 'aws-cdk-lib/aws-ec2';

import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';

export class PythonEc2BlogpostStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// IAM
// Policy for CodeDeploy bucket access
// Role that will be attached to the EC2 instance so it can be
// managed by AWS SSM
const webServerRole = new Role(this, "ec2Role", {
assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
});

// IAM policy attachment to allow access to
webServerRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
);

webServerRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonEC2RoleforAWSCodeDeploy")
);

// VPC
// This VPC has 3 public subnets, and that's it
const vpc = new Vpc(this, 'main_vpc',{
subnetConfiguration: [
{
cidrMask: 24,
name: 'pub01',
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'pub02',
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'pub03',
subnetType: SubnetType.PUBLIC,
}
]
});

// Security Groups
// This SG will only allow HTTP traffic to the Web server
const webSg = new SecurityGroup(this, 'web_sg',{
vpc,
description: "Allows Inbound HTTP traffic to the web server.",
allowAllOutbound: true,
});

webSg.addIngressRule(
Peer.anyIpv4(),
Port.tcp(80)
);

// EC2 Instance
// This is the Python Web server that we will be using
// Get the latest AmazonLinux 2 AMI for the given region
const ami = new AmazonLinuxImage({
generation: AmazonLinuxGeneration.AMAZON_LINUX_2,
cpuType: AmazonLinuxCpuType.X86_64,
});

// The actual Web EC2 Instance for the web server
const webServer = new Instance(this, 'web_server',{
vpc,
instanceType: InstanceType.of(
InstanceClass.T2,
InstanceSize.MICRO,
),
machineImage: ami,
securityGroup: webSg,
role: webServerRole,
});

// User data - used for bootstrapping
const webSGUserData =
readFileSync('./assets/configure_amz_linux_sample_app.sh','utf-8');
webServer.addUserData(webSGUserData);
// Tag the instance
cdk.Tags.of(webServer).add('application-name','python-web')
cdk.Tags.of(webServer).add('stage','prod')

// Output the public IP address of the EC2 instance
new cdk.CfnOutput(this, "IP Address", {
value: webServer.instancePublicIp,
});
}
}

SETTING UP GITHUB


Now we are going to fork the sample application to your own GitHub account and
configure a Github Token to be used by the CI/CD pipeline.

It is best practice to use tokens instead of passwords to access your GitHub
account via GitHub API or command line. Read more about creating a personal
access token.

Save the token in a safe place for later use. We will be using this token for
two purposes:

 1. To provide authentication to stage, commit, and push code from local repo to
    the GitHub repo. You may also use SSH keys for this.
 2. To connect GitHub to CodePipeline, so whenever new code is committed to
    GitHub repo it automatically triggers pipeline execution.

The token should have the scopes repo (to read the repository) and
admin:repo_hook (if you plan to use webhooks, true by default) as shown in the
image below.



Now, for AWS CodePipeline to read from this GitHub repo, we need to configure
the GitHub personal access token we just created. This token should be stored as
a plaintext secret (not a JSON secret) in AWS Secrets Manager under the exact
name github-oauth-token.

Replace GITHUB_ACCESS_TOKEN with your plaintext secret and REGION in following
command and run it:

1
2
3
4
5

aws secretsmanager create-secret \
--name github-oauth-token \
--description "Github access token for cdk" \
--secret-string GITHUB_ACCESS_TOKEN \
--region REGION


For more help, see Creating and Retrieving a Secret.

Finally, let's now go ahead and fork the Sample Application repo into our own
GitHub Account. This is how we will be interacting with this application from
now on. More information on forking repositories can be found here.

CREATING THE CI/CD PIPELINE


It's time to create a CI/CD Pipeline. This CI/CD pipeline will be responsible
for testing, deploying, and configuring our Web app on our EC2 Instance. The
pipeline itself will consist of three phases: 1/ Source - This is where the
pipeline extracts the commit from your GitHub repository we forked earlier; 2/
Build - A stage where we test the Application code using the unittest Python
Unit Testing Framework; and 3/ Deploy - Deploying and configuring the web
application on the EC2 instance using AWS CodeDeploy. Let's get back to CDK.

To start off, let's import additional modules into our main CDK stack file
lib/ec2-cdk-stack.ts:

1
2
3
4
5

import { Pipeline, Artifact } from 'aws-cdk-lib/aws-codepipeline';
import { GitHubSourceAction, CodeBuildAction, CodeDeployServerDeployAction }
from 'aws-cdk-lib/aws-codepipeline-actions';
import { PipelineProject, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild';
import { ServerDeploymentGroup, ServerApplication, InstanceTagSet } from
'aws-cdk-lib/aws-codedeploy';
import { SecretValue } from 'aws-cdk-lib';


Let's now create the pipeline and its stages, this is just us defining the
pipeline and the skeleton of different stages/phases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// CodePipeline
const pipeline = new Pipeline(this, 'python_web_pipeline',{
pipelineName: 'python-webApp',
crossAccountKeys: false, // solves the encrypted bucket issue
});

// STAGES
// Source Stage
const sourceStage = pipeline.addStage({
stageName: 'Source',
})

// Build Stage
const buildStage = pipeline.addStage({
stageName: 'Build',
})

// Deploy Stage
const deployStage = pipeline.addStage({
stageName: 'Deploy',
})


We will start with the Source stage, as here is where we connect the pipeline to
Github, so it can retrieve our code commits to be passed down the pipeline. Some
important parts to take note of here: make sure to set up the github token as a
secret in AWS Secrets Manager (check the steps above), and ensure to change the
owner parameter to match that of your GitHub username:

1
2
3
4
5
6
7
8
9
10
11
12

// Source action
const sourceOutput = new Artifact();
const githubSourceAction = new GitHubSourceAction({
actionName: 'GithubSource',
oauthToken: SecretValue.secretsManager('github-oauth-token'), // MAKE SURE TO
SET UP BEFORE
owner: 'darko-mesaros', // THIS NEEDS TO BE CHANGED TO YOUR OWN USER ID
repo: 'sample-python-web-app',
branch: 'main',
output: sourceOutput,
});

sourceStage.addAction(githubSourceAction);


On to the Build stage: we are not actually building anything, but rather testing
the code. In this stage, we are running unit tests against our code (which we
will set up later), and if successful, it continues along to the next next
stage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// Build Action
const pythonTestProject = new PipelineProject(this, 'pythonTestProject',{
environment: {
buildImage: LinuxBuildImage.AMAZON_LINUX_2_3
}
});

const pythonTestOutput = new Artifact();

const pythonTestAction = new CodeBuildAction({
actionName: 'TestPython',
project: pythonTestProject,
input: sourceOutput,
outputs: [pythonTestOutput]
});

buildStage.addAction(pythonTestAction);


And finally the Deploy stage: this stage uses CodeDeploy to deploy and configure
the web application on the EC2 instance. For this to work, we need to have the
CodeDeploy agent installed and running on the instance (which we did before with
the user data), and also we need to tell CodeDeploy which instances to target
for deployment. We will be using tags for this. If you recall, earlier in this
tutorial we tagged the EC2 instance with specific tags. Now we are using those
tags to target the instances with CodeDeploy, and deploy the code.

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

// Deploy Actions
const pythonDeployApplication = new
ServerApplication(this,"python_deploy_application",{
applicationName: 'python-webApp'
});

// Deployment group
const pythonServerDeploymentGroup = new
ServerDeploymentGroup(this,'PythonAppDeployGroup',{
application: pythonDeployApplication,
deploymentGroupName: 'PythonAppDeploymentGroup',
installAgent: true,
ec2InstanceTags: new InstanceTagSet(
{
'application-name': ['python-web'],
'stage':['prod', 'stage']
})
});

// Deployment action
const pythonDeployAction = new CodeDeployServerDeployAction({
actionName: 'PythonAppDeployment',
input: sourceOutput,
deploymentGroup: pythonServerDeploymentGroup,
});

deployStage.addAction(pythonDeployAction);

> ✅ ✅ ✅ CHECKPOINT 2 ✅ ✅ ✅

We have now completed all code changes to our CDK app, and the
lib/ec2-cdk-stack.ts file should look like this:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

import * as cdk from 'aws-cdk-lib';
import { readFileSync } from 'fs';
import { Construct } from 'constructs';

import { Vpc, SubnetType, Peer, Port, AmazonLinuxGeneration,
AmazonLinuxCpuType, Instance, SecurityGroup, AmazonLinuxImage,
InstanceClass, InstanceSize, InstanceType
} from 'aws-cdk-lib/aws-ec2';

import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';
import { Pipeline, Artifact } from 'aws-cdk-lib/aws-codepipeline';
import { GitHubSourceAction, CodeBuildAction, CodeDeployServerDeployAction }
from 'aws-cdk-lib/aws-codepipeline-actions';
import { PipelineProject, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild';
import { ServerDeploymentGroup, ServerApplication, InstanceTagSet } from
'aws-cdk-lib/aws-codedeploy';
import { SecretValue } from 'aws-cdk-lib';

export class PythonEc2BlogpostStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// IAM
// Policy for CodeDeploy bucket access
// Role that will be attached to the EC2 instance so it can be
// managed by AWS SSM
const webServerRole = new Role(this, "ec2Role", {
assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
});

// IAM policy attachment to allow access to
webServerRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
);

webServerRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonEC2RoleforAWSCodeDeploy")
);

// VPC
// This VPC has 3 public subnets, and that's it
const vpc = new Vpc(this, 'main_vpc',{
subnetConfiguration: [
{
cidrMask: 24,
name: 'pub01',
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'pub02',
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'pub03',
subnetType: SubnetType.PUBLIC,
}
]
});

// Security Groups
// This SG will only allow HTTP traffic to the Web server
const webSg = new SecurityGroup(this, 'web_sg',{
vpc,
description: "Allows Inbound HTTP traffic to the web server.",
allowAllOutbound: true,
});

webSg.addIngressRule(
Peer.anyIpv4(),
Port.tcp(80)
);

// EC2 Instance
// This is the Python Web server that we will be using
// Get the latest AmazonLinux 2 AMI for the given region
const ami = new AmazonLinuxImage({
generation: AmazonLinuxGeneration.AMAZON_LINUX_2,
cpuType: AmazonLinuxCpuType.X86_64,
});

// The actual Web EC2 Instance for the web server
const webServer = new Instance(this, 'web_server',{
vpc,
instanceType: InstanceType.of(
InstanceClass.T3,
InstanceSize.MICRO,
),
machineImage: ami,
securityGroup: webSg,
role: webServerRole,
});

// User data - used for bootstrapping
const webSGUserData =
readFileSync('./assets/configure_amz_linux_sample_app.sh','utf-8');
webServer.addUserData(webSGUserData);
// Tag the instance
cdk.Tags.of(webServer).add('application-name','python-web')
cdk.Tags.of(webServer).add('stage','prod')

// Pipeline stuff
// CodePipeline
const pipeline = new Pipeline(this, 'python_web_pipeline', {
pipelineName: 'python-webApp',
crossAccountKeys: false, // solves the encrypted bucket issue
});

// STAGES
// Source Stage
const sourceStage = pipeline.addStage({
stageName: 'Source',
});

// Build Stage
const buildStage = pipeline.addStage({
stageName: 'Build',
});

// Deploy Stage
const deployStage = pipeline.addStage({
stageName: 'Deploy',
});

// Add some action
// Source action
const sourceOutput = new Artifact();
const githubSourceAction = new GitHubSourceAction({
actionName: 'GithubSource',
oauthToken: SecretValue.secretsManager('github-oauth-token'), // SET UP BEFORE
owner: 'darko-mesaros', // THIS NEEDS TO BE CHANGED TO THE READER
repo: 'sample-python-web-app',
branch: 'main',
output: sourceOutput,
});

sourceStage.addAction(githubSourceAction);

// Build Action
const pythonTestProject = new PipelineProject(this, 'pythonTestProject', {
environment: {
buildImage: LinuxBuildImage.AMAZON_LINUX_2_3
}
});

const pythonTestOutput = new Artifact();
const pythonTestAction = new CodeBuildAction({
actionName: 'TestPython',
project: pythonTestProject,
input: sourceOutput,
outputs: [pythonTestOutput]
});

buildStage.addAction(pythonTestAction);

// Deploy Actions
const pythonDeployApplication = new
ServerApplication(this,"python_deploy_application", {
applicationName: 'python-webApp'
});

// Deployment group
const pythonServerDeploymentGroup = new
ServerDeploymentGroup(this,'PythonAppDeployGroup', {
application: pythonDeployApplication,
deploymentGroupName: 'PythonAppDeploymentGroup',
installAgent: true,
ec2InstanceTags: new InstanceTagSet(
{
'application-name': ['python-web'],
'stage':['prod', 'stage']
})
});

// Deployment action
const pythonDeployAction = new CodeDeployServerDeployAction({
actionName: 'PythonAppDeployment',
input: sourceOutput,
deploymentGroup: pythonServerDeploymentGroup,
});

deployStage.addAction(pythonDeployAction);

// Output the public IP address of the EC2 instance
new cdk.CfnOutput(this, "IP Address", {
value: webServer.instancePublicIp,
});
}
}

ADDITIONAL FILES FOR TESTING AND DEPLOYING


To properly test and deploy our application, we will need to add some additional
content to the sample repository we forked earlier. These files are used by the
CodeBuild and CodeDeploy services. On top of that, we will write a simple Python
unit test. Let's start with that.

To create our tests, in the root directory of the sample application create a
tests directory, and add the following test_sample.py file to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

import unittest
from application import application

class TestHello(unittest.TestCase):

def setUp(self):
application.testing = True
self.application = application.test_client()

def test_hello(self):
rv = self.application.get('/')
self.assertEqual(rv.status, '200 OK')

if __name__ == '__main__':
import xmlrunner
unittest.main(testRunner=xmlrunner.XMLTestRunner(output='test-reports'))
unittest.main()


This test will run the Flask application and see if it returns a 200 HTTP status
code. Simple as that. On top of this file, just for posterity, let's create a
__init__.py file in the same directory. This file can be empty so you can just
create it with the following command:

1

touch tests/__init__.py


We are now ready to create the buildspec.yml file. This file is used by
CodeDeploy as an instruction set of what it needs to do to build your code. In
our case, we are instructing it on how to run the tests. In the root directory
of the sample application, add the buildspec.yml file with the following
contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

version: 0.2

phases:
install:
runtime-versions:
python: 3.7
commands:
- echo Entered the install phase...
- pip install pipenv
- pipenv install
finally:
- echo This always runs even if the update or install command fails
build:
commands:
- echo Entered the build phase...
- echo Build started on `date`
- pipenv run python -m unittest # not an interactive session so we need to run
finally:
- echo This always runs even if the install command fails
post_build:
commands:
- echo Entered the post_build phase...
- echo Build completed on `date`


Finally, let's add some much needed files for CodeDeploy. Similarly to
CodeBuild, CodeDeploy takes a file called appspec.yml as an instruction set on
how to deploy your application to its final destination. On top of that file, we
will be adding a few shell scripts to configure and launch the application on
the server. This is needed as we need to create a specific nginx website, and do
some service restarts. But let's first create the appspec.yml file in the root
of the sample application directory, with the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

version: 0.0
os: linux
files:
- source: /
destination: /var/www/SampleApp
hooks:
BeforeInstall:
- location: scripts/setup_dirs.sh
timeout: 300
runas: root
AfterInstall:
- location: scripts/setup_services.sh
- location: scripts/pipenv.sh
timeout: 300
runas: root
ApplicationStart:
- location: scripts/start_server.sh
timeout: 300
runas: root


As you can see, here we are involving 4 different scripts in different phases of
the deployment. This is required to properly set up the EC2 instance before and
after code deployment. These scripts should sit in a directory called scripts in
the root of the sample application. These scripts should be named as follows,
and should contain the following contents:

setup_dirs.sh

1
2
3
4

#!/bin/bash -xe
mkdir -p /var/www/SampleApp
chown nginx:nginx /var/www
chown nginx:nginx /var/www/SampleApp


setup_services.sh

1
2
3
4
5
6
7
8
9
10
11
12

#!/bin/bash -xe
## Install uWSGI as a systemd service, enable it to run at boot, then start it
cp /var/www/SampleApp/sample-app.uwsgi.service
/etc/systemd/system/mywebapp.uwsgi.service
mkdir -p /var/log/uwsgi
chown nginx:nginx /var/log/uwsgi
systemctl enable mywebapp.uwsgi.service

## Copy the nginx config file, then ensure nginx starts at boot, and restart it
to load the config
cp /var/www/SampleApp/nginx-app.conf /etc/nginx/conf.d/nginx-app.conf
mkdir -p /var/log/nginx
chown nginx:nginx /var/log/nginx
systemctl enable nginx.service


pipenv.sh

1
2
3
4
5

#!/bin/bash -xe

chown nginx:nginx -R /var/www/SampleApp/
cd /var/www/SampleApp
/usr/local/bin/pipenv install


start_server.sh

1
2
3

#!/bin/bash -xe
systemctl restart mywebapp.uwsgi.service
systemctl restart nginx.service


Once all these files are created, the sample application directory should look
like this:

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

├── application.config
├── application.py
├── appspec.yml
├── buildspec.yml
├── CODE_OF_CONDUCT.md
├── configure_amz_linux_sample_app.sh
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── nginx-app.conf
├── Pipfile
├── README.md
├── sample-app.uwsgi.service
├── scripts
│   ├── pipenv.sh
│   ├── setup_dirs.sh
│   ├── setup_services.sh
│   └── start_server.sh
├── start.sh
├── static
│   ├── bootstrap
│   └── jquery
├── templates
│   └── index.html
└── tests
├── __init__.py
├── __pycache__
└── test_sample.py


Now make sure to add, commit, and push your changes to the sample code to your
GitHub Repository before we continue to the next step and deploy the
infrastructure.

BOOTSTRAP CDK


Before we can deploy our CDK app, we need to configure CDK on the account you
are deploying to. Edit the bin/ec2-cdk.ts and uncomment line 14:

1

env: { account: process.env.CDK_DEFAULT_ACCOUNT, region:
process.env.CDK_DEFAULT_REGION },


This will use the account ID and Region configured in the AWS CLI—if you have
not yet set this up, please follow this tutorial section. We also need to
bootstrap CDK in our account. This will create the required infrastructure for
CDK to manage infrastructure in your account, and it only needs to be done once
per account. If you have already done the bootstrapping, or aren't sure, you can
just run the command again. It will only bootstrap if needed. To bootstrap CDK,
run cdk bootstrap (your account ID will be different from the placeholder ones
below):

1
2
3
4
5
6

cdk bootstrap

#output
⏳ Bootstrapping environment aws://0123456789012/<region>...
✅ Environment aws://0123456789012/<region> bootstrapped
Deploying the stack


Once the bootstrapping has completed, we're ready to deploy all the
infrastructure. Run the following:

1

cdk deploy


You will be presented with the following output and confirmation screen. Because
there are security implications for our stack, you will see a summary of these
and need to confirm them before deployment proceeds. This will always be shown
if you are creating, modifying, or deleting any IAM policy, role, group, or
user, and when you change any firewall rules.



Enter y to continue with the deployment and create the resources. The CLI will
show the deployment progress, and in the end, the output we defined in our CDK
app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Do you wish to deploy these changes (y/n)? y
PythonEc2BlogpostStack: deploying...
[0%] start: Publishing
afe67465ec62603d27d77795221a45e68423c87495467b0265ecdadad80bb5e2:current
[33%] success: Published
afe67465ec62603d27d77795221a45e68423c87495467b0265ecdadad80bb5e2:current
[33%] start: Publishing
73887b77b71ab7247eaf6dc4647f03f9f1cf8f0da685460f489ec8f2106d480d:current
[66%] success: Published
73887b77b71ab7247eaf6dc4647f03f9f1cf8f0da685460f489ec8f2106d480d:current
[66%] start: Publishing
13138ebf2da51426144f6f5f4f0ad197787f52aad8b6ceb26ecff68d33cd2b78:current
[100%] success: Published
13138ebf2da51426144f6f5f4f0ad197787f52aad8b6ceb26ecff68d33cd2b78:current
Ec2CdkStack: creating CloudFormation changeset...

✅ PythonEc2BlogpostStack

✨ Deployment time: 27.74s

Outputs:
PythonEc2BlogpostStack.IPAddress = 18.236.81.182
Stack ARN:
arn:aws:cloudformation:us-west-2:123456789000:stack/PythonEc2BlogpostStack/59f1e560-grunf-11ed-afno1-06f3bbc9cf63

✨ Total time: 29.11s


Your infrastructure is now deployed, the instance is spinning up, and you can
use the outputs at the bottom that indicate the IP address of your web server.
The application will not be immediately available, as it needs to be deployed.
To check the status of the deployment, head over to the AWS CodePipeline console
and find the python-webApp pipeline. There you should see something similar to
this:



After the deployment is successful (the Deploy stage should be green), copy and
then paste the IP address of your EC2 instance in your browser, and your sample
application should be up and running. Congratulations! You have set up a Python
web application running on an EC2 instance, with a CI/CD pipeline to test and
deploy and changes!

CLEANING UP YOUR AWS ENVIRONMENT


You have now completed this tutorial, but we still need to clean up the
resources created during this tutorial. If your account is still in the Free
Tier, there will not be any monthly charges. Once out of the Free Tier, it will
cost ~$9.45 per month, or $0.0126 per hour.

To remove all the infrastructure we created, use the cdk destroy command. This
will only remove infrastructure created during this tutorial in our CDK
application. You will see a confirmation:

1
2
3
4
5
6

cdk destroy

# Enter y to approve the changes and delete any stack resources.
PythonEc2BlogpostStack: destroying ...

✅ PythonEc2BlogpostStack: destroyed


When the output shows PythonEc2BlogpostStack: destroyed, your resources have
been removed. There is one more step for the cleanup: removing the S3 bucket
used by CDK to upload the scripts and sample application. These resources aren't
deleted by CDK as a safety precaution. Open the S3 console in your browser, and
look for a bucket with a name like
pythonec2blockpoststack-<randonmunbers>-us-east-1 (yours will have a different
random number and your account number instead of 123456789012). If you see more
than one (usually if you have used the CDK asset feature before), you can sort
by Creation Date to see the latest created one. Open the bucket to confirm that
you see a directory called python-webApp. Select all the directory, then choose
actions -> delete, and follow the prompts to delete the objects. Lastly, go back
to the S3 console, and delete the bucket.

CONCLUSION


Congratulations! You have finished the Build a Web Application on Amazon EC2
tutorial using CDK to provision all infrastructure, and configured your EC2
instance to install and configure OS packages to run the sample Python web app.
If you enjoyed this tutorial, found any issues, or have feedback for us, please
send it our way!



Any opinions in this post are those of the individual author and may not reflect
the opinions of AWS.


TABLE OF CONTENTS

Introduction

Setting up the CDK project

Create the Code for the Resource Stack

Create the EC2 Instance

Setting up GitHub

Creating the CI/CD Pipeline

Additional Files for Testing and Deploying

Bootstrap CDK

Cleaning Up Your AWS Environment

Conclusion