🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Ansible 베스트 프랙티스 완벽 가이드 - 슬라이드 1/3
A

AI Generated

2025. 11. 5. · 25 Views

Ansible 베스트 프랙티스 완벽 가이드

인프라 자동화의 필수 도구인 Ansible을 실무 수준으로 활용하기 위한 베스트 프랙티스를 소개합니다. 초급 개발자도 바로 적용할 수 있는 구조화된 방법론과 실전 예제를 통해 안정적이고 유지보수 가능한 자동화 환경을 구축하는 방법을 배워보세요.


목차

  1. 디렉토리 구조 표준화 - 프로젝트 관리의 시작점
  2. Role 기반 구조화 - 재사용성의 핵심
  3. 변수 우선순위와 관리 - 혼란 없는 설정 제어
  4. Inventory 동적 관리 - 클라우드 환경 대응
  5. 멱등성 보장 - 안전한 반복 실행
  6. Ansible Vault를 통한 보안 관리 - 비밀 정보 보호
  7. Handler와 Notify - 효율적인 서비스 재시작
  8. 템플릿과 Jinja2 - 동적 설정 파일 생성
  9. 태스크 제어와 에러 핸들링 - 견고한 자동화
  10. 성능 최적화 전략 - 대규모 인프라 관리
  11. 테스트와 검증 - 배포 전 안전성 확보
  12. CI/CD 통합과 GitOps - 자동화의 자동화

1. 디렉토리 구조 표준화 - 프로젝트 관리의 시작점

시작하며

여러분이 Ansible 프로젝트를 처음 시작할 때, 파일을 어디에 두어야 할지 막막했던 경험 있으신가요? playbook 파일이 여기저기 흩어져 있고, 변수 파일을 찾느라 시간을 낭비하는 상황 말이죠.

이런 문제는 프로젝트가 커질수록 심각해집니다. 팀원들마다 다른 구조를 사용하면 협업이 어려워지고, 유지보수 비용이 기하급수적으로 증가합니다.

바로 이럴 때 필요한 것이 Ansible의 표준 디렉토리 구조입니다. 일관된 구조는 코드의 가독성을 높이고, 새로운 팀원의 온보딩 시간을 크게 단축시킵니다.

개요

간단히 말해서, Ansible 디렉토리 구조는 프로젝트의 모든 구성 요소를 체계적으로 정리하는 청사진입니다. 실무에서 여러 서버 환경을 관리하다 보면 playbook, role, inventory, 변수 파일 등이 수십 개로 늘어납니다.

표준 구조 없이 작업하면 어떤 파일이 어떤 역할을 하는지 파악하기 어렵고, 수정할 때마다 전체 프로젝트를 뒤져야 합니다. 예를 들어, 웹 서버 설정을 변경하려고 할 때 관련 파일을 찾는 데만 30분이 걸린다면 매우 비효율적이겠죠?

기존에는 모든 파일을 한 디렉토리에 몰아넣었다면, 이제는 roles, inventory, group_vars 등으로 명확히 분리할 수 있습니다. 이 구조의 핵심 특징은 첫째, 역할별 분리(roles 디렉토리), 둘째, 환경별 관리(inventory 디렉토리), 셋째, 변수의 계층화(group_vars, host_vars)입니다.

이러한 특징들이 대규모 인프라 관리를 가능하게 만들어줍니다.

코드 예제

# 표준 Ansible 프로젝트 구조
ansible-project/
├── ansible.cfg              # Ansible 설정 파일
├── inventory/
│   ├── production/          # 운영 환경 인벤토리
│   │   ├── hosts
│   │   └── group_vars/
│   └── staging/             # 스테이징 환경 인벤토리
│       ├── hosts
│       └── group_vars/
├── roles/                   # 재사용 가능한 역할들
│   ├── common/
│   ├── webserver/
│   └── database/
├── playbooks/               # 플레이북 모음
│   ├── site.yml            # 메인 플레이북
│   ├── webservers.yml
│   └── databases.yml
├── group_vars/              # 그룹 변수
│   └── all.yml
└── host_vars/               # 호스트별 변수

설명

이것이 하는 일: 표준 디렉토리 구조는 Ansible 프로젝트의 모든 구성 요소를 논리적으로 분류하고 배치하여, 개발자가 필요한 파일을 빠르게 찾고 수정할 수 있게 해줍니다. 첫 번째로, inventory 디렉토리는 관리 대상 서버들을 환경별로 분리합니다.

production과 staging을 별도로 관리함으로써 실수로 운영 서버에 테스트 설정을 배포하는 위험을 방지할 수 있습니다. 각 환경 디렉토리 내부의 hosts 파일에는 실제 서버 목록이, group_vars에는 해당 환경에 특화된 변수들이 저장됩니다.

그 다음으로, roles 디렉토리가 실행되면서 재사용 가능한 구성 단위를 제공합니다. common role에는 모든 서버에 공통으로 필요한 설정(방화벽, 로깅 등)이, webserver role에는 Apache나 Nginx 설정이 들어갑니다.

각 role은 자체적으로 tasks, handlers, templates, files 등의 하위 디렉토리를 가지며, 완전히 독립적으로 동작할 수 있습니다. 마지막으로, playbooks 디렉토리가 실제 작업을 오케스트레이션하여 최종적으로 인프라 변경을 실행합니다.

site.yml은 전체 인프라를 대상으로 하는 마스터 playbook이고, webservers.yml이나 databases.yml은 특정 서버 그룹만 대상으로 합니다. 이렇게 분리하면 필요한 부분만 선택적으로 실행할 수 있어 배포 시간을 크게 단축시킵니다.

여러분이 이 구조를 사용하면 새로운 role 추가 시 정해진 위치에만 파일을 생성하면 되고, 변수 우선순위를 명확히 이해할 수 있으며, 팀 전체가 동일한 규칙으로 협업할 수 있습니다. 특히 6개월 후 코드를 다시 봐도 어디에 무엇이 있는지 바로 알 수 있다는 점이 가장 큰 장점입니다.

실전 팁

💡 ansible.cfg 파일을 프로젝트 루트에 두면 전역 설정 대신 프로젝트별 설정을 사용할 수 있어 환경 독립성이 보장됩니다

💡 roles 디렉토리 내부에 requirements.yml을 만들어 외부 role 의존성을 명시하면 팀원들이 ansible-galaxy install -r requirements.yml 한 번으로 모든 의존성을 설치할 수 있습니다

💡 inventory 파일에 ansible_host 변수를 사용해 읽기 쉬운 호스트명과 실제 IP를 분리하세요 (예: web01 ansible_host=192.168.1.10)

💡 group_vars와 host_vars는 YAML 파일뿐 아니라 디렉토리로도 만들 수 있어, 변수가 많을 때 여러 파일로 분산 관리가 가능합니다

💡 .gitignore에 *.retry 파일과 비밀 정보가 담긴 vault 파일을 추가하여 민감한 정보가 리포지토리에 노출되지 않도록 하세요


2. Role 기반 구조화 - 재사용성의 핵심

시작하며

여러분이 10대의 웹 서버를 구성할 때, 똑같은 작업을 playbook에 10번 복사-붙여넣기 하고 계신가요? 나중에 설정을 변경하려면 10곳을 모두 수정해야 하는 악몽이 기다리고 있습니다.

이런 중복 코드는 유지보수의 적입니다. 한 곳을 수정하고 다른 곳을 깜빡하면 서버마다 다른 설정이 적용되어 예상치 못한 장애가 발생할 수 있습니다.

바로 이럴 때 필요한 것이 Ansible의 Role입니다. Role은 관련된 작업들을 하나의 재사용 가능한 단위로 패키징하여, 한 번 작성하면 어디서든 사용할 수 있게 해줍니다.

개요

간단히 말해서, Role은 특정 기능을 수행하는 Ansible 코드의 독립적인 모듈입니다. 실무에서 웹 서버, 데이터베이스, 로드밸런서 등 각각의 서버 유형은 고유한 설정 절차가 있습니다.

Role을 사용하지 않으면 이런 설정들이 여러 playbook에 흩어져 있어 일관성을 유지하기 어렵습니다. 예를 들어, Nginx 설정을 변경할 때 5개의 다른 playbook을 모두 찾아서 수정해야 한다면 실수할 가능성이 매우 높습니다.

기존에는 하나의 거대한 playbook에 모든 작업을 나열했다면, 이제는 기능별로 분리된 role을 조합하여 playbook을 구성할 수 있습니다. 이 개념의 핵심 특징은 첫째, 표준화된 디렉토리 구조(tasks, handlers, templates 등), 둘째, 완전한 독립성(role 간 의존성 최소화), 셋째, Ansible Galaxy를 통한 공유 가능성입니다.

이러한 특징들이 인프라 코드의 재사용성과 테스트 가능성을 극대화합니다.

코드 예제

# roles/nginx/tasks/main.yml
---
- name: Nginx 패키지 설치
  apt:
    name: nginx
    state: present
    update_cache: yes

- name: 커스텀 Nginx 설정 배포
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
  notify: restart nginx  # handler 트리거

- name: Nginx 서비스 시작 및 활성화
  service:
    name: nginx
    state: started
    enabled: yes

