Pràctica 2: Exemples amb Ansible i Puppet
En aquesta secció, veurem exemples pràctics més avançats que mostren la potència real d'aquestes eines en escenaris del món real.
Exemple Complet amb Ansible: Desplegar una Aplicació Multi-Tier
Imaginem que hem de desplegar una aplicació web completa amb múltiples components: un load balancer (nginx), servidors d'aplicació (Node.js), una base de dades (PostgreSQL), i un servidor de cache (Redis). Vegem com ho faríem amb Ansible.
Primer, definim el nostre inventari (inventory/production.ini):
[loadbalancers]
lb01.example.com
[appservers]
app01.example.com
app02.example.com
app03.example.com
[databases]
db01.example.com
[cache]
redis01.example.com
[production:children]
loadbalancers
appservers
databases
cache
També crearem un fitxer de variables per l'entorn de producció (inventory/group_vars/production.yml):
---
# Variables globals per producció
app_name: "myapp"
app_version: "1.5.2"
app_domain: "www.example.com"
db_name: "myapp_production"
db_user: "myapp_user"
db_password: "{{ vault_db_password }}" # Emmagatzemat encriptat amb Ansible Vault
redis_password: "{{ vault_redis_password }}"
# Configuració de l'aplicació
app_port: 3000
app_workers: 4
node_version: "18.x"
# Configuració de base de dades
postgres_version: "15"
postgres_max_connections: 100
postgres_shared_buffers: "256MB"
# Configuració de cache
redis_maxmemory: "1gb"
redis_maxmemory_policy: "allkeys-lru"
Ara crearem el playbook principal (site.yml):
---
- name: Configuració completa de la infraestructura de producció
hosts: production
become: yes
pre_tasks:
- name: Actualitzar cache de paquets
apt:
update_cache: yes
cache_valid_time: 3600
- name: Instal·lar eines bàsiques
apt:
name:
- curl
- vim
- git
- htop
- ufw
state: present
# Configurar base de dades
- name: Configurar servidor de base de dades
hosts: databases
become: yes
roles:
- role: postgres
post_tasks:
- name: Crear base de dades de l'aplicació
postgresql_db:
name: "{{ db_name }}"
encoding: UTF-8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
template: template0
become_user: postgres
- name: Crear usuari de base de dades
postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
db: "{{ db_name }}"
priv: "ALL"
become_user: postgres
- name: Configurar firewall per PostgreSQL
ufw:
rule: allow
port: '5432'
proto: tcp
from_ip: "{{ hostvars[item]['ansible_default_ipv4']['address'] }}"
loop: "{{ groups['appservers'] }}"
# Configurar cache
- name: Configurar servidor Redis
hosts: cache
become: yes
roles:
- role: redis
post_tasks:
- name: Configurar firewall per Redis
ufw:
rule: allow
port: '6379'
proto: tcp
from_ip: "{{ hostvars[item]['ansible_default_ipv4']['address'] }}"
loop: "{{ groups['appservers'] }}"
# Configurar servidors d'aplicació
- name: Configurar servidors d'aplicació
hosts: appservers
become: yes
tasks:
- name: Afegir repositori de Node.js
shell: |
curl -fsSL https://deb.nodesource.com/setup_{{ node_version }} | bash -
args:
creates: /etc/apt/sources.list.d/nodesource.list
- name: Instal·lar Node.js
apt:
name: nodejs
state: present
update_cache: yes
- name: Crear usuari per l'aplicació
user:
name: "{{ app_name }}"
system: yes
shell: /bin/false
home: "/opt/{{ app_name }}"
createhome: yes
- name: Clonar repositori de l'aplicació
git:
repo: "https://github.com/company/{{ app_name }}.git"
dest: "/opt/{{ app_name }}/app"
version: "v{{ app_version }}"
force: yes
become_user: "{{ app_name }}"
notify: Restart application
- name: Instal·lar dependències de Node.js
npm:
path: "/opt/{{ app_name }}/app"
production: yes
become_user: "{{ app_name }}"
notify: Restart application
- name: Crear fitxer de configuració de l'aplicació
template:
src: templates/app-config.js.j2
dest: "/opt/{{ app_name }}/app/config/production.js"
owner: "{{ app_name }}"
group: "{{ app_name }}"
mode: '0600'
notify: Restart application
- name: Crear servei systemd per l'aplicació
template:
src: templates/app.service.j2
dest: "/etc/systemd/system/{{ app_name }}.service"
owner: root
group: root
mode: '0644'
notify:
- Reload systemd
- Restart application
- name: Assegurar que l'aplicació està executant-se
service:
name: "{{ app_name }}"
state: started
enabled: yes
- name: Configurar firewall per l'aplicació
ufw:
rule: allow
port: "{{ app_port }}"
proto: tcp
from_ip: "{{ hostvars[groups['loadbalancers'][0]]['ansible_default_ipv4']['address'] }}"
- name: Verificar health de l'aplicació
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 5
delay: 10
register: result
until: result.status == 200
# Configurar load balancer
- name: Configurar load balancer
hosts: loadbalancers
become: yes
tasks:
- name: Instal·lar nginx
apt:
name: nginx
state: present
- name: Crear configuració d'nginx per l'aplicació
template:
src: templates/nginx-app.conf.j2
dest: "/etc/nginx/sites-available/{{ app_name }}"
owner: root
group: root
mode: '0644'
notify: Reload nginx
- name: Activar site d'nginx
file:
src: "/etc/nginx/sites-available/{{ app_name }}"
dest: "/etc/nginx/sites-enabled/{{ app_name }}"
state: link
notify: Reload nginx
- name: Desactivar site per defecte
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Reload nginx
- name: Configurar firewall per HTTP/HTTPS
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- '80'
- '443'
- name: Assegurar que nginx està executant-se
service:
name: nginx
state: started
enabled: yes
- name: Verificar que el load balancer respon
uri:
url: "http://{{ app_domain }}/health"
status_code: 200
follow_redirects: all
retries: 5
delay: 10
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
- name: Reload systemd
systemd:
daemon_reload: yes
- name: Restart application
service:
name: "{{ app_name }}"
state: restarted
El template de configuració de nginx (templates/nginx-app.conf.j2) utilitzaria balanceig de càrrega entre els servidors d'aplicació:
upstream {{ app_name }}_backend {
{% for host in groups['appservers'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port }} max_fails=3 fail_timeout=30s;
{% endfor %}
}
server {
listen 80;
server_name {{ app_domain }};
# Redirigir a HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ app_domain }};
ssl_certificate /etc/nginx/ssl/{{ app_domain }}.crt;
ssl_certificate_key /etc/nginx/ssl/{{ app_domain }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
access_log /var/log/nginx/{{ app_name }}_access.log;
error_log /var/log/nginx/{{ app_name }}_error.log;
location / {
proxy_pass http://{{ app_name }}_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-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /health {
access_log off;
proxy_pass http://{{ app_name }}_backend;
}
}
Aquest exemple mostra com Ansible pot orquestrar el desplegament d'una aplicació completa multi-tier, assegurant que cada component està correctament configurat i pot comunicar-se amb els altres. El playbook és idempotent, així que pots executar-lo múltiples vegades per actualitzar l'aplicació o corregir desviacions de configuració.
Exemple Complet amb Puppet: Gestió d'una Infraestructura Empresarial
Ara vegem el mateix escenari amb Puppet. Amb Puppet, l'aproximació és diferent perquè utilitzarem mòduls ben estructurats i Hiera per gestionar les dades.
Primer, l'estructura de directoris seria:
/etc/puppetlabs/code/environments/production/
├── manifests/
│ └── site.pp
├── modules/
│ ├── profile/
│ ├── role/
│ └── ... (altres mòduls)
├── hieradata/
│ ├── nodes/
│ ├── role/
│ ├── environment/
│ └── common.yaml
└── hiera.yaml
Utilitzarem el patró "Roles and Profiles", que és una best practice en Puppet. Els "profiles" són unitats tècniques de configuració (com "configurar nginx" o "configurar PostgreSQL"), i els "roles" són col·leccions de profiles que descriuen el propòsit del servidor (com "webserver" o "database server").
El fitxer principal (manifests/site.pp):
# Configuració per defecte per a tots els nodes
node default {
# Aplicar el role segons el fact 'role'
include "role::${::role}"
}
Definim els roles (modules/role/manifests/):
modules/role/manifests/loadbalancer.pp:
class role::loadbalancer {
include profile::base
include profile::nginx::loadbalancer
include profile::monitoring::client
}
modules/role/manifests/appserver.pp:
class role::appserver {
include profile::base
include profile::nodejs
include profile::app::myapp
include profile::monitoring::client
}
modules/role/manifests/database.pp:
class role::database {
include profile::base
include profile::postgresql::server
include profile::backup::postgres
include profile::monitoring::client
}
modules/role/manifests/cache.pp:
class role::cache {
include profile::base
include profile::redis::server
include profile::monitoring::client
}
Ara definim els profiles. Per exemple, el profile de l'aplicació (modules/profile/manifests/app/myapp.pp):
class profile::app::myapp {
# Obtenir configuració des de Hiera
$app_name = lookup('app::name')
$app_version = lookup('app::version')
$app_port = lookup('app::port')
$app_workers = lookup('app::workers')
$db_host = lookup('database::host')
$db_name = lookup('database::name')
$db_user = lookup('database::user')
$db_password = lookup('database::password')
$redis_host = lookup('redis::host')
$redis_password = lookup('redis::password')
# Crear usuari del sistema per l'aplicació
user { $app_name:
ensure => present,
system => true,
shell => '/bin/false',
home => "/opt/${app_name}",
managehome => true,
}
# Clonar el repositori de l'aplicació
vcsrepo { "/opt/${app_name}/app":
ensure => present,
provider => git,
source => "https://github.com/company/${app_name}.git",
revision => "v${app_version}",
user => $app_name,
require => User[$app_name],
}
# Instal·lar dependències de Node.js
exec { 'npm-install':
command => '/usr/bin/npm install --production',
cwd => "/opt/${app_name}/app",
user => $app_name,
refreshonly => true,
subscribe => Vcsrepo["/opt/${app_name}/app"],
notify => Service[$app_name],
}
# Crear fitxer de configuració
file { "/opt/${app_name}/app/config/production.js":
ensure => file,
content => epp('profile/app/config.js.epp', {
'db_host' => $db_host,
'db_name' => $db_name,
'db_user' => $db_user,
'db_password' => $db_password,
'redis_host' => $redis_host,
'redis_password' => $redis_password,
'app_port' => $app_port,
'app_workers' => $app_workers,
}),
owner => $app_name,
group => $app_name,
mode => '0600',
require => Vcsrepo["/opt/${app_name}/app"],
notify => Service[$app_name],
}
# Crear servei systemd
systemd::unit_file { "${app_name}.service":
content => epp('profile/app/systemd.service.epp', {
'app_name' => $app_name,
'app_port' => $app_port,
}),
notify => Service[$app_name],
}
# Assegurar que el servei està executant-se
service { $app_name:
ensure => running,
enable => true,
require => [
Systemd::Unit_file["${app_name}.service"],
Exec['npm-install'],
],
}
# Configurar firewall
firewall { "100 allow ${app_name} from loadbalancer":
dport => $app_port,
proto => 'tcp',
source => lookup('loadbalancer::ip'),
action => 'accept',
}
}
El profile del load balancer (modules/profile/manifests/nginx/loadbalancer.pp):
class profile::nginx::loadbalancer {
$app_name = lookup('app::name')
$app_domain = lookup('app::domain')
$app_port = lookup('app::port')
$app_servers = lookup('appservers') # Array d'IPs/hostnames
# Instal·lar nginx
class { 'nginx':
manage_repo => true,
}
# Definir upstream amb els servidors d'aplicació
nginx::resource::upstream { "${app_name}_backend":
members => $app_servers.map |$server| {
"${server}:${app_port} max_fails=3 fail_timeout=30s"
},
}
# Configurar virtual host HTTPS
nginx::resource::server { $app_domain:
ensure => present,
listen_port => 443,
ssl => true,
ssl_cert => "/etc/nginx/ssl/${app_domain}.crt",
ssl_key => "/etc/nginx/ssl/${app_domain}.key",
ssl_protocols => 'TLSv1.2 TLSv1.3',
proxy => "http://${app_name}_backend",
proxy_set_header => [
'Host $host',
'X-Real-IP $remote_addr',
'X-Forwarded-For $proxy_add_x_forwarded_for',
'X-Forwarded-Proto $scheme',
],
proxy_connect_timeout => '60s',
proxy_send_timeout => '60s',
proxy_read_timeout => '60s',
access_log => "/var/log/nginx/${app_name}_access.log",
error_log => "/var/log/nginx/${app_name}_error.log",
}
# Redireccionar HTTP a HTTPS
nginx::resource::server { "${app_domain}-redirect":
ensure => present,
listen_port => 80,
server_name => [$app_domain],
ssl_redirect => true,
ssl_redirect_port => 443,
}
# Configurar firewall
firewall { '100 allow http and https':
dport => [80, 443],
proto => 'tcp',
action => 'accept',
}
}
Finalment, definim les dades a Hiera. El fitxer hieradata/environment/production.yaml:
---
# Configuració de l'aplicació
app::name: 'myapp'
app::version: '1.5.2'
app::domain: 'www.example.com'
app::port: 3000
app::workers: 4
# Servidors d'aplicació
appservers:
- 'app01.example.com'
- 'app02.example.com'
- 'app03.example.com'
loadbalancer::ip: '10.0.1.10'
# Configuració de base de dades
database::host: 'db01.example.com'
database::name: 'myapp_production'
database::user: 'myapp_user'
database::password: >
ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEw...]
# Configuració de Redis
redis::host: 'redis01.example.com'
redis::password: >
ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEw...]
redis::maxmemory: '1gb'
redis::maxmemory_policy: 'allkeys-lru'
# Configuració de PostgreSQL
postgresql::version: '15'
postgresql::max_connections: 100
postgresql::shared_buffers: '256MB'
# Node.js
nodejs::version: '18.x'
Aquest exemple mostra com Puppet permet estructurar configuracions complexes de manera molt organitzada. Les dades estan completament separades del codi, els profiles són reutilitzables, i els roles descriuen clarament el propòsit de cada servidor. A més, com Puppet utilitza un model pull, aquests servidors es mantindran contínuament en l'estat desitjat, detectant i corregint qualsevol desviació automàticament.