Introduzione

Development -> Testing -> Deployment -> Dev -> Test -> Dep -> ...

Docker rende l'esecuzione di questi task estremamente più semplice.

E' conveniente generare due Dockerfile, uno per development (Dockerfile.dev) e uno per produzione.

Travis CI

  • CI serve che deploya eseguendo alcuni test
  • Collegato direttamente con account Github e con visibilità su quelle repository

Per la configurazione del progetto occorre scrivere il file ".travis.yml" in cui indichiamo:

  • iniziare una copia di Docker
  • costruire un'immagine usando il Dockerfile.dev (per i test)
  • eseguire il run dei test
  • eseguire il deploy su AWS

Passaggi:

  • "sudo: required" per i permessi
  • "script:" andremo ad indicare l'esecuzione dei test
  • se il processo di test rimane in attesa di input aggiungere "-- --coverage" per non farlo rimanere running all'infinito in ascolto sul sdin del processo.

Quindi:

language: generic
sudo: required
services:
    - docker
before_install:
    - docker build -t <nome immagine> -f Dockerfile.dev .
script:
    - docker run -e CI=true <nome immagine> <comando per eseguire i test> -- --coverage
after_success:
    - docker build -t <nome immagine prod> . ← utilizza il Dockerfile di prod

L’ultimo comando di build va eseguito per ogni servizio nel caso di una configurazione multi-container. Pushando il file nella repository GitHub verrà riconosciuto da Travis CI che eseguirà tutti gli step indicati.

AWS Hosting

Host server su cui Travis CI va a deployare.

Il servizio da utilizzare su AWS si chiama "Elastic Beanstalk".

Elastic Beanstalk

Creo la nuova applicazione indicando "Docker" come piattaforma (quest'ultimo passaggio è implicito nella creazione dell'applicazione nella versione corrente di Elastik).

In Elastic è presente un Load Balancer gestisce le chiamate ad una VM la quale contiene il container Docker il quale a sua volta contiene l'app. Questo Load Balancer ha ovviamente la peculiarità di scalare i container su diverse VMs per supportare il traffico dati. Tutto verrà gestito da Elastik, ovviamente il contro è che costui è collegato alla tua carta di credito, quindi "be careful".

Configurare Travis CI per eseguire il deploy su AWS

Aggiungere al .travis.yml:

deploy:
    provider: elasticbeanstalk
    region: "<region di AWS, es: us-west-2, es: frankfurt, lo si può recuperare dall'id dell'environment Elastik generato>"
    app: "<nome dell'app in Elastic>"
    env: "<Nomeapp-env>"
    bucket_name: "<il bucket S3, l'hard drive dove runna la VM e su cui Trevis deve copiare lo zip. Il nome si trova nel service S3 e dovrebbe essere chiamatoelasticbeanstalk-<region>-<id>>"
    bucket_path: "<nome dell'app, necessario per creare la folder all'interno del bucket>"
    on:
        branch: master

"on: branch:master" indica a Travic CI di eseguire il deploy quando viene eseguito appunto un pull del branch "master".

Utente AWS e chiavi di accesso

Le chiavi si trovano nel servizio IAM di AWS. In Users fare "Add user" che sarebbe l'utente con cui Travis andrebbe a deployare sul bucket. Spuntare "Programmatic access" e "AWS Managment Console".

Aggiungergli la policy "provide full access to AWS Elastic Beanstalk". Attenzione che la Secret access key viene mostrara solo una volta, quindi copiarsela.

Per ovvie questioni di discurezza non inserire le chiavi direttamente nel file ".travis.yml" (soprattutto la repo sarà pubblica), ma usare le variabili d'ambiente.

In Travis CI -> Settings -> tab Environment Variables:

    - AWS_ACCESS_KEY: <chiave copiata da IAM>
    - AWS_SECRET_KEY: <chiave copiata da IAM mostrata solo una volta>

Nel ".travis.yml" aggiungere, sotto "deploy":

access_key_id: $AWS_ACCESS_KEY
secret_access_key: $AWS_SECRET_KEY

Porte

Su Elastic Beanstalk va eseguito il mapping delle porte dei container Docker su quelle della VM di Elastic che ospita i containers. Nel Dockerfile aggiungere:

EXPOSE 80

Questo è un comando di "lettura" da parte degli sviluppatori normalmente, non esegue nulla. Tuttavia viene letto da Elastic Beanstalk il quale esegue automaticamente il mapping della porta 80.

Note su possibili issues da parte di Elastic Beanstalk

  • la build multi-stage potrebbe fallire, una possibile soluzione è quella di non dare un nome ai build step ma lasciarli anonimi
  • il COPY potrebbe non funzionare usando solo il punto ".", utilizzare invece "./", es:
    COPY package*.json ./

Redeploy on pull request

Su GitHub, nella repo del progetto, premere su "Compare & Pull request" per eseguire la pull request dal branch di sviluppo a master. Travis CI esegue dei test per "validare" ciò di cui è stato richiesto di fare il merge. Se i test sono stati superati (si vede sempre da GitHub) si può proseguire con il "Merge pull request" che, vista l'indicazione

on:
    branch: master

scritta nel ".travis.yml" andrà, scatenerà l'avvio del rebuild e redeploy da parte di Travis CI su AWS.

Processo di deploy di un applicazione multi-container utilizzando GitHub, Travis CI, DockerHub e AWS

Travis CI può buildare le immagini per la produzione e rilasciare queste build su Docker Hub (repositories private). Quindi Travis CI notifica AWS EB dell'aggiornamento in Docker Hub ciò conduce EB a eseguire il pull le immagini da Docker Hub e successivamente eseguire il deploy.

In principio va creato, per ogni servizio da buildare, il Dockerfile di produzione. Come abbiamo visto con applicazioni client in React e Angular occorre eseguire la copia del compilato (in "dist" o "build") e avviarlo. Ovviamente la build va eseguita in modo che sia destinata alla produzione (es: ng build --prod).