설명

이것이 하는 일: Role은 특정 기능(예: 웹 서버 설정)을 수행하는 데 필요한 모든 구성 요소를 표준화된 디렉토리 구조 안에 캡슐화하여, 다양한 프로젝트에서 일관되게 재사용할 수 있게 합니다. 첫 번째로, tasks/main.yml 파일이 role의 진입점 역할을 합니다.

여기에는 실제로 실행될 작업들이 순서대로 정의되어 있습니다. 위 예제에서는 Nginx 설치, 설정 파일 배포, 서비스 시작이라는 세 단계로 구성되어 있습니다.

각 작업은 멱등성을 가지므로 여러 번 실행해도 같은 결과를 보장합니다. 그 다음으로, template 모듈이 실행되면서 Jinja2 템플릿을 사용한 동적 설정 파일을 생성합니다.

templates/nginx.conf.j2 파일에는 변수를 포함한 설정 템플릿이 있고, 실제 배포 시 각 서버의 변수 값으로 치환됩니다. 예를 들어 worker_processes를 서버의 CPU 코어 수에 맞게 자동으로 설정할 수 있습니다.

notify 키워드는 설정이 변경되었을 때만 handler를 실행하도록 합니다. 마지막으로, handlers/main.yml에 정의된 restart nginx handler가 필요한 경우에만 실행되어 최종적으로 변경사항을 반영합니다.

Handler는 모든 task가 완료된 후 한 번만 실행되므로, 여러 작업이 설정을 변경하더라도 Nginx는 한 번만 재시작됩니다. 여러분이 이 Role을 사용하면 10대의 웹 서버든 100대든 동일한 설정을 보장할 수 있고, 설정 변경이 필요하면 role 하나만 수정하면 되며, 테스트 환경에서 검증한 role을 운영 환경에 그대로 적용할 수 있습니다.

또한 다른 프로젝트에서도 이 role을 import하여 즉시 사용할 수 있어 개발 속도가 크게 향상됩니다.

실전 팁

💡 Role의 defaults/main.yml에 기본값을 정의하고, 각 환경의 group_vars에서 필요한 값만 오버라이드하면 유연성과 안전성을 모두 확보할 수 있습니다

💡 meta/main.yml에 role 의존성을 명시하면 필요한 다른 role이 자동으로 먼저 실행됩니다 (예: nginx role이 common role에 의존)

💡 ansible-galaxy init role_name 명령어로 표준 role 구조를 자동 생성하여 일관성을 유지하세요

💡 role의 tasks를 기능별로 분리하고(install.yml, configure.yml, service.yml) main.yml에서 include_tasks로 불러오면 가독성이 향상됩니다

💡 molecule 프레임워크를 사용해 role을 독립적으로 테스트하면 배포 전에 문제를 조기에 발견할 수 있습니다


3. 변수 우선순위와 관리 - 혼란 없는 설정 제어

시작하며

여러분이 Ansible로 작업하다가 "내가 분명히 변수를 설정했는데 왜 다른 값이 적용되지?"라고 당황한 경험 있으신가요? 같은 이름의 변수가 여러 곳에 정의되어 있을 때 어떤 값이 사용될지 예측하기 어려운 상황 말이죠.

이런 문제는 특히 팀 단위로 작업할 때 심각합니다. 개발자마다 변수를 다른 곳에 정의하면 디버깅이 거의 불가능해지고, 의도하지 않은 값이 운영 서버에 배포되는 사고로 이어질 수 있습니다.

바로 이럴 때 필요한 것이 Ansible의 변수 우선순위 체계에 대한 이해입니다. 명확한 우선순위 규칙을 알면 변수 충돌을 예방하고, 환경별로 다른 값을 안전하게 관리할 수 있습니다.

개요

간단히 말해서, Ansible의 변수 우선순위는 같은 이름의 변수가 여러 곳에 정의되었을 때 어떤 값이 최종적으로 사용될지 결정하는 명확한 규칙입니다. 실무에서는 전체 환경에 공통적으로 적용될 기본값, 특정 서버 그룹에만 적용될 값, 개별 호스트의 특수한 값 등 다양한 레벨의 변수가 필요합니다.

우선순위를 이해하지 못하면 group_vars에서 정의한 값이 role의 defaults에 의해 덮어씌워지거나, 반대로 의도한 오버라이드가 적용되지 않는 문제가 발생합니다. 예를 들어, 운영 환경에서만 다른 데이터베이스 포트를 사용하려고 했는데 기본값이 적용되어 연결이 실패하는 경우가 있습니다.

기존에는 변수를 아무 곳에나 정의하고 "왜 안 되지?"를 반복했다면, 이제는 우선순위를 고려하여 적재적소에 변수를 배치할 수 있습니다. 이 개념의 핵심 특징은 첫째, 22단계로 구성된 명확한 우선순위 체인, 둘째, extra-vars가 모든 것을 오버라이드하는 최고 우선순위, 셋째, role defaults가 가장 낮은 우선순위를 가진다는 점입니다.

이러한 특징들이 유연하면서도 예측 가능한 변수 관리를 가능하게 합니다.

코드 예제

# roles/myapp/defaults/main.yml (우선순위: 가장 낮음)
app_port: 8080
app_max_connections: 100

# inventory/production/group_vars/webservers.yml (우선순위: 중간)
app_port: 80
app_environment: production

# inventory/production/host_vars/web01.yml (우선순위: 높음)
app_max_connections: 500

# playbook 실행 시 커맨드 라인 (우선순위: 가장 높음)
# ansible-playbook -i inventory/production site.yml -e "app_port=443"

# 최종 결과 (web01 호스트 기준):
# app_port: 443 (extra-vars에서)
# app_max_connections: 500 (host_vars에서)
# app_environment: production (group_vars에서)

설명

이것이 하는 일: 변수 우선순위 체계는 동일한 변수명이 여러 위치에 정의되었을 때 명확한 규칙에 따라 최종 값을 결정하여, 예측 가능하고 일관된 설정 관리를 보장합니다. 첫 번째로, role의 defaults/main.yml에 정의된 변수들은 가장 낮은 우선순위를 가집니다.

위 예제에서 app_port: 8080과 app_max_connections: 100은 "아무도 이 값을 오버라이드하지 않으면 사용될 기본값"입니다. 이는 role을 재사용 가능하게 만드는 핵심 요소로, role 개발자가 합리적인 기본값을 제공하되 사용자가 언제든 변경할 수 있게 합니다.

그 다음으로, inventory의 group_vars와 host_vars가 처리됩니다. group_vars/webservers.yml의 app_port: 80은 defaults의 8080을 오버라이드하고, host_vars/web01.yml의 app_max_connections: 500은 defaults의 100을 오버라이드합니다.

group_vars는 "이 서버 그룹의 모든 호스트에 적용", host_vars는 "이 특정 호스트에만 적용"이라는 의미입니다. 이 단계에서 web01의 app_port는 80, app_max_connections는 500이 됩니다.

마지막으로, 커맨드 라인의 -e 옵션(extra-vars)이 모든 것을 최종 오버라이드하여 app_port를 443으로 변경합니다. Extra-vars는 긴급 배포나 일회성 테스트에 유용하며, "이번만 이 값을 사용하고 싶다"는 상황에서 설정 파일을 수정하지 않고도 값을 변경할 수 있습니다.

여러분이 이 우선순위 체계를 이해하면 변수를 어디에 정의해야 할지 명확히 판단할 수 있고, 환경별/호스트별 차이를 체계적으로 관리할 수 있으며, 디버깅 시 변수의 출처를 빠르게 추적할 수 있습니다. 특히 ansible -m debug -a "var=변수명" 명령으로 최종 값과 출처를 확인하는 습관을 들이면 문제 해결 시간이 크게 단축됩니다.

실전 팁

💡 role의 vars/main.yml은 defaults보다 높은 우선순위를 가지므로, 절대 변경되면 안 되는 값은 여기에 정의하세요

💡 ansible-playbook --list-hosts와 --list-tasks로 실행 전에 대상과 작업을 미리 확인하여 변수 적용 범위를 검증하세요

💡 set_fact 모듈로 playbook 실행 중 동적으로 생성한 변수는 매우 높은 우선순위를 가지므로 조건부 로직에 유용합니다

💡 변수명에 role 이름을 접두사로 붙이면(예: nginx_port, mysql_port) 다른 role과의 충돌을 방지할 수 있습니다

💡 ansible-inventory --list 명령으로 특정 호스트의 모든 변수와 그 값을 JSON 형태로 확인하여 우선순위 적용 결과를 검증하세요


4. Inventory 동적 관리 - 클라우드 환경 대응

시작하며

여러분이 AWS나 Azure 같은 클라우드 환경에서 서버를 관리할 때, 인스턴스가 추가되거나 삭제될 때마다 inventory 파일을 수동으로 수정하고 계신가요? Auto Scaling으로 서버 수가 변동되는 환경에서는 이런 방식이 사실상 불가능합니다.

이런 문제는 현대적인 클라우드 네이티브 환경에서 치명적입니다. 수동으로 관리하는 inventory는 항상 실제 인프라와 불일치하게 되고, 존재하지 않는 서버에 배포를 시도하거나 새로운 서버를 놓치는 사고가 발생합니다.

바로 이럴 때 필요한 것이 Ansible의 동적 Inventory입니다. 클라우드 API를 통해 실시간으로 서버 목록을 가져와 항상 최신 상태를 유지할 수 있습니다.

개요

간단히 말해서, 동적 Inventory는 정적 파일 대신 스크립트나 플러그인을 사용하여 실행 시점에 관리 대상 호스트 목록을 동적으로 생성하는 방식입니다. 실무에서 클라우드 인프라는 탄력적으로 변합니다.

트래픽이 증가하면 서버가 자동으로 추가되고, 감소하면 제거됩니다. 컨테이너 오케스트레이션 환경에서는 서비스가 여러 노드로 이동하기도 합니다.

예를 들어, 마케팅 이벤트로 웹 서버가 10대에서 50대로 늘어났을 때, 이 50대의 IP를 수동으로 inventory에 추가하는 것은 비현실적입니다. 기존에는 hosts 파일에 IP 주소를 하드코딩했다면, 이제는 AWS EC2 플러그인이 태그를 기반으로 자동으로 그룹을 구성할 수 있습니다.

이 개념의 핵심 특징은 첫째, 클라우드 API와의 실시간 연동, 둘째, 태그나 메타데이터 기반의 자동 그룹화, 셋째, 캐싱을 통한 성능 최적화입니다. 이러한 특징들이 대규모 동적 인프라의 자동화를 가능하게 합니다.

코드 예제

# inventory/aws_ec2.yml (AWS EC2 동적 inventory 플러그인 설정)
---
plugin: amazon.aws.aws_ec2
regions:
  - ap-northeast-2  # 서울 리전

# 태그 기반 그룹 생성
keyed_groups:
  - key: tags.Environment
    prefix: env
  - key: tags.Role
    prefix: role
  - key: instance_type
    prefix: instance_type

# 특정 조건의 인스턴스만 포함
filters:
  instance-state-name: running
  "tag:Managed": "ansible"

# 호스트명 설정
hostnames:
  - tag:Name
  - private-ip-address

설명

이것이 하는 일: 동적 Inventory는 Ansible 실행 시점에 클라우드 제공자의 API를 호출하여 현재 실행 중인 인스턴스 정보를 수집하고, 메타데이터를 기반으로 자동으로 그룹을 구성하여 항상 최신 상태의 인프라를 대상으로 작업할 수 있게 합니다. 첫 번째로, plugin: amazon.aws.aws_ec2 선언이 AWS EC2 동적 inventory 플러그인을 활성화합니다.

이 플러그인은 AWS API에 인증하여(IAM role 또는 환경 변수 사용) 지정된 리전의 모든 EC2 인스턴스 정보를 가져옵니다. regions 리스트에 여러 리전을 추가하면 멀티 리전 인프라도 한 번에 관리할 수 있습니다.

그 다음으로, keyed_groups 섹션이 실행되면서 EC2 태그를 기반으로 Ansible 그룹을 자동 생성합니다. 예를 들어 Environment: production 태그가 있는 인스턴스는 자동으로 env_production 그룹에, Role: webserver 태그가 있으면 role_webserver 그룹에 추가됩니다.

이는 "태그로 인프라를 분류했다면, 그 분류가 곧 Ansible 그룹이 된다"는 의미입니다. 하나의 인스턴스가 여러 그룹에 동시에 속할 수 있어 매우 유연합니다.

마지막으로, filters 섹션이 불필요한 인스턴스를 제외하여 최종 inventory를 생성합니다. instance-state-name: running은 실행 중인 인스턴스만 포함하므로 stopped 상태의 서버에 배포를 시도하는 실수를 방지하고, tag:Managed: ansible은 명시적으로 Ansible 관리 대상으로 표시된 인스턴스만 선택하여 다른 팀의 서버에 실수로 접근하는 것을 막습니다.

여러분이 동적 Inventory를 사용하면 Auto Scaling 그룹의 인스턴스가 자동으로 배포 대상에 포함되고, 수동 관리로 인한 인적 오류가 제거되며, 태그 변경만으로 서버의 역할과 그룹 소속을 즉시 변경할 수 있습니다. 또한 ansible-inventory -i inventory/aws_ec2.yml --graph 명령으로 현재 구성을 시각적으로 확인하여 예상대로 그룹화되었는지 검증할 수 있습니다.

실전 팁

💡 AWS credentials는 환경 변수나 IAM role을 사용하고, inventory 파일에 직접 입력하지 마세요 (보안 위험)

💡 cache: yes와 cache_timeout을 설정하여 API 호출을 캐싱하면 대규모 인프라에서 실행 시간을 크게 단축할 수 있습니다

💡 compose 키를 사용해 인스턴스 메타데이터로부터 커스텀 변수를 생성할 수 있습니다 (예: ansible_host: private_ip_address)

💡 여러 클라우드를 사용한다면 inventory 디렉토리에 aws_ec2.yml, azure_rm.yml 등을 함께 두면 자동으로 병합됩니다

💡 --limit 옵션과 조합하여 특정 그룹이나 태그 조건의 서버만 대상으로 실행하면 더욱 세밀한 제어가 가능합니다


5. 멱등성 보장 - 안전한 반복 실행

시작하며

여러분이 배포 스크립트를 실행했는데 중간에 네트워크 오류로 실패했다면, 처음부터 다시 실행해도 안전한가요? 같은 작업을 두 번 실행했을 때 데이터가 중복 생성되거나 설정이 꼬이는 경험을 해보셨나요?

이런 문제는 자동화의 신뢰성을 떨어뜨립니다. 실패 후 재시도를 망설이게 되고, 수동으로 상태를 확인하고 복구하느라 자동화의 의미가 사라집니다.

바로 이럴 때 필요한 것이 Ansible의 멱등성(Idempotency) 원칙입니다. 같은 playbook을 여러 번 실행해도 항상 동일한 결과 상태를 보장하여 안전하게 재실행할 수 있습니다.

개요

간단히 말해서, 멱등성은 동일한 작업을 여러 번 수행해도 첫 실행 이후에는 아무것도 변경되지 않는 성질을 의미합니다. 실무에서 배포는 한 번에 성공하지 않을 수 있습니다.

네트워크 문제, 일시적인 서비스 장애, 권한 문제 등으로 중간에 실패할 수 있고, 이때 안전하게 재실행할 수 있어야 합니다. 예를 들어, 100개의 패키지를 설치하는 작업 중 50번째에서 실패했을 때, 처음부터 다시 실행해도 이미 설치된 49개는 건너뛰고 50번째부터 이어서 진행해야 효율적입니다.

기존에는 "이미 존재하는지" 확인하는 조건문을 직접 작성해야 했다면, 이제는 Ansible 모듈이 자동으로 현재 상태를 확인하고 필요한 경우에만 변경합니다. 이 개념의 핵심 특징은 첫째, 선언적 문법(원하는 최종 상태 기술), 둘째, 모듈의 자동 상태 확인, 셋째, changed 상태를 통한 실제 변경 여부 추적입니다.

이러한 특징들이 안정적이고 예측 가능한 자동화를 만들어줍니다.

코드 예제

---
# 멱등성을 보장하는 playbook 예제
- name: 웹 서버 설정
  hosts: webservers
  tasks:
    - name: Nginx 패키지 설치
      apt:
        name: nginx
        state: present  # "설치되어 있어야 함" (이미 있으면 skip)

    - name: 설정 디렉토리 생성
      file:
        path: /etc/nginx/sites-available
        state: directory  # "디렉토리여야 함" (이미 있으면 skip)
        mode: '0755'

    - name: 사용자 추가
      user:
        name: webuser
        state: present  # "존재해야 함" (이미 있으면 skip)
        shell: /bin/bash

    # 비멱등적 예제 (사용 금지)
    # - name: 로그 파일에 메시지 추가 (잘못된 방법)
    #   shell: echo "Deployed" >> /var/log/deploy.log
    #   # 실행할 때마다 중복 추가됨!

설명

이것이 하는 일: 멱등성 원칙은 Ansible이 각 작업 실행 전에 현재 시스템 상태를 확인하고, 이미 원하는 상태라면 작업을 건너뛰고, 다르다면 필요한 변경만 수행하여 항상 동일한 최종 상태를 보장합니다. 첫 번째로, state: present와 같은 선언적 매개변수가 "무엇을 할지"가 아니라 "어떤 상태여야 하는지"를 명시합니다.

apt 모듈은 Nginx 패키지를 설치하기 전에 이미 설치되어 있는지 확인하고, 있다면 아무것도 하지 않고 ok 상태를 반환합니다. 없다면 설치를 진행하고 changed 상태를 반환합니다.

이렇게 첫 실행에서는 패키지가 설치되지만, 두 번째 실행에서는 이미 있으므로 건너뜁니다. 그 다음으로, file 모듈이 실행되면서 디렉토리의 존재 여부뿐 아니라 권한까지 확인합니다.

