About Blog Contact
 
 

RSS

May 06, 2022
5 min read

Automate build and push to ECR via GitHub Actions

Let's build a simple containerised PHP application and a GitHub Actions workflow to build and push the images to ECR, which is the container registry from AWS.

Prerequisites

  • an IAM credentials with permissions to create and push to ECR repositories
  • GitHub repository with the IAM credentials declared as secrets
  • Configured awscli with the account to create the ECR repository

The application

For the sake of this demo we keep the php application minimal and reduce it to the following tree.

│ composer.json
└─public
│ │ index.php
└─src
│ │ Application.php

The composer.json file has the following content:

{
    "autoload": {
        "psr-4": {
            "Acme\\Service\\": "src/"
        }
    }
}

As said, we keep it minimal. This composer.json does only include one autoloader definition.

Here is the src/Application.php:

<?php

namespace Acme\Service;

class Application
{
    public function run(): string
    {
        return 'it works!';
    }
}

and here the public/index.php:

<?php

require_once dirname(__DIR__) . '/vendor/autoload.php';

$app = new \Acme\Service\Application();
echo $app->run();

exit(0);

That's the application. As stated in the beginning, we keep it minimal. Nevertheless, extending afterwards will be a no-brainer.

Dockerfile

Let's focus next on the Dockerfile. It will contain a multiple stages. We will build two distinct images out of it. One for php-fpm and the other for nginx.
Here is the content of the Dockerfile:

# The first stage named `base` contains the needed/wanted
# steps to execute our application in production
FROM php:8.1-fpm-alpine as base

WORKDIR /var/www/html

RUN apk --no-cache add bash nano

# The second stage named `develop` contains the software which is needed to execute
# our application in development as well as build out software for production
FROM base as develop

RUN apk add --no-cache git \
 && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
 && mkdir -p /.composer /.config \
 && chmod -R 2777 /.composer /.config

# The third stage named `build` will execute the build steps needed to execute
# our application in production. Currently only `composer install`
FROM develop as build

COPY . .

RUN composer install --no-interaction --optimize-autoloader --no-dev --prefer-dist

# The fourth stage named `production` inherits from our `base` stage
# and copies the built application files from the `build` stage
FROM base as production

COPY --from=build /var/www /var/www

# The fifth stage named `nginx` is our custom nginx image, since nginx needs the content
# of our document root. Currently only the `index.php` which is needed to proxy the
# request to our `php-fpm` container, but could potentially contain any kind
# of assets like CSS, JavaScript or images, which we need to serve
FROM nginxinc/nginx-unprivileged:1.20-alpine as nginx

COPY --from=build /var/www/html/public /var/www/html/public

I have added comments to all stages which describe what's happening there.

ECR repository

To create a new ECR repository with the awscli, you have to execute the following command:

aws ecr create-repository --repository-name acme-service

GitHub Action

Let's first commit and push everything into the main branch of our GitHub repository before we start with our GitHub Action workflow.

Next we have to declare

To define a workflow we need to create a YAML file in .github/workflows in our repository:

mkdir -p .github/workflows
touch .github/workflows/on-push-main-branch.yml

Here is the complete content of the workflow:

name: Build and push docker images

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_DEPLOY_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_DEPLOY_SECRET_ACCESS_KEY }}
          aws-region: eu-central-1
      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v1
        id: login-ecr
      - name: Build & Push Images
        run: |
          CURRENT_SHA=${GITHUB_SHA::8}
          REPOSITORY=${{ steps.login-ecr.outputs.registry }}/acme-service

          docker build . --target production -t ${REPOSITORY}:latest -t ${REPOSITORY}:${CURRENT_SHA}
          docker build . --target nginx -t ${REPOSITORY}:nginx-latest -t ${REPOSITORY}:nginx-${CURRENT_SHA}

          docker push --all-tags ${REPOSITORY}

We will go through the on-push-main-branch.yml step by step:

First comes the name and trigger declaration:

name: Build latest tag and deploy to staging

on:
  push:
    branches:
      - main
# ...

This workflow will only be executed by a push to the main branch.

Next we have the jobs declaration:

# ...
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
# ...

Our workflow contains only one job named build-and-deploy. It will run on ubuntu-latest.

The build-and-deploy job contains multiple steps.

Step 1: Checkout code

    - name: Checkout
      uses: actions/checkout@v2

This step will checkout the coe of the repository and makes it available in the working directory

Step 2: Configure AWS credentials

  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: eu-central-1

Here we configure the access to AWS via the added secret from the prerequisites. You may adapt the region to your needs.

Step 3: Login to our ECR registry

  - name: Login to Amazon ECR
    uses: aws-actions/amazon-ecr-login@v1
    id: login-ecr

This actions as well as the one before are provided by AWS and make our life really easy. We can access the registry url in the next step via the id.

Step 4: Build and push Docker images

This is the last step in our workflow which will finally build and push the needed docker images.

  - name: Build & Push Images
    run: |
      CURRENT_SHA=${GITHUB_SHA::8}
      REPOSITORY=${{ steps.login-ecr.outputs.registry }}/acme-service

      docker build . --target production -t ${REPOSITORY}:latest -t ${REPOSITORY}:${CURRENT_SHA}
      docker build . --target nginx -t ${REPOSITORY}:nginx-latest -t ${REPOSITORY}:nginx-${CURRENT_SHA}

      docker push --all-tags ${REPOSITORY}

First we declare two variables which helps us to keep the following commands short. CURRENT_SHA contains trimmed current commit sha and REPOSITORY contains the registry url (from the step before) suffixed by /acme-service which is the absolute url to our ECR repository.
Now we build our docker images with the right target from our multi-stage Dockerfile and add two tags. The first tag is the latest tag and the second tag is the unique CURRENT_SHA. For our nginx image we prepend the tags with nginx- to be able to distinguish between both images. Finally, we push all local tags for the repository.

Summary

We build a small PHP application consisting of two docker containers built out of a single multi-stage Dockerfile and configured a GitHub Actions workflow to build and push the images automatically on every push to our main branch.

Thanks for reading. I hope you can use this small tutorial as a starting point for your container journey!

Legal Notice  |  Privacy  |  RSS  |  © 2024 christlieb.eu