cloud-init è il componente che permette di eseguire configurazioni automatiche al primo avvio di una macchina virtuale. Su Amazon EC2 viene utilizzato per leggere l’user-data fornito all’istanza e applicare istruzioni che vanno dall’installazione dei pacchetti alla scrittura di file di configurazione. Funziona tramite fasi ordinate, come init, config e final, che consentono un controllo fine del processo di bootstrap. Il formato più diffuso e leggibile è quello cloud-config in YAML, anche se sono supportati script shell e messaggi multi-part MIME.
Prima di usare cloud-init è importante assicurarsi che l’AMI scelta supporti nativamente il servizio, come avviene per Ubuntu, Amazon Linux o RHEL. Occorre inoltre predisporre un Security Group con le porte necessarie, tipicamente 22 per SSH e 80 o 443 per HTTP e HTTPS, e associare un ruolo IAM all’istanza che conceda solo i permessi strettamente indispensabili, ad esempio per leggere segreti o parametri. È buona pratica abilitare IMDSv2 per rendere più sicuro l’accesso ai metadata.
I file cloud-config permettono di definire pacchetti da installare, file da scrivere con owner e permessi specifici, comandi da lanciare al termine del bootstrap, utenti e gruppi da creare, hostname da impostare e volumi da montare. La sezione runcmd
è utile per comandi che devono essere eseguiti una sola volta al termine della configurazione, mentre bootcmd
agisce in una fase molto più precoce, prima della configurazione di rete. Gli script cloud-init possono così sostituire playbook complessi quando serve solo un bootstrap leggero e veloce.
Un esempio comune consiste nel predisporre un reverse proxy NGINX e un servizio Node.js. In un file user-data in formato cloud-config si possono installare pacchetti come nginx, git e nodejs, scrivere un file di configurazione per NGINX che instrada il traffico alla porta 3000 e creare un semplice server Node.js salvato in /opt/app/server.js
. È poi possibile definire un service systemd che assicuri la partenza dell’applicazione ad ogni riavvio. Alla fine si avviano NGINX e il servizio custom con systemctl. In questo modo l’istanza EC2 risponde già a richieste HTTP subito dopo il boot.
#cloud-config
package_update: true
packages:
- nginx
- git
- nodejs
- npm
write_files:
- path: /etc/nginx/sites-available/app.conf
content: |
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
runcmd:
- ln -sf /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/default
- systemctl enable --now nginx
- mkdir -p /opt/app
- bash -lc "echo 'const http = require(\"http\"); const port=3000; http.createServer((req,res)=>{res.end(\"Hello\");}).listen(port);' > /opt/app/server.js"
- bash -lc "cat > /etc/systemd/system/app.service << 'EOF'
[Unit]
Description=Node.js App
After=network.target
[Service]
WorkingDirectory=/opt/app
ExecStart=/usr/bin/node /opt/app/server.js
Restart=always
User=ubuntu
[Install]
WantedBy=multi-user.target
EOF"
- systemctl daemon-reload
- systemctl enable --now app.service
Un approccio alternativo è quello multi-part MIME, che permette di mescolare sezioni cloud-config con script shell. In questo caso si definisce un conf file di NGINX tramite YAML e si aggiunge un blocco shell che abilita il servizio e scrive file di stato. Questo metodo è utile quando si vogliono separare chiaramente parti di configurazione e parti eseguibili.
Content-Type: multipart/mixed; boundary="===============BOUNDARY=="
--===============BOUNDARY==
Content-Type: text/cloud-config
#cloud-config
packages:
- nginx
write_files:
- path: /var/www/html/index.html
content: |
<h1>Benvenuto da cloud-config</h1>
--===============BOUNDARY==
Content-Type: text/x-shellscript
#!/bin/bash
systemctl enable --now nginx
echo "OK" > /tmp/provision.txt
--===============BOUNDARY==--
Cloud-init si adatta bene anche ad altri linguaggi. Ad esempio con Amazon Linux 2023 è possibile installare Python, Flask e Gunicorn, scrivere il codice in /opt/app/app.py
e predisporre un servizio systemd che avvii Gunicorn. NGINX viene configurato come proxy sulla porta 8000. In questo modo il provisioning crea direttamente un endpoint HTTP pronto a servire richieste Flask.
#cloud-config
packages:
- python3
- python3-pip
- nginx
write_files:
- path: /opt/app/app.py
content: |
from flask import Flask
app = Flask(__name__)
@app.get('/')
def hello():
return '<h1>Hello from Flask</h1>'
- path: /etc/systemd/system/gunicorn.service
content: |
[Unit]
Description=Gunicorn Flask App
After=network.target
[Service]
WorkingDirectory=/opt/app
ExecStart=/usr/bin/python3 -m gunicorn -w 2 -b 127.0.0.1:8000 app:app
Restart=always
User=ec2-user
[Install]
WantedBy=multi-user.target
- path: /etc/nginx/conf.d/app.conf
content: |
server {
listen 80;
location / { proxy_pass http://127.0.0.1:8000; }
}
runcmd:
- pip3 install flask gunicorn
- systemctl daemon-reload
- systemctl enable --now gunicorn
- systemctl enable --now nginx
Cloud-init consente anche di inizializzare volumi EBS. Con la sezione fs_setup
è possibile formattare un disco come ext4 e montarlo automaticamente in una directory, mentre con mounts
si definisce la persistenza del mount all’avvio. È utile per separare i dati applicativi dal sistema operativo.
Per quanto riguarda i segreti, non è consigliato inserirli direttamente nello user-data perché comparirebbero in chiaro nei log. È preferibile recuperarli a runtime da AWS Systems Manager Parameter Store o Secrets Manager, facendo leva su un ruolo IAM dedicato. In questo modo si possono esportare variabili d’ambiente da iniettare nei service systemd senza compromettere la sicurezza.
L’avvio dell’istanza con AWS CLI richiede di passare l’user-data tramite file. È sufficiente usare --user-data file://userdata.yml
per i file testuali o fileb://
se si tratta di contenuti binari. Terraform permette di ottenere lo stesso risultato grazie all’attributo user_data
nelle risorse aws_instance
, mantenendo il codice infrastrutturale sotto controllo versione.
Nelle pratiche di produzione è opportuno limitare lo user-data a operazioni leggere, lasciando che la creazione di AMI dorate con strumenti come Packer gestisca le installazioni più pesanti. È fondamentale rendere le configurazioni idempotenti, centralizzare i log verso CloudWatch Logs, applicare il principio del privilegio minimo ai ruoli IAM e gestire i rollout tramite Auto Scaling Group e Load Balancer per evitare downtime. Gli health check del Load Balancer possono testare endpoint dedicati come /healthz
per monitorare lo stato dell’applicazione.
Se qualcosa non funziona, i log principali sono in /var/log/cloud-init.log
e /var/log/cloud-init-output.log
. Con il comando cloud-init status --long
si ottiene lo stato delle fasi, mentre cloud-init clean
permette di azzerare le configurazioni e rieseguire i moduli. Questa funzionalità è utile in fase di debug o test.
Cloud-init può anche essere usato per casi più semplici, come il deploy di una pagina statica. Un file di configurazione minimo installa NGINX, scrive un file HTML in /var/www/html/index.html
e abilita il servizio. Questo dimostra come la stessa tecnologia possa gestire sia scenari complessi con proxy, microservizi e volumi, sia bootstrap essenziali per applicazioni statiche.
#cloud-config
packages: [ nginx ]
write_files:
- path: /var/www/html/index.html
content: |
<h1>Deployed via cloud-init</h1>
runcmd:
- systemctl enable --now nginx
In conclusione, usare cloud-init con user-data su Amazon EC2 permette di automatizzare in modo robusto la creazione di applicazioni web. Con file YAML ben strutturati si possono installare pacchetti, predisporre servizi e integrare fonti esterne di configurazione, garantendo ambienti coerenti e ripetibili tra sviluppo, staging e produzione.