Running A Simple Node Web Server On AWS EC2

October 11, 2018

How do you run a web server on EC2?

I've used AWS Lambda and/or Cloudformation extensively but was curious to see how difficult it is to spin up a EC2 instance and expose the right ports from the command line. Using AWS' command line interface This is a good approach for testing and playing with tools and has advantages over Cloudformation. Debugging Cloudformation can be a huge pain. Also creating and updating stacks can take a bit longer than simply running an instance from terminal.

Back to the Basics

Perquisites

  • I'll assume you've installed AWS CLI. If not, start here. You can check by running  aws --version.
  • I'll be using jq, which is like sed for JSON data. Install it here.

What parameters do we need to run an instance? We need:

  • The Amazon Machine Image (AMI) ID.
  • Private Key Name
  • Subnet ID
  • Security Group IDs
  • User Data

Before running any commands, export the AWS region you want to use so that you're not prompted for every command. I'll be using us-east-1.

export AWS_DEFAULT_REGION=us-east-1

Amazon Machine Image (AMI) ID

This identifies the instance on Amazon's marketplace. We'll stick with defaults and use the latest Amazon Linux Image.

Run:

aws ssm get-parameters --names /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 --query 'Parameters[0].[Value]' --output text

As of this writing, the latest Amazon Linux AMI is ami-0922553b7b0369273.

Private Key Name

When you create an EC2 instance on aws.amazon.com using the UI you're prompted to use an existing private key or create a new one. The private key is just the label associated with a set of public and private keys allowing SSH access to your instance.

This command lists all of your existing key pairs:

aws ec2 describe-key-pairs | jq -r '.KeyPairs'

If you don't have any keys, you can create a new one:

aws ec2 create-key-pair --key-name webserver | jq -r '.KeyMaterial' > ~/.ssh/webserver.pem

This command saves the RSA key material to ~/.ssh/webserver.pem.

Subnet ID

AWS accounts have a default VPC, subnet, and set of security groups.

Rather than create a new subnet we'll just use one of the subnets associated with the default VPC.

Here's how you list subnets:

aws ec2 describe-subnets | jq -r '.Subnets'

Security Group IDs

For a web server, we'll use two security groups: a default security group associated with the default VPC, and a custom security group that exposes ports 22 (SSH), 80 (HTTP), and 443 (HTTPS).

Get the default security group ID as follows:

aws ec2 describe-security-groups --group-name default | jq -r '.SecurityGroups[0].GroupId'

And create a new security group:

aws ec2 create-security-group --group-name webserver --description webserver | jq -r '.GroupId'

Now we need to run authorize-security-group-ingress three times (once for each port).

aws ec2 authorize-security-group-ingress --group-name webserver --protocol tcp --port 22 --cidr '0.0.0.0/0'
aws ec2 authorize-security-group-ingress --group-name webserver --protocol tcp --port 80 --cidr '0.0.0.0/0'
aws ec2 authorize-security-group-ingress --group-name webserver --protocol tcp --port 443 --cidr '0.0.0.0/0'

We need to expose port 22 for SSH access, port 80 - the port that our node server will listen to, and port 443 for HTTPS.

User Data

User data lets us run scripts when the instance is launched. In this case, we need to install node and npm on the EC2 instance when it's created. Create a new file (e.g., user-data.sh) and add:

#!/bin/bash -v
su ec2-user -c "curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash"
su ec2-user -c "source /home/ec2-user/.nvm/nvm.sh && nvm install node"
su ec2-user -c "source /home/ec2-user/.nvm/nvm.sh && nvm use node"
ln -s /home/ec2-user/.nvm/versions/node/v10.11.0/bin/node /usr/bin/node
ln -s /home/ec2-user/.nvm/versions/node/v10.11.0/bin/npm /usr/bin/npm
mkdir -p /var/www
echo -e "require('http').createServer((req, res) => {" > /var/www/server.js
echo -e "  res.setHeader('content-type', 'text/plain')" > /var/www/server.js
echo -e "  res.end('OK')" > /var/www/server.js
echo -e "}).listen(process.env.PORT || 80)" > /var/www/server.js
node /var/www/server.js

We'll supply the path to the user-data.sh script as a parameter when we create the EC2 instance like so:

aws ec2 run-instances --user-data file://path/to/your/user-data.sh

Here's what everything looks like together: