COME DEPLOYARE UN’APP DI PRODUZIONE SU UN VPS E AUTOMATIZZARE IL PROCESO CON DOCKER, GITHUB ACTIONS E AWS ECR.

In questo articolo, impareremo cosa significa spingere un’applicazione in produzione e come farlo automaticamente, vedremo Docker e GitHub Actions per questo scopo. Dato che stiamo utilizzando Docker, non mi soffermerò troppo su quali tecnologie sono state utilizzate per scrivere l’applicazione, ma piuttosto su cosa fare con l’immagine Docker stessa. La premessa qui significa un semplice server, sia esso un VPS (Virtual Private Server), un server dedicato o qualsiasi server dove hai accesso SSH per utilizzarlo. Dobbiamo eseguire delle azioni, preparare il server per ricevere ed eseguire l’app, e configurare la pipeline di distribuzione. Per la parte successiva di questo articolo, supporrò che tu abbia un server Ubuntu

La prima cosa da fare è eseguire alcune configurazioni una tantum sul server, l’obiettivo è prepararlo per i passaggi successivi. A tale scopo, dobbiamo considerare i seguenti argomenti:

Docker Compose è uno strumento che esegue molte applicazioni Docker come servizi e permette loro di comunicare tra loro, così come altre funzionalità come i volumi per lo storage dei file. Installiamo prima Docker e Docker Compose sul server; puoi seguire la guida ufficiale di installazione qui: docs.docker.com/engine/install/ubuntu. Dopo questo, è importante eseguirlo come utente non root; ecco cosa dice la documentazione ufficiale: docs.docker.com/engine/security/rootless.

Nota che puoi raggiungere lo stesso obiettivo con un altro registro se lo desideri, sto usando AWS qui perché è più semplice per me.

Siccome stiamo usando Docker, le immagini devono essere conservate e recuperate da qualche parte. A questo scopo, sto usando AWS ECR (Amazon Web Services Elastic Container Registry). È un registro Docker all’interno di un account AWS. È molto economico da usare e facile da configurare. Puoi anche usare Docker Hub per creare un repository privato per le tue immagini. Tutto inizia creando un registry ECR privato nell’account AWS. Cliccherai su “Create Repository” e inserirai il nome del repository.

Dopo aver creato un repository, puoi copiare l’URI del repository e conservarlo per dopo. Ha il formato seguente AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

Avrai anche bisogno di configurare le credenziali IAM di AWS che hanno il permesso di prelevare/push da/verso questo repository. Andiamo al servizio IAM, clicca su nuovo utente e aggiungi la seguente politica a lui: AmazonEC2ContainerRegistryFullAccess, non è necessario abilitare l’accesso alla console AWS per lui. Alla fine di questo processo, ricevi 2 chiavi da AWS, una chiave segreta e un ID della chiave segreta, tenile da parte perché ne avremo bisogno per il lavoro successivo.

Tornati sul nostro server, dobbiamo installare AWS CLI. Il modo ufficiale per installarlo è disponibile qui. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. Dopo questo, puoi testare l’installazione eseguendo il comando aws --version. A questo passo, dovrai eseguire il comando aws configure e rispondere alle domande fornendo le chiavi generate precedentemente su aws, la chiave segreta e l’id della chiave segreta. Ti verrà anche chiesto di scegliere un formato di output, semplicemente JSON, e fornire una regione predefinita, è meglio scegliere la regione dove hai creato il registro ECR in precedenza.

Nel mio flusso di lavoro ho scritto uno script shell piccolo che esegue certe azioni, è la parte chiave di questo processo, si logged nel registro, scarica l’immagine e riavvia il servizio docker corrispondente, lo chiamo semplicemente redeploy.sh e lo salvo in una cartella da cui voglio eseguire la mia app, ecco il contenuto:

#!/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"

Il primo passo di questo script consiste nel loggarsi all’account AWS con AWS CLI per ottenere un token che docker utilizzerà quando recupera l’immagine docker, ricorda che il registro è privato, non possiamo semplicemente prelevarla senza essere autenticati.

