Hoe een productie-applicatie op een VPS te deployen en het proces te automatiseren met Docker, GitHub Actions en AWS ECR.

In dit artikel zullen we leren wat het betekent om een applicatie in productie te pushen en hoe we dat automatisch kunnen laten gebeuren, we zullen daarvoor docker en GitHub in actie zien. Omdat we docker gebruiken zal ik niet te veel tijd besteden aan welke technologieën zijn gebruikt om de applicatie te schrijven in plaats van wat te doen met de docker image zelf. Het uitgangspunt betekent hier een eenvoudige server, laat het een eenvoudige VPS (Virtual Private Server) zijn, een dedicated server, of elke server waar je SSH voor hebt. We moeten acties uitvoeren, de server voorbereiden om de app te ontvangen en uit te voeren en de deployment pipeline instellen. Voor het komende deel van dit artikel ga ik ervan uit dat je een Ubuntu-server hebt

Het allereerste wat we moeten doen is een eenmalige configuratie uitvoeren op de server, met als doel deze voor te bereiden op de volgende stappen. Hiervoor moeten we de volgende onderwerpen bekijken:

Docker Compose is een hulpmiddel dat veel Docker-toepassingen als services uitvoert en ze laat communiceren met elkaar, evenals andere functies zoals volumes voor bestandopslag. Laten we eerst Docker en Docker Compose op de server installeren; je kunt de officiële installatiegids hier volgen: docs.docker.com/engine/install/ubuntu. Daarna is het belangrijk om het uit te voeren als een niet-rootgebruiker; hier is wat de officiële documentatie zegt: docs.docker.com/engine/security/rootless.

Merk op dat je hetzelfde doel ook kunt bereiken met een andere registry als je dat wilt, ik gebruik AWS hier alleen omdat het voor mij eenvoudiger is.

Since we are using Docker, the images will have to be stored and retrieved from somewhere. For this purpose, I am using AWS ECR (Amazon Web Services Elastic Container Registry). It’s a Docker registry within an AWS account. It’s very cheap to use and easy to set up. You can also use Docker Hub to create a private repository for your images. It all starts by creating an ECR private registry in the AWS account. You will click on “Create Repository” and fill in the name of the repository.

Na het maken van een repository kun je de repository URI kopiëren en deze voor later bewaren. Het heeft de volgende indeling AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

Je moet ook AWS IAM-referenties instellen die het recht hebben om te pullen/pushen naar/from deze repository. Laten we naar de IAM-service gaan, klik op nieuwe gebruiker en koppel het volgende beleid aan hem: AmazonEC2ContainerRegistryFullAccess, je hoeft geen toegang tot de AWS console voor hem in te schakelen. Aan het einde van dit proces ontvang je 2 sleutels van AWS, een geheime sleutel en een geheime sleutel-id, hou deze apart, we zullen ze nodig hebben voor het komende werk.

Terug op onze server moeten we AWS CLI installeren. De officiële manier van installeren is beschikbaar hier. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. Na dit kun je de installatie testen door het uitvoeren van het commando aws --version. Op dit stadium moet je het commando aws configure uitvoeren en vragen beantwoorden door de eerder gegenereerde sleutels op aws te verstrekken, de secret key en de secret key id. Je wordt ook gevraagd een uitvoervorm te kiezen, eenvoudigweg JSON, en een standaard regio op te geven, het is beter om de regio te kiezen waar je eerder het ECR register hebt aangemaakt.

In mijn workflow heb ik een klein shell script geschreven dat bepaalde acties uitvoert, het is de sleutel van dit proces, het logt in op het register, downloadt de afbeelding en herstart de overeenkomstige docker-service, ik noem het gewoon redeploy.sh en sla het op onder een map waarvan ik mijn app wil draaien, hier is de inhoud:

#!/bin/bash

# Retrieve AWS ECR login command
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com

