Deploying Sendy Using AWS ElasticBeanstalk and Docker

sendy-300x118In this post I’m going to guide you through a step-by-step on how to deploy your Sendy installation using AWS ElasticBeanstalk and Docker. Keep in mind that this is a production ready setup so brace yourself.

The AWS Resources

Here is a list of what you need to do before going ahead.

  1. Create an ElasticBeanstalk Application (e.g Sendy).
  2. Create an ElasticBeanstalk Environment (e.g sendy-production).
    1. You can deploy the Sample Application for now.
    2. Once your environment is created make a note of your Environment ID located on your environment dashboard right beside your DNS entry. (e.g Environment ID: e-xxxxxxxxxxx).
  3. Create a EC2 Security Group to be used by your RDS MySQL Instance (e.g: rds-sendy-production).
    1. Inbound: Open this tab and make sure you allowed your ElasticBeanstalk Instances can reach your RDS. To do so you need to locate the name of the Security Group created by your ElasticBeanstalk Environment. Just go to your EC2 Security Groups Section. And locate the ID of the security group that possessed your ElasticBeanstalk Environment ID.
    2. It will look kind of like this:
    3. You need to do that because your environment is elastic and every new created instance needs to inherit its permission to access the RDS instance.
  4. Create your RDS MySQL instance and attach the EC2 Security Group created on the previous step. Further configurations is up to you.
  5. Create a SSD or Magnetic Volume on the same Availability Zone of your Elastic Beanstalk Environment Instances. Attention to the availability zone or your deployment will fail because EC2 Instances can’t attach volumes outside of its availability zone. After doing that take a note on your Volume ID (e.g vol-XXXXXXXX).
  6. Install the awsebcli.

The Deploy Setup

Assuming that you’re already inside your Sendy’s folder you need to create a few files.

Dockerfile

FROM php:7.0.8-apache

RUN a2enmod rewrite && \
    docker-php-ext-install mysqli gettext

ENV APP_HOME /var/www/html

WORKDIR $APP_HOME
COPY . $APP_HOME

EXPOSE 80

Notice that I’m using the official php + apache2 docker image. I need to create my own Dockerfile because of Sendy’s dependencies like mysqli and gettext.

.elasticbeanstalk/config.yml

branch-defaults:
  default:
    environment: sendy-production
global:
  application_name: sendy
  default_ec2_keyname: YOUR-EC2-KEYNAME
  default_platform: Docker 1.11.1
  default_region: us-east-1
  profile: null
  sc: null

This file is important when your are about to deploy your application to ElasticBeanstalk.

Dockerrun.aws.json

{
  "AWSEBDockerrunVersion": "1",
  "Ports": [
    {
      "ContainerPort": "80"
    }
  ],
  "Volumes": [
    {
      "HostDirectory": "/uploads",
      "ContainerDirectory": "/var/www/html/uploads"
    },
    {
       "HostDirectory": "/session",
       "ContainerDirectory": "/var/www/html/session"
    }
  ],
  "Logging": "/var/log/apache2/"
}

Ports: By default apache is running on port 80, no big deal here.
Volumes: The most important thing to notice on the code above is those “Volumes”. Sendy uses the the uploads/ directory to save everything you upload to its system so it’s very important for you to have those volumes mapped from the Docker Host into your containers to make sure that in case you need to restart your containers you don’t lose your persisted data (docker containers are stateless!).

The /session:/var/www/html/session is because it is where php is saving logged user sessions. Its better to save sessions outside of the application container. It make sure that every new deployment you don’t need to clean your cookies and login again (it sucks, I know!).

.ebextensions/container_commands.config

container_commands:
  01mount:
    command: "aws ec2 attach-volume --region us-east-1 --volume-id YOUR_VOLUME_ID_HERE --instance-id $(curl -s http://169.254.169.254/latest/meta-data/instance-id) --device /dev/xvdf"
    ignoreErrors: true
  02wait:
    command: "sleep 20"
  03mkdir:
    command: "mkdir /uploads"
    test: "[ ! -d /uploads ]"
  04trymount:
    command: mount /dev/xvdf /uploads
    ignoreErrors: true
  05format-if-not-already:
    command: if find /uploads -maxdepth 0 -empty | read v; then mkfs -t ext4 /dev/xvdf; fi
  06mount:
    command: "mount /dev/xvdf /uploads"
    ignoreErrors: true
  07fix_public_dirs:
    command: "chmod -R 777 /session && chmod -R 777 /uploads"
    ignoreErrors: true