/etc/nginx/sites-available 디렉토리가 존재하지만 권한이 0755가 아니라면, 모듈은 권한만 수정하고 changed를 반환합니다. 모든 조건이 이미 충족되어 있다면 ok를 반환하고 아무것도 변경하지 않습니다.

이는 "현재 상태와 원하는 상태의 차이만큼만 변경한다"는 원칙입니다. 마지막으로, user 모듈이 webuser 계정의 존재와 속성을 확인하여 필요한 경우에만 생성하거나 수정합니다.

주석 처리된 shell 명령은 멱등성을 보장하지 않는 나쁜 예시입니다. echo로 로그를 추가하면 실행할 때마다 같은 메시지가 계속 쌓이므로, 10번 실행하면 10개의 중복 로그가 생성됩니다.

여러분이 멱등성을 보장하는 코드를 작성하면 배포 실패 시 안심하고 재시도할 수 있고, 정기적인 설정 점검(configuration drift 방지)을 위해 cron으로 주기적 실행이 가능하며, 새로운 서버든 기존 서버든 동일한 playbook으로 관리할 수 있습니다. 또한 ansible-playbook --check 옵션으로 실제 변경 없이 시뮬레이션하여 어떤 변경이 발생할지 미리 확인할 수 있습니다.

실전 팁

💡 shell이나 command 모듈 사용 시 creates나 removes 매개변수를 추가하여 멱등성을 구현하세요 (파일 존재 여부로 실행 여부 판단)

💡 lineinfile이나 blockinfile 모듈을 사용하면 설정 파일 수정도 멱등적으로 처리할 수 있습니다 (같은 내용이 있으면 skip)

💡 changed_when: false를 사용하면 항상 실행되지만 changed로 표시되지 않는 정보 수집 작업을 만들 수 있습니다

💡 --diff 옵션으로 정확히 어떤 내용이 변경되는지 diff 형식으로 확인하여 예상치 못한 변경을 조기에 발견하세요

💡 register로 작업 결과를 저장하고 changed를 확인하여 후속 작업의 조건으로 사용하면 더욱 정교한 제어가 가능합니다


6. Ansible Vault를 통한 보안 관리 - 비밀 정보 보호

시작하며

여러분이 데이터베이스 비밀번호나 API 키를 playbook에 평문으로 저장하고 Git에 커밋한 적 있나요? 나중에 리포지토리가 public으로 변경되거나 퇴사자가 접근 권한을 유지하면 심각한 보안 사고로 이어질 수 있습니다.

이런 문제는 단순히 부주의가 아니라 구조적인 문제입니다. 자동화 코드와 비밀 정보를 분리하지 않으면, 코드 공유와 보안이 양립할 수 없게 됩니다.

바로 이럴 때 필요한 것이 Ansible Vault입니다. 민감한 정보를 암호화하여 안전하게 버전 관리하고, 필요한 사람에게만 복호화 키를 공유할 수 있습니다.

개요

간단히 말해서, Ansible Vault는 변수 파일이나 전체 playbook을 AES256 암호화하여 민감한 정보를 안전하게 저장하고 관리하는 기능입니다. 실무에서 인프라 코드에는 필연적으로 비밀번호, API 키, 인증서 등의 민감한 정보가 포함됩니다.

이런 정보를 평문으로 저장하면 코드 리뷰, 공유, 백업 과정에서 유출될 위험이 높습니다. 예를 들어, 운영 데이터베이스 비밀번호가 Git 히스토리에 남아 있으면, 히스토리를 삭제하지 않는 한 영구적으로 노출된 상태입니다.

기존에는 비밀 정보를 별도 파일로 분리하고 .gitignore에 추가했다면, 이제는 암호화된 상태로 안전하게 Git에 커밋할 수 있습니다. 이 개념의 핵심 특징은 첫째, AES256 암호화를 통한 강력한 보안, 둘째, 파일 단위 또는 개별 변수 단위 암호화 선택 가능, 셋째, 실행 시 자동 복호화입니다.

이러한 특징들이 보안과 편의성을 모두 제공합니다.

코드 예제

# group_vars/production/vault.yml 파일을 암호화
# ansible-vault create group_vars/production/vault.yml 명령 실행 후

# 암호화된 내용 (실제로는 이렇게 보임):
# $ANSIBLE_VAULT;1.1;AES256
# 66386439653966636239613833...

# 복호화하면 실제 내용:
---
vault_db_password: "SuperSecret123!"
vault_api_key: "sk-abc123xyz789"
vault_ssl_key_content: |
  -----BEGIN PRIVATE KEY-----
  MIIEvgIBADANBgkqhkiG9w0BAQE...
  -----END PRIVATE KEY-----

# group_vars/production/vars.yml (평문 파일)
---
db_host: "prod-db.example.com"
db_user: "appuser"
db_password: "{{ vault_db_password }}"  # vault에서 참조
api_key: "{{ vault_api_key }}"

# playbook 실행:
# ansible-playbook -i inventory/production site.yml --ask-vault-pass

설명

이것이 하는 일: Ansible Vault는 민감한 정보를 포함한 파일을 강력한 암호화 알고리즘으로 보호하고, playbook 실행 시 비밀번호나 키 파일로 자동 복호화하여 평문 정보처럼 사용할 수 있게 하면서도 저장 시에는 완전히 암호화된 상태를 유지합니다. 첫 번째로, ansible-vault create 명령이 새로운 암호화된 파일을 생성합니다.

명령 실행 시 vault 비밀번호를 입력하면, 이후 작성하는 모든 내용이 저장 시 자동으로 암호화됩니다. 파일을 열어보면 $ANSIBLE_VAULT로 시작하는 암호화된 텍스트만 보이므로, 이 파일을 Git에 커밋해도 비밀번호는 완전히 안전합니다.

ansible-vault edit 명령으로 언제든 내용을 수정할 수 있습니다. 그 다음으로, 평문 변수 파일(vars.yml)이 실행되면서 vault 파일의 변수를 Jinja2 문법으로 참조합니다.

db_password: "{{ vault_db_password }}"는 "vault.yml의 vault_db_password 값을 여기에 사용하라"는 의미입니다. 이렇게 분리하면 민감하지 않은 설정(db_host, db_user)은 평문으로 관리하여 코드 리뷰가 쉽고, 민감한 정보만 암호화하여 보안을 유지할 수 있습니다.

마지막으로, playbook 실행 시 --ask-vault-pass 옵션이 vault 비밀번호를 요청하고, Ansible이 자동으로 vault 파일을 복호화하여 최종적으로 db_password 변수에 실제 비밀번호 값을 할당합니다. 작업 실행 중에는 평문처럼 사용되지만, 메모리에서만 존재하고 디스크에는 절대 평문으로 저장되지 않습니다.

여러분이 Ansible Vault를 사용하면 민감한 정보를 안전하게 Git에 커밋하여 팀 전체가 공유할 수 있고, 권한이 있는 사람만 복호화 키를 가지므로 접근 제어가 가능하며, 비밀번호 변경 시 vault 파일만 수정하면 되어 관리가 간편합니다. 또한 --vault-password-file 옵션으로 스크립트에서 비밀번호를 읽어와 CI/CD 파이프라인에서도 자동화할 수 있습니다.

실전 팁

💡 vault 비밀번호를 .vault_pass 파일에 저장하고 .gitignore에 추가한 후 --vault-password-file .vault_pass로 사용하면 매번 입력할 필요가 없습니다

💡 ansible-vault encrypt_string 명령으로 개별 변수만 암호화하여 파일 전체가 아닌 특정 값만 보호할 수 있습니다

💡 여러 환경(dev, staging, prod)에 각각 다른 vault 비밀번호를 사용하여 권한을 세분화하세요

💡 vault_id를 사용하면 하나의 playbook에서 여러 vault 파일을 다른 비밀번호로 관리할 수 있습니다 (예: --vault-id prod@prompt)

💡 HashiCorp Vault나 AWS Secrets Manager와 연동하는 플러그인을 사용하면 중앙 집중식 비밀 관리 시스템과 통합할 수 있습니다


7. Handler와 Notify - 효율적인 서비스 재시작

시작하며

여러분이 설정 파일 10개를 수정하는 playbook을 작성했는데, 각 파일 수정 후마다 서비스를 재시작한다면 어떻게 될까요? 서비스가 10번 재시작되면서 불필요한 다운타임이 발생하고, 배포 시간이 길어집니다.

이런 문제는 특히 웹 서버나 데이터베이스처럼 재시작에 시간이 걸리는 서비스에서 심각합니다. 여러 작업이 같은 서비스에 영향을 줄 때, 모든 변경을 완료한 후 딱 한 번만 재시작하는 것이 효율적입니다.

바로 이럴 때 필요한 것이 Ansible의 Handler와 Notify 메커니즘입니다. 변경사항을 추적하고, 모든 작업이 끝난 후 필요한 서비스만 한 번씩 재시작합니다.

개요

간단히 말해서, Handler는 특정 조건(notify)이 발생했을 때만 실행되는 특수한 작업으로, 모든 일반 작업이 완료된 후 단 한 번만 실행됩니다. 실무에서 설정 변경은 연쇄적으로 일어납니다.

