Zero downtime Jenkins Continuous Deployment con Terraform su AWS
Pubblicato: 2022-03-11Nel mondo odierno di Internet, dove letteralmente tutto deve essere attivo 24 ore su 24, 7 giorni su 7, l'affidabilità è fondamentale. Ciò si traduce in tempi di inattività quasi pari a zero per i tuoi siti Web, evitando la temuta pagina di errore "Non trovato: 404" o altre interruzioni del servizio mentre esegui il lancio della tua nuova versione.
Supponiamo di aver creato una nuova applicazione per il tuo cliente, o forse te stesso, e di essere riuscito a ottenere una buona base di utenti a cui piace la tua applicazione. Hai raccolto feedback dai tuoi utenti e vai dai tuoi sviluppatori e chiedi loro di creare nuove funzionalità e rendere l'applicazione pronta per la distribuzione. Con questo pronto, puoi interrompere l'intera applicazione e distribuire la nuova versione o creare una pipeline di distribuzione CI/CD senza tempi di inattività che farebbe tutto il noioso lavoro di spingere una nuova versione agli utenti senza intervento manuale.
In questo articolo parleremo esattamente di quest'ultimo, di come possiamo avere una pipeline di distribuzione continua di un'applicazione Web a tre livelli costruita in Node.js su AWS Cloud utilizzando Terraform come orchestratore dell'infrastruttura. Useremo Jenkins per la parte di distribuzione continua e Bitbucket per ospitare la nostra base di codice.
Archivio del codice
Utilizzeremo un'applicazione web demo a tre livelli per la quale puoi trovare il codice qui.
Il repository contiene codice sia per il Web che per il livello API. È una semplice applicazione in cui il modulo web chiama uno degli endpoint nel livello API che preleva internamente le informazioni sull'ora corrente dal database e ritorna al livello web.
La struttura del repo è la seguente:
- API: codice per il livello API
- Web: codice per il livello Web
- Terraform: codice per l'orchestrazione dell'infrastruttura tramite Terraform
- Jenkins: codice per l'agente di orchestrazione dell'infrastruttura per il server Jenkins utilizzato per la pipeline CI/CD.
Ora che abbiamo capito cosa dobbiamo distribuire, discutiamo delle cose che dobbiamo fare per distribuire questa applicazione su AWS e poi parleremo di come realizzare quella parte della pipeline CI/CD.
Immagini di cottura
Dal momento che stiamo usando Terraform per l'orchestratore dell'infrastruttura, ha più senso disporre di immagini preimpostate per ogni livello o applicazione che si desidera distribuire. E per questo, utilizzeremmo un altro prodotto di Hashicorp, ovvero Packer.
Packer è uno strumento open source che aiuta a creare un'AMI o un'immagine macchina Amazon, che verrà utilizzata per la distribuzione su AWS. Può essere utilizzato per creare immagini per diverse piattaforme come EC2, VirtualBox, VMware e altre.
Ecco un frammento di come viene utilizzato il file di configurazione Packer ( terraform/packer-ami-api.json
) per creare un'AMI per il livello API.
{ "builders": [{ "type": "amazon-ebs", "region": "eu-west-1", "source_ami": "ami-844e0bf7", "instance_type": "t2.micro", "ssh_username": "ubuntu", "ami_name": "api-instance {{timestamp}}" }], "provisioners": [ { "type": "shell", "inline": ["mkdir api", "sudo apt-get update", "sudo apt-get -y install npm nodejs-legacy"], "pause_before": "10s" }, { "type": "file", "source" : "../api/", "destination" : "api" }, { "type": "shell", "inline": ["cd api", "npm install"], "pause_before": "10s" } ] }
E devi eseguire il seguente comando per creare l'AMI:
packer build -machine-readable packer-ami-api.json
Eseguiremo questo comando dalla build Jenkins più avanti in questo articolo. In modo simile, utilizzeremo il file di configurazione di Packer ( terraform/packer-ami-web.json
) anche per il livello web.
Esaminiamo il file di configurazione di Packer sopra e capiamo cosa sta cercando di fare.
- Come accennato in precedenza, Packer può essere utilizzato per creare immagini per molte piattaforme e, poiché stiamo distribuendo la nostra applicazione su AWS, utilizzeremo il builder "amazon-ebs", poiché è il builder più semplice con cui iniziare.
- La seconda parte della configurazione richiede un elenco di provider che sono più simili a script o blocchi di codice che puoi utilizzare per configurare la tua immagine.
- Il passaggio 1 esegue un provisioner della shell per creare una cartella API e installare Node.js sull'immagine utilizzando la proprietà
inline
, che è un insieme di comandi che si desidera eseguire. - Il passaggio 2 esegue un provisioner di file per copiare il nostro codice sorgente dalla cartella API nell'istanza.
- Il passaggio 3 esegue nuovamente un provisioner della shell, ma questa volta utilizza una proprietà di script per specificare un file (terraform/scripts/install_api_software.sh) con i comandi che devono essere eseguiti.
- Il passaggio 4 copia un file di configurazione nell'istanza necessaria per Cloudwatch, che viene installata nel passaggio successivo.
- Il passaggio 5 esegue un provisioner della shell per installare l'agente AWS Cloudwatch. L'input per questo comando sarebbe il file di configurazione copiato nel passaggio precedente. Parleremo di Cloudwatch in dettaglio più avanti nell'articolo.
- Il passaggio 1 esegue un provisioner della shell per creare una cartella API e installare Node.js sull'immagine utilizzando la proprietà
Quindi, in sostanza, la configurazione di Packer contiene informazioni su quale builder desideri e quindi un set di provisioner che puoi definire in qualsiasi ordine a seconda di come desideri configurare la tua immagine.
Configurazione di una distribuzione continua Jenkins
Successivamente, esamineremo la configurazione di un server Jenkins che verrà utilizzato per la nostra pipeline CI/CD. Utilizzeremo Terraform e AWS anche per la configurazione.
Il codice Terraform per impostare Jenkins si trova all'interno della cartella jenkins/setup
. Esaminiamo alcune delle cose interessanti di questa configurazione.
- Credenziali AWS: puoi fornire l'ID della chiave di accesso AWS e la chiave di accesso segreta al provider Terraform AWS (
instance.tf
) oppure puoi fornire la posizione del file delle credenziali alla proprietàshared_credentials_file
nel provider AWS. - Ruolo IAM: poiché eseguiremo Packer e Terraform dal server Jenkins, accederanno ai servizi S3, EC2, RDS, IAM, bilanciamento del carico e scalabilità automatica su AWS. Quindi o forniamo le nostre credenziali su Jenkins affinché Packer & Terraform acceda a questi servizi oppure possiamo creare un profilo IAM (
iam.tf
), utilizzando il quale creeremmo un'istanza Jenkins. - Stato Terraform: Terraform deve mantenere lo stato dell'infrastruttura da qualche parte in un file e, con S3 (
backend.tf
), potresti semplicemente mantenerlo lì, così puoi collaborare con altri colleghi e chiunque può cambiare e distribuire dallo stato è mantenuto in una posizione remota. - Coppia di chiavi pubblica/privata: dovrai caricare la chiave pubblica della tua coppia di chiavi insieme all'istanza in modo da poter ssh nell'istanza Jenkins una volta che è attiva. Abbiamo definito una risorsa
aws_key_pair
(key.tf
) in cui specifichi la posizione della tua chiave pubblica usando le variabili Terraform.
Passaggi per la configurazione di Jenkins:
Passaggio 1: per mantenere lo stato remoto di Terraform, è necessario creare manualmente un bucket in S3 che può essere utilizzato da Terraform. Questo sarebbe l'unico passaggio fatto al di fuori di Terraform. Assicurati di eseguire AWS configure
prima di eseguire il comando seguente per specificare le tue credenziali AWS.
aws s3api create-bucket --bucket node-aws-jenkins-terraform --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1
Passaggio 2: esegui terraform init
. Questo inizializzerà lo stato e lo configurerà per essere archiviato su S3 e scaricherà il plug-in del provider AWS.
Passaggio 3: eseguire terraform apply
. Questo controllerà tutto il codice Terraform e creerà un piano e mostrerà quante risorse verranno create al termine di questo passaggio.
Passaggio 4: digita yes
, quindi il passaggio precedente inizierà a creare tutte le risorse. Al termine del comando, otterrai l'indirizzo IP pubblico del server Jenkins.
Passaggio 5: Ssh nel server Jenkins, utilizzando la tua chiave privata. ubuntu
è il nome utente predefinito per le istanze supportate da AWS EBS. Utilizzare l'indirizzo IP restituito dal comando terraform apply
.
ssh -i mykey [email protected]
Passaggio 6: avvia l'interfaccia utente Web Jenkins andando su http://34.245.4.73:8080
. La password può essere trovata in /var/lib/jenkins/secrets/initialAdminPassword
.
Passaggio 7: scegli "Installa plug-in suggeriti" e crea un utente amministratore per Jenkins.
Impostazione della pipeline CI tra Jenkins e Bitbucket
- Per questo, abbiamo bisogno di installare il plugin Bitbucket in Jenkins. Vai a Gestisci Jenkins → Gestisci plug -in e da Plugin disponibili installa il plug-in Bitbucket.
- Sul lato repository Bitbucket, vai su Impostazioni → Webhook , aggiungi un nuovo webhook. Questo hook invierà tutte le modifiche nel repository a Jenkins e ciò attiverà le pipeline.
Jenkins Pipeline per cuocere/creare immagini
- Il prossimo passo sarà creare pipeline in Jenkins.
- La prima pipeline sarà un progetto Freestyle che verrà utilizzato per creare l'AMI dell'applicazione utilizzando Packer.
- Devi specificare le credenziali e l'URL per il tuo repository Bitbucket.
- Specificare il trigger di compilazione.
- Aggiungi due passaggi di compilazione, uno per creare l'AMI per il modulo app e altri per creare l'AMI per il modulo Web.
- Una volta fatto, puoi salvare il progetto Jenkins e ora, quando esegui il push di qualsiasi cosa nel tuo repository Bitbucket, attiverà una nuova build in Jenkins che creerebbe l'AMI e spingerebbe un file Terraform contenente il numero AMI di quell'immagine al Secchio S3 che puoi vedere dalle ultime due righe nella fase di costruzione.
echo 'variable "WEB_INSTANCE_AMI" { default = "'${AMI_ID_WEB}'" }' > amivar_web.tf aws s3 cp amivar_web.tf s3://node-aws-jenkins-terraform/amivar_web.tf
Jenkins Pipeline per attivare lo script Terraform
Ora che abbiamo le AMI per l'API e i moduli Web, attiveremo una build per eseguire il codice Terraform per configurare l'intera applicazione e in seguito esamineremo i componenti nel codice Terraform, il che rende questa pipeline la distribuzione delle modifiche senza tempi di inattività del servizio.

