DevOps

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa Já leu

13 min de leitura

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa
Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa O Que o Terraform Não Faz O Terraform é excelente em provisionar infraestrutura — criar

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa

Módulo 5 · Infraestrutura como Código Prof. Ricardo Matos — Dominando DevOps & Cloud em 1 Ano


O Que o Terraform Não Faz

O Terraform é excelente em provisionar infraestrutura — criar servidores, configurar redes, provisionar bancos de dados. Mas depois que o servidor existe, o Terraform não é a ferramenta ideal para gerenciar o que acontece dentro dele. Instalar pacotes, copiar arquivos de configuração, criar usuários, iniciar serviços, aplicar patches de segurança — essas são responsabilidades de uma ferramenta de gerenciamento de configuração.

O Ansible é a ferramenta mais adotada para essa função. Ele conecta a servidores via SSH, sem necessidade de agente instalado, e executa uma série de tarefas declaradas em arquivos YAML chamados playbooks. O objetivo é garantir que o estado interno de cada servidor corresponda ao que foi declarado — independentemente do estado inicial da máquina.

A divisão de responsabilidades entre Terraform e Ansible é clara e complementar: o Terraform cria e gerencia a infraestrutura como um todo, o Ansible configura e mantém o estado interno dos servidores que o Terraform criou.


Instalação e Conceitos Fundamentais

# No Ubuntu/Debian
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible

# Via pip — recomendado para obter a versão mais recente
pip install ansible --break-system-packages

# Verifica a instalação
ansible --version

Antes de aprofundar, é importante estabelecer os quatro conceitos centrais do Ansible:

Inventory — o arquivo que lista os servidores que o Ansible vai gerenciar, organizados em grupos. É o mapa dos alvos de execução.

Playbook — o arquivo principal de automação. Descreve quais tarefas devem ser executadas em quais servidores. É onde a lógica de configuração vive.

Task — uma unidade de trabalho dentro de um playbook. Cada task usa um módulo do Ansible para executar uma ação específica — instalar um pacote, copiar um arquivo, reiniciar um serviço.

Role — uma estrutura de organização que agrupa tasks, variáveis, templates e handlers relacionados em uma unidade reutilizável. Roles são para o Ansible o que módulos são para o Terraform.


O Arquivo de Inventory

O inventory informa ao Ansible quais servidores existem e como se conectar a eles. Pode ser um arquivo estático ou gerado dinamicamente a partir de APIs de nuvem.

Inventory estático em formato INI:

# inventory/hosts.ini

# Grupo de servidores web
[webservers]
web01 ansible_host=54.123.45.10 ansible_user=ubuntu
web02 ansible_host=54.123.45.11 ansible_user=ubuntu

# Grupo de bancos de dados
[databases]
db01 ansible_host=10.0.1.50 ansible_user=ubuntu

# Grupo de servidores de cache
[cache]
redis01 ansible_host=10.0.1.60 ansible_user=ubuntu

# Grupo que agrega outros grupos
[producao:children]
webservers
databases
cache

# Variáveis aplicadas a todos os servidores do grupo webservers
[webservers:vars]
ansible_ssh_private_key_file=~/.ssh/id_ed25519
nginx_port=80
app_port=3000

# Variáveis aplicadas a todos os hosts
[all:vars]
ansible_python_interpreter=/usr/bin/python3

Inventory em YAML — mais legível para configurações complexas:

# inventory/hosts.yml
all:
  vars:
    ansible_python_interpreter: /usr/bin/python3
    ansible_ssh_private_key_file: ~/.ssh/id_ed25519

  children:
    webservers:
      hosts:
        web01:
          ansible_host: 54.123.45.10
          ansible_user: ubuntu
          nginx_port: 80
        web02:
          ansible_host: 54.123.45.11
          ansible_user: ubuntu
          nginx_port: 80
      vars:
        app_port: 3000

    databases:
      hosts:
        db01:
          ansible_host: 10.0.1.50
          ansible_user: ubuntu
          postgres_version: "16"

    cache:
      hosts:
        redis01:
          ansible_host: 10.0.1.60
          ansible_user: ubuntu

    producao:
      children:
        webservers:
        databases:
        cache:

Testando a conectividade com o inventory:

# Testa conexão com todos os hosts
ansible all -i inventory/hosts.yml -m ping

# Testa apenas os webservers
ansible webservers -i inventory/hosts.yml -m ping

# Executa um comando ad-hoc em todos os servidores
ansible all -i inventory/hosts.yml -m command -a "uptime"

# Executa com sudo
ansible webservers -i inventory/hosts.yml -m command -a "df -h" --become

O Primeiro Playbook