Nginx 설정 파일 수정, SSL 인증서 업데이트, 가상 호스트 추가 등 여러 작업이 모두 Nginx 재시작을 필요로 합니다. 각 작업마다 즉시 재시작하면 서비스가 여러 번 중단되고, 재시작 중에 다음 작업이 실행되어 오류가 발생할 수 있습니다.

예를 들어, 5개의 설정 파일을 수정하면서 Nginx가 5번 재시작되면 총 다운타임이 5배로 늘어납니다. 기존에는 마지막 작업에서만 서비스를 재시작하거나 수동으로 조건을 체크했다면, 이제는 notify와 handler가 자동으로 변경 여부를 추적하고 필요한 경우에만 재시작합니다.

이 개념의 핵심 특징은 첫째, 모든 task 완료 후 실행, 둘째, 여러 번 notify되어도 한 번만 실행, 셋째, changed 상태일 때만 트리거됩니다. 이러한 특징들이 서비스 재시작을 최적화하고 다운타임을 최소화합니다.

코드 예제

---
- name: 웹 서버 설정 업데이트
  hosts: webservers
  tasks:
    - name: Nginx 메인 설정 업데이트
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx  # 변경되면 handler 예약

    - name: SSL 인증서 배포
      copy:
        src: "{{ item }}"
        dest: /etc/nginx/ssl/
      loop:
        - cert.pem
        - key.pem
      notify: restart nginx  # 변경되면 handler 예약

    - name: 가상 호스트 설정 추가
      template:
        src: vhost.conf.j2
        dest: /etc/nginx/sites-enabled/myapp.conf
      notify: restart nginx  # 변경되면 handler 예약

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
      # 위 3개 작업 중 하나라도 changed면 한 번만 실행됨

설명

이것이 하는 일: Handler와 Notify 메커니즘은 작업이 실제로 변경을 일으켰을 때만(changed 상태) handler를 예약하고, 모든 일반 작업이 완료된 후 예약된 handler들을 중복 제거하여 각각 한 번씩만 실행함으로써 서비스 재시작을 최적화합니다. 첫 번째로, 각 task의 notify 키워드가 작업이 changed 상태를 반환할 때 지정된 handler를 실행 대기열에 추가합니다.

nginx.conf.j2 템플릿이 기존 파일과 다르면 template 모듈이 파일을 업데이트하고 changed를 반환하며, 이때 restart nginx handler가 예약됩니다. 파일이 이미 같다면 ok 상태를 반환하고 handler는 예약되지 않습니다.

그 다음으로, SSL 인증서와 가상 호스트 설정 작업도 실행되면서 각각 변경 여부를 확인하고 필요 시 같은 restart nginx handler를 예약합니다. 여기서 핵심은 같은 이름의 handler가 여러 번 예약되어도 중복이 제거된다는 점입니다.

3개 작업 모두 changed를 반환해도 restart nginx는 딱 한 번만 실행 대기열에 남습니다. 마지막으로, 모든 tasks가 완료된 후 handlers 섹션의 작업들이 순서대로 실행되어 최종적으로 Nginx를 한 번만 재시작합니다.

만약 3개 작업 중 하나도 변경되지 않았다면(모두 ok 상태) handler는 전혀 실행되지 않습니다. 이는 "설정이 이미 올바른 상태라면 서비스를 건드리지 않는다"는 원칙을 따릅니다.

여러분이 handler를 사용하면 여러 설정 변경 후 서비스를 딱 한 번만 재시작하여 다운타임을 최소화할 수 있고, 변경이 없을 때는 재시작하지 않아 안정성이 향상되며, 재시작 순서를 handlers 섹션에서 명시적으로 제어할 수 있습니다. 또한 meta: flush_handlers를 사용하면 모든 작업이 끝나기 전에 중간에 handler를 실행할 수도 있어 복잡한 시나리오에 대응할 수 있습니다.

실전 팁

💡 handler 이름은 notify에서 정확히 일치해야 하므로, 팀 내에서 명명 규칙을 정하세요 (예: "restart 서비스명" 형식)

💡 listen 키워드로 여러 handler를 그룹화하면 하나의 notify로 여러 handler를 트리거할 수 있습니다 (예: reload web services)

💡 handler도 role의 handlers/ 디렉토리에 정의할 수 있어 role의 재사용성을 높입니다

💡 handler 실행 중 실패하면 전체 playbook이 중단되므로, failed_when을 사용해 특정 오류는 무시하도록 설정할 수 있습니다

💡 --start-at-task 옵션으로 playbook을 중간부터 재실행할 때는 이전 notify가 무시되므로, 필요 시 --force-handlers로 강제 실행하세요


8. 템플릿과 Jinja2 - 동적 설정 파일 생성

시작하며

여러분이 50대의 웹 서버를 관리하는데, 각 서버마다 CPU 코어 수나 메모리 크기가 다르다면 설정 파일을 일일이 수작업으로 만들어야 할까요? 서버마다 최적화된 worker 프로세스 수나 메모리 제한을 설정하려면 수십 개의 파일을 관리해야 합니다.

이런 문제는 인프라가 이질적일수록 심각해집니다. 하드코딩된 설정 파일은 유연성이 없고, 서버 사양이 변경될 때마다 수동으로 업데이트해야 합니다.

바로 이럴 때 필요한 것이 Ansible의 템플릿 기능입니다. Jinja2 템플릿 엔진을 사용하여 변수, 조건문, 반복문으로 동적인 설정 파일을 생성할 수 있습니다.

개요

간단히 말해서, 템플릿은 변수와 로직이 포함된 파일 템플릿을 사용하여 각 호스트의 환경에 맞는 설정 파일을 자동으로 생성하는 기능입니다. 실무에서 설정 파일은 환경에 따라 달라져야 합니다.

개발 환경에서는 디버그 로깅을 활성화하고, 운영 환경에서는 성능을 최적화해야 합니다. 서버의 하드웨어 사양에 따라 worker 수나 메모리 할당도 조정해야 합니다.

예를 들어, 4코어 서버와 16코어 서버에 동일한 Nginx worker_processes를 설정하면 리소스 낭비나 성능 저하가 발생합니다. 기존에는 각 환경마다 별도의 정적 설정 파일을 유지했다면, 이제는 하나의 템플릿으로 모든 환경과 서버에 맞는 설정을 자동 생성할 수 있습니다.

이 개념의 핵심 특징은 첫째, Jinja2의 강력한 표현식(변수, 필터, 조건, 반복), 둘째, Ansible facts를 통한 시스템 정보 자동 수집, 셋째, 실행 시점의 동적 값 계산입니다. 이러한 특징들이 완전히 자동화된 설정 관리를 가능하게 합니다.

코드 예제

# templates/nginx.conf.j2
user www-data;
# 서버의 CPU 코어 수에 맞게 자동 설정
worker_processes {{ ansible_processor_vcpus }};

events {
    # 환경에 따라 다른 값 사용
    worker_connections {% if environment == 'production' %}2048{% else %}1024{% endif %};
}

http {
    # 조건부 로깅 설정
    {% if debug_mode | default(false) %}
    error_log /var/log/nginx/error.log debug;
    {% else %}
    error_log /var/log/nginx/error.log warn;
    {% endif %}

    # 변수 리스트 반복
    {% for domain in virtual_hosts %}
    server {
        server_name {{ domain.name }};
        root {{ domain.root }};
        listen {{ domain.port | default(80) }};
    }
    {% endfor %}
}

설명

이것이 하는 일: 템플릿 기능은 Jinja2 템플릿 파일에 정의된 로직과 변수를 각 호스트의 실제 값으로 치환하고, 조건문과 반복문을 평가하여 호스트별로 커스터마이즈된 최종 설정 파일을 생성한 후 대상 서버에 배포합니다. 첫 번째로, {{ ansible_processor_vcpus }} 같은 Ansible facts가 자동으로 수집된 시스템 정보로 치환됩니다.

ansible_processor_vcpus는 Ansible이 대상 서버에 접속하여 자동으로 수집한 CPU 코어 수입니다. 4코어 서버에서는 worker_processes 4로, 16코어 서버에서는 worker_processes 16으로 자동 생성되어 각 서버의 하드웨어를 최대한 활용합니다.

그 다음으로, {% if %} 조건문이 실행되면서 환경 변수에 따라 다른 설정을 적용합니다. environment == 'production'이 참이면 worker_connections 2048이 사용되고, 거짓이면 1024가 사용됩니다.

debug_mode | default(false)는 "debug_mode 변수가 정의되어 있으면 그 값을 사용하고, 없으면 false를 기본값으로 사용하라"는 의미입니다. 이렇게 하면 변수가 누락되어도 오류 없이 안전한 기본값으로 동작합니다.

마지막으로, {% for %} 반복문이 virtual_hosts 리스트를 순회하면서 여러 개의 server 블록을 자동 생성합니다. virtual_hosts: [{name: "example.com", root: "/var/www/example", port: 443}, {name: "test.com", root: "/var/www/test"}] 같은 변수가 있으면, 2개의 server 블록이 자동으로 만들어집니다.

