LabHub
배우기 러닝패스 코스

Ansible 기초 · 인벤토리가 여러 개일 때 · 이론

합쳐진 인벤토리 — 무엇이 이기고 그것을 어떻게 확인하나

LabHub 에서 이어서 보기

한 줄 요약

인벤토리 소스가 둘 이상이면 호스트와 그룹은 합쳐지고 같은 이름의 변수는 나중에 읽힌 것이 이긴다. 그 '나중' 을 정하는 것은 -i 를 적은 순서, 디렉터리 안의 파일 이름 순서, 그리고 그룹의 깊이와 사전순이다.

왜 이게 필요했나

인벤토리가 파일 하나일 때는 읽으면 안다. 그런데 파일 하나로 끝나는 조직은 드물다. 공통 호스트는 플랫폼 팀이 관리하고 서비스별 호스트는 각 팀이 관리한다. 운영과 스테이징은 파일을 나눠 실수로 섞이지 않게 한다. 클라우드 자원은 사람이 적는 대신 동적 인벤토리 플러그인이 만들어 낸다. 이렇게 되면 -i 는 두 번 이상 붙고, 어느 순간 아무도 "지금 web1 의 app_port 가 몇인가" 를 파일을 읽어서는 답할 수 없게 된다.

여기서 나는 사고는 두 종류다. 첫째는 엉뚱한 서버가 포함되는 것이다. 두 파일에 같은 그룹 이름이 있으면 호스트가 합쳐지므로, 스테이징 파일에만 있다고 믿었던 호스트가 운영 배포 대상에 들어간다. 둘째는 엉뚱한 값이 이기는 것이다. 옛 파일에 남아 있던 그룹 변수가 새 파일의 값을 덮거나, 반대로 새로 만든 group_vars/ 파일이 인벤토리 안의 값을 조용히 밀어낸다.

두 사고 다 "파일을 읽어서 추론한다" 는 방식으로는 못 막는다. 막는 방법은 하나다 — 합쳐진 결과를 실행 전에 기계에게 묻는 것.

어떻게 동작하나

-i 를 여러 번 주면

ansible-playbook -i a.ini -i b.ini site.yml 처럼 여러 번 줄 수 있다. 소스는 적은 순서대로 읽히고 결과는 하나의 인벤토리로 합쳐진다.

| 무엇 | 합쳐질 때 |
| --- | --- |
| 호스트 | 합집합. 같은 이름이면 한 호스트다 |
| 그룹 | 합집합. 같은 이름이면 구성원이 합쳐진다 |
| 같은 이름의 변수 | 나중에 읽힌 소스가 이긴다 |

같은 이름의 호스트가 두 파일에 있으면 두 대가 되는 것이 아니라 한 대로 합쳐진다. ansible_host 가 서로 다르게 적혀 있어도 마찬가지다 — 나중 것이 이기고, 앞의 값은 아무 경고 없이 사라진다. 인벤토리를 옮기다가 호스트 이름은 그대로 두고 주소만 바꾼 파일을 새로 추가했을 때 이 동작을 모르면 며칠을 헤맨다.

디렉터리를 주면

-i inventory/ 처럼 디렉터리를 주면 그 안의 파일들을 이름 순서대로 읽는다. 그래서 관례가 10-base, 20-prod, 30-overrides 처럼 숫자 접두사를 붙이는 것이다. 이름이 순서를 정하고, 순서가 승패를 정한다.

여기에 초보자를 반드시 한 번 잡는 함정이 있다. 디렉터리 인벤토리는 어떤 확장자를 기본으로 건너뛴다. 설정 항목 이름이 inventory_ignore_extensions 인데, 그 기본 목록에 .ini 가 들어 있다. .cfg, .retry, .md, .txt, 편집기 백업(.bak, 물결표로 끝나는 파일)도 마찬가지다.

그래서 inventory/hosts.ini-i inventory/hosts.ini 로는 읽히고 -i inventory/ 로는 읽히지 않는다. 아무 오류도 나지 않는다. 그냥 그 파일에 적힌 호스트가 없는 셈이 될 뿐이다. 디렉터리 안에 두는 INI 형식 인벤토리는 확장자를 떼거나(10-base) 다른 이름을 쓴다. YAML 인벤토리의 .yml·.yaml 은 목록에 없어 그대로 읽힌다.

디스크의 group_vars 와 host_vars

인벤토리 변수를 적는 자리는 둘이다. 하나는 인벤토리 파일 안([web:vars] 또는 YAML 의 vars:), 다른 하나는 인벤토리 옆의 group_vars/·host_vars/ 디렉터리다. 둘이 같은 변수를 정의하면 누가 이기는가.

