serial 과 delegate_to - 배치로 쪼개고 남에게 시킨다
한 줄 요약
무중단 배포는 배포 도구가 해 주는 것이 아니라 플레이북을 어떻게 쪼개 쓰느냐의 문제이고, 그 손잡이가 호스트를 배치로 나누는 serial 과 일부 작업을 다른 기계에 시키는 delegate_to 다.
왜 이게 필요했나
기본 동작에서 Ansible 은 모든 호스트에 대해 태스크를 하나씩 순서대로 실행한다. 태스크 1 을 전 호스트에서 끝내고 태스크 2 로 넘어간다. 서비스를 재시작하는 태스크가 있다면 그 순간 전 호스트가 동시에 내려간다. 스무 대짜리 웹 티어라면 스무 대가 함께 죽는다.
그래서 "한 번에 몇 대까지만" 이라는 제한이 필요해진다. 그런데 제한만으로는 부족하다. 배포하는 서버를 로드밸런서에서 미리 빼지 않으면 사용자 요청이 그리로 계속 들어가고, 배포 뒤에 상태를 확인하지 않으면 망가진 판이 다음 배치로 번진다. 이 셋 - 쪼개기 · 빼고 넣기 · 배치마다 판정하기 - 이 갖춰져야 무중단이라 부를 수 있다.
무중단은 쪼개기, 빼고 넣기, 배치마다 판정하기가 갖춰져야 한다고 본문은 말한다. 웹 세 대와 로드밸런서 lb1 을 둔 설명용 예시이고, 한 배치가 도는 순서다.
- 로드밸런서에서 뺀다delegate_to 로 lb1 에게 지금 배포 중인 web1 을 빼라고 시킨다. 위임된 태스크 안에서도 inventory_hostname 은 web1 이라 한 줄로 쓸 수 있고, 로그에는 web1 -> lb1 처럼 두 이름이 함께 찍힌다.
- 배포한다이 배치의 호스트에만 새 판을 올린다. serial 이 호스트 목록을 배치로 나눠 두었으므로 같은 순간 내려가는 서버는 배치 크기를 넘지 않는다.
- 확인하고 되돌려 넣는다uri 나 wait_for 로 상태를 확인한다. 이 확인이 실패해야 max_fail_percentage 가 일할 수 있다. 통과하면 로드밸런서에 다시 넣고 다음 배치로 간다.
여기서 구분할 것 배치 크기는 취향이 아니라 몇 대까지 빠져도 되는가의 답이어야 한다. 스무 대에 25 퍼센트를 걸면 다섯 대가 동시에 빠지고, 남은 열다섯 대가 평시 트래픽을 감당해야 한다.
잠깐, 예측해 보세요 복귀 태스크를 플레이 맨 끝에만 두었는데 중간 배치의 확인이 실패해 플레이가 멈췄다. 직전에 로드밸런서에서 뺀 서버는 어디에 있을까?
설명 확인 · 채점 없는 자가 점검
로드밸런서 밖에 남는다. 본문은 빼고 나서 꼭 넣어야 한다고 말하며, 복귀 태스크를 block 과 always 안에 두거나 지금 빠져 있는 서버가 있는지 따로 확인하는 절차를 권한다.
어떻게 동작하나
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 로 사실이 누구 앞에 적히는지 본다. 그리고 가운데 한 대가 상태 확인에 실패하게 만들어 세 번째 배치가 시작조차 하지 않는 것을 확인한 뒤, 마지막에 빼기·배포·확인·복귀 한 바퀴를 요약 파일로 남긴다.
기본 동작은 태스크 하나를 전 호스트에서 끝낸 뒤 다음 태스크로 넘어가는 것이다. 서비스 재시작 태스크가 있으면 그 순간 전 호스트가 함께 내려간다. serial: 2 를 걸면 web1 과 web2 가 한 배치, web3 이 다음 배치다.
- serial 없이태스크마다 전 호스트가 함께 지나간다. 재시작 태스크 하나에서 세 대가 동시에 내려가고, 스무 대짜리 웹 티어라면 스무 대가 함께 죽는다.
- serial: 2플레이가 배치마다 처음부터 다시 도는 것처럼 동작한다. 로그에 PLAY 머리글이 배치 수만큼 찍히고, run_once 와 핸들러와 max_fail_percentage 가 플레이 전체가 아니라 배치 단위가 된다.
여기서 구분할 것 배치 크기를 목록으로 적으면 카나리가 된다. serial: [1, 100%] 는 한 대를 먼저 올려 본 뒤 나머지를 한 번에 보낸다. strategy: free 는 순서를 보장하지 않아 무중단 배포에는 맞지 않는다.
잠깐, 예측해 보세요 forks 가 20 이고 serial 이 5 인 플레이가 있다. 같은 순간에 일하는 호스트는 몇 대일까?
설명 확인 · 채점 없는 자가 점검
다섯 대다. 본문은 forks 와 serial 중 작은 쪽이 실제 동시 실행 수가 된다고 말한다. forks 는 컨트롤러가 동시에 다루는 호스트 수의 전역 설정이고, serial 은 플레이의 배치 크기다.