Ansible 실전 · 플레이북을 시험한다 · 이론
린트·프로파일·예외, 그리고 관문
한 줄 요약
--syntax-check 는 YAML 이 말이 되는지까지만 보고, 품질은 ansible-lint 가 본다. 그리고 린트를 오래 살려 두는 비결은 규칙을 많이 켜는 것이 아니라 예외를 가장 좁은 범위에 두는 것이다.
왜 이게 필요했나
앤서블이 위험한 이유는 잘못 쓴 플레이북도 초록색으로 끝나기 때문이다. 이름 없는 태스크도, 파이프가 든 셸도, 권한을 정하지 않은 파일 쓰기도 전부 ok 나 changed 로 끝난다. 문제는 여섯 달 뒤에 온다.
- 리포트가 언제나
changed라 아무도 안 읽게 된다. 진짜 변경이 묻힌다. - 파일 권한을 적지 않아 서버마다
644와664가 섞인다. 대상의 umask 가 달랐던 것이다. - 실패한 태스크의 이름이
shell이라 로그를 봐도 무엇이 죽었는지 모른다. shell: a | b에서a가 죽어도 종료 코드는b의 것이라 실패가 성공으로 보고된다.
린트는 이 여섯 달을 커밋 직전으로 당겨 온다. 그런데 켜는 순간 팀은 곧바로 다음 문제를 만난다. 오래된 저장소에 린트를 걸면 지적이 수백 개 나오고, 그중 몇 개는 정말로 예외가 필요하다. 그때 예외를 어떻게 두느냐가 린트가 반년 뒤에도 살아 있을지를 정한다.
어떻게 동작하나
층이 다르다
| 도구 | 보는 것 | 못 보는 것 |
| --- | --- | --- |
| --syntax-check | YAML 구조, 플레이·태스크의 모양, 모듈 이름이 실재하는가 | 품질, 멱등성, 값이 말이 되는가 |
| ansible-lint | 이름·멱등성·권한·FQCN 같은 규약 | 실행 시점의 값 |
| assert 태스크 | 값이 말이 되는가(포트 범위, 환경 이름) | 정적인 규약 |
| molecule | 롤을 실제로 띄워 수렴·멱등성·검증 시나리오 | 위 셋을 대신하지는 않는다 |
아래로 갈수록 비싸다. 그래서 관문은 싼 것부터 돌린다.
규칙 id 와 프로파일
ansible-lint 의 지적에는 언제나 규칙 id 가 붙는다. name[play] 처럼 대괄호로 세부 항목을 가르기도 한다. 아래는 이 코스의 실습 이미지에 든 판(6.17.2)에서 실제로 나오는 것들이다.
| 규칙 id | 무엇을 잡나 |
| --- | --- |
| name[play] | 이름 없는 플레이 |
| name[missing] | 이름 없는 태스크 |
| name[casing] | 소문자로 시작하는 이름 |
| no-free-form | copy: src=a dest=b 처럼 한 줄로 적은 호출 |
| no-changed-when | 상태를 바꿀 수 있는 명령에 보고 기준이 없음 |
| risky-shell-pipe | 파이프를 쓰면서 pipefail 을 세우지 않음 |
| risky-file-permissions | 파일을 만들면서 mode 를 정하지 않음 |
| command-instead-of-module | 전용 모듈이 있는 명령을 셸로 부름 |
| fqcn[action-core] | 짧은 모듈 이름 |
규칙은 프로파일로 묶여 있다. min · basic · moderate · safety · shared · production 순으로 위로 갈수록 엄격하고, 위 프로파일은 아래의 규칙을 모두 포함한다. 그래서 오래된 저장소에 린트를 들일 때는 한 번에 production 으로 가지 않는다. basic 으로 걸어 CI 를 초록으로 만들고, 그다음 달에 moderate 로 올린다. 프로파일이 있는 이유가 이 이주 경로다.
판이 다르면 규칙 이름도 다르다. 그러니 언제나 ansible-lint -L 로 자기 판의 목록을 먼저 본다.
예외를 두는 두 자리
- name: Pack the release bundle # noqa: command-instead-of-module ansible.builtin.command: tar -czf /tmp/rel.tgz -C /srv app.conf changed_when: false# noqa: <규칙id> 는 그 태스크만 그 규칙에서 뺀다. 옆에 남아 있으므로 리뷰에서 "왜 뺐는가" 를 물을 수 있고, 이유가 사라지면 지울 수 있다.
# .ansible-lintprofile: productionexclude_paths: - legacy/skip_list: - name[casing]skip_list 는 저장소 전체에서 그 규칙을 끈다. 팀이 합의한 규약(예: 제품 이름이 소문자로 시작하는 태스크 이름을 허용한다)에만 쓴다. 지적이 많다고 여기에 규칙을 우르르 넣으면 남는 것은 "린트가 켜져 있다" 는 착각뿐이다.
exclude_paths 는 또 다르다. 규칙을 끄는 것이 아니라 그 경로를 아예 훑지 않는 것이다. 남이 만든 코드나 가르치려고 남겨 둔 나쁜 예를 넣는다. 다만 파일 이름을 직접 인자로 주면 이 목록은 무시된다 — 제외는 '훑을 때' 의 규칙이다.
설정 파일을 두는 진짜 이유는 편의가 아니라 사람의 손과 CI 가 같은 규칙으로 돌게 하는 것이다. CI 에서만 --profile production 을 주면 개발자는 통과한 줄 알고 밀고 CI 에서 떨어진다.
린트가 못 보는 자리 — assert
린트는 정적인 규약을 본다. 그런데 사고는 값에서도 난다. 포트에 80 이 들어오고, 환경 이름에 오타가 있고, 레플리카 수가 0 이 되는 일이다. 이것은 실행 시점에만 알 수 있으므로 ansible.builtin.assert 로 플레이북 스스로 묻게 한다.
- name: Assert that the port is usable ansible.builtin.assert: that: - app_port is integer - app_port >= 1024 fail_msg: "app_port must be an integer of 1024 or above, got {{ app_port }}"두 가지가 중요하다. 첫째, 아무것도 바꾸기 전에 묻는다. 절반쯤 배포하고 멈추면 되돌리는 일이 훨씬 비싸다. 둘째, fail_msg 를 반드시 쓴다. 없으면 실패 메시지가 조건식 원문으로 나와서 받는 사람이 무엇을 고쳐야 할지 모른다. 그리고 -e 로 넘긴 값은 따로 지정하지 않으면 문자열이다 — is integer 가 왜 거짓인지가 여기서 갈린다.
molecule 은 무엇을 더 해 주나
몰리는 롤을 시험하는 틀이다. 시나리오마다 대상을 띄우고(도커·포드먼·클라우드), 롤을 적용하고, 한 번 더 적용해 changed=0 인지(멱등성) 보고, 검증 플레이북으로 결과를 확인하고, 치운다. 린트가 못 보는 '정말 수렴하는가' 를 자동으로 본다.
이 실습 이미지에는 몰리가 들어 있지 않다. 실습 파드는 인터넷이 없어 설치할 수도 없고, 컨테이너를 띄우는 것도 막혀 있다. 그래서 이 모듈의 실습에서는 몰리를 빼고 그 앞단(문법·린트·전제 조건)만 세운다. 몰리를 쓰는 팀에서도 이 앞단은 그대로 필요하다 — 몰리는 몇 분이 걸리고 문법 검사는 1초도 안 걸리기 때문이다.
현장에서 만나는 모습
첫째, 도입은 언제나 '전부 고치기' 가 아니라 '더 나빠지지 않기' 로 시작한다. 낮은 프로파일로 걸어 CI 를 초록으로 만들고, 새 코드에만 높은 기준을 적용하고, 오래된 경로는 exclude_paths 에 넣어 두었다가 손댈 때 꺼낸다.
둘째, no-changed-when 지적의 절반은 진짜 결함이다. 조회 명령에 changed_when: false 를 붙이는 것은 형식을 맞추는 일이 아니라 리포트를 정직하게 만드는 일이다. 언제나 changed 인 플레이북은 정말 무언가 바뀐 날을 숨긴다.
셋째, 관문 스크립트는 반드시 막는 것까지 확인한다. 검사만 하고 언제나 0 으로 끝나는 스크립트가 실제로 흔하다. 그런 관문은 없는 것보다 나쁘다 — 검사하고 있다는 착각을 만들기 때문이다. 만든 날 일부러 나쁜 입력을 물려 0 이 아닌 값으로 끝나는지 보는 것이 그 스크립트의 첫 시험이다.
넷째, 예외에는 기한이 있다. # noqa 를 달 때 왜 달았는지를 같은 줄이나 바로 위에 한 줄로 적어 두면, 반년 뒤 그 이유가 사라졌을 때 지울 수 있다. 이유가 적혀 있지 않은 예외는 영원히 남는다.
참고 문서
- ansible-lint 규칙 목록: https://ansible.readthedocs.io/projects/lint/rules/
- 프로파일: https://ansible.readthedocs.io/projects/lint/profiles/
- 설정과 예외 처리: https://ansible.readthedocs.io/projects/lint/configuring/
- assert 모듈: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/assert_module.html
- Molecule: https://ansible.readthedocs.io/projects/molecule/
다음 실습에서 할 것
문법 검사를 통과하는 나쁜 플레이북을 일부러 쓰고, 린트가 잡는 규칙 id 를 뽑아 목록으로 남긴다. 같은 일을 하는 깨끗한 플레이북을 basic 프로파일까지 끌어올린 뒤 FQCN 과 mode 를 더해 production 까지 올린다. 모듈이 없는 명령 하나에 # noqa 로 한 줄만 예외를 두고, .ansible-lint 에 프로파일과 제외 경로와 건너뛸 규칙을 적어 저장소 전체를 한 번에 통과시킨다. assert 로 전제 조건을 막고, 마지막에 문법·린트·전제 조건을 한 번에 보는 관문 스크립트를 만들어 나쁜 디렉터리를 물렸을 때 정말 막는지까지 확인한다.