# Associating repositories with identifiers
declare -A repositories=(
    ["web"]="[REGISTRY_NAME]:latest"
)

# Check if service identifier is provided as a command line argument
if [ -z "$1" ]; then
    echo "Please provide a service identifier as a command line argument."
    exit 1
fi

service_identifier=$1

# Check if the provided service identifier exists in the repositories array
if [ -z "${repositories[$service_identifier]}" ]; then
    echo "Invalid service identifier. Available identifiers: ${!repositories[@]}"
    exit 1
fi

# pull the new image from the registry
repository=${repositories[$service_identifier]}
echo "Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
docker pull "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"

# Change directory to [APP_FOLDER] 
cd /home/ubuntu/[APP_FOLDER] || {
    echo "Failed to change directory to /home/ubuntu/[APP_FOLDER]"
    exit 1
}

# stop and restart the service, this wil force docker compose to redownload the lates image
echo "Re-running service $service_identifier"
docker compose stop "$service_identifier"
docker compose up --no-deps "$service_identifier" -d

# Remove old and un-used docker images
echo "Removing unused Docker images"
docker image prune -fa
echo "Removed Dangling Images"

De eerste stap van dit script bestaat uit het inloggen op de AWS-account met AWS CLI om een token te krijgen dat docker zal gebruiken bij het ophalen van de docker-afbeelding, onthoud dat het register privé is, we kunnen het niet zomaar ophalen zonder geverifieerd te worden.

Vervolgens declareren we een repositorylijst en associëren we deze met een identifier, de opgegeven identifier zal worden gebruikt als een opdrachtpromptarg, meer hierover later. Daarna verifiëren we of de gebruiker een argument heeft opgegeven dat overeenkomt met een bestaande service-identifier, we willen dat hij iets typt zoals ./redeploy web bijvoorbeeld, het script zal het argument web associëren met de repository web zoals in de tweede stap.

Na het hebben van de service-identifier maken we de repository URL dynamisch en voeren we een docker pull uit met deze URL. Dit zorgt ervoor dat het docker-image wordt gedownload naar ons systeem.

Het script zal nu naar de applicatiemap cd’en, /home/ubuntu/[APP_FOLDER] dit gaat ervan uit dat je alles uitvoert onder de gebruiker ubuntu en dat de HOME-map van deze gebruiker ubuntu heet, APP_FOLDER bevat de hele setup.

De volgende stap bestaat uit het stoppen en starten van de service waarna we eenvoudig oude en ongebruikte images verwijderen met de opdracht docker image prune -fa je kunt hier meer over lezen: https://docs.docker.com/reference/cli/docker/system/prune/.

Compose is het hulpprogramma dat ons hele systeem uitvoert, het heeft een bestand nodig genaamd docker-compose.yml waar je alles zult definiëren, laten we aannemen dat onze app een redis en een postgres service nodig heeft om te draaien, zo ziet het eruit:

version: '3.9'
services:
  web:
    image: "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"
    ports:
        - 8080:8080
    depends_on:
        - redis
        - db
    env_file:
        - .env

  redis:
    image: 'redis:alpine'
    ports:
      - '6379:6379'
  db:    
    image: 'postgres:14'
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      PGDATA: /var/lib/postgresql/data/pgdata
    healthcheck:
      test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ]
      timeout: 45s
      interval: 10s
      retries: 10
    ports: 
      - '5437:5432'
    volumes:
      - ./opt/postgres/data:/var/lib/postgresql/data

Uw volume ./opt/postgres/data:/var/lib/postgresql/data zal de inhoud van de Postgres-server koppelen naar de lokale schijf zodat deze niet verloren gaat wanneer de Docker-container stopt met draaien. Meer informatie over het draaien van Postgres met docker-compose vindt u hier https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. Ik heb een richtlijn genaamd env_file gebruikt, die docker-compose in staat stelt een bestand te lezen en de inhoud ervan tijdens het draaien in de Docker-container te laden, ik heb dit gedaan omdat de docker-compose-file meestal wordt gecommit naar een VCS, waar ik de omgevingsvariabele niet direct in wil opslaan via de environment-richtlijn in de service. Merk op dat onze service hier web heet, eerder hebben we een bestand redeploy.sh geschreven en we bedoelen het op deze manier uit te voeren:

 ./redeploy.sh web

