LabHub
学习 学习路径 课程

Ansible 实战

在运行之前就拦住:语法、lint 与前置条件合成一道关卡

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

문법 검사가 통과시키는 나쁜 플레이북에서 출발해 ansible-lint 의 규칙과 프로파일로 한 층씩 끌어올리고, 예외를 가장 좁은 범위로 두는 법을 익히고, assert 로 전제 조건을 먼저 막고, 이 셋을 하나의 관문 스크립트로 묶습니다.

왜 중요한가

앤서블의 위험한 점은 잘못 쓴 플레이북도 잘 돌아간다는 것입니다. 이름 없는 태스크도, 파이프가 든 셸도, 권한을 안 정한 파일 쓰기도 전부 초록색으로 끝납니다. 문제는 여섯 달 뒤에 옵니다 — 리포트가 언제나 changed 라 아무도 안 읽게 되고, 서버마다 파일 권한이 달라지고, 실패한 태스크의 이름이 shell 이라 로그를 봐도 무엇이 죽었는지 모릅니다. 린트는 그 여섯 달을 커밋 직전으로 당겨 옵니다. 다만 린트를 켜는 순간 팀은 곧바로 다음 문제를 만납니다 — 지적이 수백 개 나오고, 그중 몇 개는 정말 예외가 필요합니다. 그때 저장소 전체에서 규칙을 끄는 것한 줄만 빼는 것을 구분하지 못하면 린트는 한 달 만에 껍데기가 됩니다. 이 실습은 그 구분과, 린트가 못 보는 것(값이 말이 되는가)을 assert 로 막는 자리까지 함께 세웁니다.

단계

  1. /root/anslint/ansible.cfg 의 기본 인벤토리를 ./inventory/hosts.ini 로 두고, 그 파일에 web 그룹의 web1·web2 를 적으세요 (둘 다 ansible_host=127.0.0.1 ansible_port=2222, [all:vars]ansible_userroot). /root/anslint/messy.yml 에는 이름 없는 플레이 하나에 태스크 셋을 적습니다 — 이름 없이 파이프가 든 shell 태스크, 소문자로 시작하는 이름의 command: mkdir -p 태스크, copy: content=... dest=... 처럼 한 줄로 적은 태스크입니다. ansible-playbook --syntax-check messy.yml 을 돌려 출력을 /root/anslint/out/syntax.txt 에 저장하세요.
  2. ansible-lint -f pep8 messy.yml 의 출력을 /root/anslint/out/lint_before.txt 에 저장하세요(한 줄에 지적 하나씩 나오는 형식입니다). 그다음 같은 파일을 JSON 형식으로 린트해 걸린 규칙 id 만 중복 없이 사전순으로 한 줄씩 /root/anslint/out/rules.txt 에 저장하세요.
  3. /root/anslint/site.yml 을 새로 쓰세요 — 이름 있는 플레이 하나에 이름 있는 태스크 셋입니다. 첫째는 /root/anslint/out/data 디렉터리를 만들고, 둘째는 /root/anslint/out/app.confport=8080 한 줄을 쓰고, 셋째는 /root/anslint/out/upper-<호스트이름>.txt 에 그 호스트 이름을 대문자로 한 줄 씁니다. 셸 명령은 하나도 쓰지 않습니다. ansible-lint --profile basic site.yml 이 통과해야 하고, 플레이북을 실제로 돌려 출력을 /root/anslint/out/run.txt 에 저장하세요.
  4. /root/anslint/site.yml 의 모든 모듈을 FQCN(ansible.builtin.<모듈>)으로 바꾸고, 파일과 디렉터리를 만드는 태스크마다 mode 를 적으세요. ansible-lint --profile production site.yml 이 통과해야 하고, 그 출력을 /root/anslint/out/lint_production.txt 에 저장하세요.
  5. /root/anslint/site.yml 에 네 번째 태스크를 더하세요 — ansible.builtin.commandtar -czf /root/anslint/out/bundle.tgz -C /root/anslint/out app.conf 를 실행하고 changed_when: false 를 붙입니다. 린트는 이 태스크를 command-instead-of-module 로 잡는데, unarchive 모듈은 푸는 일만 하고 묶지는 못하므로 여기서는 셸 명령이 맞습니다. 그 한 줄만 규칙에서 빼는 주석을 달아 --profile production 을 다시 통과시키고, 플레이북을 다시 돌려 묶음을 실제로 만드세요.
  6. /root/anslint/site.yml 에 다섯 번째 태스크를 더하세요 — 이름은 소문자로 시작하는 nginx health probe 이고, /root/anslint/out/health.txtok 한 줄을 씁니다. 그리고 /root/anslint/.ansible-lint 를 만들어 profile: production, exclude_pathsmessy.ymlout/, skip_listname[casing] 을 적으세요. 인자 없이 ansible-lint 를 돌려 디렉터리 전체가 통과하는 것을 확인하고 출력을 /root/anslint/out/lint_repo.txt 에 저장하세요.
  7. /root/anslint/checks.yml 을 만드세요 — localhost 에서 팩트를 모으지 않고, 플레이 변수로 app_port: 8080, app_env: staging, allowed_envs: [staging, prod] 를 둡니다. 첫 태스크는 app_port 가 정수이고 1024 이상 65535 이하인지 확인하고, 둘째 태스크는 app_envallowed_envs 안에 있는지 확인합니다. 둘 다 ansible.builtin.assert 로 쓰고 fail_msgsuccess_msg 를 답니다. 기본값 그대로 돌린 출력을 /root/anslint/out/assert_ok.txt 에, -e app_port=80 으로 덮어써 돌린 출력을 /root/anslint/out/assert_fail.txt 에 저장하세요.
  8. /root/anslint/gate.sh 를 만드세요 — 첫 인자로 받은 디렉터리(기본값은 현재 디렉터리)로 옮겨 가 세 가지를 차례로 확인합니다. 그 디렉터리 바로 아래의 *.yml 마다 문법 검사를 하고 OK syntax <파일> 또는 FAIL syntax <파일> 을 출력하고, 인자 없이 ansible-lint 를 돌려 OK lint 또는 FAIL lint 를 출력하고, checks.yml 이 있으면 그것을 돌려 OK assert 또는 FAIL assert 를 출력합니다. 하나라도 실패하면 0 이 아닌 값으로 끝납니다. 이 관문을 지금 디렉터리에서 돌려 출력을 /root/anslint/out/gate.txt 에 저장하세요.

참고

문법 검사를 통과하는 나쁜 플레이북

/root/anslint/ansible.cfg 의 기본 인벤토리를 ./inventory/hosts.ini 로 두고, 그 파일에 web 그룹의 web1·web2 를 적으세요 (둘 다 ansible_host=127.0.0.1 ansible_port=2222, [all:vars]ansible_userroot). /root/anslint/messy.yml 에는 이름 없는 플레이 하나에 태스크 셋을 적습니다 — 이름 없이 파이프가 든 shell 태스크, 소문자로 시작하는 이름의 command: mkdir -p 태스크, copy: content=... dest=... 처럼 한 줄로 적은 태스크입니다. ansible-playbook --syntax-check messy.yml 을 돌려 출력을 /root/anslint/out/syntax.txt 에 저장하세요.

--syntax-check 는 YAML 을 읽고 플레이·태스크 구조가 말이 되는지, 모듈 이름이 실재하는지까지만 봅니다. 그 이상은 보지 않습니다 — 멱등하지 않은 명령도, 이름 없는 태스크도, 권한을 안 정한 파일 쓰기도 전부 통과합니다. 이 단계의 목적은 문법 검사를 믿으면 안 되는 이유를 눈으로 보는 것입니다. 일부러 나쁘게 쓰세요.

린트가 무엇을 잡는지 규칙 id 로 센다

ansible-lint -f pep8 messy.yml 의 출력을 /root/anslint/out/lint_before.txt 에 저장하세요(한 줄에 지적 하나씩 나오는 형식입니다). 그다음 같은 파일을 JSON 형식으로 린트해 걸린 규칙 id 만 중복 없이 사전순으로 한 줄씩 /root/anslint/out/rules.txt 에 저장하세요.

