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.
1│ composer.json2└─public3│ │ index.php4└─src5│ │ Application.php
The composer.json
file has the following content:
1{2 "autoload": {3 "psr-4": {4 "Acme\\Service\\": "src/"5 }6 }7}
As said, we keep it minimal. This composer.json
does only include one
autoloader definition.
Here is the src/Application.php
:
1<?php 2 3namespace Acme\Service; 4 5class Application 6{ 7 public function run(): string 8 { 9 return 'it works!';10 }11}
and here the public/index.php
:
1<?php2 3require_once dirname(__DIR__) . '/vendor/autoload.php';4 5$app = new \Acme\Service\Application();6echo $app->run();7 8exit(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
:
1# The first stage named `base` contains the needed/wanted 2# steps to execute our application in production 3FROM php:8.1-fpm-alpine as base 4 5WORKDIR /var/www/html 6 7RUN apk --no-cache add bash nano 8 9# The second stage named `develop` contains the software which is needed to execute10# our application in development as well as build out software for production11FROM base as develop1213RUN apk add --no-cache git \14 && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \15 && mkdir -p /.composer /.config \16 && chmod -R 2777 /.composer /.config1718# The third stage named `build` will execute the build steps needed to execute19# our application in production. Currently only `composer install`20FROM develop as build2122COPY . .2324RUN composer install --no-interaction --optimize-autoloader --no-dev --prefer-dist2526# The fourth stage named `production` inherits from our `base` stage27# and copies the built application files from the `build` stage28FROM base as production2930COPY --from=build /var/www /var/www3132# The fifth stage named `nginx` is our custom nginx image, since nginx needs the content33# of our document root. Currently only the `index.php` which is needed to proxy the34# request to our `php-fpm` container, but could potentially contain any kind35# of assets like CSS, JavaScript or images, which we need to serve36FROM nginxinc/nginx-unprivileged:1.20-alpine as nginx3738COPY --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:
1aws 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:
1mkdir -p .github/workflows2touch .github/workflows/on-push-main-branch.yml
Here is the complete content of the workflow:
1name: Build and push docker images 2 3on: 4 push: 5 branches: 6 - main 7 8jobs: 9 build-and-deploy:10 runs-on: ubuntu-latest11 steps:12 - name: Checkout13 uses: actions/checkout@v214 - name: Configure AWS credentials15 uses: aws-actions/configure-aws-credentials@v116 with:17 aws-access-key-id: ${{ secrets.AWS_DEPLOY_ACCESS_KEY_ID }}18 aws-secret-access-key: ${{ secrets.AWS_DEPLOY_SECRET_ACCESS_KEY }}19 aws-region: eu-central-120 - name: Login to Amazon ECR21 uses: aws-actions/amazon-ecr-login@v122 id: login-ecr23 - name: Build & Push Images24 run: |25 CURRENT_SHA=${GITHUB_SHA::8}26 REPOSITORY=${{ steps.login-ecr.outputs.registry }}/acme-service27 28 docker build . --target production -t ${REPOSITORY}:latest -t ${REPOSITORY}:${CURRENT_SHA}29 docker build . --target nginx -t ${REPOSITORY}:nginx-latest -t ${REPOSITORY}:nginx-${CURRENT_SHA}30 31 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:
1name: Build latest tag and deploy to staging2 3on:4 push:5 branches:6 - main7# ...
This workflow will only be executed by a push to the main
branch.
Next we have the jobs
declaration:
1# ...2jobs:3 build-and-deploy:4 runs-on: ubuntu-latest5 steps:6# ...
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
1- name: Checkout2 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
1- name: Configure AWS credentials2 uses: aws-actions/configure-aws-credentials@v13 with:4 aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}5 aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}6 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
1- name: Login to Amazon ECR2 uses: aws-actions/amazon-ecr-login@v13 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.
1- name: Build & Push Images2 run: |3 CURRENT_SHA=${GITHUB_SHA::8}4 REPOSITORY=${{ steps.login-ecr.outputs.registry }}/acme-service5 6 docker build . --target production -t ${REPOSITORY}:latest -t ${REPOSITORY}:${CURRENT_SHA}7 docker build . --target nginx -t ${REPOSITORY}:nginx-latest -t ${REPOSITORY}:nginx-${CURRENT_SHA}8 9 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!