Ansible 실전 · 무중단 배포는 플레이북의 구조 문제다 · 이론
serial 과 delegate_to - 배치로 쪼개고 남에게 시킨다
한 줄 요약
무중단 배포는 배포 도구가 해 주는 것이 아니라 플레이북을 어떻게 쪼개 쓰느냐의 문제이고, 그 손잡이가 호스트를 배치로 나누는 serial 과 일부 작업을 다른 기계에 시키는 delegate_to 다.
왜 이게 필요했나
기본 동작에서 Ansible 은 모든 호스트에 대해 태스크를 하나씩 순서대로 실행한다. 태스크 1 을 전 호스트에서 끝내고 태스크 2 로 넘어간다. 서비스를 재시작하는 태스크가 있다면 그 순간 전 호스트가 동시에 내려간다. 스무 대짜리 웹 티어라면 스무 대가 함께 죽는다.
그래서 "한 번에 몇 대까지만" 이라는 제한이 필요해진다. 그런데 제한만으로는 부족하다. 배포하는 서버를 로드밸런서에서 미리 빼지 않으면 사용자 요청이 그리로 계속 들어가고, 배포 뒤에 상태를 확인하지 않으면 망가진 판이 다음 배치로 번진다. 이 셋 - 쪼개기 · 빼고 넣기 · 배치마다 판정하기 - 이 갖춰져야 무중단이라 부를 수 있다.
어떻게 동작하나
serial 은 플레이의 호스트 목록을 배치로 나눈다.
| 적는 법 | 세 대에 대한 결과 |
| --- | --- |
| serial: 2 | web1,web2 → web3 두 배치 |
| serial: "50%" | 두 대 → 한 대 |
| serial: [1, "100%"] | web1 하나 먼저(카나리), 그다음 나머지 전부 |
목록으로 적으면 카나리 배포가 된다. 한 대에 먼저 올려 보고 괜찮으면 나머지를 한 번에 보낸다. 마지막 원소가 남은 호스트보다 작으면 그 크기로 반복된다.
여기서 반드시 알아야 할 것이 있다. serial 을 쓰면 플레이가 배치마다 처음부터 다시 도는 것처럼 동작한다. 실행 로그에 PLAY [...] 머리글이 배치 수만큼 찍히는 것이 그 증거다. 그리고 그 결과로 세 가지가 배치 단위가 된다.
run_once: true는 플레이 전체가 아니라 배치마다 한 번 돈다.- 핸들러는 플레이 끝이 아니라 배치 끝에 발화한다.
max_fail_percentage는 배치마다 판정된다.
마지막 것이 가장 중요하다. serial: 1 과 max_fail_percentage: 0 을 함께 쓰면 "한 대라도 실패하면 다음 대로 넘어가지 않는다" 가 된다. 두 번째 대에서 실패하면 세 번째 대는 시작조차 하지 않고 로그에 NO MORE HOSTS LEFT 가 찍힌다. 사고를 세 대로 번지게 하지 않는 장치가 이것이다.
비슷해 보이는 any_errors_fatal: true 는 다른 물건이다. 이쪽은 "어느 호스트에서든 실패가 나면 그 자리에서 플레이 전체를 멈춘다" 이고, 비율로 여유를 주지 않는다. 스무 대 중 한 대쯤은 실패해도 계속 가고 싶다면 max_fail_percentage 를, 한 대라도 실패하면 즉시 멈춰야 한다면 any_errors_fatal 을 쓴다.
delegate_to 는 태스크 하나를 다른 호스트에서 실행한다. 대상 목록은 그대로 두고 실행만 옮기는 것이다.
- name: 로드밸런서에서 뺀다 community.general.haproxy: state: disabled host: "{{ inventory_hostname }}" delegate_to: lb1여기서 헷갈리기 쉬운 점이 하나 있다. 위임된 태스크 안에서도 inventory_hostname 은 원래 호스트를 가리킨다. 위 예에서 inventory_hostname 은 lb1 이 아니라 지금 배포 중인 web1 이다. 그래서 "로드밸런서에게 web1 을 빼라고 시킨다" 가 한 줄로 표현된다. 실행 로그에는 changed: [web1 -> lb1] 처럼 두 이름이 함께 찍힌다.
delegate_facts: true 를 함께 주면 그 태스크가 만든 사실이 원래 호스트가 아니라 위임 대상 앞으로 저장된다. 로드밸런서에서 한 번 조회한 값을 hostvars['lb1'] 로 모든 호스트가 함께 보게 하는 식이다. 이 옵션을 빼면 사실은 원래 호스트에 남고, hostvars['lb1'] 에는 아무것도 없다.
동시성 손잡이도 알아 둘 값어치가 있다. forks 는 컨트롤러가 동시에 몇 호스트를 다룰지 정하는 전역 설정이고, serial 은 플레이의 배치 크기다. 둘 중 작은 쪽이 실제 동시 실행 수가 된다. throttle 은 태스크 하나에만 거는 제한이라 "이 API 는 초당 몇 건까지" 같은 상황에 쓴다. strategy: free 는 호스트끼리 서로를 기다리지 않게 해서 빠른 호스트가 먼저 끝나게 하지만, 순서를 보장하지 않으므로 무중단 배포에는 맞지 않는다.
현장에서 만나는 모습
첫째, 배치 크기는 용량 계산이다. 스무 대에 serial: "25%" 를 걸면 다섯 대가 동시에 빠진다. 남은 열다섯 대가 평시 트래픽을 감당하지 못하면 그 배포 자체가 장애다. 배치 크기는 취향이 아니라 "몇 대까지 빠져도 되는가" 의 답이어야 한다.
둘째, 상태 확인 없는 배치는 의미가 없다. 배포 뒤에 uri 나 wait_for 로 확인하지 않으면 망가진 판이 그대로 다음 배치로 간다. 확인 태스크가 실패해야 max_fail_percentage 가 일할 수 있다.
셋째, 빼고 나서 꼭 넣어야 한다. 배포 중에 플레이가 실패하면 마지막으로 뺀 서버가 로드밸런서 밖에 남는다. 그래서 복귀 태스크는 block/always 안에 두거나, 최소한 "지금 빠져 있는 서버가 있는가" 를 따로 확인하는 절차가 필요하다.
넷째, 위임 대상도 인벤토리에 있어야 한다. delegate_to: lb1 은 lb1 이 인벤토리에 있을 때만 그 호스트의 연결 설정을 쓴다. 없으면 이름만으로 접속을 시도한다. 로드밸런서·배스천·모니터링 서버처럼 "대상은 아니지만 일을 시키는" 호스트도 인벤토리에 적어 두는 이유다.
다음 실습에서 할 것
세 대의 웹과 로드밸런서 한 대를 인벤토리에 적고, serial 로 배치를 나눠 실제로 몇 번 도는지 기록으로 확인한다. 카나리 배치를 목록으로 만들고, run_once 와 핸들러가 배치마다 도는 것을 산출물로 세고, delegate_to 로 빼고 넣는 기록을 로드밸런서 쪽에 남기고, delegate_facts 로 사실이 누구 앞에 적히는지 본다. 그리고 가운데 한 대가 상태 확인에 실패하게 만들어 세 번째 배치가 시작조차 하지 않는 것을 확인한 뒤, 마지막에 빼기·배포·확인·복귀 한 바퀴를 요약 파일로 남긴다.
참고 문서
- [Controlling playbook execution: strategies and more](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_strategies.html)
- [Controlling where tasks run: delegation and local actions](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_delegation.html)
- [Error handling in playbooks](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html)
- [Playbook keywords](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html)
- [Handlers: running operations on change](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_handlers.html)