{{ domain.port | default(80) }}은 "port가 정의되어 있으면 그 값을, 없으면 80을 사용하라"는 필터입니다. 여러분이 템플릿을 사용하면 수십 대의 서버에 각각 최적화된 설정을 자동으로 배포할 수 있고, 새로운 가상 호스트 추가 시 변수만 수정하면 설정이 자동 생성되며, 환경(dev/staging/prod)별 차이를 하나의 템플릿으로 관리할 수 있습니다.

또한 템플릿 변경 시 모든 서버에 일관되게 반영되어 설정 drift를 방지할 수 있습니다.

실전 팁

💡 복잡한 템플릿은 validate 매개변수로 배포 전 문법 검증을 하세요 (예: validate: 'nginx -t -c %s')

💡 Jinja2의 trim_blocks와 lstrip_blocks를 사용하면 생성된 파일의 공백을 깔끔하게 정리할 수 있습니다

💡 템플릿 내에서 {{ ansible_managed }} 변수를 주석으로 추가하면 "이 파일은 Ansible이 관리함"을 명시하여 수동 편집을 방지할 수 있습니다

💡 복잡한 로직은 템플릿이 아닌 custom filter를 Python으로 작성하여 가독성을 높이세요

💡 lookup 플러그인으로 외부 파일이나 환경 변수를 템플릿에서 직접 읽어올 수 있습니다 (예: {{ lookup('env', 'HOME') }})


9. 태스크 제어와 에러 핸들링 - 견고한 자동화

시작하며

여러분이 100대의 서버에 배포하다가 3번째 서버에서 오류가 발생했을 때, 나머지 97대도 모두 중단되어야 할까요? 또는 일시적인 네트워크 오류로 패키지 다운로드가 실패했을 때 바로 포기해야 할까요?

이런 문제는 대규모 인프라 관리에서 흔하게 발생합니다. 획일적인 에러 처리는 사소한 문제로 전체 배포를 중단시키거나, 반대로 심각한 오류를 무시하고 진행하는 위험을 초래합니다.

바로 이럴 때 필요한 것이 Ansible의 태스크 제어와 에러 핸들링 메커니즘입니다. 재시도, 조건부 실행, 선택적 오류 무시 등으로 상황에 맞는 대응이 가능합니다.

개요

간단히 말해서, 태스크 제어는 작업의 실행 조건, 실패 처리, 재시도 정책 등을 세밀하게 제어하여 예외 상황에서도 안정적으로 동작하는 playbook을 만드는 기법입니다. 실무에서 모든 작업이 항상 성공하는 것은 아닙니다.

외부 API가 일시적으로 응답하지 않거나, 특정 서버에만 필요한 패키지가 있거나, 선택적 최적화 작업이 실패해도 전체 배포는 계속되어야 하는 경우가 있습니다. 예를 들어, 오래된 로그 파일을 삭제하는 작업이 실패해도 애플리케이션 배포는 중단될 필요가 없습니다.

기존에는 오류 발생 시 무조건 중단되거나 모든 오류를 무시하는 극단적인 선택을 했다면, 이제는 작업별로 다른 에러 처리 전략을 적용할 수 있습니다. 이 개념의 핵심 특징은 첫째, when 조건으로 실행 여부 결정, 둘째, ignore_errors와 failed_when으로 실패 정의 커스터마이징, 셋째, until과 retries로 자동 재시도입니다.

이러한 특징들이 복잡한 실무 시나리오를 처리할 수 있게 해줍니다.

코드 예제

---
- name: 견고한 애플리케이션 배포
  hosts: webservers
  tasks:
    - name: 외부 API에서 설정 다운로드 (재시도)
      uri:
        url: https://api.example.com/config
        return_content: yes
      register: config_data
      until: config_data.status == 200
      retries: 5  # 5번 재시도
      delay: 10   # 10초 간격

    - name: 특정 OS에서만 패키지 설치
      apt:
        name: ubuntu-specific-package
      when: ansible_distribution == 'Ubuntu'  # 조건부 실행

    - name: 선택적 최적화 (실패해도 계속)
      command: /opt/optimize.sh
      ignore_errors: yes  # 실패 무시
      register: optimize_result

    - name: 최적화 실패 시 경고만 출력
      debug:
        msg: "최적화는 실패했지만 배포는 계속됩니다"
      when: optimize_result is failed

    - name: 애플리케이션 시작 검증
      uri:
        url: http://localhost:8080/health
      register: health_check
      failed_when: health_check.status != 200 or 'healthy' not in health_check.content

설명

이것이 하는 일: 태스크 제어 메커니즘은 각 작업이 실행되기 전에 조건을 평가하고, 실행 중 실패가 발생하면 재시도 정책에 따라 자동으로 재실행하며, 실패의 정의를 커스터마이즈하여 특정 상황에서는 실패로 간주하지 않도록 함으로써 유연하고 견고한 자동화를 구현합니다. 첫 번째로, until/retries/delay 조합이 일시적인 오류에 대한 자동 재시도를 구현합니다.

uri 모듈이 API를 호출하고 결과를 config_data에 저장하는데, until: config_data.status == 200 조건이 만족될 때까지 최대 5번 재시도합니다. 첫 시도가 실패하면 10초 대기 후 두 번째 시도, 이런 식으로 계속됩니다.

이는 네트워크 지터나 서비스 재시작 같은 일시적 문제를 자동으로 해결합니다. 그 다음으로, when 조건이 실행 전에 평가되어 특정 조건에서만 작업을 실행합니다.

ansible_distribution == 'Ubuntu' 조건은 대상 서버가 Ubuntu인 경우에만 참이 되어 패키지 설치가 진행됩니다. CentOS나 다른 배포판 서버에서는 이 작업이 완전히 건너뛰어집니다(skipped 상태).

이렇게 하면 하나의 playbook으로 이질적인 환경을 관리할 수 있습니다. 마지막으로, ignore_errors와 failed_when이 실패의 정의를 재정의합니다.

ignore_errors: yes는 /opt/optimize.sh가 0이 아닌 종료 코드를 반환해도 playbook을 중단하지 않습니다. 하지만 결과는 register로 저장되어 후속 작업에서 is failed 테스트로 확인할 수 있습니다.

failed_when은 더 정교하게 "status가 200이고 content에 'healthy'가 포함된 경우에만 성공"으로 정의하여, HTTP 200이어도 응답 내용이 비정상이면 실패로 처리합니다. 여러분이 이런 제어 기법을 사용하면 일시적인 오류로 인한 불필요한 배포 실패를 방지할 수 있고, 환경별 차이를 하나의 playbook으로 처리할 수 있으며, 중요한 작업과 선택적 작업을 구분하여 적절히 대응할 수 있습니다.

또한 block/rescue/always 구문으로 try-catch-finally 같은 복잡한 에러 처리 로직도 구현할 수 있습니다.

실전 팁

💡 max_fail_percentage를 설정하면 전체 호스트의 일정 비율 이상이 실패할 때만 중단하여 부분적 실패를 허용할 수 있습니다

💡 any_errors_fatal: yes로 하나의 호스트라도 실패하면 즉시 전체 중단하여 데이터베이스 마이그레이션 같은 중요 작업을 보호하세요

💡 changed_when: false로 정보 수집 작업이 changed로 표시되지 않게 하여 실제 변경사항을 명확히 파악할 수 있습니다

💡 assert 모듈로 사전 조건을 검증하면 "디스크 여유 공간이 충분한지" 같은 요구사항을 강제할 수 있습니다

💡 rescue 블록에서 롤백 작업을 정의하면 실패 시 자동으로 이전 상태로 복구할 수 있습니다


10. 성능 최적화 전략 - 대규모 인프라 관리

시작하며

여러분이 1000대의 서버에 Ansible playbook을 실행할 때, 한 대씩 순차적으로 처리하면 몇 시간이 걸릴까요? 기본 설정으로는 5대씩 병렬 처리하므로, 1000대를 처리하려면 엄청난 시간이 소요됩니다.

이런 문제는 인프라 규모가 커질수록 심각해집니다. 배포 시간이 길어지면 롤백이 어려워지고, 긴급 패치 적용이 지연되며, 전체 운영 효율이 떨어집니다.

바로 이럴 때 필요한 것이 Ansible의 성능 최적화 전략입니다. 병렬 처리, fact 캐싱, 파이프라이닝 등으로 실행 시간을 극적으로 단축할 수 있습니다.

개요

간단히 말해서, 성능 최적화는 Ansible의 기본 동작 방식을 조정하여 대규모 인프라에서도 빠르고 효율적으로 작업을 수행할 수 있게 하는 설정과 기법의 모음입니다. 실무에서 수백 대 이상의 서버를 관리할 때 기본 설정으로는 한계가 있습니다.

매번 모든 facts를 수집하면 네트워크 트래픽이 증가하고, SSH 연결을 반복적으로 생성/해제하면 오버헤드가 발생하며, 순차적 처리는 병렬 처리 가능한 리소스를 낭비합니다. 예를 들어, 각 서버에 10초씩 걸리는 작업을 1000대에 순차적으로 하면 거의 3시간이 걸리지만, 병렬도를 높이면 몇 분으로 단축할 수 있습니다.

