Testing and Deploying Applications with GitHub, CodePipeline and Elastic Beanstalk

Motivation

For the last two and half years, I have been doing much development around automation, tools, microservices and serverless applications. Before that, I was developing applications and services in PHP. Back in those days, I was using a traditional set of tools to integrate, test and deploy my code using GitHub, Travis, Chef (AWS OpsWorks), and a whole lot of manual setup.

Recently I was wondering what it would take to launch a PHP application taking advantage of some new and old technologies that I have had the opportunity to learn and master both at work and in my own time.

The objective of this post is to define a Continuous Integration/Continuous Deployment pipeline and the application infrastructure using the following technologies:

  1. AWS CloudFormation to define the infrastructure as code
  2. CodePipeline as the primary orchestration mechanism
  3. CodeBuild to run tests and install dependencies
  4. S3 to save deployment assets
  5. SNS as the notification mechanism
  6. GitHub webhooks, protected branches and status checks.

Infrastructure as Code

All the code is defined as CloudFormation templates and the code is hosted on the GitHub repository “Elastic Beanstalk CICD”.

eb-code-pipeline

CI

Continuous integration refers to the practice of merging code into a central repository or integration branch (I refer to this as the integration branch) after a series of code quality checks have passed. There are many methodologies to enable this process including Git Flow and GitHub Flow.

In GitHub, enforce Code Quality Checks by protecting branches and require status checks to pass before pull requests can be merged.

Some of these checks include but are not limited to:

  1. Unit test
  2. Code Reviews
  3. Security validation
  4. Code analysis
  5. Linting
  6. Others

Enabling Required GitHub Status Checks:

Check the article “Enabling required status checks” for more information.

CodeBuild

AWS CodeBuild is a simple build service, and unlike some of it is more popular counterparts, you only pay for the time that the service is in use. Another advantage of CodeBuld is that you can define custom docker images for your build container and these can be hosted in Dockerhub or on Elastic Container Registry (ECR). This can be very powerful and quite simple to do if you have a bit of experience with Docker.

To add a GitHub check, we are going to use CodeBuild and trigger it every time code is pushed to the GitHub repository and every time there is a PullRequest. At this time I do not know a way to automatically set up the GitHub webhook without having to create a CloudFormation custom resource. Therefore I am just going to define the CodeBuildTest resource and set it up manually.

A CodeBuild definition is straight forward:

  CodeBuildTest:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${EnvironmentName}-${AppName}-tests"
      Description: !Sub "Run unit test for ${AppName}"
      Artifacts:
        Type: NO_ARTIFACTS
      ServiceRole: !Ref CodeBuildDeploymentRole
      Environment:
        ComputeType: !Ref CodeBuildComputeType
        EnvironmentVariables:
          - Name: ENVIRONMENT_NAME
            Value: !Ref EnvironmentName
          - Name: BUCKET_NAME
            Value: 
              Fn::ImportValue: !Sub "${EnvironmentName}-${AppName}-artifacts"
          - Name: APP_NAME
            Value: !Ref AppName
        Image: !Ref CodeBuildDockerImage
        Type: LINUX_CONTAINER
      Source:
        Type: GITHUB
        Location: !Sub "https://github.com/${GitHubOwner}/${GitHubRepo}.git"
        BuildSpec: "buildspec-test.yml

There are a few things to notice here:

  1. The environment variables passed to code build can be used to perform custom actions
  2. We specify a custom Source.BuildSpec file name, this will enable us to define a different series of steps for testing code; the buildspec.yml is used to define the steps to build the code.

The buildspec-test.yml is defined as follows:

# buildspec-test.yml
version: 0.2

phases:
  install:
    commands:
      - echo Download and install required packages
      - composer self-update
      - composer install

  pre_build:
    commands:
      - echo Create required directories
      - mkdir var
      - chmod +x bin/console
 
  build:
    commands:
      - echo run unit test
      - phpunit

  post_build:
    commands:
      - echo All done!

Since CodeBuild does not have an image to run PHP, I have created a PHP-Build image that contains several tools required to prepare a PHP application to be deployed.


CD

Continuous Deployment is the process in which teams reliably and continuously release code to a production environment. In this instance, every change that passes the automated tests and is merged into the integration branch is automatically deployed.

CodePipeline

The main orchestration mechanism is going to be AWS CodePipeline. In CodePipeline we can define different types of steps, but for the sake of this example the following process is defined:

Source > Build > Deploy

Source

This step listens for code changes in the GitHub repository. The code is downloaded into an S3 bucket. CodePipeline then triggers CodeBuild to start the build process.

Build

CodeBuild is used once again, this time around the steps will be different from the automated tests run. In this instance, the CodeBuild “phases” include:

  1. Install: required dependencies like OS packages, updates and more.
  2. Pre Build: Setup the environment to run the test, build the code, generate configuration, etc.
  3. Build: Here you can build your code or run unit tests.
  4. Post Build: perform any cleanup steps

There is an additional optional step called artifacts. While it is optional, it is essential to use it here to pass the code that we prepared during the build phase to the next step in the pipeline.

version: 0.2

phases:
  install:
    commands:
      - echo Installing required packages
      - pip install awscli
      - apt-get install zip unzip
      
      - echo Install project dependencies
      - composer install --no-dev --optimize-autoloader

  pre_build:
    commands:
      - echo Create required directories
      - mkdir var
      - chmod +x bin/console
      
  build:
    commands:
      - bin/console assetic:dump
      - bin/console assets:install
      
  post_build:
    commands:
      - echo Cleanning up!
      - rm -rf infrastructure docker test tests web/app_dev.php

artifacts:
  files:
    - '**/*'

Deploy

This is the act of publishing the latest version of the repository prepared by code build to the application server!

ElasticBeanstalk

Elastic Beanstalk is a Platform as a Service offering from AWS. It makes it easy to launch and scale applications developed in various programming languages including Java, .Net, PHP, Node.js, Python and more.

This post focuses on PHP, but it can be modified to work with other platforms.

The template defines parameters to set up the following options

  1. Application Name (This must match the code-pipeline application name)
  2. Solution Stack Version (The AWS version of the stack. See Supported Platforms for more information)
  3. Instance type
  4. Environment Type (single instance or auto-scaling)
  5. Min and Max number of instances
  6. Document Root
  7. PHP version
  8. PHP Memory Limit
  9. Environment variables for a database (user, pass, host, DB name)

EB PHP Application

The diagram shows the application setup when enabling load balancing, but it can be deployed as a single instance. It also shows an RDS instance, but the CloudFormation template does not define it.

Additional resources

The template defines an S3 bucket, the files bucket. It intends to have a place to store any assets required by the application such as images, CSS, js, and others. Ideally, the S3 would have a CloudFront distribution in front of it, but it is an exercise for the reader to implement this.

Outputs

The stack generates and exports a few values:

  1. The EB application name
  2. The EB environment name
  3. The artifacts bucket
  4. The files output bucket

Final thoughts

I believe essential to remove as many barriers as possible for developers to safely put their code into production, enabling them to have quick feedback on their work and respond faster when issues arise.

Setting up a CI/CD pipeline process early on in the life of any application can foster this process.

At the same time, it is vital for every organization to invest in good engineering practices: testing and reviewing code just to mention a couple; it is everyone’s responsibility to ensure the code is held to a very high standard, and no single team (QA or DevOps) can hold the entire responsibility.

I hope this pipeline can serve as a template to start new projects taking advantage of several tools and best practices for CI/CD, but it is by no means an extensive and you are encouraged to improve upon it.

Happy Coding!