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
Prepariamo il server
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 per eseguire l’applicazione e le sue dipendenze
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.
Configura le credenziali di AWS ECR & CLI.
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.
Configura lo script dell’applicazione runner.
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/.
Il file Docker compose
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.
Configura un servizio Linux per mantenere tutto in esecuzione
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 sonoWorkingDirectory
,ExecStart
e il comandoExecStop
, che verranno utilizzati secondo il significato del loro nome, per esempio, se il servizio si chiamamyapp
quando digiti il comandosystemctl start myapp
verrà eseguito il comandoExecStart
. 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 consystemd
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
Il server web
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.
La pipeline di compilazione
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.
Configurazione GitHub Actions
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 comandodocker build
edocker 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.
Conclusione
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.