Het web-argument is gekoppeld aan de naam van onze service, dat bestand mapt eenvoudig het argument naar een service naam in het Docker-bestand.

Op dit stadium moeten we een Linux-service maken die ervoor zorgt dat de toepassing elke keer wordt gestart wanneer de server start of onze toepassing stopt. Het volgende script zal u hierbij helpen:

[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

Laten we het analyseren !!!

  • De Unit-sectie beschrijft onze service en specificeert tot welke service onze eenheid behoort, in dit geval is het de Docker-service, dit zorgt ervoor dat onze service altijd draait wanneer de Docker-service ook draait.

  • De Service-sectie beschrijft hoe onze service moet worden uitgevoerd, de interessante delen zijn WorkingDirectory, ExecStart en ExecStop opdrachten, ze zullen worden gebruikt zoals hun naam betekent, bijvoorbeeld als de service de naam myapp heeft, wanneer je de opdracht systemctl start myapp typt, zal de opdracht ExecStart worden uitgevoerd. Je kunt meer leren over Linux-service hier https://www.redhat.com/sysadmin/systemd-oneshot-service. Meer informatie over het uitvoeren van de docker-service met systemd kun je hier vinden: https://bootvar.com/systemd-service-for-docker-compose/

Deze service moet op een manier geïnstalleerd worden dat het systeem het uitvoert wanneer nodig, je moet het opslaan in een bestand met een naam, bijvoorbeeld: myapp.service

touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce

Op dit punt wordt het herkend als een Linux service, je kunt systemctl start myapp uitvoeren om het te starten. Het volgende vereiste commando is

systemctl enable myapp.service

Dit zorgt ervoor dat de service automatisch wordt uitgevoerd door de server bij elke herstart. Je kunt hier meer leren: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

Ik heb Nginx voor deze taak gebruikt, het is klein en krachtig, wordt breed toegepast en kan fungeren als een load balancer, een statische bestandsserver, een reverse proxy en veel meer. Het eerste wat je moet doen is het installeren.

sudo apt-get install nginx

Bij deze stap zou de docker-image aan het draaien moeten zijn, laten we aannemen dat deze een app bevat die draait op de poort 8080, en dat deze poort via het docker-compose bestand aan de server is gekoppeld. We moeten een reverse proxy-configuratie opzetten tussen Nginx en onze poort. Hier is de benodigde configuratie:

upstream app_backend {
  server localhost:8080; # the appliction port
}

server {
  listen 80;
  server_name [DOMAIN_NAME];

  location / {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache_bypass $http_upgrade;
  }
}

laten we deze configuratie myapp.conf noemen en deze opslaan in een map waar nginx het kan vinden, deze map is samen met anderen genaamd /etc/nginx/conf.d/.

sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
# paste the content there

Nu moeten we het testen en de NGINX-service herstarten met de volgende opdrachten

sudo nginx -t # test if the config is valid 
sudo nginx -s reload # reload the nginx service so it will consider it

Deze configuratie zal nginx instrueren om verkeer op de poort 80 te luisteren en met het domeinnaam [DOMAIN_NAME] en het naar je app-server op de poort 8080 te sturen via de richtlijn proxy_pass, de regel location / { betekent eenvoudig alle verzoeken die beginnen met / te vangen en de acties uit te voeren die onder het location-blok zijn geschreven. Meer informatie hier https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.

Na het configureren van de server, moeten we nu de build pipeline opzetten, deze bestaat voornamelijk uit 1 stap, een GitHub Action pipeline bestand schrijven en het toevoegen aan het project, laten we beginnen.

GitHub action zal worden gebruikt om de docker image te bouwen vanuit onze broncode en deze naar het register te pushen waaruit de image wordt gepullt en uitgevoerd op de server. Ik zal een voorbeeld Dockerfile gebruiken voor deze example, maar in de praktijk moet je je eigen Dockerfile schrijven. Voor een express.js toepassing, zou het docker bestand er zo uit zien:

# Fetching the minified node image on apline linux
FROM node:slim

# Declaring env
ENV NODE_ENV production

# Setting up the work directory
WORKDIR /express-docker

# Copying all the files in our project
COPY . .

# Installing dependencies
RUN npm install

# Installing pm2 globally
RUN npm install pm2 -g

# Exposing server port
EXPOSE 8080

# Starting our application
CMD pm2 start process.yml && tail -f /dev/null

Het bouwen en uitvoeren van deze docker file zal onze toepassing op poort 8000 starten, maar in onze setup moeten we het uitvoeren met docker-compose.

De volgende stap is het opzetten van de GitHub actions pipeline. Hiervoor maak je eenvoudig een map .github/workflows in de projectroot en maak je een bestand genaamd docker-build.yml, waarin we onze pipeline gaan schrijven.

name: Build, Push to ECS and Deploy to Server

on:
  push:
    branches: ['deploy/main']

jobs:
  build:
    name: Build Web Image
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - 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: [AWS_REGION]

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

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: [REPOSITORY_NAME]
          IMAGE_TAG: latest
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Restart the service via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          port: ${{ secrets.PORT }}
          script: /home/ubntu/[APP_DIRECTORY]/redeploy.sh web

Er zijn verschillende stappen om naar te kijken hier:

  • Configure AWS credentials: hier zal het systeem de eerdere aws-sleutel die je eerder hebt gemaakt laden, je moet deze registeren in de secrets van je GitHub account

  • Build, tag, and push image to Amazon ECR : deze stap zal de opdrachten docker build en docker push uitvoeren om de docker image te maken

  • Herstart de service via SSH deze stap verbindt met de server en herstart de hele toepassing in één keer.

Deze pijplijn zal elke keer worden uitgevoerd wanneer er een pull request wordt samengevoegd tegen de deploy/main tak.

on:
  push:
    branches: ['deploy/main']

Op dit punt is het hele systeem op zijn plaats en gekoppeld, nu is het mogelijk om het te bewerken en toe te passen op uw specifieke geval. In een toekomstig artikel zal ik het proces van het bouwen van de toepassing zelf voor productie en het draaien in een dockerbestand delen.

Dit artikel probeert het proces te beschrijven dat ik gebruik om een VPS in te stellen voor automatisering met betrekking tot implementatie. Het beschrijft hoe het proces van uitvoering van de toepassing binnen de server en het bouwen van de toepassing is ingesteld, elk deel kan worden gedaan met een ander hulpmiddel, bijvoorbeeld kun je nginx vervangen door Treafik als je dat wilt, en je kunt de systemd service vervangen door een programma in supervisor en meer. Dit proces behandelt geen aanvullende zaken zoals het backed-uppen van de server of het sluiten van standaardpoorten op de servers, die zullen in toekomstige artikelen worden uitgelegd. Stel gerust een vraag als je dit aan je eigen stroom wilt aanpassen. In een ander artikel zal ik me richten op hoe je een toepassing kunt instellen om productieklaar te zijn in termen van implementatie, dat is het deel van het proces dat voor de bouw van de Docker Image komt.

Ik hoop dat u het leuk vond om te lezen.

Ik ben er om u te helpen bij de implementatie binnen uw bedrijf of team, zodat u zich kunt concentreren op uw kerntaken en geld kunt besparen voordat u het enorme potentieel van Mastodon gaat benutten.

Source:
https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr