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:

Aumente a performance do seu site com cache .htaccess

Existem várias formas de se aumentar a performance de uma aplicação web, e quando eu falo aplicação web o mesmo se aplica a sites. Uma das formas mais eficientes de lidar com performance é concentrar esforços para aumentar a sensação de carregamento instantâneo da sua aplicação.

Steve Souders escreveu um livro excelente chamado: High Performance Websites e nele, Steve, falou algo que me chamou muita atenção:

“Na maior parte da minha carreira em Internet, fui engenheiro de back-end. Como tal, eu diligentemente atacava cada projeto de performance como um exercício em otimização de back-end, me concentrando em opções de compilação, índices de bases de dados, gerenciamento de memória, etc. Existe muita atenção e vários livros dedicados a otimização performance nessas áreas, então é nelas que a maioria das pessoas emprega seu tempo, procurando melhorias. Na realidade, para a maioria das páginas de Internet, menos de 10% a 20% do tempo de resposta experimentado pelos usuários finais é gasto trazendo o documento HTML do servidor para o browser. Se você quer redurzir dramaticamente os tempos de resposta de suas páginas, tem de se concentrar nos restantes 80% a 90% da experiência do usuário final. No que sesses 80% a 90% são empregados?”

Umas das regras para se otimizar o carregamento de uma interface web, talvez uma das mais importantes, é diminuindo o número de requisições HTTP que esta interface está fazendo.

Existem diversas técnicas para se reduzir o número de requisições HTTP de uma aplicação web, porém, a que vamos abordar hoje é somente um pequeno pedaço deste trabalho, porém, uma parte muito importante dele.

A negligência e o cache.

Existe uma negligência muito grande por parte dos programadores e engenheiros com relação ao cache. Não podemos simplesmente cachear tudo e todos. Existe um nível de importancia que deve ser empregado para cada tipo de mídia que estamos cacheando. Vai de negócio para negócio, Na aplicação X os arquivos javascript podem não ser muito importante mas os arquivos mp3 são importantíssimos e vice-versa. É muito importante darmos o tempo certo de vida para cada tipo de mídia sempre olhando para o nosso negócio.

Botando a negligência de lado, por quê o cache é importante? para evitar que o usuário pague o preço. Para que ele baixe somente o que é necessário.

Diretivas .htaccess para cache

# 1 ANO
<FilesMatch "\.(ico|pdf|flv)$">
Header set Cache-Control "max-age=29030400, public"
</FilesMatch>
# 1 SEMANA
<FilesMatch "\.(jpg|jpeg|png|gif|swf)$">
Header set Cache-Control "max-age=604800, public"
</FilesMatch>
# 2 DIAS
<FilesMatch "\.(xml|txt|css|js)$">
Header set Cache-Control "max-age=172800, proxy-revalidate"
</FilesMatch>
# 1 MINUTO
<FilesMatch "\.(html|htm|php)$">
Header set Cache-Control "max-age=60, private, proxy-revalidate"
</FilesMatch>

Veja que é possível darmos um tempo de “vida” do cache de um arquivo de acordo com a sua extensão. É importante ressaltar que os tempos que foram dados no exemplo acima é somente um exemplo. Como eu disse acima, é importante você colocar na balança o que é mais importante para o seu negócio.

.htaccess Time Cheatsheet

# TIME CHEAT SHEET
#      300   5 MIN
#      600  10 MIN
#      900  15 MIN
#     1800  30 MIN
#     2700  45 MIN
#
#     3600   1 HR
#     7200   2 HR
#    10800   3 HR
#    14400   4 HR
#    18000   5 HR
#    36000  10 HR
#    39600  11 HR
#    43200  12 HR
#    46800  13 HR
#    50400  14 HR
#    54000  15 HR
#    86400  24 HR
#
#    86400   1 DAY
#   172800   2 DAY
#   259200   3 DAY
#   345600   4 DAY
#   432000   5 DAY
#   518400   6 DAY
#   604800   7 DAY
#
#   604800   1 WEEK
#  1209600   2 WEEK
#  1814400   3 WEEK
#  2419200   4 WEEK
#
#  2419200   1 MONTH
#  4838400   2 MONTH
#  7257600   3 MONTH
#  9676800   4 MONTH
# 12096000   5 MONTH
# 14515200   6 MONTH
# 16934400   7 MONTH
# 19353600   8 MONTH
# 21772800   9 MONTH
# 24192000  10 MONTH
# 26611200  11 MONTH
# 29030400  12 MONTH

Se este snipet não funcionar (o que é bem improvável) você pode utilizar este snipet em conjunto com a extensão mod_expires do apache.

<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault A300
ExpiresByType image/x-icon A2592000
ExpiresByType application/x-javascript A3600
ExpiresByType text/css A3600
ExpiresByType image/gif A604800
ExpiresByType image/png A604800
ExpiresByType image/jpeg A604800
ExpiresByType text/plain A300
ExpiresByType application/x-shockwave-flash A604800
ExpiresByType video/x-flv A604800
ExpiresByType application/pdf A604800
ExpiresByType text/html A300
</IfModule>

O legal de se utilizar o mod_expires é que ele trata o a questão do cache de acordo com o mime-type de cada arquivo e não somente pela extensão.

Tá, mais, é o ideal gerenciar desta forma?

Não. Existem formas mais inteligentes de gerenciar as mudanças dos seus arquivos e fazer com que eles sejam baixados novamente pelo navegador do usuário somente quando ele realmente for atualizado, porém, esta é uma forma um pouco mais “custosa” e eu certamente irei falar com mais profundidade sobre isso em um próximo post.

Conclusão

Podemos minimizar o número de requisições HTTP de nossa aplicação gerenciando de forma mais inteligente os arquivos que estamos incorporando dentro de nossa aplicação web utilizando recursos simples e nativos do nosso apache, como o exemplificado aqui. Esta não é a forma mais eficiente, mas com certeza, vai ajudar muito a minimizar o número de requisições HTTP que sua aplicação web fará nas nas próximas paginas que seu usuário for navegar.

Posts Relacionados:

  • Nenhum post relacionado!