Turn the shell workaround into a real plugin
한국어 원문으로 표시합니다.
목표
Jinja2 필터·테스트·룩업과 사용자 모듈을 직접 만들어 플레이북에서 부릅니다. 네 가지가 각각 어디서 실행되는지, 그 자리 차이가 무엇을 할 수 있고 무엇을 할 수 없게 만드는지를 코드로 확인합니다.
왜 중요한가
플레이북이 커지면 어느 팀에나 같은 자리가 생깁니다 — 이름을 다듬고, 숫자를 분류하고, 어딘가의 표를 찾아보는 일입니다. 표준 필터로 안 되면 사람들은 shell 과 sed 로 때우고, 그 순간 그 태스크는 멱등성도 점검 모드도 잃습니다. 플러그인은 그 자리를 위한 것입니다. 다만 아무 데나 넣으면 되는 것이 아니라, 어디서 도는가가 설계의 절반입니다. 필터·테스트·룩업은 컨트롤러에서 돌아서 대상에 아무것도 설치할 필요가 없는 대신 대상의 상태를 볼 수 없고, 모듈은 대상으로 복사되어 거기서 돌기 때문에 상태를 바꿀 수 있는 대신 멱등성과 점검 모드를 스스로 책임져야 합니다. 이 실습은 그 경계를 네 번 넘나들며 손으로 확인합니다.
단계
/root/ansplug/ansible.cfg를 만들어 기본 인벤토리를./inventory/hosts.ini로 지정하세요./root/ansplug/inventory/hosts.ini에는 그룹 셋을 적습니다 —web에web1(svc_port 8080, svc_nameWeb Front 01)과web2(9090,Web Front 02),db에db1(5432,Main DB!),edge에cache1(443,Edge Cache). 네 호스트 모두ansible_host=127.0.0.1ansible_port=2222이고[all:vars]의ansible_user는root입니다. 그다음ansible all -m ansible.builtin.ping -o를 돌려 표준 출력과 표준 오류를 함께/root/ansplug/out/ping.txt에 저장하세요./root/ansplug/filter_plugins/labfilters.py를 만들어slugify필터 하나를 등록하세요. 이 필터는 문자열을 소문자로 낮추고, 영숫자가 아닌 글자를 모두-로 바꾸고, 연달아 나온-를 하나로 줄이고, 양 끝의-를 떼어 냅니다. 문자열이 아닌 값이 들어오면AnsibleFilterError를 던집니다. 그다음/root/ansplug/slug.yml을 만들어'Web Server 01!!'를 이 필터에 넣은 결과를/root/ansplug/out/slug.txt에 쓰고 실행하세요.- 같은
/root/ansplug/filter_plugins/labfilters.py에port_class필터를 더하세요.port_class(value, privileged_below=1024, dynamic_from=49152)는 포트가privileged_below보다 작으면system,dynamic_from이상이면dynamic, 그 사이면user를 돌려주고, 정수가 아니면AnsibleFilterError를 던집니다. 그다음/root/ansplug/ports.yml을 만들어/root/ansplug/out/ports.txt에 인벤토리의 네 호스트를 이름 오름차순으로<호스트> <포트> <분류>한 줄씩 쓰고 실행하세요. /root/ansplug/test_plugins/labtests.py를 만들어reserved_port테스트를 등록하세요 — 포트 번호가 1024 보다 작으면 참입니다. 그다음/root/ansplug/reserved.yml을 만들어/root/ansplug/out/reserved.txt에 네 호스트를 이름 오름차순으로<호스트> <포트> <reserved|free>한 줄씩 쓰고 실행하세요. 판정은 반드시is reserved_port꼴로 부릅니다./root/ansplug/registry.json에{"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}를 적으세요./root/ansplug/lookup_plugins/labregistry.py를 만들어labregistry룩업을 정의합니다 — 열쇠를 받아 등록부의 값을 돌려주고,registry=키워드 인자로 다른 파일을 지정할 수 있으며(기본값은/root/ansplug/registry.json), 파일이 없거나 열쇠가 없으면AnsibleError를 던집니다. 그다음/root/ansplug/regions.yml로/root/ansplug/out/regions.txt에db·edge·web세 그룹의<그룹> <지역>을 이 순서로 쓰고 실행하세요./root/ansplug/collections/ansible_collections/labhub/site/에 컬렉션을 놓으세요 —galaxy.yml의namespace는labhub,name은site이고, 필터 파일은plugins/filter/아래에 둡니다(2·3단계에서 만든 것을 그대로 복사하면 됩니다)./root/ansplug/ansible.cfg에collections_path = ./collections를 더하고,/root/ansplug/fqcn.yml로'Prod DB 02!!'를labhub.site.slugify에 넣은 결과를/root/ansplug/out/fqcn.txt에 쓰고 실행하세요./root/ansplug/library/lab_marker.py에 사용자 모듈lab_marker를 만드세요 — 인자는path와content이고, 파일 내용이 이미 같으면changed=false, 다르면 파일을 쓰고changed=true로 끝냅니다.supports_check_mode=True를 선언하고 점검 모드에서는 쓰지 않습니다./root/ansplug/marker.yml로web그룹에 이 모듈을 돌려/root/ansplug/out/<호스트이름>.marker에 그 호스트 이름 한 줄을 쓰게 하고, 플레이북을 두 번 실행해 두 번째 실행의 출력을/root/ansplug/out/marker_run2.txt에 저장하세요./root/ansplug/report.yml로/root/ansplug/out/report.txt에 네 호스트를 이름 오름차순으로 한 줄씩 쓰세요 —<호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>이고, 이름슬러그는svc_name을slugify한 값, 포트분류는port_class, 다섯째 칸은reserved_port테스트, 지역은 그 호스트의 첫 그룹 이름을labregistry로 물은 값입니다. 플레이북을 두 번 실행하고 두 번째 실행의 출력을/root/ansplug/out/report_run2.txt에 저장하세요.
참고
filter_plugins/·test_plugins/·lookup_plugins/·library/는 플레이북 옆에 있어야 찾아집니다. 그래서 이 실습의 플레이북은 전부/root/ansplug바로 아래에 둡니다.- sshd 는 127.0.0.1 의 2222 에 떠 있습니다. 인벤토리의 네 호스트는 이름만 다를 뿐 전부 같은 서버로 붙습니다.
- 부르는 법이 종류마다 다릅니다 — 필터는
값 | 이름, 테스트는값 is 이름, 룩업은lookup('이름', 인자), 모듈은 태스크의 키입니다. - 흔한 실수: 클래스 이름을
FilterModule이 아닌 것으로 지어 놓고 '왜 못 찾지' 하는 것. 파일 이름은 자유지만 클래스 이름은 규약입니다. - 흔한 실수: 애드혹 명령으로 새 필터를 시험해 보고 안 된다고 판단하는 것 — 옆 디렉터리 탐색은 플레이북에만 걸립니다.
- 흔한 실수: 컬렉션을
ansible_collections/층 없이 놓는 것. 오류가 나지 않고 그냥 못 찾습니다. - Developing plugins · Adding modules and plugins locally · Lookup plugins · Collection structure · Developing modules
네 대를 흉내 내는 인벤토리부터 세운다
/root/ansplug/ansible.cfg 를 만들어 기본 인벤토리를 ./inventory/hosts.ini 로 지정하세요. /root/ansplug/inventory/hosts.ini 에는 그룹 셋을 적습니다 — web 에 web1(svc_port 8080, svc_name Web Front 01)과 web2(9090, Web Front 02), db 에 db1(5432, Main DB!), edge 에 cache1(443, Edge Cache). 네 호스트 모두 ansible_host=127.0.0.1 ansible_port=2222 이고 [all:vars] 의 ansible_user 는 root 입니다. 그다음 ansible all -m ansible.builtin.ping -o 를 돌려 표준 출력과 표준 오류를 함께 /root/ansplug/out/ping.txt 에 저장하세요.
이 파드의 sshd 는 127.0.0.1 의 2222 에 떠 있고 키 인증이 됩니다. 인벤토리에 이름을 여러 개 적어도 전부 같은 sshd 로 붙으므로 '여러 대' 를 흉내 낼 수 있습니다. INI 인벤토리에서 값에 공백이 들어가면 큰따옴표로 감쌉니다. 합쳐진 결과는 ansible-inventory --list 로 확인합니다.
첫 필터 — 컨트롤러에서 도는 순수 함수
/root/ansplug/filter_plugins/labfilters.py 를 만들어 slugify 필터 하나를 등록하세요. 이 필터는 문자열을 소문자로 낮추고, 영숫자가 아닌 글자를 모두 - 로 바꾸고, 연달아 나온 - 를 하나로 줄이고, 양 끝의 - 를 떼어 냅니다. 문자열이 아닌 값이 들어오면 AnsibleFilterError 를 던집니다. 그다음 /root/ansplug/slug.yml 을 만들어 'Web Server 01!!' 를 이 필터에 넣은 결과를 /root/ansplug/out/slug.txt 에 쓰고 실행하세요.
앤서블은 filter_plugins/ 안의 파이썬 파일에서 FilterModule 이라는 이름의 클래스를 찾고, 그 클래스의 filters() 가 돌려주는 사전의 열쇠를 필터 이름으로 씁니다. 이 디렉터리는 플레이북 옆에 있어야 찾아집니다 — 애드혹 명령에서는 찾지 못합니다. 필터는 컨트롤러에서 돌기 때문에 대상에 아무것도 설치할 필요가 없고, 그래서 더더욱 부수 효과 없는 순수 함수여야 합니다.
인자를 받는 필터와 잘못된 입력을 끊는 자리
같은 /root/ansplug/filter_plugins/labfilters.py 에 port_class 필터를 더하세요. port_class(value, privileged_below=1024, dynamic_from=49152) 는 포트가 privileged_below 보다 작으면 system, dynamic_from 이상이면 dynamic, 그 사이면 user 를 돌려주고, 정수가 아니면 AnsibleFilterError 를 던집니다. 그다음 /root/ansplug/ports.yml 을 만들어 /root/ansplug/out/ports.txt 에 인벤토리의 네 호스트를 이름 오름차순으로 <호스트> <포트> <분류> 한 줄씩 쓰고 실행하세요.
Jinja2 필터는 파이프 왼쪽 값을 첫 인자로 받고, 괄호 안에 적은 것이 그다음 인자가 됩니다 — {{ 3000 | port_class(2000, 4000) }} 처럼요. 기본값을 파이썬 쪽에 두면 대부분의 호출은 인자 없이 끝나고, 특별한 곳만 경계를 바꿔 부를 수 있습니다. 파이썬에서 True 는 int 의 하위형이라 isinstance(x, int) 를 통과합니다 — 불리언을 먼저 걸러야 합니다. 모든 호스트를 훑는 것은 groups['all'] 과 hostvars[...] 입니다.
테스트 플러그인 — 참거짓만 돌려주는 자리
/root/ansplug/test_plugins/labtests.py 를 만들어 reserved_port 테스트를 등록하세요 — 포트 번호가 1024 보다 작으면 참입니다. 그다음 /root/ansplug/reserved.yml 을 만들어 /root/ansplug/out/reserved.txt 에 네 호스트를 이름 오름차순으로 <호스트> <포트> <reserved|free> 한 줄씩 쓰고 실행하세요. 판정은 반드시 is reserved_port 꼴로 부릅니다.
테스트는 필터와 클래스 이름도 메서드 이름도 다릅니다 — TestModule 과 tests() 입니다. 디렉터리도 test_plugins/ 입니다. 부르는 법도 다릅니다. 필터는 값 | 이름, 테스트는 값 is 이름 이고, 테스트는 반드시 참이나 거짓만 돌려줍니다. 이 구분이 있는 이유는 when: 과 select·reject 가 테스트를 전제로 쓰이기 때문입니다 — 참거짓이 아닌 값을 돌려주면 그 자리에서 뜻이 무너집니다.
룩업 — 컨트롤러의 파일을 읽어 오는 자리
/root/ansplug/registry.json 에 {"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"} 를 적으세요. /root/ansplug/lookup_plugins/labregistry.py 를 만들어 labregistry 룩업을 정의합니다 — 열쇠를 받아 등록부의 값을 돌려주고, registry= 키워드 인자로 다른 파일을 지정할 수 있으며(기본값은 /root/ansplug/registry.json), 파일이 없거나 열쇠가 없으면 AnsibleError 를 던집니다. 그다음 /root/ansplug/regions.yml 로 /root/ansplug/out/regions.txt 에 db·edge·web 세 그룹의 <그룹> <지역> 을 이 순서로 쓰고 실행하세요.
룩업은 LookupBase 를 상속한 LookupModule 이고 진입점은 run(self, terms, variables=None, **kwargs) 입니다. terms 는 lookup('이름', 첫째, 둘째) 의 인자 목록이고 반드시 리스트를 돌려줘야 합니다. kwargs 로 온 것이 registry= 같은 키워드 인자입니다. 중요한 것은 이 코드가 컨트롤러에서 돈다는 사실입니다 — 룩업이 읽는 파일은 대상 서버가 아니라 플레이북을 실행하는 자리에 있어야 합니다.
같은 필터를 컬렉션 안으로 옮겨 FQCN 으로 부른다
/root/ansplug/collections/ansible_collections/labhub/site/ 에 컬렉션을 놓으세요 — galaxy.yml 의 namespace 는 labhub, name 은 site 이고, 필터 파일은 plugins/filter/ 아래에 둡니다(2·3단계에서 만든 것을 그대로 복사하면 됩니다). /root/ansplug/ansible.cfg 에 collections_path = ./collections 를 더하고, /root/ansplug/fqcn.yml 로 'Prod DB 02!!' 를 labhub.site.slugify 에 넣은 결과를 /root/ansplug/out/fqcn.txt 에 쓰고 실행하세요.
컬렉션의 자리는 <검색경로>/ansible_collections/<네임스페이스>/<이름>/ 으로 정해져 있습니다 — 이 세 층이 하나라도 어긋나면 아무 오류 없이 못 찾습니다. 컬렉션 안에서 필터의 자리는 filter_plugins/ 가 아니라 plugins/filter/ 입니다. 테스트는 plugins/test/, 룩업은 plugins/lookup/, 모듈은 plugins/modules/ 입니다. 제대로 찾고 있는지는 ansible-config dump | grep COLLECTIONS 로 확인합니다.
모듈은 대상에서 돈다 — 멱등성과 점검 모드는 그쪽 책임이다
/root/ansplug/library/lab_marker.py 에 사용자 모듈 lab_marker 를 만드세요 — 인자는 path 와 content 이고, 파일 내용이 이미 같으면 changed=false, 다르면 파일을 쓰고 changed=true 로 끝냅니다. supports_check_mode=True 를 선언하고 점검 모드에서는 쓰지 않습니다. /root/ansplug/marker.yml 로 web 그룹에 이 모듈을 돌려 /root/ansplug/out/<호스트이름>.marker 에 그 호스트 이름 한 줄을 쓰게 하고, 플레이북을 두 번 실행해 두 번째 실행의 출력을 /root/ansplug/out/marker_run2.txt 에 저장하세요.
필터·테스트·룩업과 달리 모듈은 대상으로 복사되어 거기서 실행됩니다. 그래서 컨트롤러의 파일을 읽을 수 없고, 대신 상태를 바꿀 수 있습니다 — 바꿀 수 있다는 것은 멱등성과 점검 모드를 스스로 책임져야 한다는 뜻입니다. 모듈의 뼈대는 AnsibleModule(argument_spec=..., supports_check_mode=True) 로 인자를 받고 module.exit_json(changed=...) 로 끝내는 것입니다. supports_check_mode 를 빠뜨리면 --check 에서 그 태스크는 아예 건너뛰어집니다 — 실패가 아니라 침묵이라 더 위험합니다. 모듈은 플레이북 옆의 library/ 에서 찾습니다.
네 종류를 한 플레이북에서 잇는다
/root/ansplug/report.yml 로 /root/ansplug/out/report.txt 에 네 호스트를 이름 오름차순으로 한 줄씩 쓰세요 — <호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역> 이고, 이름슬러그는 svc_name 을 slugify 한 값, 포트분류는 port_class, 다섯째 칸은 reserved_port 테스트, 지역은 그 호스트의 첫 그룹 이름을 labregistry 로 물은 값입니다. 플레이북을 두 번 실행하고 두 번째 실행의 출력을 /root/ansplug/out/report_run2.txt 에 저장하세요.
한 호스트가 속한 그룹 이름은 hostvars[h].group_names 에 들어 있고, 여기서는 그 첫 원소가 곧 등록부의 열쇠입니다. copy 모듈의 content 에 Jinja2 의 {% for %} 블록을 그대로 넣으면 여러 줄을 한 번에 만들 수 있습니다. 같은 입력이면 파일 내용이 같으므로 두 번째 실행은 changed=0 이어야 합니다 — 이것이 '보고서를 만드는 플레이북' 이 멱등하다는 증거입니다.