Configuring SSL on single instance AWS EB
Find the code for this post here
I’ve been running my Planet Tracker app on Heroku for a long time. This mostly due to the fact that deploying apps on Heroku (provided you don’t have system level dependencies) is dead easy. You can even set it up to run automatically after your Github CI pipeline finishes with no additional configuration – you simply click a button on the dashboard for your app, and pow! it’s deployed. While I do actually use the app in the wild, it mostly serves as a sort of a sandbox for me to play around with novel web technologies. For instance, I originally put the client code together in vanilla JS, but I ended up moving to Vue.js in order to get some experience with the framework. On the backend, I’ve played around with web sockets and asynchronous Python code.
Given the prevalence of AWS products in the wild, I decided to try out redeploying my app using some combination of AWS products. I ended up using Elasticbeanstalk (EB), as it seemed closest to what I was doing with Heroku. I had a few goals for this redeployment project:
- Keep the price as close to $0/month as possible
- Gain some experience deploying web apps on AWS
- Speed up load times
- Use a custom domain, https://planet-tracker.com
With the AWS CLI client installed, getting my app set up with EB wasn’t super difficult. After getting things set up in EB, I purchased and registerd a domain with Route 53, and connected it to my EB environment. After about an hour of messing aroud, my app was running on my new domain. My app loaded wicked fast, sometimes two or three times faster than the Heroku version.
After the app was up for a month, I got a surprise in the form of a $25 bill from AWS. It turns out that the default EB environment uses a load balancer, which charges at minimum about $20 per month. Alarmed, I started looking for a cheaper solution. It turns out that you can shut down the load balancer, but then you lose SSL, meaning that your app won’t run over HTTPS.
Searching around online, I realized I had to rebuild my app as a single instance EB environment. This means that it would only run on one EC2 instance, instead of potentially splitting the load between multiple servers. This also meant that I would have to configure SSL on my own.
I’ve long been averse to messing around with more bare metal web app deployments, because it generally involves things like reverse proxies, which I consider myself completely unqualified to configure. That said, I found some relatively simple looking EB configurations I could apply to get things up and running. Unfortunately, all of the solutions I found didn’t work for me out of the box. Older solutions hinged on using versions of Linux that don’t run on newer EC2 instances, or employed extension configuration parameters that didn’t seem to work.
I’m going to walk through the configuration files from this repo (the same one I linked at the top of the post).
.ebextensions/01_https.config
:
Resources:
sslSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}
IpProtocol: tcp
ToPort: 443
FromPort: 443
CidrIp: 0.0.0.0/0
container_commands:
00_install_epel:
command: "sudo amazon-linux-extras install -y epel"
01_install_certbot:
command: "sudo yum install -y certbot"
20_getcert:
command: "sudo certbot certonly --standalone --debug --non-interactive --email dean.shaff@gmail.com --agree-tos --domains planet-tracker.com \
--expand --renew-with-new-domains --pre-hook \"service nginx stop\""
30_link:
command: "sudo ln -sf /etc/letsencrypt/live/planet-tracker.com /etc/letsencrypt/live/ebcert"
40_cronjobsetrenewal:
command: '(crontab -l ; echo ''0 6 * * * root certbot renew --standalone --pre-hook "service nginx stop" --post-hook "service nginx start" --force-renew'') | crontab -'
These .config
files, placed in the .ebextensions
folder in the root of your project use YAML syntax. We can use them to do things like setting up new security rules and to run commands on our instance after we deploy. Walking through each section:
Resources
: This creates a new security rule for our instance, which allows ingress on port 443, for HTTPS. I copy and pasted this code from this post.container_commands
: This is a list of commands run before our app is deployed.- Notice that in the first two commands I’m installing
epel
and thencertbot
. Normally we’d do this in a separatepackages
section of the file, but I’ve found that we’re not able to enable or installamazon-linux-extras
packages in this section. - The third command gets the SSL (??) certificate using certbot.
- The fourth command creates a symlink beween the newly created certificate and another location, which will be used in our nginx configuration.
- The last command creates a cron job that will periodically renew our SSL certificate
- Notice that in the first two commands I’m installing
(Note that if you were to use this with your own site, you’d have to modify all the occurences of dean.shaff@gmail.com
and planet-tracker.com
to your own email and domain, respectively.)
We have two more files, in .platform/nginx/conf.d
: 000_http_redirect_custom.conf
and https_custom.conf
. The structure of the .platform
directory is deliberate: it tells EB to put those .conf
files in /etc/nginx/conf.d/
on the EC2 instance associated with our EB environment. These files allow us to modify our nginx (the public facing reverse proxy) configuration to allow HTTPS connections. I didn’t write our modify these files, but the interesting bit is in the .platform/nginx.conf.d/https_custom.conf
file:
server {
listen 443 default ssl;
server_name localhost;
error_page 497 https://$host$request_uri;
ssl_certificate /etc/letsencrypt/live/ebcert/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ebcert/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:8000;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
In the ssl_certificate
and ssl_certificate_key
lines, we see that nginx is getting the SSL certificates we symlinked in the container_commands
section of 01_https.config
!
For the sake of completeness, here’s the contents of .platform/nginx.conf.d/https_custom.conf/000_http_redirect_custom.conf
:
server {
listen 80;
return 301 https://$host$request_uri;
}
In the end, I was able to get my app running on https://planet-tracker.com
(notice the “s” in “https”!), and I’m not incurring any more expenses than I was on Heroku. My very informal testing seems to indicate that my EB site doesn’t load any faster than the Heroku version, however. Nonetheless, I gained some valuable experience playing around with AWS.