Quindi dichiariamo un elenco di repository e li associamo a un qualche identificatore; l’identificatore specificato sarà usato come argomento della riga di comando, come vedremo più avanti. Dopo di che verifichiamo se l’utente ha fornito un argomento che corrisponde a un identificatore di servizio esistente, vogliamo che digiti qualcosa come ./redeploy web per esempio, lo script assocerà l’argomento web al repository web come nel secondo passo.

Dopo aver ottenuto l’identificatore di servizio, creiamo l’URL del repository dinamicamente ed eseguiamo un docker pull con esso. Questo assicura che l’immagine di docker venga scaricata nel nostro sistema.

Lo script ora si collega alla cartella delle applicazioni, /home/ubuntu/[APP_FOLDER] questo presuppone che si stia eseguendo tutto sotto l’utente ubuntu e che la sua cartella HOME si chiami ubuntu, APP_FOLDER contiene l’intera configurazione.

Il passo successivo consiste nell’arrestare e avviare il servizio, dopodiché è sufficiente rimuovere le immagini vecchie e inutilizzate con il comando docker image prune -fa che potete approfondire qui: https://docs.docker.com/reference/cli/docker/system/prune/.

Compose è l’utility che gestisce il nostro intero sistema, ha bisogno di un file chiamato docker-compose.yml dove si definirà tutto, supponiamo che la nostra applicazione abbia bisogno di un servizio redis e di un servizio postgres per funzionare, ecco come apparirà:

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

Il tuo volume ./opt/postgres/data:/var/lib/postgresql/data mappzerà il contenuto del server Postgres sul disco locale, quindi non si perderà quando il container Docker si ferma. Scopri di più su come eseguire Postgres con docker-compose qui https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. Ho utilizzato una direttiva chiamata env_file che permette a docker-compose di leggere un file e caricarne il contenuto nel container Docker in tempo di esecuzione, ho fatto così perché di solito il file docker-compose viene committato in un VCS e non voglio mantenere la variabile d’ambiente direttamente tramite la direttiva environment nel servizio. Nota che il nostro servizio è chiamato web qui, in precedenza abbiamo scritto un file redeploy.sh e intendiamo eseguirlo così:

 ./redeploy.sh web

Il parametro web è collegato al nome del nostro servizio, quel file sta semplicemente mappando il parametro a un nome di servizio nel file Docker.

A questo punto, dobbiamo creare un servizio Linux che si assicuri di avviare l’applicazione ogni volta che il server si avvia o la nostra applicazione si arresta. Il seguente script ti aiuterà a farlo:

[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

Analizziamo !!!

  • La sezione Unit descrive il nostro servizio e specifica a quale servizio fa parte la nostra unità, in questo caso è il servizio Docker, questo garantirà che il nostro servizio venga sempre eseguito quando il servizio Docker è in esecuzione.

  • La sezione Service descrive come eseguire il nostro servizio, le parti interessanti sono WorkingDirectory, ExecStart e il comando ExecStop, che verranno utilizzati secondo il significato del loro nome, per esempio, se il servizio si chiama myapp quando digiti il comando systemctl start myapp verrà eseguito il comando ExecStart. Puoi scoprire di più sui servizi Linux qui https://www.redhat.com/sysadmin/systemd-oneshot-service. Scopri di più su come eseguire il servizio docker con systemd qui: https://bootvar.com/systemd-service-for-docker-compose/

Questo servizio deve essere installato in modo che il sistema lo esegua quando necessario; è necessario salvarlo in un file con un nome, ad esempio: myapp.service

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

A questo punto è riconosciuto come servizio Linux, si può eseguire systemctl start myapp per avviarlo. Il comando successivo richiesto è

systemctl enable myapp.service

Questo farà in modo che il servizio venga eseguito automaticamente dal server a ogni riavvio. Per saperne di più, consultare il sito: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

Ho usato Nginx per questo compito, è piccolo e potente, è molto usato e può agire come bilanciatore di carico, server di file statici, reverse proxy e molto altro. La prima cosa da fare è installarlo.

sudo apt-get install nginx

A questo punto si suppone che l’immagine docker sia in esecuzione, supponiamo che contenga un’applicazione in esecuzione sulla porta 80, e che tale porta sia legata al server tramite il file docker-compose. Dobbiamo impostare una configurazione di reverse proxy tra Nginx e la nostra porta. Ecco la configurazione necessaria :

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;
  }
}

