LabHub
배우기 러닝패스 코스

Ansible Fundamentals

If you must reach for the shell, what do you have to own yourself?

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) 로 거르면 됩니다.