inventory/  10-base            [web:vars] app_port=8080  group_vars/    all.yml          app_port: 9000   owner: platform    web.yml          app_port: 9090  host_vars/    web1.yml         app_port: 9999

결과는 이렇다. web1 은 9999, web2 는 9090, 다른 그룹의 호스트는 9000. 인벤토리 파일 안에 적은 8080 은 아무 데도 나타나지 않는다. 공식 문서의 우선순위 목록에서 "inventory file or script group vars" 가 "inventory group_vars/*" 보다 아래에 있기 때문이다. 같은 디렉터리에 있어도 파일 안의 그룹 변수가 가장 약하다는 것 — 이것이 실무에서 가장 자주 사람을 놀라게 하는 자리다.

순서를 한 줄로 외우면 이렇다. 인벤토리 파일 안 그룹 변수 → group_vars/all → group_vars/그룹 → 인벤토리 파일 안 호스트 변수 → host_vars/호스트. 뒤로 갈수록 좁고, 좁은 것이 이긴다.

group_vars/ 디렉터리는 인벤토리 옆플레이북 옆 두 곳에 있을 수 있고, 같은 이름이면 플레이북 옆이 이긴다. 두 곳에 나눠 두면 반년 뒤에 아무도 값을 못 찾으므로, 한 곳으로 정하고 팀 규칙으로 적어 두는 편이 낫다.

같은 깊이의 그룹이 겹칠 때

한 호스트가 여러 그룹에 속하고 그 그룹들이 같은 변수를 정의하면 어떻게 되나. 기본 규칙은 사전순으로 병합하고 뒤가 이긴다. alphazulu 에 모두 속한 호스트는 zulu 의 값을 갖는다. 사전순이 의미를 담고 있을 리 없으니, 이 기본값에 기대는 설계는 그 자체가 위험 신호다.

기본값을 뒤집는 손잡이가 ansible_group_priority 다. 기본값은 1 이고, 숫자가 클수록 나중에 병합되어 이긴다.

[alpha:vars]color=from-alphaansible_group_priority=10[zulu:vars]color=from-zulu

이렇게 두면 사전순으로는 뒤인 zulu 를 제치고 alpha 가 이긴다. 주의할 것이 하나 있다 — 이 변수는 인벤토리 소스 안에 적어야 듣는다. group_vars/alpha.yml 에 적으면 듣지 않는다. 그 파일들을 읽어 들이는 순서를 정하는 데 쓰이는 값이라, 그 파일 안에 적으면 이미 늦기 때문이다.

그리고 이 우선순위는 같은 깊이의 그룹 사이에서만 의미가 있다. 부모와 자식 사이는 깊이가 이긴다.

자식이 부모를 이긴다

[prod:children] 아래에 webdb 를 넣으면 webprod 의 자식이다. 두 그룹이 같은 변수를 정의하면 자식인 web 이 이긴다. 사전순과는 무관하다 — 부모 이름이 zprod 라 사전순으로 뒤여도 자식이 이긴다.

이 규칙은 자연스럽다. 부모는 넓고 자식은 좁으며, 언제나 좁은 것이 이긴다. prod 에 공통 기본값을 두고 web 에서 필요한 것만 덮어쓰는 설계가 그래서 성립한다. all 은 모든 그룹의 조상이므로 가장 넓고 가장 약하다.

합쳐진 결과를 묻는 법

인벤토리를 의심할 때 파일을 노려보는 것은 시간 낭비다. 답은 세 명령에 있다.

| 명령 | 답해 주는 것 |
| --- | --- |
| ansible-inventory -i ... --graph | 그룹 구조와 구성원. 어떤 호스트가 어느 그룹에 있나 |
| ansible-inventory -i ... --list | 합쳐진 전체를 JSON 으로. 스크립트로 검사하기 좋다 |
| ansible-inventory -i ... --host web1 | 그 호스트가 최종적으로 갖는 변수 전부 |

--host 는 특히 값을 셀 때 쓴다. "web1 의 app_port 는 결국 몇인가" 를 3초 만에 답한다. --graph --vars 를 주면 그래프에 변수까지 함께 보여 준다. CI 에 이 명령의 결과를 검사하는 단계를 하나 두면 "엉뚱한 서버가 포함됐다" 는 사고를 배포 전에 잡을 수 있다.

호스트 패턴 — 대상을 좁히는 문법

인벤토리가 커지면 "운영의 웹 서버 중 스테이징에 속하지 않은 것" 같은 대상이 필요해진다. 패턴 문법이 그 일을 한다.

