

Discover more from INUVEON
Automatisierte NextJS Builds/ Deployments (GitHub, AWS ECR, Fargate Service, ECS)- Teil 1/4
Im ersten Teil des Tutorials zeige ich, wie du deine NextJS App als Docker Image auf der ECR (Elastic Container Registry) bereitstellen kannst und vorher ECR mit Terraform ausrollst.
In diesem Tutorial geht es, wie bereits im letzten Beitrag versprochen, um ganz praktische Dinge. Ich halte mich demnach gar nicht lange mit einer großen Vorrede auf, sondern steige direkt ins Thema ein. Zielstellung des Tutorials ist es, eine NodeJS Applikation (NextJS) so zu automatisieren, dass sie über einen GitHub Workflow als Fargate Service auf AWS verfügbar gemacht werden kann.
Inhalte des Tutorials
Teil I - Docker Image/ ECR Terraform Deployment & Image pushen
Teil III - Fargate Terraform Deployment on AWS (Infrastructure as Code)
Teil IV - Optimierung des Dockerfiles für Produktion
Kontext
Stelle dir vor, du baust gerade deine megacoole Webapplikation, beispielsweise mit NextJS. Natürlich möchtest du, dass die Stakeholder schnellstmöglich Feedback geben können und musst sie deshalb regelmäßig zur Verfügung stellen. Docker Container sind dafür eine hervorragende Möglichkeit. Cloud Technologien wie beispielweise AWS oder Azure bieten uns großartige Möglichkeiten, von Beginn sowohl die Akzeptanz und Stabilität unserer Applikation zu prüfen.
Wir werden uns in diesem Tutorial mit der kontinuierlichen Bereitstellung (CI/CD) von Web-Applikationen beschäftigen. All das, was wir hier besprechen werden, ist aus meiner Sicht ein essenzieller Teil und ein initialer Schritt in der zeitgemäßen Entwicklung von Software. Ich möchte an dieser Stelle auch darauf hinweisen, dass es zum Handwerkszeug eines Entwicklers gehören sollte, das Rollout der Lösung zu automatisieren. Mit all den Tools und dem Wissen, über welches wir inzwischen verfügen, ist die Bereitstellung auf keinen Fall ein losgelöster Schritt am Ende eines Projektes.
Ich werde also in dieser Tutorial-Serie zeigen, wie wir ohne großen Aufwand eine NextJS App voll automatisiert “production ready” bekommen. In der heutigen Folge beschäftigen wir uns mit der Erstellung einer Container Registry (ECR) und eines initialen Docker Images.
Was gebraucht wird
Du benötigst nicht viel auf deiner Maschine, um dieses Tutorial mitzumachen.
NodeJS Version 14 oder höher
lokale Docker Installation, z.B. Docker Desktop (Mac oder Windows)
GitHub Account; Git CLI
AWS Account (falls du keinen haben solltest, kannst du dir kostenfrei einen erstellen)
AWS CLI
Grundsätzlich ist das Tutorial auf andere Technologien übertragbar. Es kann also auch anstelle eine NextJS App eine andere NodeJS Integration verwendet werden, oder anstatt GitHub beispielsweise GitLab usw.
Um zu prüfen, ob NodeJS und in welcher Version auf deiner Maschine verfügbar ist, kannst du folgenden Befehl im Terminal ausführen …
$ node -v
v16.14.2
…und für Git CLI und AWS CLI wie folgt:
$ git --version
$ aws --version
Alles vorhanden? Legen wir endlich los…
Beispiel NextJS App
Lasst uns zuerst ein Beispiel Projekt mithilfe von ‘create-next-app’. Gehe dazu in den Source-Ordner, in dem du das Projekt erstellen möchtest. Rufe dann den folgenden Befehl auf.
$ yarn create next-app
Gib dem Projekt den gewünschten Namen. Ich nenne es in diesem Beispiel wie folgt.
What is your project named? › cicd-tutorial
Press ENTER.
Gehe danach in den Projektordner und teste, ob die Applikation lauffähig ist.
$ cd cicd-tutorial
$ yarn run dev
yarn run v1.22.17
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
wait - compiling...
event - compiled client and server successfully in 821 ms (124 modules)
Solltest du den hier gezeigten Output sehen, dann hat alles geklappt.
Im Browser kannst du die URL http://localhost:3000 aufrufen. Du siehst folgenden Output, wenn alles geklappt hat.
Cool! Das war easy.
Docker Image vorbereiten
Als Nächstes benötigen wir ein Dockerfile, um die Applikation zu containerisieren. Dafür erstellen im Root-Folder unseres Projektes eine Datei mit dem Namen “Dockerfile”.
$ touch Dockerfile
Füge folgenden Inhalt in das soeben erstellte Dockerfile.
FROM node:16-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
USER node
EXPOSE 3000
CMD ["yarn", "start"]
In diesem Teil des Tutorials konzentrieren wir uns im Augenblick nicht darauf, das Dockerfile für den Produktionseinsatz vorzubereiten. Das werden wir im letzten Teil dieser Serie machen. Zunächst geht es darum, grundsätzlich zu verstehen, wie wir unsere App in einen Docker-Container bekommen und als Image auf der AWS ECR ablegen können.
Erstellen wir nun ein lokales Image, um zu sehen, ob das Dockerfile so funktionsfähig ist.
Dafür rufen wir den folgenden Befehl im Terminal auf.
$ docker build -t <image-name> .
Die Option -t bedeutet, dass wir dem Image einen Namen geben. In meinem Falle ist es “cicd-tutorial”. Achtung: Der Punkt am Ende ist erforderlich, was bedeutet, dass das Dockerfile im lokalen Verzeichnis verwendet werden soll.
Mit dem folgenden Befehl können wir prüfen, ob das Docker Image lokal verfügbar ist.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cicd-turorial latest f43f5a09ac05 6 minutes ago 1.06GB
Ok, super! Es ist da.
Probieren wir nun, ob die Applikation im Container wie erwartet läuft und mappen den Port 3000 auf unser lokales Netzwerk, damit wir die Webapplikation im Browser auch erreichen können. Wenn du einen anderen Namen verwendet hast, musst “cicd-tutorial” entsprechend ersetzen.
$ docker run -p 3000:3000 cicd-turorial
Danach kannst du die Applikation im Browser wieder aufrufen und solltest denselben Output wie oben schon gezeigt erhalten.
Wie bereits gesagt, habe ich das Dockerfile zunächst bewusst einfach gehalten, da ich gerne später nochmals darauf eingehen möchte, wie wir die App “production-ready” bekommen.
Elastic Container Registry (ECR)
Ok, wir haben inzwischen sicher gestellt, dass unsere App im Container lauffähig ist und werden uns nun um die Erstellung einer Container Registry auf AWS kümmern. AWS stellt einen Dienst mit dem Namen “Elastic Container Registry” (kurz ECR) zur Verfügung, mit dem wir Container Images in einem privaten Repository bereitstellen können.
Da ich ein Fan von vollständiger Automatisierung bin, werden wir diese Registry natürlich nicht manuell erstellen und uns durch die Menüs klicken, sondern Terraform verwenden. Terraform ist ein “Infrstructure as Code” (kurz IAC) Tool. IAC ermöglicht die Definition, den Einsatz und die Aktualisierung der Infrastruktur mit Code. Im Grunde wird alles, was benötigt wird, um eine Anwendung zu betreiben, im Code dargestellt. Das heißt, Server, Datenbanken, Netzwerke, Konfiguration, Protokollierung, Dokumentation, Tests, Bereitstellungsprozesse. IAC ist die Grundlage für den Betrieb moderner Anwendungen in einer Cloud-Umgebung.
Um die benötigte Infrastruktur mithilfe von Terraform zu beschreiben, müssen wir zunächst die Struktur unseres Projektes erweitern.
Wir legen dafür einen neuen Ordner mit dem Namen “terraform” im Root-Verzeichnis unseres Projektes an, um die Infrastruktur als Terraform Code dort abzubilden.
Da die Erstellung der ECR ein initialer Schritt ist und wir später einen weiteren Deployment Schritt zur Erstellung des Fargate Services auf dem Elastic Container Service (ECS) benötigen werden, empfehle ich, einen separaten Unterordner für die Container Registry anzulegen.
$ mkdir terraform && mkdir terraform/registry
Wir haben nun folgende Projekt-Struktur.
.
├── pages
│ └── api
├── public
├── styles
└── terraform
└── registry
Im nächsten Schritt erstellen wir im Ordner /terraform/registry eine Datei mit dem Namen “main.tf”, in welcher wir unseren ersten einfachen Terraform-Skript ablegen werden, welcher der Erstellung unserer Container Registry dient.
terraform {
required_version = ">= 1.0.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 3.68.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.1.0"
}
}
}
variable "registry_name" {
type = string
}
variable "aws_region" {
type = string
}
provider "aws" {
region = var.aws_region
}
resource "aws_ecr_repository" "repository" {
name = var.registry_name
image_tag_mutability = "MUTABLE"
tags = {
Name = var.registry_name
}
image_scanning_configuration {
scan_on_push = true
}
}
output "registry_id" {
description = "The account ID of the registry holding the repository."
value = aws_ecr_repository.repository.registry_id
}
output "repository_name" {
description = "The name of the repository."
value = aws_ecr_repository.repository.name
}
output "repository_url" {
description = "The URL of the repository."
value = aws_ecr_repository.repository.repository_url
}
Wechseln wir nun in das Verzeichnis ‘terraform/registry’, um Terraform zu initialisieren.
$ cd terraform/registry/
$ terraform init
Terraform has been successfully initialized!
Im File main.tf sind die beiden Variablen ‘registry_name’ und ‘aws_region’ definiert. Diese müssen natürlich noch gesetzt werden, bevor wir deployen können. Dafür erstellen wir ein tfvars File, in welchem alle Variablen definiert werden können. Mit der Verwendung unterschiedlicher tfvars Files für verschiedene Umgebungen können wir in einem weiteren Schritt unterschiedliche Stages managen. Wie das funktioniert, erkläre ich in einer späteren Folge des Tutorials.
Zunächst begnügen wir uns damit, die beiden Variablen in einem separierten File abzulegen. Dafür erstellen wir im Ordner ‘terraform/registry’ die Dateien vars.tfvars mit dem folgenden Inhalt. Natürlich können die Werte frei gewählt werden. Gültige Werte aller verfügbaren AWS Regionen sind hier zu finden.
registry_name = "codewithrico-cicd-pipeline"
aws_region = "eu-central-1"
Um nun das ECR Repository mittels Terraform Deployment auf dem AWS-Account auszurollen, müssen natürlich die AWS_ACCESS_KEY_ID und AWS_SECRET_ACCESS_KEY als Umgebungsvariablen auf deiner lokalen Maschine gesetzt sein.
Du bekommst diese Variablen entweder über den Profile-Menüpunkt → Security credentials → Access Keys in der AWS Console. Ich empfehle allerdings, SSO auf dem AWS Account einzurichten und einen dedizierten User anzulegen. Danach erreichst du deine AWS Console wie folgt: https://<yourorganization>.awsapps.com/start#/
Nach deinem Login kannst du die Werte dort einfach kopieren und im Terminal einfügen.
Zusätzlich sollte noch MFA konfiguriert sein. Vorteil bei der Nutzung von SSO ist, dass die AWS Key und Secret nur begrenzt gültig sind; z. B. 12 Stunden.
Solltest du nicht wissen, wie die Konfiguration von SSO auf AWS geht, ich werde demnächst näher darauf eingehen. Für den Moment ist es wichtig, dass die beiden Umgebungsvariablen in deinem Terminal gesetzt sind.
Wir können endlich unsere Container Registry auf AWS ausrollen.
$ terraform apply -auto-approve -var-file=vars.tfvars
Durch die Option -auto-approve sind keine weiteren Eingaben erforderlich. Mit -var-file zeigen wir an, wo sich unsere vordefinieren Variablen befinden. Auf Sinn und Zweck der Nutzung eins var-Files gehe ich einer späteren Folge wie oben schon erwähnt noch näher ein.
Nach einem erfolgreichen Deployment sollten wir den folgenden Output im Terminal sehen.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
registry_id = "<your account id>"
repository_name = "codewithrico-cicd-pipeline"
repository_url = "<your account id>.dkr.ecr.eu-central-1.amazonaws.com/codewithrico-cicd-pipeline"
Großartig! Wir haben eine Container Registry am Start.
Push the Image
Fassen wir kurz zusammen, was wir bisher gemacht haben:
Beispiel-Projekt inklusive Dockerfile erstellt
Docker Image lokal gebaut und Container getestet
Terraform Code für das Deployment der ECR erstellt
Container Repository auf AWS mit Terraform ausgerollt
Nun fehlt noch etwas Leben auf der Container Registry, oder?! Es ist also an der Zeit unsere NextJS App als Image auf der ECR abzulegen.
Zunächst müssen wir uns auf der ECR einloggen, damit wir unser Docker Image dahin pushen können.
Dafür holen wir uns die Account ID wie folgt.
$ export ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)
Damit können wir uns schon auf fast auf der ECR über den Terminal einloggen. Wir müssen vorher nur noch die AWS_REGION als Umgebungsvariable setzen, in der sich das Container Repository befindet. In meinem Fall ist das Frankfurt.
$ export AWS_REGION=eu-central-1
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
Bei Erfolg sehen wir den Output “Login Succeeded” im Terminal.
Im finalen Schritt müssen wir noch richtig taggen und auf die Container Registry pushen. Zunächst holen wir uns die korrekte Repository URL mit dem folgenden Command.
$ export REPOSITORY_NAME=<name of your registry>
$ export REPOSITORY_URL=${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY_NAME}
Als Name der Registry bzw. Repositories musst du den Namen verwenden, den du in der vars.tfvars festgelegt hattest. In der nächsten Ausgabe werde ich näher darauf eingehen, wie es möglich ist, diese Variablen in einem GitHub Workflow voll automatisch durchzureichen.
Um das Image nun zu pushen, sind noch die folgenden Schritte erforderlich.
$ docker build --platform linux/amd64 -t ${REPOSITORY_NAME}:latest .
$ docker tag ${REPOSITORY_NAME}:latest ${REPOSITORY_URL}:latest
$ docker push ${REPOSITORY_URL}:latest
Danach können wir über die AWS Console prüfen, ob das Image verfügbar ist.
Wie bereits im Text angekündigt, werden wir in der nächsten Folge des Tutorials tiefer in GitHub Workflows eintauchen und die Erstellung der ECR und des Images in einen GitHub Workflow packen. Automatisierung und CI/CD Workflows wird demnach unser nächster Schwerpunkt sein.
Den Beispiel Code findest du auf GitHub hier.
Wenn du jemanden kennst, für den dieses Tutorial auch interessant sein könnte, dann teile diesen Artikel gerne.