Um playbook é um arquivo YAML que define uma ou mais plays. Cada play associa um conjunto de tasks a um grupo de hosts:

# playbooks/configurar-servidor-web.yml
---
- name: Configura servidor web com Nginx e Docker
  hosts: webservers
  become: true  # equivalente ao sudo
  gather_facts: true  # coleta informações do sistema antes de executar

  vars:
    nginx_version: "1.24.*"
    app_user: "deploy"
    app_dir: "/opt/minha-api"
    docker_compose_version: "2.24.0"

  tasks:
    - name: Atualiza o cache do apt
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600  # não atualiza se cache tem menos de 1 hora

    - name: Instala pacotes essenciais
      ansible.builtin.apt:
        name:
          - curl
          - wget
          - git
          - unzip
          - htop
          - ufw
          - fail2ban
          - python3-pip
        state: present

    - name: Instala o Nginx
      ansible.builtin.apt:
        name: "nginx={{ nginx_version }}"
        state: present

    - name: Garante que o Nginx está iniciado e habilitado
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: true

    - name: Cria o usuário de deploy
      ansible.builtin.user:
        name: "{{ app_user }}"
        shell: /bin/bash
        create_home: true
        groups: docker
        append: true

    - name: Cria o diretório da aplicação
      ansible.builtin.file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: "0755"

    - name: Copia o arquivo de configuração do Nginx
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/minha-api
        owner: root
        group: root
        mode: "0644"
      notify: Recarrega Nginx

    - name: Habilita o site no Nginx
      ansible.builtin.file:
        src: /etc/nginx/sites-available/minha-api
        dest: /etc/nginx/sites-enabled/minha-api
        state: link
      notify: Recarrega Nginx

    - name: Remove a configuração padrão do Nginx
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Recarrega Nginx

  handlers:
    - name: Recarrega Nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded

Os handlers são tasks especiais que só são executados quando notificados por outras tasks e apenas uma vez ao final do playbook — mesmo que múltiplas tasks os notifiquem. São o mecanismo correto para reiniciar serviços após mudanças de configuração.


Templates com Jinja2

O Ansible usa o motor de templates Jinja2 para gerar arquivos de configuração dinâmicos. Variáveis do playbook, do inventory e de facts do sistema ficam disponíveis nos templates:

# templates/nginx.conf.j2
server {
    listen {{ nginx_port | default(80) }};
    server_name {{ ansible_hostname }};

    access_log /var/log/nginx/{{ app_user }}-access.log;
    error_log  /var/log/nginx/{{ app_user }}-error.log;

    location / {
        proxy_pass         http://127.0.0.1:{{ app_port }};
        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_cache_bypass $http_upgrade;

        proxy_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
    }

    location /health {
        access_log off;
        proxy_pass http://127.0.0.1:{{ app_port }}/health;
    }

    # Configurações de segurança
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    {% if ssl_enabled | default(false) %}
    listen 443 ssl;
    ssl_certificate     /etc/ssl/certs/{{ ansible_hostname }}.crt;
    ssl_certificate_key /etc/ssl/private/{{ ansible_hostname }}.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    {% endif %}
}

Roles: Organizando Automações Reutilizáveis

Quando os playbooks crescem, roles são a solução para organizar e reutilizar código. A estrutura de uma role segue uma convenção rígida que o Ansible reconhece automaticamente:

roles/
└── docker/
    ├── tasks/
    │   ├── main.yml          # Ponto de entrada das tasks
    │   ├── install.yml       # Tasks de instalação
    │   └── configure.yml     # Tasks de configuração
    ├── handlers/
    │   └── main.yml          # Handlers da role
    ├── templates/
    │   └── daemon.json.j2    # Templates de configuração
    ├── files/
    │   └── docker-cleanup.sh # Arquivos estáticos
    ├── vars/
    │   └── main.yml          # Variáveis fixas da role
    ├── defaults/
    │   └── main.yml          # Variáveis com valores padrão
    └── meta/
        └── main.yml          # Metadados e dependências da role

Implementação completa da role de Docker:

# roles/docker/defaults/main.yml
---
docker_edition: "ce"
docker_version: "5:25.0.0-1~ubuntu.22.04~jammy"
docker_compose_version: "2.24.0"
docker_users: []
docker_daemon_options:
  log-driver: "json-file"
  log-opts:
    max-size: "100m"
    max-file: "3"
  storage-driver: "overlay2"
# roles/docker/tasks/main.yml
---
- name: Inclui tasks de instalação
  ansible.builtin.import_tasks: install.yml

- name: Inclui tasks de configuração
  ansible.builtin.import_tasks: configure.yml
# roles/docker/tasks/install.yml
---
- name: Remove versões antigas do Docker
  ansible.builtin.apt:
    name:
      - docker
      - docker-engine
      - docker.io
      - containerd
      - runc
    state: absent

- name: Instala dependências do repositório Docker
  ansible.builtin.apt:
    name:
      - ca-certificates
      - curl
      - gnupg
      - lsb-release
    state: present
    update_cache: true

- name: Cria diretório para keyrings
  ansible.builtin.file:
    path: /etc/apt/keyrings
    state: directory
    mode: "0755"

- name: Adiciona a chave GPG oficial do Docker
  ansible.builtin.apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    keyring: /etc/apt/keyrings/docker.gpg
    state: present

- name: Adiciona o repositório do Docker
  ansible.builtin.apt_repository:
    repo: >
      deb [arch={{ ansible_architecture }}
      signed-by=/etc/apt/keyrings/docker.gpg]
      https://download.docker.com/linux/ubuntu
      {{ ansible_distribution_release }} stable
    state: present
    filename: docker

- name: Instala o Docker Engine
  ansible.builtin.apt:
    name:
      - "docker-{{ docker_edition }}={{ docker_version }}"
      - docker-ce-cli
      - containerd.io
      - docker-buildx-plugin
      - docker-compose-plugin
    state: present
    update_cache: true

- name: Garante que o Docker está iniciado e habilitado
  ansible.builtin.systemd:
    name: docker
    state: started
    enabled: true
# roles/docker/tasks/configure.yml
---
- name: Copia configuração do Docker daemon
  ansible.builtin.template:
    src: daemon.json.j2
    dest: /etc/docker/daemon.json
    owner: root
    group: root
    mode: "0644"
  notify: Reinicia Docker

- name: Adiciona usuários ao grupo docker
  ansible.builtin.user:
    name: "{{ item }}"
    groups: docker
    append: true
  loop: "{{ docker_users }}"
  when: docker_users | length > 0

- name: Instala o Docker Compose standalone
  ansible.builtin.get_url:
    url: "https://github.com/docker/compose/releases/download/v{{ docker_compose_version }}/docker-compose-linux-{{ ansible_architecture }}"
    dest: /usr/local/bin/docker-compose
    mode: "0755"
    owner: root
    group: root
