Bare-metal klaster z Kubernetes w 10 minut…?

Pozazdrościłem koledze Sławkowi Wolakowi technicznego artykułu dot. kontenerów i RexRay i pomyślałem, że nie będę gorszy i również napiszę o czymś ze stajni Dockera. Dodatkowym motorem był mój wykład na poznańskim spotkaniu grupy Amazon Web Services (tutaj relacja), gdzie wykorzystałem Ansible do utworzenia środowiska Continuous Integration i Continuous Deployment na bazie klastra z Docker Swarm i Gitlab  jako narzędzia do budowania, testowania i wrzucania obrazów do rejestru.

 

Po zebraniu tych inspiracji w jeden nurt podjąłem decyzję, że temat na dzisiaj to… bare-metal deployment klastra z Kubernetes  na CentOS przy pomocy Ansible . Poza samą brudną robotą postaram się też wykazać również czas postawienia takiego środowiska. Zapraszam.

Jeszcze tylko kilka słów o samym Kubernetes – to otwarta platformą służącą do zarządzania klastrem maszyn z zainstalowanym silnikiem Dockera. Zezwala na automatyczne skalowanie horyzontalne, tworzenie loadbalancerów i usług, łatwy upgrade i rollback wersji aplikacji oraz wiele innych ciekawych smaczków. Zdecydowanie można go postrzegać jako alternatywa dla Docker Swarm i produkt Docker Datacenter. Dobra, jedziemy!

Środowisko laboratoryjne

Środowisko laboratoryjne to mój workstation:

  • MacBook Pro (Retina, 13-inch, Early 2015)
  • Procesor 2,7 GHz Intel Core i5
  • 8 GB 1867 MHz DDR3

Maszyny zostaną stworzone z wykorzystaniem narzędzia na bazie Vagrant (korzystając z providera –  VirtualBox), połączone siecią i konfigurowane przy pomocy Ansible.

kubernetes-01 Kontroler
kubernetes-02 Minion
kubernetes-03 Minion

Nadajemy im następującą adresację:

kubernetes-01 192.168.50.101
kubernetes-02 192.168.50.102
kubernetes-03 192.168.50.103

Powołanie środowiska

Prace zaczniemy od stworzenia czystych maszyn wirtualnych w środowisku. Maszyny powołujemy przy pomocy narzędzia Vagrant. W tym celu tworzymy plik Vagrantfile.

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  config.vm.box_check_update = true
  config.vm.synced_folder ".", "/vagrant", disabled: true

  config.ssh.keys_only = true
  config.ssh.insert_key = false
  config.ssh.paranoid = false

  (1..3).each do |i|
    config.vm.define "kubernetes-0#{i}" do |j|
      j.vm.hostname = "kubernetes-0#{i}"
      j.vm.network "private_network", ip: "192.168.50.10#{i}"

      j.vm.provision :hosts do |p|
        p.autoconfigure = true
        p.sync_hosts = true
        p.add_localhost_hostnames = false
      end

      j.vm.provider "virtualbox" do |v|
          v.memory = 512
          v.cpus = 2
          v.gui = false
          v.name = "kubernetes-0#{i}"
          v.customize ["modifyvm", :id, "--cpuexecutioncap", "50"]
      end
    end
  end

end

Następnie uruchamiamy zdefniowane maszyny wirtualne; wchodzimy do katalogu z plikiem konfiguracyjnym Vagrantfile i uruchamiamy następującą komendę.

$ time (vagrant up)
Bringing machine 'kubernetes-01' up with 'virtualbox' provider...
Bringing machine 'kubernetes-02' up with 'virtualbox' provider...
Bringing machine 'kubernetes-03' up with 'virtualbox' provider...
==> kubernetes-01: Importing base box 'centos/7'...
==> kubernetes-01: Matching MAC address for NAT networking...
==> kubernetes-01: Checking if box 'centos/7' is up to date...
==> kubernetes-01: Setting the name of the VM: kubernetes-01
    [...]
