Ansible Playbook 자동화 심화

Ansible Playbook이란?

Ansible은 에이전트 없이 SSH만으로 서버를 구성·배포·관리하는 인프라 자동화 도구다. Playbook은 YAML로 작성하는 자동화 시나리오로, 서버 프로비저닝, 애플리케이션 배포, 설정 관리를 선언적으로 정의한다. Terraform이 인프라 생성에 집중한다면, Ansible은 인프라 위의 구성 관리에 강점이 있다.

구분 Terraform Ansible
주 용도 인프라 프로비저닝 (VM, 네트워크) 구성 관리, 앱 배포
에이전트 없음 (API 호출) 없음 (SSH)
상태 관리 State 파일 멱등성 모듈
언어 HCL YAML + Jinja2

프로젝트 디렉터리 구조

Ansible 프로젝트는 표준 디렉터리 구조를 따르면 팀 협업과 유지보수가 쉬워진다.

ansible-project/
├── ansible.cfg              # Ansible 설정
├── inventory/
│   ├── production/
│   │   ├── hosts.yml        # 프로덕션 호스트
│   │   └── group_vars/
│   │       ├── all.yml      # 공통 변수
│   │       ├── webservers.yml
│   │       └── databases.yml
│   └── staging/
│       ├── hosts.yml
│       └── group_vars/
│           └── all.yml
├── playbooks/
│   ├── site.yml             # 메인 플레이북
│   ├── deploy.yml           # 앱 배포
│   └── setup.yml            # 서버 초기 설정
├── roles/
│   ├── common/
│   ├── nginx/
│   ├── app/
│   └── monitoring/
└── templates/
    └── nginx.conf.j2

인벤토리: 호스트 그룹 관리

# inventory/production/hosts.yml
all:
  children:
    webservers:
      hosts:
        web-01:
          ansible_host: 10.0.1.10
          ansible_user: deploy
          app_port: 3000
        web-02:
          ansible_host: 10.0.1.11
          ansible_user: deploy
          app_port: 3000
    databases:
      hosts:
        db-primary:
          ansible_host: 10.0.2.10
          ansible_user: admin
          pg_role: primary
        db-replica:
          ansible_host: 10.0.2.11
          ansible_user: admin
          pg_role: replica
    monitoring:
      hosts:
        monitor-01:
          ansible_host: 10.0.3.10

# group_vars/all.yml — 전체 공통 변수
---
app_name: myapp
app_env: production
deploy_user: deploy
ntp_servers:
  - ntp1.example.com
  - ntp2.example.com
ssh_allowed_keys:
  - "ssh-ed25519 AAAA... admin@company"

Playbook 기본 구조

# playbooks/setup.yml — 서버 초기 설정
---
- name: 공통 서버 설정
  hosts: all
  become: true  # sudo 권한
  gather_facts: true

  vars:
    packages:
      - curl
      - vim
      - htop
      - jq
      - fail2ban

  tasks:
    - name: 시스템 패키지 업데이트
      apt:
        update_cache: true
        upgrade: safe
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

    - name: 필수 패키지 설치
      apt:
        name: "{{ packages }}"
        state: present

    - name: 타임존 설정
      timezone:
        name: Asia/Seoul

    - name: NTP 설정
      template:
        src: templates/ntp.conf.j2
        dest: /etc/ntp.conf
        owner: root
        mode: '0644'
      notify: restart ntp

    - name: 배포 사용자 생성
      user:
        name: "{{ deploy_user }}"
        shell: /bin/bash
        groups: sudo
        create_home: true

    - name: SSH 키 등록
      authorized_key:
        user: "{{ deploy_user }}"
        key: "{{ item }}"
        state: present
      loop: "{{ ssh_allowed_keys }}"

    - name: 패스워드 인증 비활성화
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PasswordAuthentication'
        line: 'PasswordAuthentication no'
      notify: restart sshd

  handlers:
    - name: restart ntp
      service:
        name: ntp
        state: restarted

    - name: restart sshd
      service:
        name: sshd
        state: restarted

Role: 재사용 가능한 자동화 단위

Role은 태스크, 핸들러, 템플릿, 변수를 하나의 논리적 단위로 패키징한다. Ansible Galaxy에서 커뮤니티 Role을 가져오거나, 자체 Role을 만들어 재사용할 수 있다.

# roles/nginx/ 디렉터리 구조
roles/nginx/
├── tasks/
│   ├── main.yml
│   └── vhost.yml
├── templates/
│   ├── nginx.conf.j2
│   └── vhost.conf.j2
├── handlers/
│   └── main.yml
├── defaults/
│   └── main.yml    # 기본 변수 (오버라이드 가능)
└── vars/
    └── main.yml    # 고정 변수

# roles/nginx/defaults/main.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_client_max_body_size: 50m
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_vhosts: []

# roles/nginx/tasks/main.yml
---
- name: Nginx 설치
  apt:
    name: nginx
    state: present

- name: Nginx 메인 설정
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    mode: '0644'
  notify: reload nginx

- name: vhost 설정
  include_tasks: vhost.yml
  loop: "{{ nginx_vhosts }}"
  loop_control:
    loop_var: vhost

- name: Nginx 시작 및 활성화
  service:
    name: nginx
    state: started
    enabled: true

# roles/nginx/handlers/main.yml
---
- name: reload nginx
  service:
    name: nginx
    state: reloaded

- name: restart nginx
  service:
    name: nginx
    state: restarted

Jinja2 템플릿 실전

Ansible의 템플릿 엔진 Jinja2로 설정 파일을 동적으로 생성한다. 조건문, 반복문, 필터를 활용하면 복잡한 설정도 깔끔하게 관리할 수 있다.

