Salta el contingut

Pràctica 3: Case Study Infraestructura Completa

En aquesta secció, veurem un case study complet on muntem una infraestructura des de zero per una startup fictícia anomenada "CloudShop", una plataforma de comerç electrònic.

Context del Projecte

CloudShop és una startup que està llançant la seva plataforma de comerç electrònic. Esperan tenir creixement ràpid i necessiten una infraestructura que pugui escalar. L'equip de desenvolupament és petit (3 desenvolupadors i 1 DevOps engineer) i necessiten automatitzar tant com sigui possible.

Requisits

L'arquitectura necessita: - Múltiples entorns: desenvolupament, staging, i producció - Servidors web redundants per alta disponibilitat - Base de dades amb replicació - Sistema de caching - Monitorització i alerting - Backups automàtics - Desplegaments zero-downtime - Capacitat d'escalar ràpidament afegint més servidors

Decisió: Utilitzar Ansible

L'equip decideix utilitzar Ansible per les següents raons: - L'equip és petit i Ansible és més fàcil d'aprendre - No volen la complexitat de mantenir un Puppet Server - Volen integrar l'automatització directament amb el seu pipeline CI/CD - El nombre de servidors serà relativament petit inicialment (menys de 50)

Estructura del Projecte

L'equip estructura el seu projecte Ansible així:

cloudshop-infrastructure/
├── ansible.cfg
├── inventories/
│   ├── development/
│   │   ├── hosts.ini
│   │   └── group_vars/
│   │       ├── all.yml
│   │       └── webservers.yml
│   ├── staging/
│   │   ├── hosts.ini
│   │   └── group_vars/
│   ├── production/
│   │   ├── hosts.ini
│   │   └── group_vars/
├── playbooks/
│   ├── site.yml
│   ├── webservers.yml
│   ├── databases.yml
│   ├── deploy-app.yml
│   └── backup.yml
├── roles/
│   ├── common/
│   ├── nginx/
│   ├── nodejs/
│   ├── postgresql/
│   ├── redis/
│   ├── monitoring/
│   └── backup/
├── group_vars/
│   └── all/
│       ├── vault.yml (encriptat)
│       └── common.yml
└── README.md

Implementació Pas a Pas

Pas 1: Configuració Inicial

L'equip crea el fitxer ansible.cfg amb configuració bàsica:

[defaults]
inventory = inventories/production/hosts.ini
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
roles_path = roles
callbacks_enabled = timer, profile_tasks

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

Pas 2: Definir l'Inventari

Per a producció (inventories/production/hosts.ini):

[loadbalancers]
lb01.cloudshop.com ansible_host=203.0.113.10

[webservers]
web01.cloudshop.com ansible_host=203.0.113.20
web02.cloudshop.com ansible_host=203.0.113.21

[databases]
db01.cloudshop.com ansible_host=203.0.113.30
db02.cloudshop.com ansible_host=203.0.113.31 postgresql_role=replica

[cache]
redis01.cloudshop.com ansible_host=203.0.113.40

[monitoring]
monitor01.cloudshop.com ansible_host=203.0.113.50

[production:children]
loadbalancers
webservers
databases
cache
monitoring

[production:vars]
ansible_user=ubuntu
ansible_become=yes
environment=production

Pas 3: Crear el Role "Common"

Aquest role configura aspectes comuns a tots els servidors. El fitxer roles/common/tasks/main.yml:

---
- name: Configurar hostname
  hostname:
    name: "{{ inventory_hostname }}"

- name: Actualitzar tots els paquets
  apt:
    upgrade: dist
    update_cache: yes
    cache_valid_time: 3600
  when: common_auto_upgrade | default(true)

- name: Instal·lar paquets essencials
  apt:
    name:
      - vim
      - curl
      - wget
      - git
      - htop
      - unzip
      - ntp
      - fail2ban
      - ufw
    state: present

- name: Configurar timezone
  timezone:
    name: "{{ common_timezone | default('Europe/Madrid') }}"

- name: Configurar NTP
  service:
    name: ntp
    state: started
    enabled: yes

