Salta el contingut

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.