기존에는 기본 설정으로 느린 배포를 감수했다면, 이제는 ansible.cfg 튜닝과 playbook 최적화로 10배 이상 빠르게 만들 수 있습니다. 이 개념의 핵심 특징은 첫째, forks 증가로 병렬 처리 강화, 둘째, fact 캐싱으로 중복 수집 방지, 셋째, pipelining으로 SSH 오버헤드 감소입니다.

이러한 특징들이 대규모 인프라에서도 실용적인 배포 시간을 가능하게 합니다.

코드 예제

# ansible.cfg 최적화 설정
[defaults]
# 병렬 처리 수 증가 (기본 5 -> 50)
forks = 50

# SSH 연결 재사용
[ssh_connection]
pipelining = True  # SSH 파이프라이닝 활성화
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

# Fact 캐싱 (Redis 사용)
[defaults]
gathering = smart  # 필요한 경우에만 수집
fact_caching = redis
fact_caching_timeout = 3600  # 1시간 캐시 유지
fact_caching_connection = localhost:6379:0

# 성능 최적화된 playbook 예제
---
- name: 대규모 배포
  hosts: all
  gather_facts: no  # facts가 불필요하면 수집 안 함
  strategy: free    # 호스트별 독립 실행 (기다리지 않음)
  tasks:
    - name: 필요한 fact만 수집
      setup:
        gather_subset:
          - '!all'
          - '!min'
          - network  # 네트워크 정보만

설명

이것이 하는 일: 성능 최적화 전략은 Ansible이 동시에 처리하는 호스트 수를 늘리고, SSH 연결을 재사용하여 네트워크 오버헤드를 줄이며, 이전에 수집한 facts를 캐싱하여 중복 작업을 제거함으로써 전체 실행 시간을 대폭 단축시킵니다. 첫 번째로, forks = 50 설정이 동시에 50대의 서버를 병렬 처리할 수 있게 합니다.

기본값 5와 비교하면 10배 빠릅니다. 물론 Ansible 컨트롤 노드의 CPU와 메모리, 네트워크 대역폭을 고려해야 하며, 너무 높이면 시스템에 부하를 줄 수 있습니다.

일반적으로 컨트롤 노드의 CPU 코어 수의 2-3배 정도가 적정합니다. 그 다음으로, pipelining = True와 ControlMaster 설정이 SSH 연결 오버헤드를 극적으로 감소시킵니다.

파이프라이닝은 여러 작업을 하나의 SSH 연결로 처리하여 매번 새로운 세션을 만들지 않습니다. ControlMaster는 SSH 연결을 60초간 재사용하여, 같은 호스트에 대한 후속 연결이 즉시 이루어집니다.

이는 특히 작업이 많은 playbook에서 효과가 큽니다. 마지막으로, fact 캐싱이 반복적인 playbook 실행 시 facts 수집 시간을 제거합니다.

gathering = smart는 캐시에 최신 facts가 있으면 수집을 건너뛰고, 없거나 만료되었으면 수집합니다. Redis에 1시간 동안 캐싱되므로, 같은 서버에 여러 번 배포할 때 첫 실행 이후는 facts 수집 시간이 0에 가깝습니다.

strategy: free는 각 호스트가 독립적으로 진행하여 느린 서버가 빠른 서버를 기다리게 하지 않습니다. 여러분이 이런 최적화를 적용하면 수백 대 서버 배포 시간을 몇 시간에서 몇 분으로 단축할 수 있고, 네트워크 트래픽과 시스템 부하를 크게 줄일 수 있으며, 긴급 패치를 빠르게 적용하여 보안 위협에 신속히 대응할 수 있습니다.

또한 ansible-playbook --syntax-check와 --check 모드로 실제 실행 전에 검증하여 불필요한 재실행을 방지할 수 있습니다.

실전 팁

💡 mitogen 플러그인을 사용하면 추가 설정 없이 5-10배 성능 향상을 얻을 수 있습니다 (SSH 오버헤드를 Python으로 대체)

💡 gather_subset으로 필요한 facts만 수집하면 네트워크 사용량을 크게 줄일 수 있습니다 (예: '!all,!min,network')

💡 async와 poll을 사용해 시간이 오래 걸리는 작업을 백그라운드로 실행하고 다른 작업을 동시에 진행하세요

💡 serial 키워드로 롤링 업데이트를 구현하면 전체 서비스 중단 없이 점진적으로 배포할 수 있습니다

💡 ansible-playbook --list-tasks와 --start-at-task로 불필요한 작업을 건너뛰어 테스트 시간을 단축하세요


11. 테스트와 검증 - 배포 전 안전성 확보

시작하며

여러분이 작성한 playbook을 운영 환경에 바로 실행하기 전에 걱정되지 않나요? 문법 오류나 로직 실수로 인해 실제 서비스에 장애가 발생하면 어떡하죠?

이런 문제는 복잡한 playbook일수록 심각합니다. 수백 줄의 코드와 여러 role이 조합된 경우 예상치 못한 부작용이 발생할 수 있고, 운영 환경에서 처음 발견되면 복구 시간과 비용이 엄청납니다.

바로 이럴 때 필요한 것이 Ansible의 테스트와 검증 도구들입니다. dry-run, syntax check, linting, 통합 테스트 등으로 배포 전에 문제를 발견할 수 있습니다.

개요

간단히 말해서, 테스트와 검증은 playbook을 실제 환경에 적용하기 전에 다양한 수준의 검사를 수행하여 문법 오류, 로직 실수, 잠재적 문제를 조기에 발견하고 수정하는 프로세스입니다. 실무에서 "내 컴퓨터에서는 잘 됐는데"는 통하지 않습니다.

개발 환경과 운영 환경의 미묘한 차이, 예상하지 못한 서버 상태, 권한 문제 등이 실제 배포 시 문제를 일으킵니다. 예를 들어, 변수명을 잘못 입력했거나, 파일 경로가 환경마다 다르거나, 방화벽 규칙으로 특정 포트가 막혀 있는 경우 미리 발견하지 못하면 배포 중 장애로 이어집니다.

기존에는 운영 환경에서 직접 실행해보며 문제를 발견했다면, 이제는 여러 단계의 자동화된 검증으로 배포 전에 대부분의 문제를 잡아낼 수 있습니다. 이 개념의 핵심 특징은 첫째, 다층 검증(문법 -> 정적 분석 -> 드라이런 -> 통합 테스트), 둘째, 안전한 시뮬레이션(--check 모드), 셋째, 지속적인 품질 관리(CI/CD 통합)입니다.

이러한 특징들이 프로덕션 배포의 위험을 최소화합니다.

코드 예제

# 1. 문법 검증
# ansible-playbook playbooks/site.yml --syntax-check

# 2. Dry-run (실제 변경 없이 시뮬레이션)
# ansible-playbook playbooks/site.yml --check --diff

# 3. Ansible Lint로 베스트 프랙티스 검증
# .ansible-lint 설정 파일
---
skip_list:
  - '306'  # 특정 규칙 무시

# 실행: ansible-lint playbooks/site.yml

# 4. Molecule을 통한 통합 테스트
# molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: ubuntu-test
    image: ubuntu:20.04
provisioner:
  name: ansible
  playbooks:
    converge: ../../playbooks/site.yml
verifier:
  name: ansible

# 5. Assert를 통한 사전 조건 검증
---
- name: 배포 전 검증
  hosts: webservers
  tasks:
    - name: 필수 조건 확인
      assert:
        that:
          - ansible_memtotal_mb >= 2048  # 최소 2GB 메모리
          - ansible_distribution in ['Ubuntu', 'CentOS']
          - ansible_python_version is version('3.6', '>=')
        fail_msg: "서버가 최소 요구사항을 충족하지 않습니다"
        success_msg: "모든 사전 조건 충족"

설명

이것이 하는 일: 테스트와 검증 프로세스는 여러 단계의 자동화된 검사를 통해 playbook의 문법 오류, 코딩 스타일 위반, 로직 실수, 환경 불일치를 배포 전에 발견하고, 실제 변경을 가하지 않는 시뮬레이션으로 예상 결과를 미리 확인하여 프로덕션 배포의 안정성을 보장합니다. 첫 번째로, --syntax-check가 가장 기본적인 YAML 문법과 Ansible 구조를 검증합니다.

들여쓰기 오류, 잘못된 모듈 이름, 필수 매개변수 누락 등을 즉시 발견합니다. 이는 가장 빠르게 실행되며(실제 연결 없음) 명백한 오류를 걸러냅니다.

CI/CD 파이프라인의 첫 단계로 적합합니다. 그 다음으로, --check --diff 옵션이 실제로 서버에 연결하여 시뮬레이션을 실행합니다.

--check는 모든 작업을 "실제로 실행했다면 무슨 일이 일어날지" 예측하지만 실제 변경은 하지 않습니다. --diff는 파일 변경사항을 unified diff 형식으로 보여줘 "정확히 어떤 줄이 변경될지" 미리 확인할 수 있습니다.

운영 배포 전 최종 확인으로 매우 유용합니다. Ansible Lint는 베스트 프랙티스와 일반적인 실수를 정적 분석으로 찾아냅니다.

"shell 대신 command를 사용하라", "become을 불필요하게 사용하지 마라" 같은 수백 가지 규칙을 자동으로 검사합니다. 팀 전체가 일관된 코딩 스타일을 유지하고, 잠재적 보안 문제를 조기에 발견할 수 있습니다.

Molecule은 Docker나 가상머신에 격리된 테스트 환경을 자동으로 생성하고, playbook을 실행한 후 결과를 검증하는 통합 테스트 프레임워크입니다. 실제 운영과 유사한 환경에서 end-to-end 테스트를 수행하여 "이 playbook이 실제로 원하는 상태를 만드는가"를 확인합니다.

마지막으로, assert 모듈이 런타임에 사전 조건을 강제합니다. 메모리가 부족한 서버, 지원하지 않는 OS 버전, 오래된 Python 등을 배포 초기에 감지하여 중간에 실패하는 것을 방지합니다.

that 조건이 모두 참이어야 진행되므로 "최소 요구사항"을 코드로 명시할 수 있습니다. 여러분이 이런 테스트 전략을 사용하면 프로덕션 장애를 90% 이상 사전에 방지할 수 있고, 코드 리뷰 품질이 향상되며, 새로운 팀원도 안전하게 playbook을 수정할 수 있습니다.

또한 CI/CD 파이프라인에 통합하여 모든 변경사항이 자동으로 검증되는 환경을 구축할 수 있습니다.

실전 팁

💡 pre-commit hook으로 ansible-lint를 Git 커밋 전에 자동 실행하여 잘못된 코드가 리포지토리에 들어가는 것을 원천 차단하세요

💡 --check 모드에서 일부 모듈(shell, command 등)은 정확한 예측이 불가능하므로 check_mode: no로 실제 실행하거나 skip하세요

💡 molecule test --all로 여러 플랫폼(Ubuntu, CentOS, Debian)에서 동시에 테스트하여 호환성을 보장하세요

💡 ansible-playbook --list-hosts, --list-tasks로 실행 전에 대상과 작업 목록을 확인하여 잘못된 타겟을 방지하세요

💡 TestInfra나 Serverspec 같은 인프라 테스트 도구와 조합하여 "배포 후 실제로 서비스가 작동하는지" 검증하세요


12. CI/CD 통합과 GitOps - 자동화의 자동화

시작하며

여러분이 매번 수동으로 ansible-playbook 명령을 실행하고, 배포 이력을 별도로 기록하고, 롤백이 필요하면 예전 커밋을 찾아 다시 실행하고 계신가요? 이런 수동 프로세스는 실수를 유발하고 추적을 어렵게 만듭니다.

이런 문제는 팀 규모가 커지고 배포 빈도가 높아질수록 심각해집니다. "누가, 언제, 무엇을 배포했는지" 추적이 안 되고, 동시에 여러 사람이 배포하면 충돌이 발생하며, 긴급 롤백 시 혼란이 생깁니다.

바로 이럴 때 필요한 것이 Ansible과 CI/CD의 통합입니다. Git 커밋을 트리거로 자동 배포하고, 모든 변경사항이 추적되며, 롤백은 Git revert만으로 가능해집니다.

개요

간단히 말해서, CI/CD 통합은 Ansible playbook을 Git으로 관리하고, 코드 변경 시 자동으로 테스트와 배포가 실행되도록 하여 수동 개입을 최소화하고 변경 이력을 완벽히 추적하는 GitOps 방식의 인프라 관리입니다. 실무에서 인프라 변경도 애플리케이션 코드처럼 관리되어야 합니다.

누가 언제 무엇을 왜 변경했는지 기록되고, 코드 리뷰를 거치며, 자동 테스트로 검증되고, 승인 후 자동 배포되는 체계가 필요합니다. 예를 들어, 새로운 보안 패치를 적용할 때 담당자가 수동으로 명령어를 실행하는 대신, Pull Request를 만들고 리뷰받고 머지하면 자동으로 모든 서버에 배포되는 방식이 훨씬 안전하고 추적 가능합니다.

기존에는 playbook을 로컬에서 실행하고 슬랙에 "배포 완료"라고 수동으로 알렸다면, 이제는 Git 커밋 하나로 테스트, 배포, 알림이 자동으로 이루어집니다. 이 개념의 핵심 특징은 첫째, Git을 단일 진실 공급원(Single Source of Truth)으로 사용, 둘째, 자동화된 테스트와 배포 파이프라인, 셋째, 감사 추적과 롤백의 용이성입니다.

이러한 특징들이 대규모 팀에서도 안전하고 효율적인 인프라 관리를 가능하게 합니다.

코드 예제

# .gitlab-ci.yml (GitLab CI/CD 파이프라인)
---
stages:
  - validate
  - test
  - deploy

variables:
  ANSIBLE_FORCE_COLOR: "true"
  ANSIBLE_HOST_KEY_CHECKING: "false"

# 1단계: 문법과 린트 검증
validate:
  stage: validate
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook --syntax-check playbooks/site.yml
    - ansible-lint playbooks/site.yml
  only:
    - merge_requests
    - master

# 2단계: Molecule 테스트
test:
  stage: test
  image: quay.io/ansible/molecule:latest
  script:
    - molecule test
  only:
    - merge_requests

# 3단계: 스테이징 배포 (자동)
deploy_staging:
  stage: deploy
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook -i inventory/staging playbooks/site.yml --vault-password-file $VAULT_PASS
  environment:
    name: staging
  only:
    - master

# 4단계: 프로덕션 배포 (수동 승인)
deploy_production:
  stage: deploy
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook -i inventory/production playbooks/site.yml --vault-password-file $VAULT_PASS
  environment:
    name: production
  when: manual  # 수동 승인 필요
  only:
    - master

설명

이것이 하는 일: CI/CD 통합은 Git 리포지토리의 변경사항을 감지하여 자동으로 파이프라인을 시작하고, 순차적으로 검증, 테스트, 배포 단계를 실행하며, 각 단계의 결과를 기록하고 실패 시 알림을 보내어 인프라 변경을 완전히 자동화하고 추적 가능하게 만듭니다. 첫 번째로, validate 단계가 모든 Pull Request와 master 브랜치 커밋에 대해 자동으로 실행됩니다.

Docker 컨테이너 안에서 ansible-playbook --syntax-check와 ansible-lint가 실행되어 문법 오류와 코딩 스타일 위반을 검출합니다. 이 단계가 실패하면 전체 파이프라인이 중단되어 잘못된 코드가 배포되지 않습니다.

개발자는 코드를 푸시한 지 몇 초 만에 피드백을 받습니다. 그 다음으로, test 단계가 Molecule을 사용한 통합 테스트를 실행합니다.

격리된 Docker 환경에서 실제 playbook을 실행하여 원하는 결과가 나오는지 검증합니다. 이는 Pull Request에만 실행되어 master에 머지되기 전에 품질을 보장합니다.

테스트가 통과해야만 리뷰어가 승인할 수 있도록 정책을 설정할 수 있습니다. deploy_staging 단계는 master 브랜치에 머지되면 자동으로 스테이징 환경에 배포합니다.

$VAULT_PASS는 GitLab의 Secret Variables에 암호화되어 저장된 vault 비밀번호로, 민감한 정보를 안전하게 관리합니다. environment: staging은 GitLab이 배포 이력을 추적하고 시각화할 수 있게 합니다.

마지막으로, deploy_production 단계는 when: manual로 설정되어 자동 실행되지 않고 사람의 승인을 기다립니다. 스테이징 배포가 성공하면 담당자가 GitLab UI에서 "Deploy to Production" 버튼을 클릭하여 수동으로 트리거합니다.

이는 프로덕션 배포에 대한 최종 관문으로, 의도하지 않은 자동 배포를 방지합니다. 여러분이 CI/CD를 통합하면 모든 인프라 변경이 Git 히스토리에 기록되어 "누가 왜 변경했는지" 영구적으로 추적되고, 문제 발생 시 git revert로 즉시 이전 상태로 롤백할 수 있으며, Pull Request 리뷰로 동료 검토를 강제하여 실수를 줄일 수 있습니다.

또한 Slack이나 Email 알림을 연동하여 배포 성공/실패를 팀 전체가 실시간으로 알 수 있습니다.

실전 팁

💡 GitLab/GitHub의 Protected Branches와 Required Reviews를 설정하여 master 브랜치에 직접 푸시를 막고 반드시 리뷰를 거치도록 강제하세요

💡 ArgoCD나 Flux 같은 GitOps 도구와 결합하면 Kubernetes 환경에서도 동일한 선언적 관리가 가능합니다

💡 파이프라인에 --check --diff를 먼저 실행하고 결과를 아티팩트로 저장하여 실제 배포 전에 변경사항을 리뷰할 수 있습니다

💡 Semantic Versioning과 Git tags를 사용하여 각 배포 버전을 명확히 관리하고, 특정 버전으로의 롤백을 쉽게 하세요

💡 Blue-Green 또는 Canary 배포 전략을 파이프라인에 통합하여 무중단 배포와 점진적 롤아웃을 구현하세요


#Ansible#Playbook#Inventory#Roles#Vault#Python

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.