chiamiamo questa configurazione myapp.conf e salviamola in una cartella dove nginx la troverà, che tra l’altro si chiama /etc/nginx/conf.d/.

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

Ora non ci resta che testarla e riavviare il servizio NGINX con i seguenti comandi

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

Questa configurazione istruirà nginx ad ascoltare il traffico sulla porta 80 e con il nome di dominio [DOMAIN_NAME] e a inviarlo al server dell’applicazione sulla porta 8080 tramite la direttiva proxy_pass , la riga location / { significa semplicemente catturare tutte le richieste che iniziano con / ed eseguire le azioni scritte sotto il blocco location. Per saperne di più, consultare https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.

Dopo aver configurato il server, dobbiamo impostare la pipeline di build ora, che principalmente consta di 1 passo, scrivere un file di pipeline di GitHub Action e aggiungerlo al progetto, andiamo.

GitHub action将被用于从我们的源代码构建Docker镜像并将其推送到注册表,从该注册表中拉取镜像并在服务器上执行。我将在这个例子中使用一个示例Dockerfile,但在实践中,你需要编写自己的Dockerfile。 Per un’applicazione express.js, il file docker sarebbe così:

# 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

Costruire ed eseguire questo file docker avvierà la nostra applicazione sulla porta 8000, ma nella nostra configurazione, dovremo eseguirlo con docker-compose.

La prossima cosa è configurare la pipeline di GitHub actions. Per farlo, crea una cartella .github/workflows nella radice del progetto e crea un file chiamato docker-build.yml, scriveremo la nostra pipeline al suo interno.

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

Qui ci sono diversi passaggi da esaminare:

  • Configura le credenziali AWS: qui il sistema caricherà la chiave aws che hai creato in precedenza, dovrai registrarle nei secrets del tuo account GitHub

  • Build, tag e push immagine su Amazon ECR: questo passo eseguirà il comando docker build e docker push per creare l’immagine docker

  • Riavvia il servizio tramite SSH questa fase si connetterà al server e riavvierà l’intera applicazione in una volta sola.

Questa pipeline scatterà ogni volta che ci sarà una richiesta di pull unita al ramo deploy/main.

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

A questo punto l’intero sistema è in posto e legato, ora è possibile modificare e applicarlo al tuo caso specifico. In un futuro articolo, condividerò il processo di costruzione dell’applicazione stessa per la produzione e l’esecuzione in un file docker.

Questo articolo tenta di descrivere il processo che utilizzo per configurare un VPS per l’automazione quando si tratta di distribuzione. Descrive come impostare il processo di esecuzione dell’applicazione all’interno del server e il processo di costruzione dell’applicazione, ciascuna parte può essere eseguita con un altro strumento, per esempio, puoi sostituire nginx con Treafik se lo desideri, e puoi sostituire il servizio systemd con un programma in supervisor e altro. Questo processo non copre cose aggiuntive come il backup del server o la chiusura delle porte predefinite sui server, queste saranno spiegate in futuri articoli. Sentiti libero di fare una domanda se vuoi adattare questo al tuo flusso. In un altro articolo mi concentrerò su come configurare un’applicazione per essere pronta per la produzione in termini di distribuzione, quella è la parte del processo che viene prima della costruzione dell’Immagine Docker.

Spero che tu abbia enjoyed la lettura.

Sono qui per aiutarti a implementare questo nella tua azienda o team, permettendoti di concentrarti sui tuoi compiti principali e risparmiare denaro prima di sfruttare il vasto potenziale di Mastodon.

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