==> kubernetes-01: Setting hostname...
==> kubernetes-01: Configuring and enabling network interfaces...
==> kubernetes-01: Running provisioner: hosts...
==> kubernetes-02: Importing base box 'centos/7'...
==> kubernetes-02: Matching MAC address for NAT networking...
==> kubernetes-02: Checking if box 'centos/7' is up to date...
==> kubernetes-02: Setting the name of the VM: kubernetes-02
    [...]
==> kubernetes-02: Machine booted and ready!
    [...]
==> kubernetes-02: Setting hostname...
==> kubernetes-02: Configuring and enabling network interfaces...
==> kubernetes-02: Running provisioner: hosts...
==> kubernetes-01: Updating hosts on: kubernetes-01
==> kubernetes-03: Importing base box 'centos/7'...
==> kubernetes-03: Matching MAC address for NAT networking...
==> kubernetes-03: Checking if box 'centos/7' is up to date...
==> kubernetes-03: Setting the name of the VM: kubernetes-03
    [..]
==> kubernetes-03: Machine booted and ready!
    [...]
==> kubernetes-03: Setting hostname...
==> kubernetes-03: Configuring and enabling network interfaces...
==> kubernetes-03: Running provisioner: hosts...
==> kubernetes-01: Updating hosts on: kubernetes-01
==> kubernetes-02: Updating hosts on: kubernetes-02
( vagrant up; )  36.69s user 28.12s system 33% cpu 3:11.33 total
$

Pro-forma. time nie jest wymagane. Posłuży tylko za dowód czasu stworzenia środowiska.

Wchodząc teraz do konsoli VirtualBox możemy zauważyć maszyny.

screen-shot-2016-12-11-at-09-13-48

Ansible

Zgodnie z tym, co zostało opisane we wstępnie, wykonam pełen provisioning powołanych maszyn wirtualnych za pomocą Ansible. W tym celu utworzyłem plik inwentarza1 o następującej treści:

[kubernetes-master]
kubernetes-01   ansible_host=192.168.50.101


[kubernetes-minions]
kubernetes-02   ansible_host=192.168.50.102
kubernetes-03   ansible_host=192.168.50.103

Weryfikuję dostępność maszyn z mojej lokalnej maszyny przy pomocy modułu ping.

$ ansible -i inventory.ini -m ping -o all
kubernetes-01 | SUCCESS => {"changed": false, "ping": "pong"}
kubernetes-03 | SUCCESS => {"changed": false, "ping": "pong"}
kubernetes-02 | SUCCESS => {"changed": false, "ping": "pong"}
$

Teraz, celem poprawnej konfiguracji maszyn, muszę dostosować zmienne środowiska do swoich ról. W tym celu, wpisuję następujące zmienne środowiskowe w pliku group_vars/all.yml:

---

centsible_etcd_cluster_hosts: 'kubernetes-master'
centsible_flanneld_etcd_cluster_hosts: '{{ centsible_etcd_cluster_hosts }}'

centsible_kubernetes_master_address: 'http://kubernetes-01:8080'
centsible_kubernetes_minion_kubelet_apiserver: '{{ centsible_kubernetes_master_address }}'

Role są zdefiniowane w taki sposób, aby automatycznie na bazie inwentarza oraz przynależności poszczególnych hostów do grup, tworzyły pliki konfiguracyjne klastra. Dlatego w pliku zostały zadeklarowane takie zmienne jak:

centsible_etcd_cluster_hosts

centsible_flanneld_etcd_cluster_hosts

Teraz pozostało stworzyć playbook, który posłuży nam za logiczny opis konfiguracji. Tworzymy, zatem, plik site.yml o następującej treści:

---


- hosts: 'all'
  become: 'true'
  gather_facts: 'true'
  any_errors_fatal: 'true'
  roles:
    - 'centsible.repository-kubernetes'


- hosts: 'kubernetes-master'
  become: 'true'
  gather_facts: 'true'
  any_errors_fatal: 'true'
  roles:
    - 'centsible.etcd'
    - 'centsible.kubernetes-flanneld'
    - 'centsible.flanneld'
    - 'centsible.kubernetes-master'


- hosts: 'kubernetes-minions'
  become: 'true'
  gather_facts: 'true'
  any_errors_fatal: 'true'
  roles:
    - 'centsible.flanneld'
    - 'centsible.kubernetes-minion'

