Transform Your Deployment Game: A Step-by-Step Guide to EC2 Instance Provisioning with CloudFormation and the Power of NextJS & Sanity Stack
What a week! Incredible how fast 2023 is going by.
For this tutorial, I will describe how you can maximize your deployment process by using a CloudFormation template
This is the same CloudFromation template I use to deploy my blog site www.stackfails.io. The tech stack is Nextjs and sanity.io.
Side note; If you haven’t tried this Stack out yet it is a powerful combination of Next.js and sanity.io. You can achieve incredible Lighthouse scores like I did. So why wait? Embrace the latest technology and elevate your online presence today. Follow me on Medium and clap for this article to stay up to date with all my tips and tricks on boosting your blog’s performance!
By the end of this tutorial, you will have a solid understanding of how to provision the CloudFormation template. This template will provision an EC2 instance with Nginx running a reverse proxy. In addition, we will set up the code deploy agent so that you can include this instance in your continuous integration (CI) and continuous delivery (CD) pipeline. (I will cover this in a future article). Also we will provision two security groups, one for SSH access and one for TCP connections on HTTP and HTTPS.
We will set up an AWS IAM ROLE for your instance to assume to connect to an S3 bucket to pull template files in. We will configure SSH Access for git hub( you can easily swap to bit bucket or another provider). Finally we will assign a predefined Elastic IP so your site will always stay up.
Before we dive in, let’s go over a few assumptions. Firstly, you should already have an AWS account and domain name.
Secondly, your code should be hosted on a service such as GitHub or Bitbucket. Thirdly, you will have to create an elastic IP address for the EC2 instance we will make. If you need to learn how to do it, check out this link from AWS on how to create one.
Once you have completed your Elastic IP, take note of the Allocation ID, as we will need it for the CloudFormation template and the ALIAS record you will create with your DNS provider.
I am not going to touch on how to create an ALIAS record as it varies from provider to provider, but if you’re wondering what an ALIAS record is and why you should use one with AWS, make sure to check out this article I wrote here:
With AWS, managing parameters is easy. Go to the AWS Console and navigate to the Parameter Store. In this tutorial, we will need to create two parameters to store your SSH key for automated GitHub access and your .env values that will be injected into the build via the {{resolve:ssm:ssh-key}} syntax in the CloudFormation template.
If you have never used AWS Parameter Store before check out the documentation here:
One word of note; AWS has a newer service called AWS Secrets Manager. Which they do recommend using now over the parameter store. One significant benefit of Secrets manager over the parameter store is it can automate the rotation of passwords. These parameters are very static so the parameter store will be fine in this case.
Now for the magic!
Here is the template which will turn you into a Dev Ops guru. All joking aside, this is going to make your life easier.
AWSTemplateFormatVersion: "2010-09-09"
Description:
Deploy any node based app to an Nginx server in minutes ,
This templates creates a instance with code deploy agent for use with a code deploy pipeline
ec2 server is configured with node 16 and git.
www.StackFails.io Aaron Janes
Parameters:
# Will retrieve the latest linux AMI
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
SiteName:
Description: Url of the site provide the apex and the www value
Type: String
Default: "stackfails.io www.stackfails.io"
MinLength: 1
MyS3BucketName:
Description: Name of an existing S3 bucket to download template files from. Not required
Type: String
Default: " "
ElasticIPAddress:
Description: The allocation Id of the EIP you created
Type: String
Default: "Your EIP"
MinLength: 1
GitCloneLink:
Description: Provide the clone link for your version controlled repo
Type: String
Default: "git@github.com:username/your-repo-folder.git"
MinLength: 1
RepoFolderName:
Description: The name of the folder your app clones into
Type: String
Default: "your-repo-folder"
MinLength: 1
KeyName:
Description: The name of your ssh key pem file to ssh to your instance
Type: String
Default: "Your ssh.pem file"
MinLength: 1
Resources:
Webserver:
Type: AWS::EC2::Instance
Metadata:
Comment: Basic Nginx Server Setup
AWS::CloudFormation::Init:
config:
packages:
yum:
ruby: [ ]
wget: [ ]
git: [ ]
files:
# Configure the reverse proxy for the site domain
# ideally this will come from the S3 bucket, but it is here to highlight the base config
/etc/nginx/conf.d/stackfails.conf:
content: !Sub |
server {
client_max_body_size 64M;
listen 80;
server_name ${SiteName};
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_redirect off;
}
}
"/etc/cfn/cfn-hup.conf":
content:
!Sub |
[main]
stack=${AWS::StackId}
region=${AWS::Region}
mode: "000400"
owner: "root"
group: "root"
"/etc/cfn/hooks.d/cfn-auto-reloader.conf":
content: !Sub |
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.WebServerHost.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource WebServerHost --region ${AWS::Region}
mode: "000400"
owner: "root"
group: "root"
commands:
# Add your ssh key from AWS SSM to your instance
00-create-ssh-key:
command: !Sub |
touch ~/.ssh/id_rsa
echo "{{resolve:ssm:ssh-key}}" >> ~/.ssh/id_rsa
chmod 700 ~/.ssh/
chmod 600 ~/.ssh/id_rsa
#Install code deploy agent so instance can be used by cd pipeline
01-install-cd:
command: !Sub |
wget https://aws-codedeploy-${AWS::Region}.s3.amazonaws.com/latest/install
chmod +x ./install
./install auto
service codedeploy-agent status
02-install-nginx:
command: sudo amazon-linux-extras install nginx1 -y
03-run-nginx:
command: sudo service nginx start
# Install node 16
04-install-node:
command: !Sub |
curl --silent --location https://rpm.nodesource.com/setup_16.x | bash -
yum -y install nodejs
#Clone your app from git hub but can be configured to use any version control
05-clone-app:
command: !Sub |
mkdir /var/webapp
cd /var/webapp
ssh -o "StrictHostKeyChecking=no" git@github.com
git clone ${GitCloneLink}
# Add environment variables from aws SSM
06-add-app-env:
command: !Sub |
cd /var/webapp/{RepoFolderName}
touch .env
echo "{{resolve:ssm:stack-fail-env}}" >> /var/webapp/${RepoFolderName}/.env
#Install and build app
07-install-start-app:
command: !Sub |
cd /var/webapp/${RepoFolderName}
npm install
npm run build
# Set up a process manager https://pm2.keymetrics.io/docs/usage/quick-start/
08-install-start-process-manager:
command: !Sub |
cd /var/webapp/${RepoFolderName}
npm install -g pm2
pm2 start npm --name "${RepoFolderName}" -- start
pm2 startup
pm2 save
Services:
sysvinit:
codedeploy-agent:
enabled: true
ensureRunning: true
nginx:
enabled: true
ensureRunning: true
files:
- /etc/nginx/conf.d/stackfails.conf
cfn-hup:
enable: 'true'
ensureRunning: 'true'
files:
- "/etc/cfn/cfn-hup.conf"
- "/etc/cfn/hooks.d/cfn-auto-reloader.conf"
# Define S3 access credentials
AWS::CloudFormation::Authentication:
S3AccessCreds:
type: S3
buckets:
- !Sub ${MyS3BucketName}
roleName: !Ref InstanceRole
CreationPolicy:
ResourceSignal:
Timeout: PT15M
Count: 1
Properties:
# Always use the latest Linux AMI
ImageId: !Ref LatestAmiId
AvailabilityZone: us-east-1a
InstanceType: t2.small
KeyName: !Ref KeyName
SecurityGroups:
- !Ref SSHSecurityGroup
- !Ref ServerSecurityGroup
UserData:
Fn::Base64:
!Sub |
#!/bin/bash -xe
# Get the latest CloudFormation helper scripts
yum install -y aws-cfn-bootstrap
# Start cfn-init
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Webserver --region ${AWS::Region}
# cfn-init completed so signal success or not
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource Webserver --region ${AWS::Region}
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: 'sts:AssumeRole'
Principal:
Service: ec2.amazonaws.com
Effect: Allow
Sid: ''
Policies:
- PolicyName: AuthenticatedS3GetObjects
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- 's3:GetObject'
Resource: !Sub 'arn:aws:s3:::${MyS3BucketName}/*'
Effect: Allow
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref InstanceRole
SSHSecurityGroup:
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable SSH access via port 22 you should limit the ip address
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
ServerSecurityGroup:
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP and HTTPS connections from specified CIDR ranges
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
EIPAssociation:
Type: "AWS::EC2::EIPAssociation"
Properties:
AllocationId: !Ref ElasticIPAddress
InstanceId: !Ref Webserver
Outputs:
InstanceId:
Description: The ID of the instance
Value: !Ref Webserver
PublicIP:
Description: The public IP of the instance
Value: !GetAtt Webserver.PublicIp
PublicDNS:
Description: The public DNS name of the instance
Value: !GetAtt Webserver.PublicDnsName
SecurityGroup:
Description: The security group of the instance
Value: !Ref ServerSecurityGroup
SSHSecurityGroup:
Description: The security group of the instance
Value: !Ref SSHSecurityGroup
Head to CloudFormation in the AWS console and click on create a stack.
Once the subsequent page loads, select:
The template is ready. Upload the template from file and then click next.
Then enter your stack name and fill in the required parameters.
I have set up the parameters to accept an S3 Bucket so that this template can access additional files. You can optimize the Nginx server configuration for production, and ideally you will download that from the S3 at run time. I have configured that feature of the CloudFormation stack for you, but it is beyond the scope of this Tutorial.
Click next after completing the form.
On the next screen, we will accept all the default values and click next and on the subsequent page, we will also take the default values.
Now your stack will run, create the desired resources, and launch your web app. Visit the URL you provided in the configuration to visit your newly deployed site.
Here are a few tips:
This template uses the cfn-init scripts to configure things. If you run into trouble, you can SSH into the instance and check the logs to see what has failed.
sudo su
cat /var/log/cfn-init.log
The logs will show you where the issue is in your configuration.
My next article will walk you through configuring HTTPS for this stack.
You can easily set up dev and staging environments now by configuring the branch that the CloudFormation template pulls from
In conclusion, setting up an EC2 instance with Nginx and CloudFormation is a great way to streamline your DevOps workflow and take your web applications to the next level. Following the steps outlined in this article, you can quickly and easily deploy a highly scalable and secure environment perfect for your next project.
If you found this article helpful, please hit the clap button and show your support. And if you are interested in learning more about DevOps and Front-end technology related topics, be sure to follow me on Medium for more great content.
Thank you for reading!