Continuous Integration for Laravel in AWS ElasticBeanstalk via Travis-CI

At Victory we use Laravel a lot - having worked with every PHP framework with decent traction in the last 20 years, this one works well and makes the most sense - it solves the important problems a framework should solve without trying to solve everything.

At Victory we use Laravel a lot - having worked with every PHP framework with decent traction in the last 20 years, this one works well and makes the most sense - it solves the important problems a framework should solve without trying to solve everything.

When we start a new project (or a major renovation) pulling this code base shaves about half a day in setup. For this tutorial feel free to pull the codebase down and follow along.

Development

This sample environment is running on Laravel Homestead, and integrated into the codebase with some extra provisioners. This isn't ideal - we are working on an environment that is closer to AWS Linux and provisioned from Ansible. But, as of this writing it's not done. For the purposes of this article we'll be fine, but environmental parity is important for more advanced topics.

Branch Management

When starting off, enable a good workflow. Setting a CI and CD habit starts here. We typically use Gitflow. Generally for a new project we set the staging branch as the default and build and deploy that to a staging environment when code is merged to it. Master likewise will build and deploy to the production environment. Set these good habits now before you have real users on a production environment.

Laravel -> Travis-CI

In this example the following happen:

  • Travis builds and tests our code,
  • Create an artifact,
  • Load it to Beanstalk,
  • Deploy it to the right environment.

Let's break that down a bit.

Testing

To test the code, we will need:

  • An environment as close to production as practical
  • A database with test data
  • An Elasticsearch index with test data
  • Composer packages
  • Javascript and CSS compiled

Note I didn't say anything about what sort of tests you plan to run, code coverage, etc. This solution allows you to take full advantage of the testing built into Laravel. In this codebase we have also incorporated PHPMD to test for things like Cyclometric Complexity and Code Complexity (this keeps things readable).

Write tests!

Environment

This is where the example will fall apart a bit - ElasticBeanstalk runs on AWS Linux, which is a variant of RedHat / Fedora (as far as I can tell). Travis likes Ubuntu, Homestead is run in Ubuntu. Theoretically you can set up Beanstalk to run on any AMI you like, but in practice it's a headache and violates Victory's value of simple is better than custom.

For our purposes we will be happy with ensuring that we're on the same version of PHP (7.1). For the rest of the environment talk, let's look at the .travis.yml file - I'll break it down into pieces in the post - feel free to go look at the one in the repo:

language: php
addons:
  apt:
    packages:
     - oracle-java9-set-default
php:
- '7.1'
jdk:
  - oraclejdk8
services:
- mysql
sudo: required
dist: trusty
group: deprecated-2017Q4

The above snippet determines:

  • we'll be running php 7.1,
  • installing Oracle Java 9 (we'll need that for Elasticsearch)
  • installing MySql
  • running on Ubuntu Trusty (14.04)

The group: deprecated-2017Q4 is a holdover as we had a little trouble with the latest environment.

In the real world, the above describes a container with the specified configuration where the rest of the build and test tasks occcur.

A Database with Test Data

The next couple of lines in the .travis.yml file create the default database:

before_install:
- mysql -e 'CREATE DATABASE homestead;'

Things to know:

  • Travis serves MySql with the username travis and no password.
  • If we wanted to load it with data we can do that with the db:seed artisan command or create our own artisan command specific to the project.

Just remember that anything you do here will cause the entire build to wait on the data to be loaded. Make sure you can test with a small subset of data.

Your feedback loop should always be as short as possible.

The before script

As the name implies, these commands run before the build begins.

before_script:
- cp .env.travis .env
- composer install --prefer-dist --no-interaction
- php artisan cache:clear
- php artisan key:generate
- nvm install 7.7.1
- npm install -s npm@latest -g
- npm install -s -g jshint
- npm install -s

In this script:

  • we set up the .env by copying .env.travis to .env.
  • composer packages are installed
  • the app is competely set up.

Script

We are big Elasticsearch fans - not just for it's use as a search engine but also for high-speed denormalized front end caching, so we include it by default in every build. To get Elasticsearch on Travis (version 5.5.3 for parity with the version we're using from Amazon's Elasticsearch Service)

So, the script section is where do some final set up of the codebase. AFter that, testing occurs.

script:
- sudo apt-get purge elasticsearch
- curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.5.3.deb && sudo dpkg -i --force-confnew elasticsearch-5.5.3.deb && sudo service elasticsearch start
- wget -q --waitretry=1 --retry-connrefused -T 20 -O - http://127.0.0.1:9200
- php artisan es:indices:create
- php artisan migrate
- npm run production
- vendor/bin/phpmd app text codesize design naming unusedcode
- vendor/bin/phpunit --testdox --coverage-text tests

To ensure we get the right version of Elasticsearch:

  • first ensure it is not installed already
  • grab the specific one we want and install it.
  • The third line allows the service some time to get started up.
  • The artisan command es:indices:create creates the indices.
  • The MySql database is set up with the migrate command,
  • Javascript is compiled with npm run production
  • Tests are run with the last two commands.

If any of this fails, the build fails.

Laravel on ElasticBeanstalk

At this point the application is built and tested. We could stop here and have a nice CI pipeline. But - we want more. Specifically, we want this puppy to launch itself into Beanstalk.

So let's talk Beanstalk - specifically architecture. There are a few things to keep in mind:

  1. Servers are cattle, not pets. Don't get attached. Nothing should need lives on the box longer than a user session. Use S3 as your primary file store.
  2. The servers are hard to get to on purpose. Our setup is designed with emergency-only server access in mind. Don't expect to get logs easily. Services like Papertrail and Bugsnag are your friends.
  3. This up for is for production. So the routes and config files are cached. You can't use any closures in the route files, and you should be meticulous about running your environment variable through the config files (don't use the env helper in the code - only in config files).
  4. If you want to use Laravel's Queues or Scheduled Tasks (both fantastic) you will need something on the box to run the workers. More on that below.