Konfiguracja maszyn

Uruchamiamy playbook site.yml. Pełny log dostępny w załączniku.

$ time (ansible-playbook -i inventory.ini site.yml)
[...]
PLAY RECAP
*********************************************************************
kubernetes-01              : ok=19   changed=15   unreachable=0    failed=0
kubernetes-02              : ok=11   changed=9    unreachable=0    failed=0
kubernetes-03              : ok=11   changed=9    unreachable=0    failed=0
( ansible-playbook -i inventory.ini site.yml; )  25.97s user 6.44s system 7% cpu 6:48.54 total
$

Weryfikacja klastra

Logując się do maszyny kubernetes-01, możemy zweryfikować:

  • poprawność działania klastra etcd,
  • widoczność minions i ich dostępność do pracy.
[root@kubernetes-01 ~]# etcdctl cluster-health
member aa9d9612ca8e7522 is healthy: got healthy result from http://kubernetes-01:2379
cluster is healthy
[root@kubernetes-01 ~]#
[root@kubernetes-01 ~]# kubectl get nodes
NAME STATUS AGE
kubernetes-02 Ready 52m
kubernetes-03 Ready 52m
[root@kubernetes-01 ~]#

Dla ostatecznej weryfikacji klastra wykonajmy testowy deployment. Plik deploymentu wygląda następująco.

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

Inicjujemy deployment.

[root@kubernetes-01 ~]# kubectl create -f nginx-deployment.yaml --record
deployment "nginx-deployment" created
[root@kubernetes-01 ~]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx-deployment 3 3 3 0 10s
[root@kubernetes-01 ~]#

Logując się teraz do maszyn pracujących w trybie minion, widzimy dostępność kontenerów i podów.

