Multi-Plattform Docker-Images mit Github Actions bauen
- von Nicolai Mainiero
Nicolai Mainiero, Softwarearchitekt bei sidion
Mit der zunehmenden Verbreitung von Computern mit ARM64 Architektur, wie zum Beispiel Raspberry Pi, AWS Gravitron oder Ampere Altra von Microsoft, zur Entwicklung und Betrieb von Anwendungen müssen auch Build-Pipelines für Containerimages etabliert werden, die nicht nur für eine sondern mehrere Plattformen Images bauen kann.
Github Actions
GitHub Actions ist eine Plattform für kontinuierliche Integration und Bereitstellung (CI/CD), mit der die Build-, Test- und Bereitstellungspipeline automatisiert werden kann. Diese Workflows werden durch Ereignisse, die im Repository stattfinden gestartet. Die Auswahl an möglichen Ereignissen ist sehr umfangreich, das kann ein neu erzeugtes Issue oder eine Änderung an einem Issue sein, ein neuer Pull-Request oder ein Fork sein. Darüber hinaus kann auch noch gefiltert werden, dass zum Beispiel nur ein Push auf den main Branch den Workflow startet. Eine vollständige Beschreibung wie ein Workflow gestartet werden kann findet sich bei Github ^1. Um ein Build zu automatisieren, bietet es sich an auf Push Ereignisse zu reagieren.
on:
push:
branches: [ main ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ main ]
Die Workflows werden als .yaml deklarativ beschrieben. Diese Beschreibung muss dann in einem speziellen Verzeichnis (.github/workflows
) im Repository abgelegt werden. Github wertet diese Dateien dann entsprechend aus und startet bei einem passenden Ereignis den Workflow. In das Verzeichnis kann mehr als eine Datei abgelegt werden, um verschiedene Workflows zu managen.
Marketplace
Öffnet man eine solche Workflow Beschreibung zur Bearbeitung direkt auf Github, bekommt man einen Editor, der neben der Dokumentation auch den Marketplace anzeigt. Dieser Marktplatz bietet unzählige sogenannter Actions an, die in einem Workflow verwendet werden können. Von so grundsätzlichen Dingen wie zum Beispiel die richtige Java Version zu installieren und den PATH zu konfigurieren, über eine Cache Action, die Artifakte und Dependencies zwischenspeichert bis zu einer Deploy Action für AWS ECS.
Für diesen Anwendungsfall benötigen wir vor allem die von Docker zur Verfügung gestellten Actions, um ein Container Image zu bauen. Zunächst wird ein geeigneter Job im Workflow definiert. Dieser ist notwendig, um den Runner auszuwählen mit dem die nachfolgenden Schritte ausgeführt werden. Github bietet hier eine Auswahl verschiedener vorkonfigurierter Umgebungen an (siehe ^2). Für dieses Beispiel ist die Ubuntu Umgebung die geeignetste.
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
Actions
Jetzt sind nur noch die Notwendigen Einzelschritte in dem Job zu definieren. Im Wesentlichen sind das folgende fünf Stück, um das Container Image zu bauen: Checkout, Setup von QEMU, Setup von Docker Buildx, Login bei Docker Hub und Metadaten bestimmen, Build und Push zu Docker Hub.
Checkout
Eine von Github bereitgestellte Standardfunktion um das Repository in einer bestimmten Version auszuchecken. Wenn nichts Weiteres angegeben wird, wird das aktuelle Repository mit Hilfe des personal access token ausgecheckt. Es wird auch nur ein einziger Commit und nicht die gesamte Historie ausgecheckt.
- name: Checkout repository
uses: actions/checkout@v2
QEMU
Die QEMU Action von docker installiert den gleichnamigen Systememulator, damit die verschiedenen Systemarchitekturen nachfolgend zur Verfügung stehen. Neben ARM64 kann hier auch noch riscv64 oder arm ausgewählt werden. Standardmäßig werden alle Architekturen installiert.
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
Buildx
Buildx ist ein Plugin, dass das normal Docker CLI um weitere Features, wie zum Beispiel die Möglichkeit gegen mehrere Knoten gleichzeitig ein Image bauen zu können, nachrüstet. Genau dieses Feature erlaubt es verschiedene QEMU Konten zu starten und diese dann zum Bauen der verschiedenen Architekturen zu verwenden.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
Login bei Docker Hub
Damit das Image dann auch auf Docker Hub verfügbar wird, muss sich dort angemeldet werden. Dazu wird auf Docker Hub ein Access Token erzeugt und im Repository als Secret hinterlegt. In dem Workflow kann dann darauf mit ${{ secrets.DOCKER_TOKEN }}
zugegriffen werden.
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' # Nur ausführen, wenn es kein PR ist
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
Metadaten bestimmen, Build und Push
Zuletzt werden noch die Metadaten für das Image bestimmt. Neben dem Namen des Images können auch die Tags angepasst werden. Beispielsweise könnten Branches eigene Tags erhalten. Danach kann das Image dann gebaut und auf Docker Hub hochgeladen werden. Hier ist es nun entscheidend alle Plattformen anzugeben, für die man das Image gebaut haben möchte.
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
type=ref,enable=true,priority=600,prefix=pr-,suffix=,event=pr
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: ./
platforms: linux/amd64,linux/arm64 # Hier die gewünschen Architekturen auswählen
push: ${{ github.event_name != 'pull_request' }} # Nur ausführen, wenn es kein PR ist
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Die vollständige Pipeline sieht dann so aus:
name: ci
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [ main ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ main ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ 'nmainiero' }}/${{ 'ecs-logging-demo' }}
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Image name
run: echo "$IMAGE_NAME"
shell: bash
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
type=ref,enable=true,priority=600,prefix=pr-,suffix=,event=pr
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: ./
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Pipeline Aktuell halten
Diese Pipeline kann man mit Hilfe eines weiteren Workflows aktuell halten. Dazu gibt es den Github Dependabot, der automatisch PRs erzeugt, wenn eine Dependency in einer neuen Version erschienen ist. Dazu kann man einfach die Datei .github/dependabot.yml mit folgendem Inhalt erstellen:
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
Fazit
Github Actions sind ein flexibles und mächtiges Tool, dass es erlaubt schnell, unkompliziert und automatisch Docker Images für verschiedene Rechnerarchitekturen zu bauen und auf Docker Hub zur Verfügung zu stellen. Durch den modularen Aufbau in einzelne Schritte können diese bei Bedarf ausgetauscht werden, um zum Beispiel das Image in einer anderen Registry zu veröffentlichen.
Durch den Einsatz von QEMU und Buildx können Images für verschiedene Architekturen gebaut werden, auch wenn die CI-Pipeline auf einer ganz anderen Architektur basiert. Damit ist es problemlos möglich die wirtschaftlichste Lösung, unabhängig von der Architektur, auszuwählen.