# roles/docker/handlers/main.yml
---
- name: Reinicia Docker
  ansible.builtin.systemd:
    name: docker
    state: restarted
{# roles/docker/templates/daemon.json.j2 #}
{{ docker_daemon_options | to_nice_json }}

Um Playbook Completo Usando Roles

# playbooks/site.yml
---
- name: Configura servidores base
  hosts: all
  become: true
  gather_facts: true

  roles:
    - role: base
      vars:
        timezone: "America/Sao_Paulo"
        ntp_servers:
          - "0.br.pool.ntp.org"
          - "1.br.pool.ntp.org"

- name: Configura servidores web
  hosts: webservers
  become: true

  roles:
    - role: docker
      vars:
        docker_users:
          - ubuntu
          - deploy
    - role: nginx
      vars:
        nginx_port: 80
        app_port: 3000
    - role: aplicacao
      vars:
        app_image: "ghcr.io/minha-empresa/minha-api:{{ app_version }}"

- name: Configura servidores de banco de dados
  hosts: databases
  become: true

  roles:
    - role: postgres
      vars:
        postgres_version: "16"
        postgres_max_connections: 200
        postgres_shared_buffers: "256MB"

Variáveis e Precedência

O Ansible tem um sistema de precedência de variáveis com 22 níveis. Os mais relevantes na prática, do menor para o maior:

role defaults          → valores padrão das roles — facilmente sobrescritos
inventory vars         → variáveis definidas no inventory
playbook vars          → variáveis definidas no playbook
role vars              → variáveis fixas das roles
extra vars (-e)        → variáveis passadas na linha de comando — maior precedência

Usar a precedência corretamente torna as configurações flexíveis sem sacrificar clareza:

# Sobrescreve variáveis na linha de comando — útil para deploys
ansible-playbook \
  -i inventory/hosts.yml \
  playbooks/site.yml \
  -e "app_version=1.5.0" \
  -e "ambiente=producao"

Ansible Vault: Criptografando Segredos

Variáveis sensíveis — senhas, tokens, chaves — nunca devem ficar em texto puro nos arquivos do Ansible. O Ansible Vault criptografa arquivos ou strings individuais usando AES-256:

# Cria um arquivo de variáveis criptografado
ansible-vault create inventory/group_vars/all/vault.yml

# Edita um arquivo vault existente
ansible-vault edit inventory/group_vars/all/vault.yml

# Criptografa um arquivo existente
ansible-vault encrypt inventory/group_vars/databases/vars.yml

# Descriptografa para visualização
ansible-vault view inventory/group_vars/all/vault.yml

# Criptografa apenas uma string
ansible-vault encrypt_string 'senha_super_secreta' --name 'db_password'

Estrutura recomendada para variáveis com vault:

# inventory/group_vars/databases/vars.yml — não criptografado
postgres_user: app
postgres_db: producao
postgres_port: 5432

# inventory/group_vars/databases/vault.yml — criptografado com vault
vault_postgres_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  66386439653236336462626566653061373932...
# inventory/group_vars/databases/vars.yml
# Referencia a variável do vault com um nome sem prefixo vault_
postgres_password: "{{ vault_postgres_password }}"

Executando playbooks com vault:

# Solicita a senha interativamente
ansible-playbook playbooks/site.yml --ask-vault-pass

# Lê a senha de um arquivo (útil em CI/CD)
echo "minha-senha-vault" > .vault-password
chmod 600 .vault-password
ansible-playbook playbooks/site.yml --vault-password-file .vault-password

# Variável de ambiente
export ANSIBLE_VAULT_PASSWORD_FILE=.vault-password
ansible-playbook playbooks/site.yml

Idempotência: O Princípio Central

A idempotência é o princípio que garante que executar o mesmo playbook múltiplas vezes produz sempre o mesmo resultado — sem efeitos colaterais cumulativos. Instalar o Nginx uma segunda vez não deve causar erro. Criar um usuário que já existe não deve falhar.

A maioria dos módulos do Ansible é idempotente por design — o módulo apt verifica se o pacote já está instalado antes de tentar instalá-lo, o módulo file verifica se o arquivo já existe com as permissões corretas antes de modificá-lo. É responsabilidade do autor do playbook garantir que tasks personalizadas com command ou shell também sejam idempotentes:

# Não idempotente — executa sempre, mesmo se já foi feito
- name: Inicializa o banco de dados
  ansible.builtin.command: npm run db:migrate

# Idempotente — cria um arquivo de flag após a primeira execução
- name: Verifica se o banco já foi inicializado
  ansible.builtin.stat:
    path: /opt/app/.db-initialized
  register: db_initialized

- name: Inicializa o banco de dados
  ansible.builtin.command: npm run db:migrate
  when: not db_initialized.stat.exists

- name: Marca banco como inicializado
  ansible.builtin.file:
    path: /opt/app/.db-initialized
    state: touch
  when: not db_initialized.stat.exists

O Que Vem a Seguir

O próximo artigo fecha o Módulo 5 combinando Terraform e Ansible em um fluxo de trabalho unificado — provisionar a infraestrutura com Terraform e em seguida configurá-la com Ansible, formando um pipeline completo de IaC do zero ao servidor pronto para receber a aplicação.


Referências para Aprofundamento

Documentação oficial - Ansible Documentation — docs.ansible.com — Documentação completa do Ansible, incluindo referência de todos os módulos disponíveis, guias de roles e boas práticas. - Ansible Galaxy — galaxy.ansible.com — Repositório público de roles prontas. Equivalente ao Terraform Registry para o ecossistema Ansible. Contém roles mantidas pela comunidade para as ferramentas mais comuns.

Boas práticas - Ansible Best Practices — docs.ansible.com — Guia oficial de boas práticas cobrindo estrutura de projetos, nomenclatura e organização de inventories. - Jeff Geerling — Ansible for DevOps — Livro amplamente recomendado pela comunidade, com exemplos práticos de automação de servidores. O autor mantém dezenas de roles públicas no Ansible Galaxy.

Prática - Ansible Molecule — molecule.readthedocs.io — Framework para testes de roles Ansible. Permite testar roles em containers Docker ou máquinas virtuais antes de aplicá-las em produção.


Artigo 29 de 52 · Módulo 5 — Infraestrutura como Código Prof. Ricardo Matos · Série Dominando DevOps & Cloud em 1 Ano


you asked

Continue


claude response

Comentários

Mais em DevOps

Projeto Capstone: Arquitetura do Sistema Completo
Projeto Capstone: Arquitetura do Sistema Completo

Os quarenta e sete artigos anteriores cobriram, em progressão cuidadosa, cada...

Gitea: Self-Hosted Leve para Times Menores
Gitea: Self-Hosted Leve para Times Menores

O GitLab self-hosted é poderoso — e pesado. Uma instalação completa consume v...

Capstone: Operações em Produção e Retrospectiva da Jornada
Capstone: Operações em Produção e Retrospectiva da Jornada

Um sistema de software não termina quando o último deploy é feito. Ele começa...