[root@kubernetes-02 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3732a096c8e8 nginx:1.7.9 "nginx -g 'daemon off" About a minute ago Up About a minute k8s_nginx.4580025_nginx-deployment-2035384211-40xo6_default_af13202f-bf86-11e6-b806-525400d87180_2193112c
64d87909d12d registry.access.redhat.com/rhel7/pod-infrastructure:latest "/pod" About a minute ago Up About a minute k8s_POD.c36b0a77_nginx-deployment-2035384211-40xo6_default_af13202f-bf86-11e6-b806-525400d87180_2d81b186
[root@kubernetes-02 ~]#
[vagrant@kubernetes-03 ~]$ sudo -i
[root@kubernetes-03 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ac69709d91a8 nginx:1.7.9 "nginx -g 'daemon off" About a minute ago Up About a minute k8s_nginx.4580025_nginx-deployment-2035384211-4hshi_default_af135165-bf86-11e6-b806-525400d87180_d50337b2
17e0e5f3c79f nginx:1.7.9 "nginx -g 'daemon off" About a minute ago Up About a minute k8s_nginx.4580025_nginx-deployment-2035384211-itn8l_default_af130916-bf86-11e6-b806-525400d87180_127d9453
5137b148f407 registry.access.redhat.com/rhel7/pod-infrastructure:latest "/pod" 2 minutes ago Up 2 minutes k8s_POD.c36b0a77_nginx-deployment-2035384211-4hshi_default_af135165-bf86-11e6-b806-525400d87180_bd0b20d4
16af044f1288 registry.access.redhat.com/rhel7/pod-infrastructure:latest "/pod" 2 minutes ago Up 2 minutes k8s_POD.c36b0a77_nginx-deployment-2035384211-itn8l_default_af130916-bf86-11e6-b806-525400d87180_34ffb31e
[root@kubernetes-03 ~]#

Absolutnie nic nie stoi na przeszkodzie, aby do ról dodać smoke tests, które będą wykonywały powyższe czynności.

Plik site.yml

Plik site.yml  jest playbookiem Ansible i przypisuje maszynom odpowiednie role. Rolą w Ansible nazywamy zestaw zadań, triggerów, zmiennych, akcji, które zostaną wykonane na maszynie. Paramtery takie jak gather_facts czy become nie są istotne z punktu widzenia tematu artykułu.

Intuicyjnie widać, której maszynie jaką rolę przypisałem. Playbook utworzył klaster etcd z jednej maszyny. Zainstalował potrzebne pakiety, wykonał deployment pliku konfiguracyjnego, uruchomił usługę i upewnił się o jej autostarcie.

Podobnie skonfigurował flanneld. Upewnił się o tym, że potrzebne paczki są zainstalowane, nadpisał plik konfiguracyjny, skorelował go z plikiem konfiguracyjnym etcd, wypełnił poprawnymi wartościami i uruchomił usługę.

Tak samo z Kubernetes. Role: centsible.kuberenetes-minion oraz centsible.kubernetes-master odpowiednio skonfigurowały usługi, a na końcu przypisały node do mastra.

Idempotentność

Warto również wykazać idempotentność ról. Idempotentność w kontekście algebry oznacza nie więcej jak niezmienniczość ze względu na wynik przy dwukrotnym zastosowaniu tej samej funkcji. Tutaj jest nie inaczej. Jeśli dwukrotnie zastosujemy rolę, oczekujemy, że nie będzie zmian w środowisku, chyba, że stan środowiska się zmienił.

Ponownie uruchomienie tego samego playbooka skutkuje stanem OK na każdym zadaniu. Oznacza to, że cała konfiguracja klastra jest odwzorowana kodem. Idempotentność została wykazana w załączniku.

Czas budowy klastra

Sumując dwie metryki mamy:  6:48.54 + 3:11.33 ~ 10 min.

Pewnie gdybym usunął to ograniczenie na procesor i przydzielił trochę więcej RAMu, zszedłbym poniżej tej wartości. 😉

Słowem końcowym

Oczywiście, artykuł można bardzo szeroko rozszerzyć. Mógłbym wykazać jak tymi samymi rolami rozszerzać klaster horyzontalnie, zarówno w roli master jak i node. Rozszerzyć klaster do pełnego high-availability i wykazać redundancję. Dołożyć rolę od kluczy i wymagać autoryzacji certyfikatem klienckim. Dodać monitoring. Pokazać Kubernetes dashboard.

Jeśli tylko będzie zapotrzebowanie; nie omieszkam rozszerzyć tego artykułu.

Referencje

W artykule wykorzystałem następujące technologie:

Załączniki

Uruchomienie site.yml

$ time (ansible-playbook -i inventory.ini site.yml)

PLAY [all]
*********************************************************************

TASK [setup]
*******************************************************************
ok: [kubernetes-01]
ok: [kubernetes-02]
ok: [kubernetes-03]

TASK [centsible.repository-kubernetes : ensure public keys are present]
********
changed: [kubernetes-02] =>
(item=https://packages.cloud.google.com/yum/doc/yum-key.gpg)
changed: [kubernetes-03] =>
(item=https://packages.cloud.google.com/yum/doc/yum-key.gpg)
changed: [kubernetes-01] =>
(item=https://packages.cloud.google.com/yum/doc/yum-key.gpg)
changed: [kubernetes-01] =>
(item=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)
changed: [kubernetes-02] =>
(item=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)
changed: [kubernetes-03] =>
(item=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)

TASK [centsible.repository-kubernetes : ensure repository is present]
**********
changed: [kubernetes-03]
changed: [kubernetes-02]
changed: [kubernetes-01]

PLAY [kubernetes-master]
*******************************************************

TASK [setup]
*******************************************************************
ok: [kubernetes-01]

TASK [centsible.etcd : ensure packages are installed]
**************************
changed: [kubernetes-01] => (item=[u'etcd'])

TASK [centsible.etcd : ensure data directories are present]
********************
changed: [kubernetes-01] => (item=/var/lib/etcd/data)
changed: [kubernetes-01] => (item=/var/lib/etcd/wal)

TASK [centsible.etcd : deploy configuration file]
******************************
changed: [kubernetes-01]

TASK [centsible.etcd : ensure services are running]
****************************
changed: [kubernetes-01] => (item=etcd)

TASK [centsible.kubernetes-flanneld : ensure flanneld config dir is present in
etcd] ***
changed: [kubernetes-01]

TASK [centsible.kubernetes-flanneld : ensure flanneld config key is present in
etcd] ***
ok: [kubernetes-01]

TASK [centsible.flanneld : ensure packages are installed]
**********************
changed: [kubernetes-01] => (item=[u'flannel'])

TASK [centsible.flanneld : deploy configuration file]
**************************
changed: [kubernetes-01]

TASK [centsible.flanneld : ensure service is started]
**************************
changed: [kubernetes-01] => (item=flanneld)

TASK [centsible.kubernetes-master : ensure packages are installed]
*************
changed: [kubernetes-01] => (item=[u'kubernetes-master'])

TASK [centsible.kubernetes-master : deploy /etc/kubernetes/config]
*************
changed: [kubernetes-01]

TASK [centsible.kubernetes-master : deploy /etc/kubernetes/apiserver]
**********
changed: [kubernetes-01]

TASK [centsible.kubernetes-master : ensure services are up and running]
********
changed: [kubernetes-01] => (item=kube-apiserver)
changed: [kubernetes-01] => (item=kube-controller-manager)
changed: [kubernetes-01] => (item=kube-scheduler)

RUNNING HANDLER [centsible.etcd : restart etcd]
********************************
changed: [kubernetes-01] => (item=etcd)

RUNNING HANDLER [centsible.kubernetes-master : restart kubernetes-master]
******
ok: [kubernetes-01] => (item=kube-apiserver)
ok: [kubernetes-01] => (item=kube-controller-manager)
ok: [kubernetes-01] => (item=kube-scheduler)

PLAY [kubernetes-minions]
******************************************************

TASK [setup]
*******************************************************************
ok: [kubernetes-02]
ok: [kubernetes-03]

TASK [centsible.flanneld : ensure packages are installed]
**********************
changed: [kubernetes-03] => (item=[u'flannel'])
changed: [kubernetes-02] => (item=[u'flannel'])

TASK [centsible.flanneld : deploy configuration file]
**************************
changed: [kubernetes-02]
changed: [kubernetes-03]

TASK [centsible.flanneld : ensure service is started]
**************************
changed: [kubernetes-02] => (item=flanneld)
changed: [kubernetes-03] => (item=flanneld)

TASK [centsible.kubernetes-minion : ensure packages are installed]
*************
changed: [kubernetes-02] => (item=[u'kubernetes-node'])
changed: [kubernetes-03] => (item=[u'kubernetes-node'])

TASK [centsible.kubernetes-minion : deploy /etc/kubernetes/kubelet]
************
changed: [kubernetes-02]
changed: [kubernetes-03]

TASK [centsible.kubernetes-minion : ensure services are up and running]
********
changed: [kubernetes-03] => (item=kube-proxy)
changed: [kubernetes-02] => (item=kube-proxy)
changed: [kubernetes-03] => (item=kubelet)
changed: [kubernetes-02] => (item=kubelet)

RUNNING HANDLER [centsible.kubernetes-minion : restart kubernetes-minion]
******
changed: [kubernetes-03] => (item=kube-proxy)
changed: [kubernetes-02] => (item=kube-proxy)
changed: [kubernetes-03] => (item=kubelet)
changed: [kubernetes-02] => (item=kubelet)

PLAY RECAP
*********************************************************************
kubernetes-01              : ok=19   changed=15   unreachable=0    failed=0
kubernetes-02              : ok=11   changed=9    unreachable=0    failed=0
kubernetes-03              : ok=11   changed=9    unreachable=0    failed=0

( ansible-playbook -i inventory.ini site.yml; )  25.97s user 6.44s system 7%
cpu 6:48.54 total
$

Idempotentność

$ time (ansible-playbook -i inventory.ini site.yml)

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [kubernetes-01]
ok: [kubernetes-03]
ok: [kubernetes-02]

TASK [centsible.repository-kubernetes : ensure public keys are present] ********
ok: [kubernetes-01] => (item=https://packages.cloud.google.com/yum/doc/yum-key.gpg)
ok: [kubernetes-03] => (item=https://packages.cloud.google.com/yum/doc/yum-key.gpg)
ok: [kubernetes-02] => (item=https://packages.cloud.google.com/yum/doc/yum-key.gpg)
ok: [kubernetes-01] => (item=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)
ok: [kubernetes-03] => (item=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)
ok: [kubernetes-02] => (item=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg)

TASK [centsible.repository-kubernetes : ensure repository is present] **********
ok: [kubernetes-01]
ok: [kubernetes-02]
ok: [kubernetes-03]

PLAY [kubernetes-master] *******************************************************

TASK [setup] *******************************************************************
ok: [kubernetes-01]

TASK [centsible.etcd : ensure packages are installed] **************************
ok: [kubernetes-01] => (item=[u'etcd'])

TASK [centsible.etcd : ensure data directories are present] ********************
ok: [kubernetes-01] => (item=/var/lib/etcd/data)
ok: [kubernetes-01] => (item=/var/lib/etcd/wal)

TASK [centsible.etcd : deploy configuration file] ******************************
ok: [kubernetes-01]

TASK [centsible.etcd : ensure services are running] ****************************
ok: [kubernetes-01] => (item=etcd)

TASK [centsible.kubernetes-flanneld : ensure flanneld config dir is present in etcd] ***
ok: [kubernetes-01]

TASK [centsible.kubernetes-flanneld : ensure flanneld config key is present in etcd] ***
ok: [kubernetes-01]

TASK [centsible.flanneld : ensure packages are installed] **********************
ok: [kubernetes-01] => (item=[u'flannel'])

TASK [centsible.flanneld : deploy configuration file] **************************
ok: [kubernetes-01]

TASK [centsible.flanneld : ensure service is started] **************************
ok: [kubernetes-01] => (item=flanneld)

TASK [centsible.kubernetes-master : ensure packages are installed] *************
ok: [kubernetes-01] => (item=[u'kubernetes-master'])

TASK [centsible.kubernetes-master : deploy /etc/kubernetes/config] *************
ok: [kubernetes-01]

TASK [centsible.kubernetes-master : deploy /etc/kubernetes/apiserver] **********
ok: [kubernetes-01]

TASK [centsible.kubernetes-master : ensure services are up and running] ********
ok: [kubernetes-01] => (item=kube-apiserver)
ok: [kubernetes-01] => (item=kube-controller-manager)
ok: [kubernetes-01] => (item=kube-scheduler)

PLAY [kubernetes-minions] ******************************************************

TASK [setup] *******************************************************************
ok: [kubernetes-02]
ok: [kubernetes-03]

TASK [centsible.flanneld : ensure packages are installed] **********************
ok: [kubernetes-02] => (item=[u'flannel'])
ok: [kubernetes-03] => (item=[u'flannel'])

TASK [centsible.flanneld : deploy configuration file] **************************
ok: [kubernetes-02]
ok: [kubernetes-03]

TASK [centsible.flanneld : ensure service is started] **************************
ok: [kubernetes-03] => (item=flanneld)
ok: [kubernetes-02] => (item=flanneld)

TASK [centsible.kubernetes-minion : ensure packages are installed] *************
ok: [kubernetes-03] => (item=[u'kubernetes-node'])
ok: [kubernetes-02] => (item=[u'kubernetes-node'])

TASK [centsible.kubernetes-minion : deploy /etc/kubernetes/kubelet] ************
ok: [kubernetes-03]
ok: [kubernetes-02]

TASK [centsible.kubernetes-minion : ensure services are up and running] ********
ok: [kubernetes-03] => (item=kube-proxy)
ok: [kubernetes-02] => (item=kube-proxy)
ok: [kubernetes-03] => (item=kubelet)
ok: [kubernetes-02] => (item=kubelet)

PLAY RECAP *********************************************************************
kubernetes-01 : ok=19 changed=0 unreachable=0 failed=0
kubernetes-02 : ok=10 changed=0 unreachable=0 failed=0
kubernetes-03 : ok=10 changed=0 unreachable=0 failed=0

( ansible-playbook -i inventory.ini site.yml; ) 6.28s user 2.05s system 22% cpu 37.068 total
$

  1. Dla osób zaznajomionych z Ansible. Jest możliwość wykorzystywania dynamicznego inwentarza dla Vagrant. Takie skrypty są dostępne publicznie.