01mount: Replace the YOUR_VOLUME_ID_HERE by your Volume ID created before. Also make sure that –region us-east-1 is correct. This file will make sure that we will be using the Volume created earlier exclusive to our uploads and your data will never be lost in case you explode your machine somehow.
02wait: The commend aws ec2 attach-volume is async so we need this otherwise further commands would fail.
03mkdir: Create the /uploads dir in case it doesn’t exists.
04trymount and 05: When you first create a EC2 Volume you need to make sure that it has a file system in it so this is what it does.
06mount: Uses the Volume we created for the /uploads folder.

.ebextensions/files.config

files:
  "/opt/elasticbeanstalk/hooks/appdeploy/pre/files.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      set -x
      docker run -v /uploads:/var/www/html/uploads --rm aws_beanstalk/staging-app chown -R www-data: /var/www/html/uploads
  "/opt/elasticbeanstalk/hooks/appdeploy/enact/rewrite_nginx_config.py":
    group: root
    owner: root
    mode: "000755"
    content: |-
      #! /usr/bin/python

      """
      Modifies nginx configuration file on AWS Elastic Beanstalk to support WebSocket connections.
      """

      import os
      import re

      VERSION="v1.0.4"
      NGINX_CONF_FILE = '/etc/nginx/sites-available/elasticbeanstalk-nginx-docker-proxy.conf'
      NGINX_CONFIG = """
      # nginx.conf
      # v1.0.4

      map $http_upgrade $connection_upgrade {
        default   "upgrade";
        ""      "";
      }

      server {
        listen 80;

        gzip on;
        gzip_comp_level 4;
        gzip_types text/html text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
        client_max_body_size 100M;

        location ~ "^/assets/" {
          root /var/app/current/public;
          gzip_static on;
          expires max;
          add_header Cache-Control public;
          add_header ETag "";
        }

        location / {
          proxy_pass      http://docker;
          proxy_http_version  1.1;

          proxy_set_header  Connection           $connection_upgrade;
          proxy_set_header  Upgrade              $http_upgrade;
          proxy_set_header  Host                 $host;
          proxy_set_header  X-Real-IP            $http_x_real_ip;
          proxy_set_header  X-Forwarded-For      $http_x_forwarded_for;
        }
      }
      """

      def has_same_nginx_file_version():
        f = open(NGINX_CONF_FILE, "r")
        conf = f.read()
        ret = re.search(VERSION, conf)
        f.close()

        return ret != None

      def write_nginx_conf_file():
        f = open(NGINX_CONF_FILE, "w")
        f.write(NGINX_CONFIG)
        f.close()

      def restart_nginx():
        print("-- service nginx restart")
        os.system("service nginx restart")

      def main():
        if has_same_nginx_file_version():
          print("-- already configured, do nothing.")
        else :
          write_nginx_conf_file()
          restart_nginx()

      if __name__ == "__main__":
        main()

The file above is necessary because apache runs your application with www-data user and this user doesn’t exists on your Docker Host. And the second file is for overwriting nginx’s beanstalk configuration for accepting upload of bigger files.

includes/config.php

/* MySQL database connection credentials (please place values between the apostrophes) */
$dbHost = $_ENV['MYSQL_DATABASE_HOST']; //MySQL Hostname
$dbUser = $_ENV['MYSQL_DATABASE_USER']; //MySQL Username
$dbPass = $_ENV['MYSQL_DATABASE_PASSWORD']; //MySQL Password
$dbName = $_ENV['MYSQL_DATABASE_NAME']; //MySQL Database Name

.htaccess
Add these to your .htaccess file:


# for enabling upload of bigger CSV files
LimitRequestBody 104857600
php_value max_execution_time 0
php_value post_max_size 60M
php_value upload_max_filesize 50M

# to avoid session problems originally saved to /tmp.
php_value session.save_path '/var/www/html/session'

Make sure that you are using environment variables on that file so we can inject them into your ElasticBeanstalk Environment.

Set your database configuration as Environment Variables

eb setenv MYSQL_DATABASE_HOST="XXXX" MYSQL_DATABASE_USER="XXXX" MYSQL_DATABASE_PASSWORD="XXXX" MYSQL_DATABASE_NAME="XXX" -e sendy-production

Deploy it

Commit your changes and then:

eb deploy sendy-production

Boom! Now make sure that you follow Sendy’s Guide Lines in order to setup your app correctly.

Let me know if you have any questions on the comments section bellow. Maybe some detail is missing and I could add it to the post. Remember! Sharing is caring! ;-D

Posts Relacionados: