플레이북 예시: 지속적 전달과 롤링 업그레이드

플레이북 예시: 지속적 전달과 롤링 업그레이드 (Playbook Example: Continuous Delivery and Rolling Upgrades)

소프트웨어를 업데이트할 때 다운타임 없이 안전하게 배포하고 싶은 건 모든 운영자들의 바람이에요. 이 문서는 Ansible의 가장 완성도 높은 예시 중 하나인 lamp_haproxy 플레이북을 템플릿으로 삼아, 웹 애플리케이션 스택을 무중단(제로 다운타임) 롤링 업그레이드하는 방법을 상세히 설명합니다.

출처: 문서

본문

지속적 전달(CD)이란? (What is continuous delivery?)

지속적 전달(CD)은 소프트웨어 애플리케이션에 업데이트를 자주 제공하는 것을 의미해요. 더 자주 업데이트하면 특정 시간대를 기다릴 필요가 없고, 조직은 변화에 대응하는 과정에 더 익숙해집니다.

일부 Ansible 사용자는 최종 사용자에게 매시간 또는 그보다 더 자주, 때로는 승인된 코드 변경이 있을 때마다 업데이트를 배포해요. 이를 달성하려면 업데이트를 신속하게 무중단 방식으로 적용할 수 있는 도구가 필요합니다.

이 문서는 Ansible의 가장 완전한 예시 플레이북 중 하나인 lamp_haproxy를 템플릿으로 사용해 이 목표를 어떻게 달성하는지 자세히 설명합니다. 이 예시는 롤, 템플릿, 그룹 변수 등 많은 Ansible 기능을 사용하며, 웹 애플리케이션 스택의 무중단 롤링 업그레이드를 할 수 있는 오케스트레이션 플레이북도 함께 제공합니다.

플레이북은 Apache, PHP, MySQL, Nagios, HAProxy를 CentOS 기반 서버 집합에 배포해요. 이 플레이북을 어떻게 실행하는지는 여기서 다루지 않습니다(실행 방법은 GitHub 프로젝트의 포함된 README와 예시를 읽으세요). 대신 플레이북의 각 부분을 자세히 살펴보고 무엇을 하는지 설명할게요.

사이트 배포 (Site deployment)

site.yml부터 시작해 봅시다. 이것은 사이트 전체 배포 플레이북으로, 사이트를 초기에 배포하고 모든 서버에 업데이트를 푸시하는 데 사용할 수 있어요:

---
# This playbook deploys the whole application stack in this site.

# Apply common configuration to all hosts
- hosts: all

  roles:
  - common

# Configure and deploy database servers.
- hosts: dbservers

  roles:
  - db

# Configure and deploy the web servers. Note that we include two roles
# here, the 'base-apache' role which simply sets up Apache, and 'web'
# which includes our example web application.

- hosts: webservers

  roles:
  - base-apache
  - web

# Configure and deploy the load balancer(s).
- hosts: lbservers

  roles:
  - haproxy

# Configure and deploy the Nagios monitoring node(s).
- hosts: monitoring

  roles:
  - base-apache
  - nagios

참고 (Note)

playbook이나 play 같은 용어에 익숙하지 않다면 "플레이북 작업(Working with playbooks)"을 먼저 확인하세요.

이 플레이북에는 5개의 플레이가 있어요. 첫 번째는 all 호스트를 대상으로 common 롤을 모든 호스트에 적용합니다. 이는 yum 저장소 구성, 방화벽 구성, 그리고 모든 서버에 적용해야 하는 그 외 모든 것들을 위한 것입니다.

다음 네 개의 플레이는 특정 호스트 그룹을 대상으로 해당 서버에 특정 롤을 적용해요. Nagios 모니터링, 데이터베이스, 웹 애플리케이션을 위한 롤과 함께, 기본 Apache 구성을 설치하고 구성하는 base-apache 롤을 구현했어요. 이 롤은 샘플 웹 애플리케이션과 Nagios 호스트에서 모두 사용됩니다.

재사용 가능한 콘텐츠: 롤 (Reusable content: roles)

이쯤이면 Ansible에서 롤이 무엇이고 어떻게 동작하는지 어느 정도 이해했을 거예요. 롤은 tasks, handlers, templates, files 같은 콘텐츠를 재사용 가능한 구성 요소로 조직하는 방법입니다.

이 예시에는 common, base-apache, db, haproxy, nagios, web 여섯 개의 롤이 있어요. 롤을 어떻게 조직하는지는 여러분과 애플리케이션에 달려 있지만, 대부분의 사이트는 모든 시스템에 적용되는 하나 이상의 공통 롤을 두고, 그다음 사이트의 특정 부분을 설치·구성하는 일련의 애플리케이션별 롤을 둡니다.

롤은 변수와 의존성을 가질 수 있고, 롤에 매개변수를 전달해 동작을 수정할 수 있어요. 롤에 대한 자세한 내용은 "롤(Roles)" 섹션에서 읽을 수 있습니다.

구성: 그룹 변수 (Configuration: group variables)

그룹 변수는 서버 그룹에 적용되는 변수예요. 템플릿과 플레이북에서 동작을 사용자화하고, 쉽게 변경할 수 있는 설정과 매개변수를 제공하는 데 사용됩니다. 인벤토리와 같은 위치의 group_vars 디렉터리에 저장됩니다.

다음은 lamp_haproxy의 group_vars/all 파일입니다. 예상대로 이 변수들은 인벤토리의 모든 머신에 적용됩니다:

---
httpd_port: 80
ntpserver: 192.0.2.23

이것은 YAML 파일로, 더 복잡한 변수 구조를 위해 리스트와 딕셔너리를 만들 수 있어요. 여기서는 웹 서버 포트와, 시간 동기화에 사용할 NTP 서버라는 두 변수만 설정하고 있습니다.

다음은 또 다른 그룹 변수 파일입니다. group_vars/dbservers로, dbservers 그룹의 호스트에 적용됩니다:

---
mysqlservice: mysqld
mysql_port: 3306
dbuser: root
dbname: foodb
upassword: usersecret

예시를 보면 webservers 그룹과 lbservers 그룹에 대한 그룹 변수도 비슷하게 있어요.

이 변수들은 다양한 곳에서 사용됩니다. 다음과 같이 플레이북에서 사용할 수 있어요(roles/db/tasks/main.yml):

- name: Create Application Database
  mysql_db:
    name: "{{ dbname }}"
    state: present

- name: Create Application DB User
  mysql_user:
    name: "{{ dbuser }}"
    password: "{{ upassword }}"
    priv: "*.*:ALL"
    host: '%'
    state: present

다음과 같이 템플릿에서도 사용할 수 있어요(roles/common/templates/ntp.conf.j2):

driftfile /var/lib/ntp/drift

restrict 127.0.0.1
restrict -6 ::1

server {{ ntpserver }}

includefile /etc/ntp/crypto/pw

keys /etc/ntp/keys

템플릿과 변수 모두에서 {{ }} 변수 치환 문법이 같다는 걸 알 수 있어요. 중괄호 안의 문법은 Jinja2이며, 내부 데이터에 온갖 연산을 수행하고 다양한 필터를 적용할 수 있습니다. 템플릿에서는 for 루프와 if 문을 사용해 더 복잡한 상황도 처리할 수 있어요. roles/common/templates/iptables.j2에서처럼:

{% if inventory_hostname in groups['dbservers'] %}
-A INPUT -p tcp  --dport 3306 -j  ACCEPT
{% endif %}

이것은 현재 작업 중인 머신의 인벤토리 이름(inventory_hostname)이 인벤토리 그룹 dbservers에 존재하는지 테스트합니다. 존재하면 그 머신은 포트 3306에 대한 iptables ACCEPT 라인을 받게 됩니다.

같은 템플릿의 또 다른 예시:

{% for host in groups['monitoring'] %}
-A INPUT -p tcp -s {{ hostvars[host].ansible_default_ipv4.address }} --dport 5666 -j ACCEPT
{% endfor %}

이것은 monitoring 그룹의 모든 호스트를 반복하며, 각 모니터링 호스트의 기본 IPv4 주소에 대한 ACCEPT 라인을 현재 머신의 iptables 구성에 추가해 Nagios가 그 호스트들을 모니터링할 수 있게 합니다.

Jinja2와 그 기능에 대해 훨씬 더 많이 배울 수 있으며, Ansible 변수 전반에 대해서는 "변수 사용(Using variables)" 섹션에서 더 읽을 수 있습니다.

롤링 업그레이드 (The rolling upgrade)

이제 웹 서버, 로드 밸런서, 모니터링이 완전히 배포된 사이트가 있습니다. 어떻게 업데이트할까요? 여기서 Ansible의 오케스트레이션 기능이 등장합니다. 일부 애플리케이션은 '오케스트레이션'을 단순한 순서 지정이나 명령 폭격(command-blasting)으로 의미하지만, Ansible은 오케스트레이션을 '악단처럼 머신을 지휘하는 것'이라고 말하며 이를 위한 상당히 정교한 엔진을 가지고 있습니다.

Ansible은 다중 계층 애플리케이션에 대해 조정된 방식으로 작업을 수행할 수 있어, 웹 애플리케이션의 정교한 무중단 롤링 업그레이드를 쉽게 오케스트레이션할 수 있어요. 이것은 rolling_update.yml이라는 별도의 플레이북으로 구현됩니다.

플레이북을 보면 두 개의 플레이로 구성되어 있음을 알 수 있어요. 첫 번째 플레이는 매우 단순하며 다음과 같습니다:

- hosts: monitoring
  tasks: []

무슨 일이 일어나고 왜 작업이 없을까요? Ansible은 서버를 작업하기 전에 서버에서 "팩트(facts)"를 수집한다는 점을 알고 있을 거예요. 이 팩트는 네트워킹 정보, OS/배포판 버전 등 모든 종류의 것에 유용합니다. 이 경우 업데이트를 수행하기 전에 환경의 모든 모니터링 서버에 대해 무언가를 알아야 하므로, 이 단순한 플레이는 모니터링 서버에서 팩트 수집 단계를 강제합니다. 이 패턴을 가끔 볼 수 있는데, 알아두면 유용한 트릭입니다.

다음 부분은 업데이트 플레이입니다. 첫 부분은 다음과 같습니다:

- hosts: webservers
  user: root
  serial: 1

이것은 webservers 그룹에서 동작하는 일반적인 플레이 정의예요. serial 키워드는 Ansible이 한 번에 몇 대의 서버에서 동작할지 알려줍니다. 지정되지 않으면 Ansible은 구성 파일에 지정된 기본 "forks" 한도까지 이 작업을 병렬화합니다. 하지만 무중단 롤링 업그레이드에서는 한 번에 그렇게 많은 호스트에서 동작하고 싶지 않을 수 있어요. 웹 서버가 몇 대뿐이라면 serial을 1로 설정해 한 번에 한 호스트씩 처리하고 싶을 것입니다. 100대라면 serial을 10으로 설정해 한 번에 10대씩 처리할 수도 있겠죠.

업데이트 플레이의 다음 부분:

pre_tasks:
- name: disable nagios alerts for this host webserver service
  nagios:
    action: disable_alerts
    host: "{{ inventory_hostname }}"
    services: webserver
  delegate_to: "{{ item }}"
  loop: "{{ groups.monitoring }}"

- name: disable the server in haproxy
  shell: echo "disable server myapplb/{{ inventory_hostname }}" | socat stdio /var/lib/haproxy/stats
  delegate_to: "{{ item }}"
  loop: "{{ groups.lbservers }}"

참고 (Note)

serial 키워드는 플레이를 '배치(batch)' 단위로 실행하도록 강제해요. 각 배치는 호스트의 부분집합을 가진 완전한 플레이로 간주됩니다. 이는 플레이 동작에 몇 가지 결과를 가져옵니다. 예를 들어 배치의 모든 호스트가 실패하면 플레이가 실패하고, 이는 전체 실행을 실패시킵니다. max_fail_percentage와 결합할 때 이를 고려해야 합니다.

pre_tasks 키워드는 롤이 호출되기 전에 실행할 작업을 나열하게 해줘요. 잠시 후에 더 명확해질 거예요. 이 작업들의 이름을 보면 현재 업데이트 중인 웹 서버에 대해 Nagios 알림을 비활성화하고, 그 웹 서버를 HAProxy 로드 밸런싱 풀에서 제거하고 있음을 알 수 있습니다.

delegate_toloop 인자를 함께 사용하면 Ansible은 각 모니터링 서버와 로드 밸런서를 반복하며, 그 모니터링 또는 로드 밸런싱 서버에서 웹 서버 "대신" 그 작업을 수행(위임)해요. 프로그래밍 용어로, 바깥 루프는 웹 서버 목록이고 안쪽 루프는 모니터링 서버 목록입니다.

HAProxy 단계가 조금 복잡해 보일 수 있어요. 이 예시에서 HAProxy를 사용하는 이유는 무료로 사용할 수 있기 때문입니다. 인프라에 (예를 들어) F5나 Netscaler가 있거나 AWS Elastic IP 설정이 있다면 Ansible 모듈로 통신할 수 있습니다. nagios 대신 다른 모니터링 모듈을 사용하고 싶을 수도 있지만, 이것은 'pre tasks' 섹션의 주요 목표, 즉 서버를 모니터링에서 빼고 로테이션에서 빼는 것을 보여줍니다.

다음 단계는 웹 서버에 적절한 롤을 다시 적용하는 것입니다. 이렇게 하면 webbase-apache 롤의 구성 관리 선언이 웹 서버에 적용되며, 웹 애플리케이션 코드 자체의 업데이트도 포함됩니다. 꼭 이렇게 해야 하는 것은 아니고 순수하게 웹 애플리케이션만 업데이트할 수도 있지만, 이것은 롤을 사용해 작업을 재사용하는 좋은 예시입니다:

roles:
- common
- base-apache
- web

마지막으로 post_tasks 섹션에서 Nagios 구성에 대한 변경을 되돌리고 웹 서버를 로드 밸런싱 풀에 다시 넣습니다:

post_tasks:
- name: Enable the server in haproxy
  shell: echo "enable server myapplb/{{ inventory_hostname }}" | socat stdio /var/lib/haproxy/stats
  delegate_to: "{{ item }}"
  loop: "{{ groups.lbservers }}"

- name: re-enable nagios alerts
  nagios:
    action: enable_alerts
    host: "{{ inventory_hostname }}"
    services: webserver
  delegate_to: "{{ item }}"
  loop: "{{ groups.monitoring }}"

다시 말하지만, Netscaler나 F5, Elastic Load Balancer를 사용한다면 적절한 모듈을 대신 사용하면 됩니다.

다른 로드 밸런서 관리하기 (Managing other load balancers)

이 예시에서는 웹 서버 앞에 단순한 HAProxy 로드 밸런서를 사용해요. 구성하고 관리하기 쉽습니다. 앞서 언급했듯 Ansible은 Citrix NetScaler, F5 BigIP, Amazon Elastic Load Balancers 등 다양한 다른 로드 밸런서를 지원합니다.

다른 로드 밸런서의 경우 셸 명령을 보내야 할 수도 있고(위에서 HAProxy에 했던 것처럼), 로드 밸런서가 API를 노출하면 API를 호출해야 할 수도 있어요. Ansible이 모듈을 가진 로드 밸런서는 API에 접속한다면 local_action으로 실행하고 싶을 수 있습니다. 로컬 액션에 대한 자세한 내용은 "작업 실행 위치 제어: 위임과 로컬 액션" 섹션에서 읽을 수 있습니다. 모듈이 없는 하드웨어를 위해 흥미로운 것을 개발하게 된다면 그건 훌륭한 기여가 될 거예요!

엔드투엔드 지속적 전달 (Continuous delivery end-to-end)

이제 애플리케이션에 업데이트를 배포하는 자동화된 방법이 생겼습니다. 이걸 어떻게 모두 연결할까요? 많은 조직은 Jenkins나 Atlassian Bamboo 같은 지속적 통합(CI) 도구를 사용해 개발, 테스트, 릴리스, 배포 단계를 연결합니다. Gerrit 같은 도구를 사용해 애플리케이션 코드 자체나 Ansible 플레이북, 또는 둘 모두에 커밋에 코드 리뷰 단계를 추가하고 싶을 수도 있습니다.

환경에 따라 테스트 환경에 지속적으로 배포하고, 그 환경에 대해 통합 테스트 배터리를 실행한 다음 프로덕션에 자동으로 배포할 수도 있습니다. 또는 간단하게 롤링 업데이트를 사용해 요청 시 테스트나 프로덕션에 배포할 수도 있습니다. 이 모든 것은 여러분에게 달려 있어요.

지속적 통합 시스템과 통합하려면 ansible-playbook 명령줄 도구로 플레이북 실행을 쉽게 트리거할 수 있고, AWX를 사용한다면 tower-cli 명령이나 내장 REST API를 사용할 수 있습니다. (tower-cli의 'joblaunch' 명령은 REST API를 통해 원격 작업을 생성하며 꽤 멋집니다.)

이것은 Ansible로 다중 계층 애플리케이션을 구조화하고 그 앱에 대한 작업을 오케스트레이션하며, 궁극적으로 고객에게 지속적 전달을 하는 방법의 좋은 감을 줄 거예요. 롤링 업그레이드의 아이디어를 앱의 여러 다른 부분으로 확장할 수 있습니다. 프론트엔드 웹 서버를 애플리케이션 서버와 함께 추가하거나, SQL 데이터베이스를 NoSQL 데이터베이스로 교체할 수도 있죠. Ansible은 복잡한 환경을 쉽게 관리하고 일반적인 작업을 자동화하는 능력을 줍니다.

더 보기 (See also)

  • 플레이북 작업 — 플레이북 소개.
  • 롤 (Roles) — 플레이북 롤 소개.
  • 변수 사용 (Using variables) — Ansible 변수 소개.
  • Ansible.com: 지속적 전달 (Continuous Delivery) — Ansible로 지속적 전달 소개.

더 알아보기 (Learn more)

  • 이 예시에서 쓴 serial, delegate_to, pre_tasks/post_tasks 같은 기능의 세부 동작은 "플레이북 실행 제어: 전략과 그 외(playbooks_strategies)"와 "위임(playbooks_delegation)" 페이지에서 확인할 수 있어요.