Przetwarzanie danych wymaga softu, a soft trzeba zainstalować. Gdy skala rośnie konieczna jest automatyzacja za pomocą Ansible, Puppet, Chef, Terraform i jeszcze to innych wynalazków. W tym artykule dowiesz się jak wygenerować niezliczoną ilość certyfikatów za pomocą kilku kliknięć w Ansible.
Co to jest Ansible?
Otwierałeś/aś kiedyś 9 okienek w konsoli z opcją pisania jednocześnie na wszystkich naraz? Powiedzmy, że taki właśnie problem rozwiązuje Ansible. Za jego pomocą możesz wykonać jakąś komendę/akcję na grupie maszyn. Wygląda to mniej więcej tak:
ansible -a 'echo elo mordeczko' localhost localhost | CHANGED | rc=0 >> elo mordeczko
Oczywiście to prosty przykład. Wskazałem grupę jako localhost
. O inventory
wspomnę później na przykładzie. Siłą Ansible są moduły, które wykonują konkretne operacje dla podanych parametrów.
Co to jest Ansible Playbook?
Rzadko kiedy pojedyncza operacja wystarczy. Musimy skopiować jakiś plik, potem go rozpakować, utworzyć użytkownika, utworzyć folder, przenieść pliki konfiguracyjne i jeszcze wiele innych rzeczy. Może chesz postawić klaster Apache Spark na 20 serwerach? Po to właśnie są playbook’i. Wykonujemy sekwencję operacji na wskazanych hostach. Poniżej przykład inspirowany z dokumentacji modułu file:
--- - name: Do something hosts: localhost tasks: - name: Create a directory if it does not exist ansible.builtin.file: path: /etc/some_directory state: directory mode: '0755' ---
Problem do rozwiązania
Instalujesz klaster Elasticsearch? Potrzebujesz certyfikaty. Instalujesz klaster Apache Kafka/Redpanda? Potrzebujesz certyfikaty. Chcesz wystawić Apache Zeppelin/Jupyter’a po TLS? Potrzebujesz certyfikatu lub CSR które podpisze Ci zaufany urząd certyfikacji (CA).
Nie wiem jak Ty, ale ja zawsze na nowo szukam zaklęć openssl
, które rozwiążą mój problem. Postanowiłem utworzyć playbook, który będzie robił to za mnie.
Rozwiązanie
Inventory
Hosty, grupy i zmienne definiujemy w inventory. Pod ten przykład utworzyłem grupę elastic
oraz vip_hosts
. Niektóre hosty mają zmienne.
all: children: elastic: hosts: es-01: ansible_host: 10.0.0.10 some_var: "hey" es-02: ansible_host: 10.0.0.11 some_other_var: "hey you" es-03: ansible_host: 10.0.0.12 es-04: ansible_host: 10.0.0.13 es-05: ansible_host: 10.0.0.14 vip_hosts: hosts: host-01: ansible_host: 10.0.0.20 important_var: "oh no!" host-02: ansible_host: 10.0.0.21 important_var: "anyway..."
Jak dobrać się do właściwości hostów?
Chcemy utworzyć certyfikaty dla grupy hostów, więc fajnie było by dobrać się do ich zmiennych i właściwości. Wstępnie, wykorzystamy do tego moduł debug. Docelowy playbook chcę wykonywać na `localhost`, więc będzie trzeba iterować w trochę inny sposób niż “normalnie”.
--- - name: Debug inventory hosts: localhost vars: my_group: elastic tasks: - name: debug groups debug: msg: "{{ groups }}" - name: debug vip_hosts group debug: msg: "{{ groups['vip_hosts'] }}" - name: debug host es-2 debug: msg: "{{ hostvars['es-02'] }}" - name: debug each host of my_group debug: msg: "{{ hostvars[item] }}" with_items: "{{ groups[my_group] }}" ...
Przeanalizujmy to co powyżej. Po wykonaniu ansible-playbook -i inventory.yml debug.yml
, task debug groups
wyrzuci nam kolekcje grup i ich elementy zdefiniowane w inventory.yml
.
TASK [debug groups] *********************************************************************************************************************************************** ok: [localhost] => { "msg": { "all": [ "es-01", "es-02", "es-03", "es-04", "es-05", "host-01", "host-02" ], "elastic": [ "es-01", "es-02", "es-03", "es-04", "es-05" ], "ungrouped": [], "vip_hosts": [ "host-01", "host-02" ] } }
Kolejny odniesie się tylko do grupy vip_hosts
.
TASK [debug vip_hosts group] ************************************************************************************************************************************** ok: [localhost] => { "msg": [ "host-01", "host-02" ] }
hostvars
da nam możliwość odniesienia się do konkretnej właściwości danego hosta.
ok: [localhost] => { "msg": { "ansible_check_mode": false, "ansible_diff_mode": false, "ansible_facts": {}, "ansible_forks": 5, "ansible_host": "10.0.0.11", "ansible_inventory_sources": [ "/home/maciej/ansible-playbook-certificate-generator/inventory.yml" ], "ansible_playbook_python": "/usr/bin/python3", "ansible_run_tags": [ "all" ], "ansible_skip_tags": [], "ansible_verbosity": 0, "ansible_version": { "full": "2.9.6", "major": 2, "minor": 9, "revision": 6, "string": "2.9.6" }, "group_names": [ "elastic" ], "groups": { "all": [ "es-01", "es-02", "es-03", "es-04", "es-05", "host-01", "host-02" ], "elastic": [ "es-01", "es-02", "es-03", "es-04", "es-05" ], "ungrouped": [], "vip_hosts": [ "host-01", "host-02" ] }, "inventory_dir": "/home/maciej/ansible-playbook-certificate-generator", "inventory_file": "/home/maciej/ansible-playbook-certificate-generator/inventory.yml", "inventory_hostname": "es-02", "inventory_hostname_short": "es-02", "playbook_dir": "/home/maciej/ansible-playbook-certificate-generator", "some_other_var": "hey you" } }
Wykorzystując hostvars
, groups
, zmienną my_group
oraz pętlę with_items
możemy dobrać się do każdego hosta danej grupy.
TASK [debug each host of my_group] ******************************************************************************************************************************** ok: [localhost] => (item=es-01) => { "msg": { ... } } ok: [localhost] => (item=es-02) => { "msg": { ... } } ok: [localhost] => (item=es-03) => { "msg": { ... } } ok: [localhost] => (item=es-04) => { "msg": { ... } } ok: [localhost] => (item=es-05) => { "msg": { ... } }
Generowanie CA
Najpierw musimy wygenerować nasz urząd certyfikacji (no, chyba że już go masz). Poniżej cały playbook. Zmienne są raczej oczywiste. Pierwszy task tworzy foldery, a kolejne generuję klucz, CSR, certyfikat w formacie PEM i PKCS#12. W skrypcie jest sporo zmiennych, ale wydają mi się w miarę czytelne.
--- - name: Create CA hosts: localhost vars: certs_dir: "{{ playbook_dir }}/my_certs" country: "PL" common_name: "My Awesome CA" organization_name: "wiaderko" tasks: - name: create folders file: path: "{{ item }}" state: directory loop: - "{{ certs_dir }}" - "{{ certs_dir }}/ca" - name: create CA key openssl_privatekey: path: "{{ certs_dir }}/ca/ca.key" register: ca_key - name: create the CA CSR openssl_csr: path: "{{ certs_dir }}/ca/ca.csr" privatekey_path: "{{ ca_key.filename }}" country_name: "{{ country }}" common_name: "{{ common_name }}" organization_name: "{{ organization_name }}" basic_constraints: ["CA:TRUE"] register: ca_csr - name: sign the CA CSR openssl_certificate: path: "{{ certs_dir }}/ca/ca.crt" csr_path: "{{ ca_csr.filename }}" privatekey_path: "{{ ca_key.filename }}" provider: selfsigned register: ca_crt - name: create PKCS file openssl_pkcs12: action: export path: "{{ certs_dir }}/ca/ca.p12" friendly_name: "{{ common_name }}" privatekey_path: "{{ ca_key.filename }}" certificate_path: "{{ certs_dir }}/ca/ca.crt" state: present ...
Po uruchomieniu ansible-playbook -i inventory.yml create_ca.yml
widzimy utworzone pliki:
tree . . ├── README.md ├── create_ca.yml ├── debug.yml ├── inventory.yml └── my_certs └── ca ├── ca.crt ├── ca.csr ├── ca.key └── ca.p12
Są nawet zjadliwe przez openssl’a:
openssl x509 -in my_certs/ca/ca.crt -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 20:d9:7c:81:8d:93:2c:bf:7a:0d:bc:0a:9f:82:12:a8:5b:6b:fb:40 Signature Algorithm: sha256WithRSAEncryption Issuer: C = PL, O = wiaderko, CN = My Awesome CA Validity Not Before: Nov 14 18:53:09 2021 GMT Not After : Nov 12 18:53:09 2031 GMT Subject: C = PL, O = wiaderko, CN = My Awesome CA Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (4096 bit) Modulus: ...
Generowanie certyfikatów – wersja 1
Zabieramy się za generowanie certyfikatów. Tym na białym koniu wjedzie with_items
gdzie będą iterowane kolejne hosty.
Naszym oczom powinno pojawić się coś podobnego:
➜ ansible-playbook-certificate-generator git:(main) ✗ ansible-playbook -i inventory.yml create_certs_v1.yml PLAY [Create certs for the group] ********************************************************************************************************************************* TASK [Gathering Facts] ******************************************************************************************************************************************** ok: [localhost] TASK [create dirs] ************************************************************************************************************************************************ changed: [localhost] => (item=es-01) changed: [localhost] => (item=es-02) changed: [localhost] => (item=es-03) changed: [localhost] => (item=es-04) changed: [localhost] => (item=es-05) TASK [create host certificate key] ******************************************************************************************************************************** changed: [localhost] => (item=es-01) changed: [localhost] => (item=es-02) changed: [localhost] => (item=es-03) changed: [localhost] => (item=es-04) changed: [localhost] => (item=es-05) TASK [create the CSR] ********************************************************************************************************************************************* changed: [localhost] => (item=es-01) changed: [localhost] => (item=es-02) changed: [localhost] => (item=es-03) changed: [localhost] => (item=es-04) changed: [localhost] => (item=es-05) TASK [sign the CSR with CA] *************************************************************************************************************************************** changed: [localhost] => (item=es-01) changed: [localhost] => (item=es-02) changed: [localhost] => (item=es-03) changed: [localhost] => (item=es-04) changed: [localhost] => (item=es-05) TASK [create PKCS file] ******************************************************************************************************************************************* changed: [localhost] => (item=es-01) changed: [localhost] => (item=es-02) changed: [localhost] => (item=es-03) changed: [localhost] => (item=es-04) changed: [localhost] => (item=es-05) PLAY RECAP ******************************************************************************************************************************************************** localhost : ok=6 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Po odpaleniu openssl x509 -in my_certs/es-02/cert.crt -text -noout
zobaczymy informacje jednego z utworzonych certyfikatów. W tym przypadku nie podawaliśmy Subject Alternative Name (SAN), więc przypisana została wartość Common Name (CN).
X509v3 extensions: X509v3 Subject Alternative Name: DNS:es-02
Certyfikaty możemy zweryfikować openssl’em.
openssl verify -CAfile ./my_certs/ca/ca.crt ./my_certs/es-02/cert.crt ./my_certs/es-02/cert.crt: OK
Generowanie certyfikatów – wersja 2 (SAN)
Czasami wystawiamy serwis pod wieloma nazwami domenowymi lub adresami IP, stąd potrzebne jest zdefiniowanie Subject Alternative Name. Dodajmy więc do jednego z host’ów parametr z listą SAN.
... es-03: ansible_host: 10.0.0.12 san: - IP:10.0.0.12 - IP:11.11.11.11 - DNS:wiaderko.pl - DNS:ansible-fanboy.pl ...
Wpisywanie ręcznie SAN wszystkim hostom w inventory było by czasochłonenne i nudne. Wykorzystamy default(omit)
, czyli parametr ten będzie pomijany w przypadku braku zmiennej san
.
TASK [create the CSR] ********************************************************************************************************************************************* ok: [localhost] => (item=es-01) ok: [localhost] => (item=es-02) changed: [localhost] => (item=es-03) ok: [localhost] => (item=es-04) ok: [localhost] => (item=es-05)
Ponowne odpalenie playbooka spowodowało zmianę tylko hosta z dodanym SAN’em.
... X509v3 extensions: X509v3 Subject Alternative Name: IP Address:10.0.0.12, IP Address:11.11.11.11, DNS:wiaderko.pl, DNS:ansible-fanboy.pl ...
A efekt jest widoczny powyżej 😁.
Podsumowanie
Baw się i używaj Ansible. Można zautomatyzować sporo powtarzalnych i nudnych zadań. Każdy wie, że lepiej poświecić 6 godzin na automatyzacje zadania które zajmuje nam 3 minuty raz do roku 😅.
Repozytorium
zorteran/ansible-playbook-certificate-generator (github.com)