- Creiamo un altro progetto Jenkins freestyle,
nodejs-terraform
, che eseguirà il codice Terraform per distribuire l'applicazione. - Per prima cosa creeremo una credenziale di tipo "testo segreto" nel dominio delle credenziali globali, che verrà utilizzata come input per lo script Terraform. Dal momento che non vogliamo codificare la password per il servizio RDS all'interno di Terraform e Git, passiamo quella proprietà usando le credenziali Jenkins.
- È necessario definire le credenziali e l'URL in modo simile all'altro progetto.
- Nella sezione trigger di build, collegheremo questo progetto con l'altro in modo che questo progetto inizi quando il precedente è terminato.
- Quindi potremmo configurare le credenziali che abbiamo aggiunto in precedenza al progetto utilizzando i collegamenti, quindi è disponibile nella fase di compilazione.
- Ora siamo pronti per aggiungere una fase di compilazione, che scaricherà i file di script Terraform (
amivar_api.tf
eamivar_web.tf
) che sono stati caricati su S3 dal progetto precedente e quindi eseguirà il codice Terraform per creare l'intera applicazione su AWS.
Se tutto è configurato correttamente, ora se esegui il push di qualsiasi codice nel tuo repository Bitbucket, dovrebbe attivare il primo progetto Jenkins seguito dal secondo e dovresti avere la tua applicazione distribuita su AWS.
Configurazione di Terraform Zero tempi di inattività per AWS
Ora discutiamo di cosa c'è nel codice Terraform che fa sì che questa pipeline distribuisca il codice senza tempi di inattività.
La prima cosa è che Terraform fornisce questi blocchi di configurazione del ciclo di vita per le risorse all'interno delle quali hai un'opzione create_before_destroy
come flag che significa letteralmente che Terraform dovrebbe creare una nuova risorsa dello stesso tipo prima di distruggere la risorsa corrente.
Ora sfruttiamo questa funzionalità nelle risorse aws_autoscaling_group
e aws_launch_configuration
. Quindi aws_launch_configuration
configura il tipo di istanza EC2 di cui eseguire il provisioning e il modo in cui installiamo il software su tale istanza e la risorsa aws_autoscaling_group
fornisce un gruppo di scalabilità automatica AWS.
Un problema interessante qui è che tutte le risorse in Terraform dovrebbero avere un nome univoco e una combinazione di tipi. Quindi, a meno che tu non abbia un nome diverso per il nuovo aws_autoscaling_group
e aws_launch_configuration
, non sarà possibile distruggere quello corrente.
Terraform gestisce questo vincolo fornendo una proprietà name_prefix
alla risorsa aws_launch_configuration
. Una volta definita questa proprietà, Terraform aggiungerà un suffisso univoco a tutte le risorse aws_launch_configuration
e quindi potrai utilizzare quel nome univoco per creare una risorsa aws_autoscaling_group
.
Puoi controllare il codice per tutto quanto sopra in terraform/autoscaling-api.tf
resource "aws_launch_configuration" "api-launchconfig" { name_prefix = "api-launchconfig-" image_ instance_type = "t2.micro" security_groups = ["${aws_security_group.api-instance.id}"] user_data = "${data.template_file.api-shell-script.rendered}" iam_instance_profile = "${aws_iam_instance_profile.CloudWatchAgentServerRole-instanceprofile.name}" connection { user = "${var.INSTANCE_USERNAME}" private_key = "${file("${var.PATH_TO_PRIVATE_KEY}")}" } lifecycle { create_before_destroy = true } } resource "aws_autoscaling_group" "api-autoscaling" { name = "${aws_launch_configuration.api-launchconfig.name}-asg" vpc_zone_identifier = ["${aws_subnet.main-public-1.id}"] launch_configuration = "${aws_launch_configuration.api-launchconfig.name}" min_size = 2 max_size = 2 health_check_grace_period = 300 health_check_type = "ELB" load_balancers = ["${aws_elb.api-elb.name}"] force_delete = true lifecycle { create_before_destroy = true } tag { key = "Name" value = "api ec2 instance" propagate_at_launch = true } }
E la seconda sfida con implementazioni a zero tempi di inattività è assicurarsi che la tua nuova distribuzione sia pronta per iniziare a ricevere la richiesta. La semplice distribuzione e avvio di una nuova istanza EC2 non è sufficiente in alcune situazioni.
Per risolvere questo problema, aws_launch_configuration
ha una proprietà user_data
che supporta la proprietà nativa di user_data
di AWS autoscaling utilizzando la quale puoi passare qualsiasi script che desideri eseguire all'avvio di nuove istanze come parte del gruppo di scalabilità automatica. Nel nostro esempio, seguiamo il registro del server dell'app e attendiamo che sia presente il messaggio di avvio. Puoi anche controllare il server HTTP e vedere quando sono attivi.
until tail /var/log/syslog | grep 'node ./bin/www' > /dev/null; do sleep 5; done
Insieme a ciò, puoi anche abilitare un controllo ELB a livello di risorsa aws_autoscaling_group
, che assicurerà che la nuova istanza sia stata aggiunta per superare il controllo ELB prima che Terraform distrugga le vecchie istanze. Ecco come appare il controllo ELB per il livello API; verifica che l'endpoint /api/status
restituisca il successo.
resource "aws_elb" "api-elb" { name = "api-elb" subnets = ["${aws_subnet.main-public-1.id}"] security_groups = ["${aws_security_group.elb-securitygroup.id}"] listener { instance_port = "${var.API_PORT}" instance_protocol = "http" lb_port = 80 lb_protocol = "http" } health_check { healthy_threshold = 2 unhealthy_threshold = 2 timeout = 3 target = "HTTP:${var.API_PORT}/api/status" interval = 30 } cross_zone_load_balancing = true connection_draining = true connection_draining_timeout = 400 tags { Name = "my-elb" } }
Riepilogo e passaggi successivi
Quindi questo ci porta alla fine di questo articolo; si spera che a questo punto la tua applicazione sia già distribuita e in esecuzione con una pipeline CI/CD senza tempi di inattività utilizzando una distribuzione Jenkins e le migliori pratiche Terraform o sei leggermente più a tuo agio nell'esplorare questo territorio e rendere le tue distribuzioni che richiedono un intervento manuale minimo come possibile.
In questo articolo, la strategia di distribuzione utilizzata è chiamata distribuzione blu-verde in cui abbiamo un'installazione corrente (blu) che riceve traffico in tempo reale mentre stiamo distribuendo e testando la nuova versione (verde) e quindi le sostituiamo una volta che la nuova versione è tutto pronto. Oltre a questa strategia, ci sono altri modi per distribuire l'applicazione, che è spiegato bene in questo articolo, Introduzione alle strategie di distribuzione. Adattare un'altra strategia ora è semplice come configurare la pipeline Jenkins.
Inoltre, in questo articolo, ho presupposto che tutte le nuove modifiche nell'API, nel Web e nei livelli di dati siano compatibili, quindi non devi preoccuparti che la nuova versione parli con una versione precedente. Ma, in realtà, potrebbe non essere sempre così. Per risolvere questo problema, mentre progetti la tua nuova versione/funzionalità, pensa sempre al livello di compatibilità con le versioni precedenti, altrimenti dovrai modificare le tue distribuzioni per gestire anche quella situazione.
Anche il test di integrazione è qualcosa che manca in questa pipeline di distribuzione. Poiché non vuoi che nulla venga rilasciato all'utente finale senza essere testato, è sicuramente qualcosa da tenere a mente quando arriva il momento di applicare queste strategie ai tuoi progetti.
Se sei interessato a saperne di più su come funziona Terraform e su come puoi distribuire su AWS utilizzando la tecnologia, ti consiglio Terraform AWS Cloud: Sane Infrastructure Management dove il collega Toptaler Radoslaw Szalski spiega Terraform e poi ti mostra i passaggi necessari per configurare un multi -impostazione Terraform pronta per l'ambiente e la produzione per una squadra