LabHub
배우기 러닝패스 코스

Ansible実戦

フィルターが無くて shell で誤魔化していたものをプラグインにする

LabHub 에서 이어서 보기

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

목표

Jinja2 필터·테스트·룩업과 사용자 모듈을 직접 만들어 플레이북에서 부릅니다. 네 가지가 각각 어디서 실행되는지, 그 자리 차이가 무엇을 할 수 있고 무엇을 할 수 없게 만드는지를 코드로 확인합니다.

왜 중요한가

플레이북이 커지면 어느 팀에나 같은 자리가 생깁니다 — 이름을 다듬고, 숫자를 분류하고, 어딘가의 표를 찾아보는 일입니다. 표준 필터로 안 되면 사람들은 shellsed 로 때우고, 그 순간 그 태스크는 멱등성도 점검 모드도 잃습니다. 플러그인은 그 자리를 위한 것입니다. 다만 아무 데나 넣으면 되는 것이 아니라, 어디서 도는가가 설계의 절반입니다. 필터·테스트·룩업은 컨트롤러에서 돌아서 대상에 아무것도 설치할 필요가 없는 대신 대상의 상태를 볼 수 없고, 모듈은 대상으로 복사되어 거기서 돌기 때문에 상태를 바꿀 수 있는 대신 멱등성과 점검 모드를 스스로 책임져야 합니다. 이 실습은 그 경계를 네 번 넘나들며 손으로 확인합니다.

단계

  1. /root/ansplug/ansible.cfg 를 만들어 기본 인벤토리를 ./inventory/hosts.ini 로 지정하세요. /root/ansplug/inventory/hosts.ini 에는 그룹 셋을 적습니다 — webweb1(svc_port 8080, svc_name Web Front 01)과 web2(9090, Web Front 02), dbdb1(5432, Main DB!), edgecache1(443, Edge Cache). 네 호스트 모두 ansible_host=127.0.0.1 ansible_port=2222 이고 [all:vars]ansible_userroot 입니다. 그다음 ansible all -m ansible.builtin.ping -o 를 돌려 표준 출력과 표준 오류를 함께 /root/ansplug/out/ping.txt 에 저장하세요.
  2. /root/ansplug/filter_plugins/labfilters.py 를 만들어 slugify 필터 하나를 등록하세요. 이 필터는 문자열을 소문자로 낮추고, 영숫자가 아닌 글자를 모두 - 로 바꾸고, 연달아 나온 - 를 하나로 줄이고, 양 끝의 - 를 떼어 냅니다. 문자열이 아닌 값이 들어오면 AnsibleFilterError 를 던집니다. 그다음 /root/ansplug/slug.yml 을 만들어 'Web Server 01!!' 를 이 필터에 넣은 결과를 /root/ansplug/out/slug.txt 에 쓰고 실행하세요.
  3. 같은 /root/ansplug/filter_plugins/labfilters.pyport_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 에 인벤토리의 네 호스트를 이름 오름차순으로 <호스트> <포트> <분류> 한 줄씩 쓰고 실행하세요.
  4. /root/ansplug/test_plugins/labtests.py 를 만들어 reserved_port 테스트를 등록하세요 — 포트 번호가 1024 보다 작으면 참입니다. 그다음 /root/ansplug/reserved.yml 을 만들어 /root/ansplug/out/reserved.txt 에 네 호스트를 이름 오름차순으로 <호스트> <포트> <reserved|free> 한 줄씩 쓰고 실행하세요. 판정은 반드시 is reserved_port 꼴로 부릅니다.
  5. /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.txtdb·edge·web 세 그룹의 <그룹> <지역> 을 이 순서로 쓰고 실행하세요.
  6. /root/ansplug/collections/ansible_collections/labhub/site/ 에 컬렉션을 놓으세요 — galaxy.ymlnamespacelabhub, namesite 이고, 필터 파일은 plugins/filter/ 아래에 둡니다(2·3단계에서 만든 것을 그대로 복사하면 됩니다). /root/ansplug/ansible.cfgcollections_path = ./collections 를 더하고, /root/ansplug/fqcn.yml'Prod DB 02!!'labhub.site.slugify 에 넣은 결과를 /root/ansplug/out/fqcn.txt 에 쓰고 실행하세요.
  7. /root/ansplug/library/lab_marker.py 에 사용자 모듈 lab_marker 를 만드세요 — 인자는 pathcontent 이고, 파일 내용이 이미 같으면 changed=false, 다르면 파일을 쓰고 changed=true 로 끝냅니다. supports_check_mode=True 를 선언하고 점검 모드에서는 쓰지 않습니다. /root/ansplug/marker.ymlweb 그룹에 이 모듈을 돌려 /root/ansplug/out/<호스트이름>.marker 에 그 호스트 이름 한 줄을 쓰게 하고, 플레이북을 두 번 실행해 두 번째 실행의 출력을 /root/ansplug/out/marker_run2.txt 에 저장하세요.
  8. /root/ansplug/report.yml/root/ansplug/out/report.txt 에 네 호스트를 이름 오름차순으로 한 줄씩 쓰세요 — <호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역> 이고, 이름슬러그는 svc_nameslugify 한 값, 포트분류는 port_class, 다섯째 칸은 reserved_port 테스트, 지역은 그 호스트의 첫 그룹 이름을 labregistry 로 물은 값입니다. 플레이북을 두 번 실행하고 두 번째 실행의 출력을 /root/ansplug/out/report_run2.txt 에 저장하세요.

참고

네 대를 흉내 내는 인벤토리부터 세운다

/root/ansplug/ansible.cfg 를 만들어 기본 인벤토리를 ./inventory/hosts.ini 로 지정하세요. /root/ansplug/inventory/hosts.ini 에는 그룹 셋을 적습니다 — webweb1(svc_port 8080, svc_name Web Front 01)과 web2(9090, Web Front 02), dbdb1(5432, Main DB!), edgecache1(443, Edge Cache). 네 호스트 모두 ansible_host=127.0.0.1 ansible_port=2222 이고 [all:vars]ansible_userroot 입니다. 그다음 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.pyport_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) }} 처럼요. 기본값을 파이썬 쪽에 두면 대부분의 호출은 인자 없이 끝나고, 특별한 곳만 경계를 바꿔 부를 수 있습니다. 파이썬에서 Trueint 의 하위형이라 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 꼴로 부릅니다.

테스트는 필터와 클래스 이름도 메서드 이름도 다릅니다 — TestModuletests() 입니다. 디렉터리도 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.txtdb·edge·web 세 그룹의 <그룹> <지역> 을 이 순서로 쓰고 실행하세요.

룩업은 LookupBase 를 상속한 LookupModule 이고 진입점은 run(self, terms, variables=None, **kwargs) 입니다. termslookup('이름', 첫째, 둘째) 의 인자 목록이고 반드시 리스트를 돌려줘야 합니다. kwargs 로 온 것이 registry= 같은 키워드 인자입니다. 중요한 것은 이 코드가 컨트롤러에서 돈다는 사실입니다 — 룩업이 읽는 파일은 대상 서버가 아니라 플레이북을 실행하는 자리에 있어야 합니다.

같은 필터를 컬렉션 안으로 옮겨 FQCN 으로 부른다

/root/ansplug/collections/ansible_collections/labhub/site/ 에 컬렉션을 놓으세요 — galaxy.ymlnamespacelabhub, namesite 이고, 필터 파일은 plugins/filter/ 아래에 둡니다(2·3단계에서 만든 것을 그대로 복사하면 됩니다). /root/ansplug/ansible.cfgcollections_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 를 만드세요 — 인자는 pathcontent 이고, 파일 내용이 이미 같으면 changed=false, 다르면 파일을 쓰고 changed=true 로 끝냅니다. supports_check_mode=True 를 선언하고 점검 모드에서는 쓰지 않습니다. /root/ansplug/marker.ymlweb 그룹에 이 모듈을 돌려 /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_nameslugify 한 값, 포트분류는 port_class, 다섯째 칸은 reserved_port 테스트, 지역은 그 호스트의 첫 그룹 이름을 labregistry 로 물은 값입니다. 플레이북을 두 번 실행하고 두 번째 실행의 출력을 /root/ansplug/out/report_run2.txt 에 저장하세요.

한 호스트가 속한 그룹 이름은 hostvars[h].group_names 에 들어 있고, 여기서는 그 첫 원소가 곧 등록부의 열쇠입니다. copy 모듈의 content 에 Jinja2 의 {% for %} 블록을 그대로 넣으면 여러 줄을 한 번에 만들 수 있습니다. 같은 입력이면 파일 내용이 같으므로 두 번째 실행은 changed=0 이어야 합니다 — 이것이 '보고서를 만드는 플레이북' 이 멱등하다는 증거입니다.