Docker Maestro → Dołącz już dziś! »»

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.

Inicjalizacja kontenera
Źródło https://unsplash.com

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. 

Docker

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.



Dołącz do newslettera

Już ponad 850 osób zapisało się na mój newsletter (zero spamu! - tylko informacje o nowych artykułach oraz ciekawe nowinki ze świata konteneryzacji).

Zapisując się, jako bonus prześlę Ci "21 Praktycznych Przykładów Użycia Dockera Dla Developerów" oraz zbiór "10 Najlepszych Praktyk Tworzenia Dockerfile"



Damian Naprawa

Praktykujący pasjonat konteneryzacji. Lubi dzielić się wiedzą, prowadząc warsztaty i szkolenia. Chętnie występuje również w roli prelegenta na meetupach i konferencjach. Uczestnik globalnego programu partnerskiego Docker Enablement. Pracuje z Dockerem na co dzień od kilku lat. Odpowiedzialny za tworzenie i utrzymanie systemów działających w oparciu o kontenery. Fan automatyzacji oraz podejścia "As a Code".

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 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *