CI/CD (Continuous Integration and Deployment) is tough to get started on, especially if you're a small team constantly dealing with new projects that require re-establishment of the same CI/CD workflows. The basic premise, for those with no background, is how to continuous integrate code into a codebase from a team of developers, and also have structures in place to deploy that code. It's more complicated than that, but that's a pretty good synopsis.
Though there are many tools (GitLab, looking at you) that are incredibly powerful, Github Actions can also run smaller project CI/CD workflows efficiently and without fuss. We do not believe in overcomplicating things if it's not necessary so we use the simple option when possible (hence our name, Minima)
Below, a way oversimplified example of this - obviously in reality we would have different environments etc, but you get the point.
Problem Statement
It should be possible to branch from main, test changes locally in the docker-compose setup, then merge the branch into main.
Solution + Implementation
This is a very simplified example, with no Kubernetes or clusters at all. For these purposes there can be one remote instance, running one Docker compose project.
The workflow should be as follows:
- A dev branches from main and does work
- Once ready, that dev can submit a pull request to merge the work branch
- A code owner can review that pull request and approve
- Once approved, the pull request will merge which will trigger a github action
- This Github Action will build and push the new Docker images based on the new code
Github Action trigger
This GitHub action will be in the repository, but we don't want to have copy-paste the same code to every new repository we make. So actually, this Github action in the .github folder, will effectively just call the other Github Action.
name: CI/CD Pipeline Trigger
on:
pull_request:
types: [closed]
branches:
- main
jobs:
call-reusable-workflow:
# if: github.event.pull_request.merged == true
uses: ${your_organization_name}/shared-github-actions/.github/workflows/ci-template.yml@main
with:
ecr-repository-prefix: ${your_ECR_repo}
secrets: inherit
As you can see this Github Action is pretty simple. On a PR merged from main, it will call a Github Action from the shared-github-actions repository and pass it the ECR prefix. ECR is Amazon's container registry and is how this CI/CD workflow will build containers for the project.
GitHub Action Main
This is the Github Action where the real meat is. This action will effectively get the latest version of the code with the merged branch, build all of the docker containers for each service, and then tag them all with a random hash as well as the "LATEST" tag and push them to the container repository.
name: CI Pipeline
on:
workflow_call:
inputs:
ecr-repository-prefix:
required: false
type: string
jobs:
build-and-push:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
# see: https://github.com/aws-actions/amazon-ecr-login
- name: Log in to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Fetch All Branches
run: git fetch --all
- name: Build Docker images
id: build-images
run: |
docker compose -p ${{ vars.project_name }} build --no-cache
services=$(docker compose config --services)
for service in $services; do
docker tag ${{ vars.project_name }}-${service} ${service}:latest
fi
done
- name: Tag and Push Docker images to Amazon ECR
run: |
export $(cat .env | xargs) # Load environment variables from .env
IMAGE_TAG=${GITHUB_SHA::7}
services=$(docker compose config --services)
for service in $services; do
REPOSITORY_URI=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ vars.project_name }}
IMAGE_NAME=${REPOSITORY_URI}:production-${service}-$IMAGE_TAG
LATEST_IMAGE_NAME=$IMAGE_NAME-latest
# Tag images
docker tag ${{ vars.project_name }}-${service} ${IMAGE_NAME}
docker tag ${{ vars.project_name }}-${service} ${LATEST_IMAGE_NAME}
# Push images using Docker CLI
docker push ${IMAGE_NAME}
docker push ${LATEST_IMAGE_NAME}
fi
done