- name: Configurar limits del sistema
  pam_limits:
    domain: "*"
    limit_type: "{{ item.type }}"
    limit_item: "{{ item.item }}"
    value: "{{ item.value }}"
  loop:
    - { type: 'soft', item: 'nofile', value: '65536' }
    - { type: 'hard', item: 'nofile', value: '65536' }
    - { type: 'soft', item: 'nproc', value: '32768' }
    - { type: 'hard', item: 'nproc', value: '32768' }

- name: Configurar firewall per SSH
  ufw:
    rule: allow
    port: '22'
    proto: tcp

- name: Activar firewall
  ufw:
    state: enabled
    policy: deny

- name: Configurar fail2ban
  template:
    src: jail.local.j2
    dest: /etc/fail2ban/jail.local
    owner: root
    group: root
    mode: '0644'
  notify: Restart fail2ban

- name: Assegurar que fail2ban està executant-se
  service:
    name: fail2ban
    state: started
    enabled: yes

- name: Crear usuaris administradors
  user:
    name: "{{ item.name }}"
    groups: sudo
    append: yes
    shell: /bin/bash
  loop: "{{ admin_users }}"

- name: Afegir claus SSH públiques
  authorized_key:
    user: "{{ item.name }}"
    key: "{{ item.ssh_key }}"
    state: present
  loop: "{{ admin_users }}"

- name: Deshabilitar autenticació amb password
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PasswordAuthentication'
    line: 'PasswordAuthentication no'
    state: present
  notify: Restart sshd

Pas 4: Implementar Desplegament Zero-Downtime

L'equip crea un playbook especial per desplegar l'aplicació sense temps d'inactivitat (playbooks/deploy-app.yml):

---
- name: Desplegar aplicació amb zero-downtime
  hosts: webservers
  become: yes
  serial: 1  # Desplegar un servidor cada vegada

  vars:
    app_name: "cloudshop"
    app_repo: "https://github.com/cloudshop/shop-app.git"
    app_version: "{{ deploy_version | default('main') }}"
    health_check_url: "http://localhost:3000/health"

  pre_tasks:
    - name: Verificar que el servidor està healthy abans de desplegar
      uri:
        url: "{{ health_check_url }}"
        status_code: 200
      register: pre_health
      failed_when: false

    - name: Avortar si el servidor ja està unhealthy
      fail:
        msg: "El servidor {{ inventory_hostname }} ja estava unhealthy abans del desplegament"
      when: pre_health.status != 200

    - name: Treure servidor del load balancer
      uri:
        url: "http://{{ hostvars[groups['loadbalancers'][0]]['ansible_host'] }}/admin/disable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
      become: no

    - name: Esperar que les connexions existents acabin
      wait_for:
        timeout: 30

  tasks:
    - name: Crear directori de backup
      file:
        path: "/opt/{{ app_name }}/backups"
        state: directory
        owner: "{{ app_name }}"
        group: "{{ app_name }}"

    - name: Fer backup de la versió actual
      command: >
        cp -r /opt/{{ app_name }}/app /opt/{{ app_name }}/backups/app-{{ ansible_date_time.iso8601_basic_short }}
      args:
        creates: "/opt/{{ app_name }}/backups/app-{{ ansible_date_time.iso8601_basic_short }}"
      when: pre_health.status == 200

    - name: Clonar nova versió de l'aplicació
      git:
        repo: "{{ app_repo }}"
        dest: "/opt/{{ app_name }}/app"
        version: "{{ app_version }}"
        force: yes
      become_user: "{{ app_name }}"
      register: git_result

    - name: Instal·lar dependències si han canviat
      npm:
        path: "/opt/{{ app_name }}/app"
        production: yes
      become_user: "{{ app_name }}"
      when: git_result.changed

    - name: Executar migracions de base de dades
      command: npm run migrate
      args:
        chdir: "/opt/{{ app_name }}/app"
      become_user: "{{ app_name }}"
      when: git_result.changed
      run_once: yes  # Les migracions només cal executar-les una vegada

    - name: Reiniciar aplicació
      service:
        name: "{{ app_name }}"
        state: restarted
      when: git_result.changed

    - name: Esperar que l'aplicació estigui disponible
      uri:
        url: "{{ health_check_url }}"
        status_code: 200
      register: result
      until: result.status == 200
      retries: 30
      delay: 2

    - name: Executar smoke tests
      uri:
        url: "http://localhost:3000/{{ item }}"
        status_code: 200
      loop:
        - "health"
        - "api/products"
        - "api/categories"

  post_tasks:
    - name: Tornar a afegir servidor al load balancer
      uri:
        url: "http://{{ hostvars[groups['loadbalancers'][0]]['ansible_host'] }}/admin/enable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
      become: no

    - name: Esperar que el load balancer reconegui el servidor
      wait_for:
        timeout: 10

    - name: Verificar que el servidor està rebent tràfic
      uri:
        url: "http://{{ hostvars[groups['loadbalancers'][0]]['ansible_host'] }}/health"
        status_code: 200
      delegate_to: localhost
      become: no

  rescue:
    - name: Rollback en cas d'error
      block:
        - name: Restaurar versió anterior
          command: >
            rsync -a --delete /opt/{{ app_name }}/backups/app-*/ /opt/{{ app_name }}/app/
          args:
            removes: "/opt/{{ app_name }}/backups/"

        - name: Reiniciar aplicació amb versió anterior
          service:
            name: "{{ app_name }}"
            state: restarted

        - name: Verificar que el rollback ha funcionat
          uri:
            url: "{{ health_check_url }}"
            status_code: 200
          retries: 10
          delay: 3

        - name: Notificar l'equip del rollback
          slack:
            token: "{{ slack_token }}"
            msg: "⚠️ Desplegament fallit a {{ inventory_hostname }}. S'ha fet rollback automàticament."
            channel: "#deployments"
          delegate_to: localhost