| 기호 | 뜻 | 예 |
| --- | --- | --- |
| : | 합집합 | web:db |
| :& | 교집합 | web:&prod |
| :! | 차집합 | prod:!staging |
| * | 글롭 | web*.example.com |
| ~ | 정규식 (앞에 붙인다) | 정규식으로 이름 고르기 |

셋을 이어 붙이면 web:&prod:!staging 이 된다. web 이면서 prod 이고 staging 은 아닌 호스트다. 실행 전에 --list-hosts 로 반드시 확인한다. 특히 차집합은 뺀 결과가 비어 있어도 오류가 아니라서, 아무 대상 없이 초록불로 끝나는 배포가 여기서 나온다. --limit 도 같은 문법을 쓰며, 플레이북의 hosts: 에 적은 패턴을 다시 한 번 좁힌다.

동적 인벤토리는 이 그림의 어디에 있나

클라우드에서는 호스트 목록을 사람이 적지 않는다. aws_ec2, gcp_compute 같은 인벤토리 플러그인이 API 를 물어 호스트와 그룹을 만들어 낸다. 합쳐지는 규칙은 똑같다 — 동적 소스도 -i 에 적힌 하나의 소스일 뿐이고, 순서에 따라 정적 파일과 섞인다. 흔한 설계가 "호스트 목록은 동적 소스에서, 팀이 정하는 값은 정적 group_vars/ 에서" 인 이유가 여기 있다. 동적 소스가 그룹 이름만 만들어 주면 그 이름에 맞는 group_vars/ 파일이 값을 붙인다. (이 실습 환경에는 인터넷도 자격 증명도 없어 동적 플러그인을 돌려 볼 수는 없다. 규칙이 같다는 것만 기억하면 된다.)

현장에서 만나는 모습

사례 1 — 디렉터리가 파일 하나를 통째로 삼킨 날. 인벤토리를 디렉터리로 옮기면서 기존 hosts.ini 를 그대로 inventory/ 안에 넣었다. ansible-inventory --graph 를 돌려 보니 그룹이 절반만 나왔다. 아무 오류도 없었다. 원인은 .ini 확장자가 기본 무시 목록에 있다는 것이었고, 파일 이름에서 확장자를 떼자 그 자리에서 해결됐다. 그날 이후 인벤토리를 바꾸는 모든 PR 에 --graph 출력의 차이를 붙이는 규칙이 생겼다.

사례 2 — group_vars 를 만들었더니 값이 안 바뀌던 일. 반대 방향의 사고도 있었다. 급하게 포트를 바꾸려고 인벤토리 파일 안의 [web:vars] 를 고쳤는데 값이 안 바뀌었다. 몇 달 전 누군가 group_vars/web.yml 을 만들어 두었고, 그쪽이 더 셌다. 파일 안의 그룹 변수가 가장 약한 자리라는 것을 그때 배웠다. 지금은 인벤토리 파일에 변수를 아예 적지 않고 group_vars/ 한 곳만 쓴다. 자리를 줄이는 것이 우선순위를 외우는 것보다 낫다.

사례 3 — 사전순에 기대던 설정. appbackend 두 그룹에 같은 호스트가 있었고 둘 다 java_opts 를 정의했다. 사전순으로 backend 가 이기고 있었는데, 어느 날 그룹 이름을 app 에서 zapp 으로 바꾸는 정리 작업이 있었다. 이름만 바꿨는데 승자가 뒤집혔고 힙 설정이 통째로 달라졌다. 이런 자리에는 ansible_group_priority 로 의도를 명시하거나, 애초에 한 호스트가 같은 변수를 정의하는 두 그룹에 속하지 않게 설계를 고쳐야 한다.

다음 실습에서 할 것

인벤토리 파일 두 개를 만들어 -i 를 두 번 주는 것부터 시작한다. 같은 것을 디렉터리에 넣어 파일 이름 순서가 승패를 정하는 것을 확인하고, .ini 확장자 파일 하나를 함께 두어 그것이 조용히 건너뛰어진다는 사실을 직접 본다. 그다음 group_vars/all · group_vars/그룹 · host_vars/호스트 를 만들어 호스트마다 다른 값이 이기는 것을 재고, 인벤토리 파일 안의 변수가 그 셋보다 약하다는 것을 확인한다. 같은 깊이의 두 그룹에서 사전순을 ansible_group_priority 로 뒤집어 보고, 부모와 자식에서는 깊이가 이기는 것을 본다. 마지막에는 패턴으로 대상을 좁혀 결과를 파일로 남기고, "이 변수는 결국 어느 값이 이기는가" 를 답하는 도구를 직접 만든다.

참고 문서