# templates/nginx.conf.j2
worker_processes {{ nginx_worker_processes }};

events {
    worker_connections {{ nginx_worker_connections }};
}

http {
    client_max_body_size {{ nginx_client_max_body_size }};
    ssl_protocols {{ nginx_ssl_protocols }};

{% for vhost in nginx_vhosts %}
    server {
        listen {{ vhost.port | default(80) }};
        server_name {{ vhost.server_name }};

{% if vhost.ssl | default(false) %}
        listen 443 ssl;
        ssl_certificate /etc/ssl/certs/{{ vhost.server_name }}.pem;
        ssl_certificate_key /etc/ssl/private/{{ vhost.server_name }}.key;
{% endif %}

{% for location in vhost.locations | default([]) %}
        location {{ location.path }} {
            proxy_pass http://{{ location.upstream }};
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
{% if location.websocket | default(false) %}
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
{% endif %}
        }
{% endfor %}
    }
{% endfor %}
}

배포 Playbook: 무중단 배포

# playbooks/deploy.yml — 롤링 배포
---
- name: 애플리케이션 배포
  hosts: webservers
  become: true
  serial: 1              # 한 번에 1대씩 (롤링)
  max_fail_percentage: 0  # 1대라도 실패하면 중단

  vars:
    app_repo: git@github.com:company/myapp.git
    app_dir: /opt/{{ app_name }}
    app_version: "{{ deploy_version | default('main') }}"

  pre_tasks:
    - name: 로드밸런서에서 제거
      uri:
        url: "http://lb.internal/api/deregister"
        method: POST
        body_format: json
        body:
          host: "{{ inventory_hostname }}"
      delegate_to: localhost

    - name: 활성 연결 드레이닝 대기
      wait_for:
        timeout: 10

  tasks:
    - name: 코드 배포
      git:
        repo: "{{ app_repo }}"
        dest: "{{ app_dir }}"
        version: "{{ app_version }}"
        force: true
      register: git_result

    - name: 의존성 설치
      command: npm ci --production
      args:
        chdir: "{{ app_dir }}"
      when: git_result.changed

    - name: 환경 변수 설정
      template:
        src: templates/env.j2
        dest: "{{ app_dir }}/.env"
        owner: "{{ deploy_user }}"
        mode: '0600'

    - name: 서비스 재시작
      systemd:
        name: "{{ app_name }}"
        state: restarted
        daemon_reload: true

    - name: 헬스체크 대기
      uri:
        url: "http://localhost:{{ app_port }}/health"
        status_code: 200
      register: health
      until: health.status == 200
      retries: 30
      delay: 2

  post_tasks:
    - name: 로드밸런서에 등록
      uri:
        url: "http://lb.internal/api/register"
        method: POST
        body_format: json
        body:
          host: "{{ inventory_hostname }}"
      delegate_to: localhost

Docker BuildKit 캐시 최적화에서 다룬 Docker 빌드와 Ansible 배포를 결합하면 이미지 빌드부터 서버 배포까지 완전한 CI/CD 파이프라인을 구축할 수 있다.

Vault: 시크릿 관리

Ansible Vault는 비밀번호, API 키 등 민감한 변수를 암호화해서 Git에 안전하게 커밋할 수 있다.

# 파일 암호화
ansible-vault encrypt inventory/production/group_vars/vault.yml

# 암호화된 변수 파일
# inventory/production/group_vars/vault.yml
---
vault_db_password: "super-secret-password"
vault_api_key: "sk-1234567890"
vault_jwt_secret: "32-char-minimum-secret-here..."

# 일반 변수에서 vault 변수 참조
# inventory/production/group_vars/all.yml
---
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"

# 실행 시 vault 비밀번호 입력
ansible-playbook playbooks/deploy.yml --ask-vault-pass

# 또는 비밀번호 파일 사용
ansible-playbook playbooks/deploy.yml --vault-password-file ~/.vault_pass

조건문·반복문·에러 핸들링

# 조건부 실행
- name: Ubuntu에서만 실행
  apt:
    name: nginx
  when: ansible_distribution == "Ubuntu"

# 반복문
- name: 여러 사용자 생성
  user:
    name: "{{ item.name }}"
    groups: "{{ item.groups | join(',') }}"
  loop:
    - { name: alice, groups: [sudo, docker] }
    - { name: bob, groups: [docker] }

# 에러 무시하고 계속 진행
- name: 선택적 서비스 중지
  service:
    name: old-service
    state: stopped
  ignore_errors: true

# 블록 기반 에러 핸들링
- name: 배포 with 롤백
  block:
    - name: 새 버전 배포
      command: ./deploy.sh
    - name: 마이그레이션 실행
      command: npm run migrate
  rescue:
    - name: 롤백 실행
      command: ./rollback.sh
    - name: 알림 전송
      uri:
        url: "{{ slack_webhook }}"
        method: POST
        body_format: json
        body:
          text: "배포 실패! 롤백 완료: {{ inventory_hostname }}"
  always:
    - name: 로그 수집
      fetch:
        src: /var/log/deploy.log
        dest: "./logs/{{ inventory_hostname }}/"
        flat: true

Terraform State 관리 심화에서 다룬 인프라 프로비저닝 후 Ansible로 구성 관리를 이어가면 완전한 IaC 파이프라인이 완성된다.

마무리

Ansible Playbook은 SSH 기반의 에이전트리스 구성 관리 표준이다. Role로 재사용 가능한 자동화 단위를 만들고, Jinja2 템플릿으로 동적 설정을 생성하며, Vault로 시크릿을 안전하게 관리한다. serial 옵션의 롤링 배포와 block/rescue 에러 핸들링을 조합하면 프로덕션 무중단 배포까지 안정적으로 자동화할 수 있다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux