언제나 그렇듯 삽질 후 깔끔하게 정리한다. VM끼리 ssh로 연결하고 싶었다. 보통은 쉘 스크립트로 프로비저닝했다. 그런데 ansible로 해보니 몇가지 어려운 점이 있었다: - authorized_keys를 만들 때 각 VM에서 만든 공개키를 ‘모아야’ 한다. - SSH key를 ‘새로 만든 VM만’ authorized_keys를 갱신해야 한다.
SSH key gen은 Self-signed Certificate을 해보며 잘 이해해서 ansible-galaxy에 있는 community.crypto.openssh_keypair로 대체했다.
Special varibables
VM을 up하고 아무도 SSH 키 쌍이 없는 상태에선 모두 키를 만들고 공개키를 모아서 모두의 authorized_keys 파일에 써주어야 한다. 만든 공개키는 register를 통해 변수로 받을 수 있었지만, 이를 다른 VM으로 어떻게 넘길지 떠오르지 않았다. SSH가 안되는 마당에 VM끼리 소통할 수 있나?
문제를 추상화하여 질문을 올렸다. 답은 몇가지 특별한 변수들에 담겨 있었다(마법을 찾고 있었는데 정말로 ’마법 변수’였다): - hostvars: 모든 인벤토리 호스트의 변수를 담고 있다. - ansible_play_hosts: 현재 play 중인 호스트명 리스트.
community.crypto.openssh_keypair 모듈의 실행 결과를 변수에 등록(register) 했기 때문에, 이 태스크 실행 이후엔 hostvars 변수에 담겨 있다. hostvars는 호스트명을 키로 하는 해시테이블이기 때문에 extract를 통해 매핑한다(Jinja2 builtin이 아닌 ansible의 구현이다):
- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) community.crypto.openssh_keypair: state: present path: /tmp/id_ssh_rsa register: ssh_result- name: Collect the pubkeys set_fact: pubkeys: "{{ ansible_play_hosts | map('extract', hostvars, ['ssh_result', 'public_keys']) }}"
YAML
복사
•
map의 세번짜 인자처럼 nested attribute에 접근할 수 있다.
뒤에도 또 나오지만, ansible_play_hosts(_all) | map('extact', hostvars, ...) 이것이 정말 많이 쓰는 패턴 같다. Ansible은 인벤토리 호스트마다 독립적으로 실행하지만 전체 인벤토리에 대해 변수를 접근하고 싶을 때, 즉 각 호스트 변수들을 모을 때 이 메타데이터를 사용하는것 같다.
키 만든 호스트만 거르기
만약 처음 실행하는게 아니라 VM을 새로 추가한다면 새 VM에만 SSH 키 쌍을 만들고 이 공개키만 모든 VM의 authorized_keys에 추가해주어야 한다. 새로 만든 SSH 공개키와 그 호스트만 알면 될 것이다. 호스트로 식별해 authorized_keys 파일에 blockinfile 처리하면 된다.
이번에도 호스트 변수를 사용한다. 나는 새로 키를 만들 기준으로 ’호스트 $HOME/.ssh/ 에 키 파일이 있는지’로 판단했다. 이 여부 리스트와 ansible_play_hosts를 elementwise AND-ing했다:
- name: Check the ssh key exists stat: path: /home/vagrant/.ssh/id_ssh_rsa register: key_stat- name: Collect the hosts generating the new key set_fact: new_key_hosts: "{{ ansible_play_hosts | map('extract', hostvars, ['key_stat', 'stat', 'exists']) | zip(ansible_play_hosts) | rejectattr('0') | map(attribute='1') }}"
YAML
복사
•
ansible_play_hosts가 ["vm1", "vm2", "vm3"] 이고 이 중 vm2에 /home/vagrant/.ssh/id_ssh_rsa 가 없다면, 첫번째 파이프({{ ansible_play_hosts | map('extract', hostvars, ['key_stat', 'stat', 'exists']) }})까지의 결과는 [true, false, true]이다.
•
이걸 다시 ansible_play_hosts와 zip하여 list의 list를 만들고 true인 것만 거른다(AND-ing).
디버거
맨 처음엔 현재 태스크 when 조건에 만족해서 skip 안하고 실행 중인 호스트만 들고 있는 또 다른 ’마법 변수’가 있는지 찾아 보았다(ansible_running_hosts?). 이 과정에서 디버거의 존재를 알게 되었다. 예시는 debug 모듈에 붙였지만, 아무 태스크나 debugger: always를 추가하면 쉽게 사용할 수 있다:
- name: debug debug: var: pubkeysright debugger: always
YAML
복사
check mode로 실행해도 디버거는 걸린다. 여기선 task_vars 변수부터 접근하면 특별한 변수를 포함한 변수들에 접근할 수 있다. “hosts”가 있는 변수 이름을 보았지만 기대한 마법 변수는 없었다(내가 못 찾은걸수도…):
[cluster1-master1] TASK: ssh : debug (debug)> p list(filter(lambda x: "hosts" in x, task_vars.keys()))['new_key_hosts', 'new_pubkey_and_hosts', 'ansible_play_hosts_all', 'ansible_play_hosts', 'play_hosts', 'ansible_current_hosts', 'ansible_failed_hosts']
Shell
복사
file state=present?
이 작업을 하며 알게 된 또 다른 소소한 사실인데, file 모듈에 웬만하면 있을거라는 state=present가 없다. 그래서 state=touch로 하면 playbook 실행마다 불필요하게 change가 생겼다. present와 비슷한 동작을 하도록 access_time과 modification_time에 preserve를 선언해준다:
- name: Touch the ssh config file file: state: touch path: /home/vagrant/.ssh/config mode: "0644" access_time: preserve modification_time: preserve
YAML
복사
참고
•
https://stackoverflow.com/questions/71391592/ansible-reduce-to-the-variable-from-registers-of-each-host
•
https://stackoverflow.com/questions/63619004/ansible-set-fact-for-all-hosts-in-play-when-only-one-host-has-a-met-condition
•
https://zwischenzugs.com/2021/08/27/five-ansible-techniques-i-wish-id-known-earlier/
•
https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html#magic-variables
•
https://stackoverflow.com/questions/63619004/ansible-set-fact-for-all-hosts-in-play-when-only-one-host-has-a-met-condition