Quindi occorrerà eseguire un "multi-stage-deployment" come descritto precedentemente (build + copia di quanto buildato all'interno del server + esposizione della porta per EB).

Nel caso ci sia un sistema di proxy che gestisce il routing e lo smista tra applicazione client e applicazione server, è necessario fare le dovute configurazioni per distribuire correttamente le chiamate da un'unica porta esterna alle due porte interne dei servizi (come deve anche essere fatta ovviamente in fase di sviluppo). Nel caso delle applicazioni server è possibile che il Dockerfile sia uguale al Dockerfile.dev, ossia sia sufficiente eseguire le copie necessarie, la configurazione a dovere del web container e l'avvio del wc con l'applicativo.

Quindi si crea ed esegue il push sulla repository su Github, si esegue la pull request e quindi il merge che in Trevis CI andrà a scatenare la build di produzione.

Configurazione Travis CI per eseguire il deploy su Docker Hub

Per fare funzionare Trevis CI, come visto prima, occorre scrivere il file ".travis.yml". Occorre inoltre buildare la versione test del progetto. Dopodiché Travis CI eseguirà la pull su Docker Hub e cominicherà ad EB di eseguire la pull dell'immagine aggiornata. Ai comandi di build ed esecuzione dei test (services, before_install, script, after_success…) va aggiunta la configurazione del deploy su Docker Hub. Ciò consiste in:

  • configurare le environment variables in Travis CI inserendo le proprie credenziali. Nella repository del progetto -> More options -> Settings, nella sezione delle variabili d'ambiente inserire:
    - DOCKER_ID = <proprio docker id>
    - DOCKER_PASSWORD = <propria docker password>

Inserire quindi nell'"after_success" del “.travis.yml”, dopo le varie build:

- echo "$DOCKER_PASSSWORD" | docker login -u "$DOCKER_ID" --password-stdin

Questo consentirà a Trevis CI di eseguire l'accesso a Docker Hub. Quindi aggiungere:

- docker push <nome immagine>

per ogni service/container/immagine da pushare appunto su Docker.

Per avere più di una repository privata su Docker Hub occorre pagare.

Configurazione del Dockerrun.aws.json

Nella directory del progetto aggiungere il file "Dockerrun.aws.json" il quale andrà a dare le informazioni necessarie ad AWS EB. Avendo già a disposizione le immagini, in questo file va indicato dove andare a pullare le immagini da GitHub.

Per capire come eseguire i container EB fa riferimento ad Amazon Elastic Container Service (ECS) dove vanno definiti delle "task definition".

Documentazione per la scrittura delle task definition (paragrafo Container Definitions).

La struttura del file è la seguente:

{
    "AWSEBDockerrunVersion": 1,
    "containerDefinitions": [
        {
            "name": "<nome che si intende dare>",
            "image": "<nome dell'immagine su Docker Hub",
            "hostname": "<nome del servizio usato nel file docker-compose.yml e riconosciuto dagli altri servizi. E' opzionale, inserire solo se altri servizi hanno accesso al container>"
            "essential": <false o true, se imposto "true" nel caso in cui il container crashasse vengono anche gli altri vanno in shut down. Almeno un container deve essere segnato come essential. Normalmente si imposta "true" il container contenente la configurazione del web container>
            "portMappings": [
                {
                    "hostPort": 80,
                    "containerPort": 80
                    <necessario solo per chi deve esporre le porte pubblicamente, ovvero il web container>
                }
            ],
            "links": ["<nome container>", "<nome container>"] <sono necessari al webcontainer per accedere agli altri container quando deve fare per esempio il rederiect delle chiamate al server o al client. Il nome è quello usato come "name" della container definition>,
        }
        ... <container definition degli altri containers> ...
    ]
}

Configurazione AWS EB e AWS ECS

In AWS accedo al servizio Elastic Beanstalk e creo la nuova applicazione configurando l'environment come "Web Server" e utilizzando "Multi-container Docker" come preconfigured platform.

Ora, in produzione l'architettura dei conteiner multipli cambia rispetto al locale. I data services (in meomry data cache, database...) non verranno gestiti all'interno dell'istanza di Elastic Beanstalk ma su servizi separati di AWS (Elastic Cache per Redis e memory data stores, AWS Relational Database Service per i database relazionali).

AWS Elastic Cache:

  • crea e mantiene le istanze di Redis (o altri servizi di in memory data cache) automaticamente
  • molto semplice da scalare
  • sistema integrato di log e manutenzione
  • garanzia sulla sicurezza dei dati fornita dalle conoscenze degli esperti di AWS
  • più facile da migrare rispetto a quanto lo sarebbe in EB

AWS Relational Database Service:

  • crea e mantiene le istanze di Postgres (o il dbms che sto usando) automaticamente
  • molto semplice da scalare
  • sisteama integrato di log e manutenzione
  • garanzia sulla sicurezza dei dati fornita dalle conoscenze degli esperti di AWS
  • backup e rollback automatizzati
  • più facile da migrare rispetto a quanto lo sarebbe in EB

Ovviamente questi servizi hanno diversi livelli di costi, sull'ordine del dollaro al mese, ma fanno risparmiare sicuramente molto tempo. Di default RDS ed EC non comunicano automaticamente con le istanze EB. Occorre quindi configurare una serie di "link".

Virtual Private Cloud (VPC)

Il VPC è un ambiente cloud raggiungibile solo dai servizi del mio account configurato sulla region sulla quale le mie istanze sono ospitate. Di default sono a disposizione un VPC per ogni regione.

Raggiungo la VPC Dashboard su AWS dove troverò già presente il VPC di default della regione su cui sono impostato.

Sfrutteremo questo servizio per generare un Security Group ossia delle "firewall rules".

Configurando Elastic Beanstalk è stato generato un Security Group che consente il traffico sulla porta 80 da qualsiasi indirizzo IP. Queste configurazioni sono visibili nel tab "Security Groups" del servizio VPC. Cliccando sul SG posso vedere e modificare le tiplogie di configurazioni (in entrata e uscita, dove 0.0.0.0:80 significa tutti gli indirizzi IP worldwide).

Conoscendo ciò è possibile configurare un nuovo Security Group che consenta l'accesso di traffico da ogni altro AWS al servizio che possiede questo Security Group. Questo mette in comunicazione i miei servizi. Occorre ovviamente eseguire la configurazione nella stessa region dei servizi che si vogliono mettere in comunicazione.

AWS Relational Database Service (RDS)

Raggiungo la dashboard del servizio RDS e creo un nuovo database scegliendo il DBMS (Amazon Aurora, MySQL, PostgreSQL, Oracle, MSQL Server ecc...) e spunto "only enable options eligible for RDS Free Usage Tier" se non intendo pagare.

Completo la configurazione del db (allocated storage, db instance class) indicando il nome e lo username e la password. Questi ultimi due devono coincidere con quanto configurato nelle variabili d'ambiente del docker-compose.

Proseguendo nelle configurazioni avanzate posso indicare il VPC e spuntare "no" nella scelta "pubblic accessibility". Vado quindi a selezionare anche il VPC Security Group di appartenenza.

Quindi indico il nome del database, la porta e imposto la ricorrenza dei backup.

AWS Elastic Cache (EC)

Raggiungo la dashboard del servizio EC e creo un "Amazon ElastiCache cluster" selezionando il cluster engine tra Redis (spuntando se desidero o meno il cluster) e altri engine.

  • inserisco nome e il node type (attenzione al plan che si seleziona qui per i costi della memoria) e le "replicas" (numero di node types), più sono più saranno alte le performance;
  • configuro del Subnet Group: indicare il VPC ID, il nome e le subnet a cui può avere accesso;
  • configuro il VPC Security Group e completo la creazione del cluster.

Configurazione del Security Group

Navigo alla dashboard di VPC, qui navigo alla sezione Security Groups. Creo un nuovo Security Gruop destinato alla comunicazione tra i tre servizi:

inserisco il nome del tag e del gruppo e la descrizione;

seleziono la VPC di default.

Seleziono quindi il nuovo SG e navigo al tab "Inbound Rules" e vado a restringere il traffico degli indirizzi IP e delle porte. Indico nel Port Range le porte dei servizi (es. "5432-6379" per indicare le porte di Postgres e Redis) e come Source indico il Security Group stesso il che indica che possono raggiungere i servizi di questo SG solo coloro che appartengono al SG stesso.

Applicazione del Security Group ai servizi:

  • raggiungo la dashboard di ElasticCache, quindi seleziono il cluster e premo "Modify". Qui vado a modificare il Security Group indicando quello appena applicato. Applico la modifica e attendo che venga completata;
  • raggiungo la dashboard di RDS, quindi nella sezione "instances" seleziono il database che voglio collegare. Nella sezione "Detail" premo "Modify" e nella sezione "Network & Security" seleziono il Security Group da impostare. Si possono programmare le modifiche per farle avvenire ad un certo orario o immediatamente;
  • raggiungo la dashboard di Elastic Beanstalk, nella sezione "Configuration" premo "Modify" nella sezione "Instances" e nel campo EC2 Security Group spunto il Security Group a cui aggiungere l'istanza.

Configuro le variabili d'ambiente (utilizzate nel “docker-compose.yml”) per consentire ai servizi in EB di raggiungere i servizi ospitati negli ambienti EC e RDS. Navigo all'environment dell'applicazione in EB, nel tab Configuration, sezione Environment properties.

Es (le variabili dipendono dalla configurazione nel docker-compose):

- REDIS_HOST: in Elastic Cache, nel Redis creato, copio il campo "Primary Endpoint" ricordandosi di tenere esclusa la porta;
- REDIS_PORT: la porta indicata separatamente;
- PGUSER: lo user indicato nella configurazione del db;
- PGPASSWORD: la password indicato nella configurazione del db;
- PGHOST: in RDS, nella sezione instances, clicco sul db e nella sezione "Connect" copio l'Endpoint;
- PGDATABASE: nome del db;
- PGPORT: la porta.

Ogni container descritto nel file "Dockerrun.aws.json" ha accesso alle variabili d'ambiente qui impostate.

Configurare Travis CI per comunicare ad EB l’aggiornamento delle immagini

Aggiungere la sezione "deploy" al file ".travis.yml". Per farlo ci servono le chiavi dello IAM user. Vado nel service IAM e creo il nuovo utente con nome, spunto "multi-programmatic-access" e aggiungo alle policies tutte quelle relative ad “AWSElasticBeanstalk”. Copio l'Acess Key ID e la Secret access key.

Quindi nella repository di Travis CI vado nelle opzioni e setto le variabili d'ambiente AWS_ACCESS_KEY e AWS_SECRET_KEY, dove già avevo impostato quelle di Docker. Dopodiché in AWS creo il bucket (o ne utilizzo uno già esistente). Quindi aggiorno il file ".travis.yml" aggiungendo:

deploy:
    provider: elastickbeanstalk
    region: <region su cui è collocato l'environment, ottenibile dal sub-domain dell'url dell'applicazione>
    app: <nome app>
    env: <nome environment>
    bucket_name: <nome del bucket>
    bucket_path: <nome del path>
    on:
        branch: master
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY

Nel caso dovesse verificarsi l'errore "Missing <nome bucket> error" aggiugnere:

edge: true
provider: elasticbeanstalk
 ...

alla sezione deploy nel ".travis.yml", cosa che forzerà Travis a usare la versione v2 (sperimentale) del dpl script priva del bug.

Altre informazioni

Allocazione di memoria dei container

Nelle containerDefinitions, nel file Dockerrun.aws.json, posso specificare l'allocazione di memoria dinamica da rendere disponibole:

"memory": 128

EB logs

Navigo nell'applicazione e nella sezione logs dove posso trovare una tabella dei file log che posso consultare.

Conclusioni

Docker ci ha consentito di ridurre tutto il processo di testing, deploy e pubblicazione ad un semplice file .yml, andando a strutturare una deployment pipeline drammaticamente più semplice da controllorare e gestire.

Previous Post Next Post