LabHub
学习 学习路径 课程

Ansible 基础

非得用 shell 的话,你要自己扛下什么

在 LabHub 中继续学习

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

목표

같은 명령을 commandshell 로 각각 던져 무엇이 갈리는지 숫자로 재고, 셸을 써야 할 때 보고 기준과 실패 기준과 파이프 종료 코드를 직접 세우는 법을 손으로 익힙니다. 마지막에는 가드 없이 셸로 나가는 태스크를 찾아내는 감사 도구를 만듭니다.

왜 중요한가

Ansible 을 처음 쓰면 플레이북이 SSH 로 실행되는 셸 스크립트가 되기 쉽습니다. 돌기는 도는데 '지금 이미 그 상태인가'·'이번에 무엇이 바뀌었나'·'실패인가 성공인가' 에 아무것도 답하지 못하는 플레이북이 됩니다. 모듈은 그 세 가지에 답하려고 만들어진 것이고, 그래서 같은 일을 하는 모듈이 있으면 그쪽이 먼저입니다. 그렇다고 셸을 영영 안 쓸 수는 없습니다 — 모듈이 없는 일은 늘 남습니다. 중요한 것은 셸로 나가는 순간 Ansible 이 대신 해 주던 판단이 전부 사라진다는 사실을 알고, 그 판단을 손으로 다시 써 넣는 것입니다. 이 실습은 그 판단 세 가지를 하나씩 세워 봅니다.

단계

  1. /root/ansmod/hosts.ini 를 만드세요 — [web] 그룹에 web1·web2 를 넣고 둘 다 ansible_host=127.0.0.1, ansible_port=2222 를 갖게 하며, [all:vars]ansible_user=root 를 둡니다. 그다음 ansible-doc -s ansible.builtin.command 의 출력을 /root/ansmod/out/doc-command.txt 에, ansible-doc -s ansible.builtin.shell 의 출력을 /root/ansmod/out/doc-shell.txt 에 저장하세요.
  2. /root/ansmod/files/ 에 빈 파일 a.txt·b.txt·c.txt 세 개를 만드세요. /root/ansmod/boundary.yml 을 만들어 web1 에 네 태스크를 돌리세요 — ls /root/ansmod/files/*.txtcommand 로 한 번(실패해도 넘어가게), ls /root/ansmod/files/*.txt | wc -lshell 로 한 번, echo one two three | wc -wcommand 로 한 번, 같은 것을 shell 로 한 번입니다. 네 결과를 /root/ansmod/out/boundary.txt 에 정확히 네 줄로 남기세요: glob command rc=<값> / glob shell stdout=<값> / pipe command stdout=<값> / pipe shell stdout=<값>.
  3. /root/ansmod/report.yml 을 만드세요. web1 에서 ansible.builtin.commandid -un 을 실행해 whoregister 하고, 그 반환값에서 네 칸만 뽑아 /root/ansmod/out/result.json 에 JSON 으로 저장하세요 — rc·stdout·changed 는 반환값 그대로, cmd 는 반환값의 인자 목록을 공백으로 이어 붙인 문자열입니다. 이 단계에서는 changed_when 을 달지 않습니다.
  4. /root/ansmod/report.yml 에 태스크를 둘 더 넣으세요. 하나는 cat /etc/hostnamecommand 로 돌려 hn 으로 register 하고 changed_when: false 를 답니다. 다른 하나는 shellgrep -c "^nosuchuser:" /etc/passwd 를 돌려 hits 로 register 하고, changed_when: false 와 함께 종료 코드가 0 이나 1 이 아닐 때만 실패로 치는 failed_when 을 답니다. 그리고 /root/ansmod/out/result.json 에 세 칸을 더하세요 — hostname_changed(hn 의 changed), grep_rc(hits 의 rc), grep_failed(hits 의 failed). 플레이북은 끝까지 돌아야 합니다.
  5. /root/ansmod/pipe.yml 을 만드세요. 같은 파이프라인 cat /root/ansmod/missing.txt | wc -l 을 두 번 돌립니다 — 한 번은 그냥 shell 로(bare 로 register), 한 번은 set -o pipefail 을 앞에 붙이고 executable/bin/bash 로 지정해서(guarded 로 register). 둘 다 ignore_errors: truechanged_when: false 를 답니다. 결과를 /root/ansmod/out/pipe.json 에 네 칸으로 남기세요 — bare_rc·bare_failed·guarded_rc·guarded_failed. missing.txt 는 만들지 마세요.
  6. /root/ansmod/modernize.yml 을 만드세요. commandshell 도 한 번도 쓰지 않고 다음을 하세요 — /root/ansmod/app 디렉터리를 권한 0750 으로 만들고, /root/ansmod/app/app.confenv=lab 한 줄을 권한 0640 으로 쓰고, 두 경로의 상태를 모듈로 읽어 /root/ansmod/out/modernize.json 에 다섯 칸으로 남깁니다 — dir_mode·dir_isdir·conf_mode·conf_size·conf_checksum_len(체크섬 문자열의 길이).
  7. /root/ansmod/bootstrap.yml 을 만드세요. ansible.builtin.rawcommand -v python3 || echo NOPYTHON 을 실행해 register 하고, 줄 끝 공백과 CR 을 걷어낸 경로만 /root/ansmod/out/raw.txt 에 한 줄로 저장하세요(changed_when: false 를 답니다). 그리고 지금까지 만든 다섯 플레이북(boundary.yml·report.yml·pipe.yml·modernize.yml·bootstrap.yml)의 모든 태스크ansible.builtin. 으로 시작하는 FQCN 을 쓰도록 정리하세요.
  8. 먼저 검사 대상이 될 /root/ansmod/legacy.yml 을 만드세요 — 태스크 네 개이고, ansible --versioncommand 로 돌리되 changed_when: false 를 단 것 하나, mkdir -p /root/ansmod/legacy/logsshell 로 돌리는 것 하나, echo seeded > /root/ansmod/legacy/logs/stamp.txtshell 로 돌리는 것 하나, ls /root/ansmod/legacy/logscommand 로 돌리는 것 하나입니다(뒤 셋에는 가드를 달지 않습니다). 그다음 /root/ansmod/shell-audit.sh <플레이북경로> 를 만드세요: 그 플레이북에서 command 또는 shell 을 쓰면서 changed_when 이 없는 태스크의 이름만 한 줄씩 사전순으로 출력합니다(짧은 이름과 FQCN 을 둘 다 인식해야 합니다). 마지막으로 ./shell-audit.sh /root/ansmod/legacy.yml 의 출력을 /root/ansmod/out/audit.txt 에 저장하세요.

참고

대상을 적고 모듈을 문서에서 찾는다

/root/ansmod/hosts.ini 를 만드세요 — [web] 그룹에 web1·web2 를 넣고 둘 다 ansible_host=127.0.0.1, ansible_port=2222 를 갖게 하며, [all:vars]ansible_user=root 를 둡니다. 그다음 ansible-doc -s ansible.builtin.command 의 출력을 /root/ansmod/out/doc-command.txt 에, ansible-doc -s ansible.builtin.shell 의 출력을 /root/ansmod/out/doc-shell.txt 에 저장하세요.

인벤토리가 있어야 아무것도 시작되지 않습니다. 이 파드의 sshd 는 127.0.0.1:2222 에 떠 있고, 호스트를 둘 적어도 둘 다 같은 sshd 로 붙습니다. ansible-doc 은 인터넷 검색이 아니라 지금 이 기계에 설치된 것의 목록입니다. -s 는 플레이북에 붙여 넣을 뼈대를 냅니다. 두 파일을 나란히 놓고 한쪽에만 있는 옵션 이름을 찾아보세요 — 그게 두 모듈의 성질 차이를 그대로 보여 줍니다.

같은 명령을 command 와 shell 로 던져 차이를 잰다

/root/ansmod/files/ 에 빈 파일 a.txt·b.txt·c.txt 세 개를 만드세요. /root/ansmod/boundary.yml 을 만들어 web1 에 네 태스크를 돌리세요 — ls /root/ansmod/files/*.txtcommand 로 한 번(실패해도 넘어가게), ls /root/ansmod/files/*.txt | wc -lshell 로 한 번, echo one two three | wc -wcommand 로 한 번, 같은 것을 shell 로 한 번입니다. 네 결과를 /root/ansmod/out/boundary.txt 에 정확히 네 줄로 남기세요: glob command rc=<값> / glob shell stdout=<값> / pipe command stdout=<값> / pipe shell stdout=<값>.

command 는 받은 문자열을 낱말로 쪼개 그대로 실행 파일에 넘깁니다 — 중간에 셸이 없으니 글롭도 파이프도 셸 문법으로 해석되지 않습니다. 글롭은 명령이 0 이 아닌 코드로 죽어 바로 눈에 띄지만, 파이프는 성공한 척 합니다. 그 차이를 숫자로 남기는 것이 이 단계의 전부입니다. 실패하는 태스크에서 플레이북이 멈추지 않게 하려면 ignore_errors: true 를 붙이고, 조회뿐이니 changed_when: false 도 함께 답니다.

모듈이 돌려주는 JSON 을 register 로 받아 읽는다

/root/ansmod/report.yml 을 만드세요. web1 에서 ansible.builtin.commandid -un 을 실행해 whoregister 하고, 그 반환값에서 네 칸만 뽑아 /root/ansmod/out/result.json 에 JSON 으로 저장하세요 — rc·stdout·changed 는 반환값 그대로, cmd 는 반환값의 인자 목록을 공백으로 이어 붙인 문자열입니다. 이 단계에서는 changed_when 을 달지 않습니다.

모듈의 반환값은 문자열이 아니라 키가 있는 JSON 이고, register 는 그 JSON 을 통째로 변수에 담습니다. rc·stdout·stdout_lines·stderr·changed·failed·cmd 가 들어 있습니다 — cmd 는 실제로 실행된 인자 목록이라 문자열로 만들려면 이어 붙여야 합니다. 딕셔너리를 그대로 JSON 문자열로 만들어 주는 필터가 있습니다. 그리고 이 태스크가 아무것도 안 바꿨는데 changed 가 무엇으로 나오는지 눈으로 확인해 두세요 — 다음 단계의 출발점입니다.

changed_when 과 failed_when 으로 보고 기준을 직접 세운다

/root/ansmod/report.yml 에 태스크를 둘 더 넣으세요. 하나는 cat /etc/hostnamecommand 로 돌려 hn 으로 register 하고 changed_when: false 를 답니다. 다른 하나는 shellgrep -c "^nosuchuser:" /etc/passwd 를 돌려 hits 로 register 하고, changed_when: false 와 함께 종료 코드가 0 이나 1 이 아닐 때만 실패로 치는 failed_when 을 답니다. 그리고 /root/ansmod/out/result.json 에 세 칸을 더하세요 — hostname_changed(hn 의 changed), grep_rc(hits 의 rc), grep_failed(hits 의 failed). 플레이북은 끝까지 돌아야 합니다.

grep -c 는 찾은 게 없으면 종료 코드 1 을 돌려줍니다. 그건 오류가 아니라 '0건' 이라는 답인데, 기본 판정은 0 이 아니면 실패라 플레이북이 거기서 멈춥니다. failed_when 은 실패의 정의를 사람이 다시 쓰는 자리이고, 조건식 안에서는 방금 register 한 변수를 그대로 쓸 수 있습니다. changed_when: false 를 단 태스크와 안 단 태스크의 changed 값이 result.json 에서 어떻게 갈리는지 비교해 보세요.

파이프가 실패를 삼키는 것을 숫자로 확인하고 막는다

/root/ansmod/pipe.yml 을 만드세요. 같은 파이프라인 cat /root/ansmod/missing.txt | wc -l 을 두 번 돌립니다 — 한 번은 그냥 shell 로(bare 로 register), 한 번은 set -o pipefail 을 앞에 붙이고 executable/bin/bash 로 지정해서(guarded 로 register). 둘 다 ignore_errors: truechanged_when: false 를 답니다. 결과를 /root/ansmod/out/pipe.json 에 네 칸으로 남기세요 — bare_rc·bare_failed·guarded_rc·guarded_failed. missing.txt 는 만들지 마세요.

셸에서 파이프라인의 종료 코드는 마지막 명령의 것입니다. 앞에서 cat 이 죽어도 wc 가 0 으로 끝나면 전체가 0 이고, 그 태스크는 초록색으로 지나갑니다. set -o pipefail 은 파이프라인 안 어느 하나라도 실패하면 전체를 실패로 만듭니다. 다만 기본 셸(/bin/sh)이 그 옵션을 모를 수 있으니 셸을 명시해야 합니다. 두 rc 값이 다르게 나오면 성공입니다 — 그 차이가 ansible-lint 의 risky-shell-pipe 규칙이 존재하는 이유입니다.

셸 세 줄을 file·copy·stat 모듈로 옮긴다

/root/ansmod/modernize.yml 을 만드세요. commandshell 도 한 번도 쓰지 않고 다음을 하세요 — /root/ansmod/app 디렉터리를 권한 0750 으로 만들고, /root/ansmod/app/app.confenv=lab 한 줄을 권한 0640 으로 쓰고, 두 경로의 상태를 모듈로 읽어 /root/ansmod/out/modernize.json 에 다섯 칸으로 남깁니다 — dir_mode·dir_isdir·conf_mode·conf_size·conf_checksum_len(체크섬 문자열의 길이).

mkdir -pchmod 를 한 번에 대신하는 모듈, echo > 를 대신하는 모듈, ls -l 이나 stat 명령을 대신하는 모듈이 각각 있습니다. 세 이름이 떠오르지 않으면 ansible-doc -l ansible.builtin | grep -i <낱말> 로 찾으세요 — 1단계에서 문서를 뽑아 둔 이유가 이겁니다. 상태를 읽는 모듈의 반환값은 stat 이라는 키 아래에 mode·isdir·size·checksum 이 들어 있습니다. 권한은 문자열이라 따옴표를 빠뜨리면 8진수가 10진수로 읽힙니다.

raw 로 파이썬을 찾고 플레이북을 FQCN 으로 정리한다

/root/ansmod/bootstrap.yml 을 만드세요. ansible.builtin.rawcommand -v python3 || echo NOPYTHON 을 실행해 register 하고, 줄 끝 공백과 CR 을 걷어낸 경로만 /root/ansmod/out/raw.txt 에 한 줄로 저장하세요(changed_when: false 를 답니다). 그리고 지금까지 만든 다섯 플레이북(boundary.yml·report.yml·pipe.yml·modernize.yml·bootstrap.yml)의 모든 태스크ansible.builtin. 으로 시작하는 FQCN 을 쓰도록 정리하세요.

raw 는 SSH 로 문자열을 그대로 던지고 나온 것을 그대로 받습니다 — 그래서 대상에 파이썬이 없어도 돌지만, 받은 문자열에는 CR 과 줄바꿈이 그대로 붙어 옵니다. 양끝 공백을 걷어내는 Jinja 필터가 하나 있습니다. 파일에 CR 이 남아 있는지는 od -c 로 확인할 수 있습니다. FQCN 정리는 손이 아니라 눈으로 합니다 — yq -r '.[].tasks[] | keys | .[]' <파일> 로 태스크가 쓰는 키를 전부 뽑아 보면 짧은 이름이 한눈에 보입니다.

가드 없이 셸로 나가는 태스크를 찾아내는 감사 도구

먼저 검사 대상이 될 /root/ansmod/legacy.yml 을 만드세요 — 태스크 네 개이고, ansible --versioncommand 로 돌리되 changed_when: false 를 단 것 하나, mkdir -p /root/ansmod/legacy/logsshell 로 돌리는 것 하나, echo seeded > /root/ansmod/legacy/logs/stamp.txtshell 로 돌리는 것 하나, ls /root/ansmod/legacy/logscommand 로 돌리는 것 하나입니다(뒤 셋에는 가드를 달지 않습니다). 그다음 /root/ansmod/shell-audit.sh <플레이북경로> 를 만드세요: 그 플레이북에서 command 또는 shell 을 쓰면서 changed_when 이 없는 태스크의 이름만 한 줄씩 사전순으로 출력합니다(짧은 이름과 FQCN 을 둘 다 인식해야 합니다). 마지막으로 ./shell-audit.sh /root/ansmod/legacy.yml 의 출력을 /root/ansmod/out/audit.txt 에 저장하세요.

리뷰에서 '이 shell 정말 필요한가' 를 사람이 매번 묻는 대신 도구가 묻게 만드는 단계입니다. 도구가 표기 방식에 따라 눈을 감으면 감사가 아닙니다 — shell:ansible.builtin.shell: 을 모두 같은 것으로 봐야 합니다. 이미지에 든 yq 는 mikefarah 판이라 .[].tasks[] 로 플레이 목록을 따라 들어가고, 점이 들어간 키는 .["ansible.builtin.shell"] 처럼 대괄호로 씁니다. 없는 키를 물으면 null 이 나오니 select(... != null) 로 거르면 됩니다.