Inicjalizacja danych kontenera to zagwozdka, z którą spotyka się wiele osób. Przeglądając pytania na forach i grupach tematycznych, jak i otrzymując wiadomości na priv, zauważyłem, że jest to dość powszechny problem. Postanowiłem więc odpowiedzieć na to w formie blog posta, abyś i Ty mógł/mogła z tego skorzystać. Dzisiejszy wpis dotyczyć będzie inicjalizacji danych kontenera za pomocą innego kontenera. Całość zostanie omówiona na przykładzie popularnego obecnie narzędzia — jakim jest HashiCorp Vault.
Wprowadzenie
Skoro to czytasz, to doskonale wiesz jak, Docker może ułatwić życie. Szczególnie mowa tutaj o środowiskach developerskich — i na nich głównie się skupimy.
Docker świetnie radzi sobie z uruchamianiem różnego rodzaju usług na potrzeby developerskie (i nie tylko). Mam tutaj na myśli usługi takie jak bazy danych (MySQL), serwery WWW (Nginx), brokery wiadomości (RabbitMq) czy narzędzia do przechowywania sekretów (HashiCorp Vault).
Często na potrzeby developerskie istnieje potrzeba inicjalizacji tychże usług zaraz po starcie. Przykładowo może to być inicjalizacja poprzez wczytanie danych, czy inicjalizacja konfiguracji danej usługi.
W niektórych przypadkach problem ten można rozwiązać za pomocą volumenów, czyli podmontowania danych lub plików konfiguracyjnych z hosta do kontenera. Nie zawsze jest jednak taka możliwość. Czasami musimy zainicjalizować usługę działającą w kontenerze danymi, ale dopiero po jego starcie. Tym tematem dzisiaj się zajmiemy.
Opis problemu
W jednym z projektów, nad którym ostatnio pracowałem, do przechowywania sekretów wykorzystaliśmy narzędzie HashiCorp Vault. Architektura i specyfika projektu wymagała pobrania sekretów z Vaulta przez kilka mikroserwisów — podczas ich startupu. Może się to wydawać oczywiste, ale warto dodać, że wspomniane mikroserwisy również były uruchamiane z poziomu Dockera.
Aby ułatwić sobie proces developmentu i mieć możliwość uruchomienia całego ekosystemu, potrzebowaliśmy automatyczną inicjalizację Vaulta testowymi danymi. Nad projektem pracowało kilka osób i naszym celem było stworzenie przenośnego środowiska.
Idealnym rozwiązaniem było uruchomienie wszystkich komponentów za pomocą tylko jednego polecenia. Dzięki temu każda nowa osoba w zespole mogła uruchomić cały projekt — pobierając kod źródłowy z repozytorium i wpisując jedno polecenie. Jedyne wymaganie to zainstalowany wcześniej Docker. Tak też się stało.
HashiCorp Vault z poziomu Dockera
Moim celem było uruchomienie całego projektu za pomocą jednego polecenie. Pierwszym pomysłem, który przychodzi w takiej sytuacji do głowy, jest skorzystanie z docker-compose. Aby uruchomić Vaulta jednym poleceniem, przygotowany został następujący plik docker-compose.yml
.
version: "3.6" services: vault: image: vault:1.3.2 container_name: vault ports: - "8200:8200" cap_add: - IPC_LOCK environment: - VAULT_ADDR=http://127.0.0.1:8200 - VAULT_DEV_ROOT_TOKEN_ID=21ebe9d7-a536-4b32-ae16-99b56352122b networks: - network-dev volumes: - vaultdata:/vault restart: unless-stopped networks: network-dev: volumes: vaultdata:
Dzięki temu, już na tym etapie możemy uruchomić Vaulta za pomocą jednego polecenia: docker-compose up -d
.
Kontener pomocniczy
Vault posiada wbudowaną opcję zarządzania zdalnymi instancjami. Dzieje się to za pomocą Vault CLI, które może komunikować się z innymi instancjami poprzez HTTP API. Wystarczy adres URL zdalnej instancji Vaulta oraz token do autoryzacji.
W tym celu wystarczy użyć polecenia:
vault login -address="REMOTE_ADDRES" <ACCESS_TOKEN>
Oznacza to, że z poziomu CLI, możemy skomunikować się z inną instancją Vaulta, znając tylko jego adres i token. To otwiera możliwość na stworzenie pomocniczego kontenera, który posłuży do inicjalizacji głównej instancji.
W tym celu stworzony został plik o nazwie Dockerfile.vaultinit
. Jego zawartość znajduje się poniżej:
FROM vault:1.3.2 COPY ./vault_init.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/vault_init.sh \ && ln -s /usr/local/bin/vault_init.sh / ENTRYPOINT ["vault_init.sh"]
Wewnątrz powyższego Dockerfile dodawany jest plik vault_init.sh
, który zawiera zbiór poleceń służących do inicjalizacji głównej instancji Vaulta. Stanie się on głównym procesem pomocniczego kontenera. Dzieje się tak z powodu wskazania go w instrukcji ENTRYPOINT.
Plik inicjalizacyjny to nic innego jak zbiór poleceń specyficznych dla Vault CLI. Przykładowa zawartość pliku vault_init.sh
znajduje się poniżej.
#!/bin/sh while ! nc -z vault 8200; do sleep 3; done vault login 21ebe9d7-a536-4b32-ae16-99b56352122b vault secrets enable -version=2 -path=MyEngine kv vault kv put MyEngine/MyFirstSecret Url=http://test.com/ vault kv put MyEngine/MySecondSecret Url=http://test221.com/ exit 0
Co tutaj się dzieje?
Po pierwsze w pętli while
sprawdzamy połączenie z głównym kontenerem. Wykorzystujemy do tego narzędzie nc, którego zadaniem jest upewnienie się, że usługa w głównym kontenerze może przyjąć połączenie. Następnie logujemy się do głównej instancji. Na pierwszy rzut oka we wszystkich poleceniach brakuje tutaj parametru: -address="REMOTE_ADDRES"
Parametr ten będzie dodawany automatycznie „w tle” dzięki dodatkowej zmiennej środowiskowej VAULT_ADDR
, którą przekażemy do kontenera.
Pozostałe polecenia służą do dodania nowego engine’a oraz przykładowych sekretów.
Inicjalizacja kontenera – rozwiązanie
Na ten moment w głównym katalogu projektu znajdują się trzy pliki.
Ostatnim krokiem jest dodanie kontenera pomocniczego do pliku docker-compose.yml. Zauważ, że za pomocą składni build
wskazujemy plik Dockerfile.vaultinit
. Dzięki temu osoba uruchamiająca skrypty po raz pierwszy nadal będzie mogła użyć tylko jednego polecenia: docker-compose up
.
vault-initializer: build: context: . dockerfile: Dockerfile.vaultinit container_name: vault-initializer cap_add: - IPC_LOCK environment: - VAULT_ADDR=http://vault:8200 depends_on: - "vault" networks: - network-dev
Na działanie tego rozwiązania mają wpływ również dwa inne czynniki.
Po pierwsze, przekazanie zmiennej środowiskowej VAULT_ADDR=http://vault:8200
oraz instrukcja depends_on: "vault"
.
Dzięki tym wpisom kontener pomocniczy otrzymuje adres głównej instancji Vaulta, który będzie domyślnym adresem podczas komunikacji za pomocą Vault CLI. Dodatkowo instrukcja depends_on
sprawi, że kontener pomocniczy zostanie stworzony dopiero po stworzeniu głównego kontenera.
Oznacza to, że wszystkie instrukcje znajdujące się w pliku vault_init.sh
zostaną wykonane, tak jakby za każdym razem dodawana był parametr -address "http://vault:8200"
. Przykład poniżej.
vault login -address="http://vault:8200" 21ebe9d7-a536-4b32-ae16-99b56352122b vault secrets enable -address="http://vault:8200" -version=2 -path=MyEngine kv
Komunikacja między kontenerami
Komunikacja między kontenerem pomocniczym a kontenerem głównym jest możliwa, ponieważ oba kontenery po uruchomieniu zostaną podpięte do tej samej sieci —network-dev
.
Sieć ta zdefiniowana jest w pliku docker-compose.yml
i zostanie automatycznie utworzona. Komunikacja z kontenera pomocniczego do kontenera głównego odbywa się po nazwie usługi —vault
.
Finalnie plik docker-compose.yml
może wyglądać następująco:
version: "3.6" services: vault: image: vault:1.3.2 container_name: vault ports: - "8200:8200" cap_add: - IPC_LOCK environment: - VAULT_ADDR=http://127.0.0.1:8200 - VAULT_DEV_ROOT_TOKEN_ID=21ebe9d7-a536-4b32-ae16-99b56352122b networks: - network-dev volumes: - vaultdata:/vault restart: unless-stopped vault-initializer: build: context: . dockerfile: Dockerfile.vaultinit container_name: vault-initializer cap_add: - IPC_LOCK environment: - VAULT_ADDR=http://vault:8200 depends_on: - "vault" networks: - network-dev networks: network-dev: volumes: vaultdata:
Jedyne co pozostaje, to wpisanie polecenia docker-compose up
, które spowoduje uruchomienie Vaulta, a następnie jego inicjalizację przez kontener pomocniczy. Warto jeszcze zaznaczyć, że po zakończeniu procesu inicjalizacji, kontener pomocniczy kończy swoje działanie. Odpowiedzialne za to jest znajdujące się w pliku vault_init.sh
polecenie exit 0
, które wysyła sygnał zatrzymania kontenera.
Podsumowanie
Omawiany przykład pokazuje tylko jeden ze sposobów na zainicjalizowanie kontenera (danych) z wykorzystaniem kontenera pomocniczego. Należy podkreślić, że rozwiązanie jest przeznaczone wyłącznie do zastosowań developerskich. Produkcyjna konfiguracja Vaulta znacznie się różni i należy mieć tego świadomość.
W niektórych przypadkach twórcy obrazu przewidzieli możliwość inicjalizacji i jest na to gotowa metoda. Bardzo dobrym przykładem jest PostgreSQL, gdzie skrypty inicjalizujące wystarczy dodać do katalogu /docker-entrypoint-initdb.d/
. Mogą to być zarówno skrypty .sql
jak i .sh
, które wykonają się bezpośrednio po starcie kontenera.
2 Komentarze
Anon · 30 kwietnia 2020 o 8 h 41 min
A nie można było zamiast tworzyć drugi kontener po prostu uruchomić ten skrypt inicjujący z shella? Nazwać go np. create-and-init.sh i wykonać w nim docker-compose a potem to czekanie i inicjalizację? Hype jest tak przeogromny, że za niedługo do wykonania „ls” będzie potrzebny docker.
Damian Naprawa · 30 kwietnia 2020 o 9 h 51 min
Dzięki za komentarz. Jasne – można byłoby odpalić z shella, ale wymagało by to posiadania zainstalowanego lokalnie Vault CLI.
Kontener pomocniczy ma już w sobie Vault CLI. Dzięki temu rozwiązaniu możemy uruchomić cały projekt na dowolnym systemie operacyjnym – bez potrzeby instalowania Vault CLI. Dodatkowo oprócz Vaulta wewnątrz pliku docker-compose.yml było jeszcze 6 innych usług. Dzięki temu jednym poleceniem „docker-compose up” wstaje cały projekt 🙂