Note that we reason from the the production setting back towards the development environment. That is because "production" means "to produce the money that is my paycheck." It must work in production first.

Travis -> Beanstalk

Moving down the travis.yml file to the deploy the application:

Notifications

If you like notifications Travis keeps you informed. The details are beyond the scope of this post. For our purposes we have Slack notifications coming to specific channnels regardless of success of failure.

notifications:
  slack:
    rooms:
      secure: "REDACTED"
    on_success: always
    on_failure: always

before_deploy

This is where any final cleanup of your code happens before the artifact is created.

before_deploy:
- rm .env
- rm .env.travis
- rm .env.example
- touch .env
- export ARTIFACT_PRE=$(echo $TRAVIS_REPO_SLUG | sed  s_^.*/__)
- export ARTIFACT_NAME=${ARTIFACT_PRE}-${TRAVIS_BRANCH}-$(
  echo ${TRAVIS_COMMIT} | cut -b 1-8
  )-$(
  date -u +%FT%T%Z
  ).zip
- export ELASTIC_BEANSTALK_LABEL=$(echo $ARTIFACT_NAME | sed s_.zip__)
- zip $ARTIFACT_NAME -q -r * .[^.]*
- ls -la $ARTIFACT_NAME

In this case:

  • clean out the .env files to make sure we don't confuse Beanstalk,
  • create an artifact name and export it,
  • zip up the artifact.

Notice we make an empty .env file, that keeps Laravel from complaining.

branches

Optionally you can specify which branches should be considered for builds.

branches:
  only:
    - master
    - staging

For this example we set them to only build on a push to master or staging.

deploy

Travis is quick to say that Elastic Beanstalk support is in beta, so be ready for things to change. For the last 6 months we've been using it with little issue. If you look in the .travis.yml file you'll see two deployment providers - one of these is for staging and the other master - in a production site the master branch will go straight to production once tests have passed.

For the sake of space only one deployment provider is annotated.

deploy:
  - provider: elasticbeanstalk
    skip_cleanup: true
    zip_file: "$ARTIFACT_NAME"
    access_key_id:
      secure: REDACTED
    secret_access_key:
      secure: REDACTED
    region: us-east-1
    app: meetup-sample
    env: MeetupSample-env
    bucket_name: elasticbeanstalk-us-east-1-732770059798
    on:
      branch: staging

Here's what you see above:

  • deploy: - this is the start of the deploy section
  • skip_cleanup: this prevents Travis from resetting what it did to built the artifact
  • zip_file: - name of the artifact
  • access_key_id and secret_access_key - These are AWS keys - they have to be in the codebase so please encrypt them.
  • region - AWS Region
  • app and env - The Beanstalk information
  • bucket_name - All Elastic Beanstalk environments in a region share one bucket for artifact storage
  • on: branch: This limits this particular deploy profile to a specific branch
  • Optional - only_create_app_version - if you set this to true then the app version will be uploaded but NOT deployed

Elastic Beanstalk

Elastic Beanstalk provides us with preconfigured machines, auto scaling and other simplifying services for running an applicaiton in production.

EB Extentions

This example will work almost perfectly out of the box - but we will need to do a few things on the servers. This is whereb*.ebextenstions* comes in handy. EB Extensions are a the configuration management system for Elastic Beanstalk. Think Ansible, but more rudimentary and not idempotent.

They are yaml files which run provisioning on the server - simple stuff, but effective. A couple in the codebase are included to play with.

Order matters and EB Extensions run in alphabetical order. Adopt the idea of naming them with a leading number (and leave yourself some room in between).

  • 05_supervisor.config - This is a pretty complicated script that will install and set up supervisord for Laravel's queue system
  • 11_database.config - Initially this script just ran artisan migrate - hence the name - but now it also handles the general cleanup and optimization of the code. Note on the migrate command there's the inclusion of leader_only: true - this ensures that the script is only run on the first server to push code out.
  • 13_forcessl.config - This adds a file to the apache config that will read the headers from the load balancer and redirect to https:// when a user comes in insecurely.
  • 16_phpini.config - this is to update the php.ini - the only thing in there now allows for bigger file uploads.
  • 90_friendly_shell.config - this makes the shell of the servers a little cleaner, nice for debugging

If any of these items fail to run, the deployment will be rolled back. Debugging that is outside the scope of this article, but there's plenty of documentation on it.

Introductory Slides

Below is the conceptual introduction given during the Austin PHP Meetup. We'd like to thank them for the opportunity to present this material.