ansible-lint 의 출력 형식은 -f 로 고릅니다 — 사람이 읽을 기본 형식, 한 줄에 하나인 pep8, 도구가 읽을 json 이 있습니다. JSON 의 각 항목에는 규칙 id 가 check_name 이라는 이름으로 들어 있습니다. jqsort -u 로 뽑으면 됩니다. 규칙 id 는 name[play] 처럼 대괄호로 세부 항목을 구분합니다 — 같은 name 규칙이라도 어느 자리가 걸렸는지 id 로 알 수 있습니다. 린트가 위반을 찾으면 0 이 아닌 값으로 끝나므로, 저장할 때 그 점을 고려하세요.

basic 프로파일까지 끌어올린다

/root/anslint/site.yml 을 새로 쓰세요 — 이름 있는 플레이 하나에 이름 있는 태스크 셋입니다. 첫째는 /root/anslint/out/data 디렉터리를 만들고, 둘째는 /root/anslint/out/app.confport=8080 한 줄을 쓰고, 셋째는 /root/anslint/out/upper-<호스트이름>.txt 에 그 호스트 이름을 대문자로 한 줄 씁니다. 셸 명령은 하나도 쓰지 않습니다. ansible-lint --profile basic site.yml 이 통과해야 하고, 플레이북을 실제로 돌려 출력을 /root/anslint/out/run.txt 에 저장하세요.

프로파일은 규칙을 묶어 놓은 층입니다 — min · basic · moderate · safety · shared · production 순으로 위로 갈수록 엄격해지고, 위 프로파일은 아래 프로파일의 규칙을 모두 포함합니다. 한 번에 production 까지 가려 하지 말고 한 층씩 올리는 것이 실무의 순서입니다. basic 이 잡는 것은 주로 '이름이 없다' 와 '한 줄 자유 형식으로 적었다' 입니다. 셸 명령을 모듈로 바꾸면 no-changed-when 도 함께 사라집니다.

production 프로파일이 더 요구하는 두 가지

/root/anslint/site.yml 의 모든 모듈을 FQCN(ansible.builtin.<모듈>)으로 바꾸고, 파일과 디렉터리를 만드는 태스크마다 mode 를 적으세요. ansible-lint --profile production site.yml 이 통과해야 하고, 그 출력을 /root/anslint/out/lint_production.txt 에 저장하세요.

production 프로파일이 추가로 요구하는 것 가운데 가장 자주 부딪히는 둘이 이것입니다. FQCN 은 이름 충돌을 막습니다 — 컬렉션이 늘어나면 copy 라는 이름이 여러 곳에 생기고, 짧은 이름은 검색 경로 순서에 따라 다른 모듈이 될 수 있습니다. mode 를 적으라는 규칙은 권한이 운에 맡겨지는 것을 막습니다. 적지 않으면 대상의 umask 가 정하는데, 그 값은 서버마다 다릅니다. 어느 규칙이 어느 프로파일에 속하는지는 린트 출력 끝의 요약표에 나옵니다.

규칙은 살려 두고 한 줄만 뺀다

/root/anslint/site.yml 에 네 번째 태스크를 더하세요 — ansible.builtin.commandtar -czf /root/anslint/out/bundle.tgz -C /root/anslint/out app.conf 를 실행하고 changed_when: false 를 붙입니다. 린트는 이 태스크를 command-instead-of-module 로 잡는데, unarchive 모듈은 푸는 일만 하고 묶지는 못하므로 여기서는 셸 명령이 맞습니다. 그 한 줄만 규칙에서 빼는 주석을 달아 --profile production 을 다시 통과시키고, 플레이북을 다시 돌려 묶음을 실제로 만드세요.

# noqa: <규칙id> 를 태스크의 어느 줄에든 달면 그 태스크만 그 규칙에서 빠집니다. 여러 규칙을 뺄 때는 공백으로 나열합니다. 이것과 설정 파일의 skip_list 는 성격이 전혀 다릅니다 — noqa 는 '여기만 예외' 라 옆 사람이 리뷰에서 이유를 물을 수 있고, skip_list 는 '저장소 전체에서 이 규칙은 안 본다' 라 그 규칙이 사실상 없어집니다. 예외를 둘 때는 언제나 가장 좁은 범위를 고릅니다. 지금 걸린 규칙 id 는 2단계에서 뽑아 본 목록과 같은 꼴입니다.

저장소 전체에 걸리는 규칙을 설정 파일에 적는다

/root/anslint/site.yml 에 다섯 번째 태스크를 더하세요 — 이름은 소문자로 시작하는 nginx health probe 이고, /root/anslint/out/health.txtok 한 줄을 씁니다. 그리고 /root/anslint/.ansible-lint 를 만들어 profile: production, exclude_pathsmessy.ymlout/, skip_listname[casing] 을 적으세요. 인자 없이 ansible-lint 를 돌려 디렉터리 전체가 통과하는 것을 확인하고 출력을 /root/anslint/out/lint_repo.txt 에 저장하세요.

설정 파일이 있으면 --profile 을 매번 손으로 주지 않아도 되고, CI 와 사람의 손이 같은 규칙으로 돕니다 — 이것이 설정 파일을 두는 진짜 이유입니다. exclude_paths 는 '이 경로는 아예 보지 마라' 입니다. 가르치려고 남겨 둔 나쁜 예나 남이 만든 코드를 넣습니다. 다만 파일 이름을 직접 인자로 주면 제외 목록은 무시됩니다 — 제외는 '훑을 때' 의 규칙입니다. skip_list 에 넣는 것은 팀이 합의한 예외여야 합니다. 여기서는 제품 이름이 소문자로 시작하는 태스크 이름을 쓰기로 했다고 보면 됩니다.

값이 말이 되는지 플레이북 스스로 묻게 한다

/root/anslint/checks.yml 을 만드세요 — localhost 에서 팩트를 모으지 않고, 플레이 변수로 app_port: 8080, app_env: staging, allowed_envs: [staging, prod] 를 둡니다. 첫 태스크는 app_port 가 정수이고 1024 이상 65535 이하인지 확인하고, 둘째 태스크는 app_envallowed_envs 안에 있는지 확인합니다. 둘 다 ansible.builtin.assert 로 쓰고 fail_msgsuccess_msg 를 답니다. 기본값 그대로 돌린 출력을 /root/anslint/out/assert_ok.txt 에, -e app_port=80 으로 덮어써 돌린 출력을 /root/anslint/out/assert_fail.txt 에 저장하세요.

assertthat 에 적은 조건이 모두 참인지 봅니다. 하나라도 거짓이면 그 호스트에서 플레이가 멈춥니다 — 이것이 목적입니다. 전제 조건은 아무것도 바꾸기 전에 확인해야 합니다. 절반쯤 배포하고 나서 멈추면 되돌리는 일이 훨씬 비쌉니다. fail_msg 를 안 쓰면 실패 메시지가 조건식 원문으로 나와서 받는 사람이 무엇을 고쳐야 할지 모릅니다. -e 로 넘긴 값은 따로 지정하지 않으면 문자열입니다 — is integer 가 왜 거짓인지 여기서 보게 됩니다. 실패하는 실행은 0 이 아닌 값으로 끝나므로 출력을 저장할 때 그 점을 고려하세요.

세 층을 하나의 관문으로 묶는다

/root/anslint/gate.sh 를 만드세요 — 첫 인자로 받은 디렉터리(기본값은 현재 디렉터리)로 옮겨 가 세 가지를 차례로 확인합니다. 그 디렉터리 바로 아래의 *.yml 마다 문법 검사를 하고 OK syntax <파일> 또는 FAIL syntax <파일> 을 출력하고, 인자 없이 ansible-lint 를 돌려 OK lint 또는 FAIL lint 를 출력하고, checks.yml 이 있으면 그것을 돌려 OK assert 또는 FAIL assert 를 출력합니다. 하나라도 실패하면 0 이 아닌 값으로 끝납니다. 이 관문을 지금 디렉터리에서 돌려 출력을 /root/anslint/out/gate.txt 에 저장하세요.

관문의 값어치는 막는 데 있습니다. 통과만 시키고 늘 0 으로 끝나는 스크립트는 없는 것과 같고, 오히려 '검사하고 있다' 는 착각을 만들어 더 나쁩니다. 그래서 만든 뒤에는 반드시 나쁜 입력을 물려 봐야 합니다 — 임시 디렉터리에 일부러 나쁜 플레이북을 하나 두고 그 디렉터리를 인자로 줘 보세요. 세 층을 이 순서로 두는 이유도 있습니다. 문법 검사는 1초도 안 걸리고, 린트는 몇 초, 전제 조건 검사는 실제로 앤서블을 돌립니다 — 싼 것부터 돌려야 잘못된 커밋이 빨리 돌아갑니다.