Aquest playbook implementa un desplegament sofisticat que: 1. Treu cada servidor del load balancer abans de desplegar 2. Fa un backup de la versió actual 3. Desplega la nova versió 4. Executa tests de verificació 5. Torna a afegir el servidor al load balancer 6. Si alguna cosa falla, fa rollback automàticament

Pas 5: Integració amb CI/CD

L'equip integra Ansible amb el seu pipeline de GitHub Actions (.github/workflows/deploy.yml):

name: Deploy to Production

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
      with:
        repository: cloudshop/infrastructure
        token: ${{ secrets.INFRA_REPO_TOKEN }}

    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'

    - name: Install Ansible
      run: |
        pip install ansible==9.0.0
        ansible-galaxy collection install community.general

    - name: Configure SSH
      run: |
        mkdir -p ~/.ssh
        echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan -H lb01.cloudshop.com >> ~/.ssh/known_hosts

    - name: Decrypt Ansible Vault
      run: |
        echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > .vault_pass

    - name: Run Ansible Playbook
      run: |
        ansible-playbook -i inventories/production/hosts.ini \
          playbooks/deploy-app.yml \
          --vault-password-file .vault_pass \
          -e "deploy_version=${{ github.ref_name }}"

    - name: Notify Slack on Success
      if: success()
      uses: 8398a7/action-slack@v3
      with:
        status: success
        text: '✅ Desplegament exitós a producció: ${{ github.ref_name }}'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

    - name: Notify Slack on Failure
      if: failure()
      uses: 8398a7/action-slack@v3
      with:
        status: failure
        text: '❌ Desplegament fallit a producció: ${{ github.ref_name }}'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Resultats del Case Study

Després d'implementar aquesta infraestructura automatitzada, CloudShop va aconseguir:

  • Temps de desplegament reduït: De 2 hores manualment a 15 minuts automatitzat
  • Zero-downtime deployments: Els clients no veuen interrupcions durant desplegaments
  • Consistència: Tots els entorns (dev, staging, prod) són idèntics
  • Recuperació ràpida: Rollbacks automàtics si alguna cosa falla
  • Escalabilitat: Poden afegir nous servidors executant un playbook
  • Documentació viva: L'infraestructura és codi, així que està auto-documentada
  • Auditoria: Tots els canvis estan versionats a Git

Aquest case study mostra com una petita startup pot implementar pràctiques DevOps professionals utilitzant Ansible, aconseguint infraestructura fiable i escalable sense necessitar un equip gran.