Scaling down to single instance Elastic Beanstalk.

April 13, 2015


When you are looking to reduce your AWS costs, you might consider removing the Load balancer. At $20 per month it accounts for a large proportion of the hosting costs for a small application. Is it really needed right now? Down the line when your application grows, you can easily bring it back - right?

AWS architecture with LB

Elastic beanstalk is handy for provisioning and deployment, and thankfully the load balancer is not compulsory. So the good news is we can keep using EB for provisioning and deployment. But what about SSL ? and what alternative is there to the health_check “heartbeat” we use to drive delayed jobs?

Before proceeding, it is worth considering what we lose when we drop the load balancer: This article describes the commonly ignored or misunderstood features of Load balancers: “ELB Internals, Security and Troubleshooting” by Ruggero Tonelli, 31 January 2014

So here is how we flipped to “single instance” Elastic Beanstalk for a site with light traffic, and how we deal with SSL and found a “heartbeat” alternative.

Use Cron to provide the health_check “heartbeat”

Linux comes with cron, so this isnt AWS-specific. To keep things simple, we just use cron to ping the health_check URL every minute. See Running cron in elastic beanstalk auto-scaling environment by Paulo Poiati, 25 August 2013

The schedule logic is inside the application itself. So our .ebextensions/cron.config looks like this:

# .ebextensions/cron.config

files:
  "/tmp/cron_jobs" :
    mode: "000777"
    content: |
      * * * * * curl http://0.0.0.0/health_check/custom
    encoding: plain

container_commands:
  01_remove_old_cron_jobs:
    command: "crontab -r || exit 0"
  02_cronjobs:
    command: "crontab /tmp/cron_jobs"
    leader_only: true

Handle SSL on the EC2 instance

The AWS documentation is here: Configuring SSL for Single-Instance Environments, however it is not complete, and after several hours we gave up trying to get ssl to work on passenger standalone, and eventually got success with puma.

With Passenger - 64bit Amazon Linux 2014.09 v1.0.9 running Ruby 2.1 (Passenger Standalone) - we succeeded in opening port 443 for https on a Amazon EC2 instance Security Group (SG) but failed to get nginx to listen on port 443 and use the certificate and private key. We got net::ERR_CONNECTION_REFUSED when browsing to the site.

To create the puma environment with the “64bit Amazon Linux 2014.09 v1.1.0 running Ruby 2.1 (Puma)” AMI, we used the EB cli v3.1 tool. In a one line command you can provision and deploy a single instance puma environment:

$ eb create dev-env -p "64bit Amazon Linux 2014.09 v1.1.0 running Ruby 2.1 (Puma)" --single -i t2.micro --envvars key1=value1,key2=value2...

While that is running, you still have to manually add the new SG to the DB SG and open port 443 to HTTPS. We got net::ERR_TOO_MANY_REDIRECTS when browsing to the site, but this was easily fixed by adding this to the ssl.config:

// ssl.config
...
  proxy_set_header    X-Forwarded-Proto   https;
...

So our .ebextensions/ssl.config now looks like this:

// ssl.config

Resources:
  sslSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: example_id
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443
      CidrIp: 0.0.0.0/0

files:
  "/etc/nginx/conf.d/ssl.conf" :
    content: |
      # HTTPS server

      server {
          listen       443;
          server_name  localhost example.co.uk www.example.co.uk;

          ssl                  on;
          ssl_certificate      /etc/pki/tls/certs/server.crt;
          ssl_certificate_key  /etc/pki/tls/certs/server.key;

          ssl_session_timeout  5m;

          ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
          ssl_ciphers    ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS;
          ssl_prefer_server_ciphers   on;

          add_header Strict-Transport-Security "max-age=31536000";

          location / {
              proxy_pass          http://my_app;
              proxy_set_header    Host                $host;
              proxy_set_header    X-Real-IP           $remote_addr;
              proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
              proxy_set_header    X-Forwarded-Proto   https;
          }

          location /assets {
            alias /var/app/current/public/assets;
            gzip_static on;
            gzip on;
            expires max;
            add_header Cache-Control public;
          }

          location /public {
            alias /var/app/current/public;
            gzip_static on;
            gzip on;
            expires max;
            add_header Cache-Control public;
          }
      }

  "/etc/pki/tls/certs/server.crt" :
    content: |
      -----BEGIN CERTIFICATE-----
      MTYwMTE4MjIzOTM4WjBIMSEwHwYDVV...
      -----END CERTIFICATE-----

  "/etc/pki/tls/certs/server.key" :
    content: |
      -----BEGIN RSA PRIVATE KEY-----
      6JqCpm3OYCIzx4fNsecDUoA+Varg+s5yHC...
      -----END RSA PRIVATE KEY-----

container_commands:
  01restart_nginx:
    command: "service nginx restart"

After swapping the CNAME’s we terminated the Load balanced environment and are now running on a leaner single instance Elastic Beanstalk!

AWS architecture without
LB

© 2018 Keith P | Follow on Twitter | Git