V přechozích dílech miniseriálu o Dockeru jsme se podívali na to, jak pracovat s kontejnerizační technologií Docker (http://mojefedora.cz/zaciname-s-dockerem-na-fedore/), jaké obrazy jsou dnes k dispozici (http://mojefedora.cz/docker-obrazy-nejen-pro-fedoru/) a ukázali jsme si základy při vytváření Docker kontejnerů (http://mojefedora.cz/stavime-svuj-prvni-docker-kontejner/). Dnes se podíváme na trochu hlouběji na to, jak vytvořit obraz pro PostgreSQL databázi, funkční jak na klasickém systému, tak na platformě OpenShift (platforma jako služba, PaaS).

Instalace balíku a její specifika

Začněme od nuly, tedy s téměř prázdným Dockerfilem, který, jak víme z předchozího dílu, popisuje, jak vytvořit obraz pro Docker kontejner:

#> cat Dockerfile
FROM fedora:23
RUN dnf -y install postgresql-server

Náš kontejner bude vycházet z tzv. base obrazu Fedory, verze 23, kam pomocí dnf nainstalujeme balík se serverem PostgreSQL. Obraz můžeme vytvořit (zbuildit) pomocí následujícího příkazu (pojmenujeme ho postgresql):

#> docker build -t postgresql .

V tomto případě by výsledný obraz kromě základního obsahu (tedy postgresql-server balíku a jeho RPM závislostí) obsahoval i pár souborů, které v obrazu nechceme. Jsou to dočasné soubory instalátoru dnf, které by dělaly výsledný obraz zbytečně velký. Dále to jsou některé soubory označené jako dokumentační, například manuálové stránky -- ty v kontejneru stejně nebudeme používat.

Pokud se těchto zbytečností chceme zbavit, nemůžeme to udělat přidáním dalšího příkazu RUN, musíme to naopak udělat již v příkazu, kde balík instalujeme. To protože Docker pro každý příkaz vytváří vlastní vrstvu, která bude součástí i výsledného obrazu. Obraz tedy vytvoříme lépe takto:

RUN dnf -y --setopt=tsflags=nodocs install postgresql-server && \
    dnf clean all

Máme tedy nyní obraz, který obsahuje databázi PostgreSQL a nic navíc. Stačí to, abychom byli spokojeni? Popravdě nám toho ještě dost chybí abychom mohli obraz rozumně použít zejména v PaaS. Pro Paas, jakož i pro ostatní rozumné a pohodlné využití, budeme chtít, aby se při spuštění kontejneru spustil přímo databázový server. Budeme tak vlastně vytvářet mikroslužbu, která následně poběží kdesi v cloudu. Stejný přístup je vhodné využít i při vytváření jiných obrazů pro PaaS, ať již jde o jinou databázi, jiný typ serveru (Apache HTTPD) nebo obecně obraz s vaší aplikací.

Inicializace databáze

Jelikož spuštění serveru PostgreSQL není "one-liner", neboť musíme nejdříve inicializovat databázi, potom nastavit rozumný přístup a nakonfigurovat server aby naslouchal na všech rozhraních, vytvoříme si na tyto všechny věci spouštěcí skript:

#!/bin/bash

initdb

echo "host all all 0.0.0.0/0 md5" >${PGDATA}/pg_hba.conf
echo "listen_addresses = '*'" >${PGDATA}/postgresql.conf

exec postgres "$@"

Použití exec pro spuštění hlavního procesu má výhody i nevýhody, je dobré mít na paměti obojí. Výhodou oproti forkování je správný přenos signálů bez nutnosti toto ošetřovat jakkoliv speciálně. Nicméně na druhé straně ne všechny démoni podporují správné akceptování návratového kódu svých potomků a v některých případech mohou tím pádem vznikat tzv. zoombie procesy. Jelikož PostgreSQL je slušný server, který po sobě uklízí, můžeme zde exec bez problémů použít.

Tento skript přidáme do obrazu a zároveň nadefinujeme pár proměnných prostředí, které databázovému démonu řeknou, kde jsou uvnitř kontejneru uloženy databázové soubory. Tento skript potom nastavíme jako výchozí příkaz, který se spustí, pokud není uživatelem specifikovaný žádný jiný příkaz explicitně při vytváření kontejneru.

ENV HOME=/var/lib/pgsql
ENV PGDATA=/var/lib/pgsql/data
ADD run-postgresql /usr/bin/
CMD [ "/usr/bin/run-postgresql" ]

Pro zvýšení bezpečnosti (nezapomínejte, že kontejnery sdílí linuxové jádro mezi sebou i s hostitelským systémem) budeme chtít spouštět kontejner pod uživatelem jiným, než je root:

ENV PGUSER=postgres
USER 26

Nyní můžeme obraz vytvořit, přičemž nezapomeneme démonovi docker říct, aby nepoužíval cache -- nechceme přeci použít nějaký starou, neaktuální a potenciálně zranitelnou verzi:

#> docker build -t postgresql --no-cache=true .

Spouštíme databázový kontejner

Kontejner z výsledného obrazu spustíme a pojmenujeme nějakým pěkným identifikátorem (např. pgsql1) a zároveň nastavíme exportování síťových portů ven na hosta pomocí -p parametru:

#> docker run -ti -p 5432:5432 --name pgsql1 postgresql

Jakou bude mít výsledný kontejner IP adresu lze zjistit pomocí docker inspect příkazu:

#> docker inspect --format='{{.NetworkSettings.IPAddress}}' pgsql1
172.17.0.2

A nyní nám nic nebrání připojit se k právě spuštěnému a již inicializovanému databázovému serveru, například pomocí psql klienta na hostovském stroji:

#> psql -h 172.17.0.2
Password: _

Perfektní, databázový server se nás ptá na heslo, takže funguje! Počkat, ale jaké heslo vlastně? Nastavili jsme snad nějaké? Máme snad nějaké výchozí heslo?

Odpověď na obě poslední otázky je nikoliv. Výchozí hesla jsou vesměs velmi špatný nápad, protože kladou na uživatele určité nároky, jako nutnost změnit ho po spuštění. No a spoléhat, že to uživatel udělá je trochu alibistické. Mnoho uživatelů může tento krok přeskočit a nebo opomenout v domnění, že tak učiní později.

Pojďme raději dát uživateli možnost heslo změnit již při vytváření kontejneru. To můžeme udělat například následující úpravou spouštěcího skriptu run-postgresql, který již přidáváme do obrazu:

echo "local all postgres peer" >>${PGDATA}/pg_hba.conf
pg_ctl -w start -o "-h ''"
psql --command "ALTER USER \"postgres\" WITH ENCRYPTED PASSWORD '${POSTGRESQL_ADMIN_PASSWORD}';"
pg_ctl stop
exec postgres "$@"

Co tento kus kódu dělá, je v první řadě úprava pg_hba.conf souboru, která umožní uživateli postgres přihlašovat se přes lokální unix socket. To potom využijeme takovým způsobem, že nastartujeme server PostgreSQL lokálně, tedy bez naslouchání na síťovém portu, a pomocí psql příkazu nastavíme heslo pro administrátora. Lokálně běžící server potom vypneme a později spustíme již normálně.

Samotné nastavení hesla si zaslouží také malé vysvětlení -- používáme tu proměnnou prostředí POSTGRESQL_ADMIN_PASSWORD, kde ta se tu vzala? No je to právě ta část, kterou uživatel zadá při spouštění docker kontejneru. Pomocí volby -e můžeme zadávat běžícímu kontejneru libovolné proměnné prostředí, v tomto případě akorát musíme zajistit, aby uživatel danou proměnnou vždy zadal. Tento kus kódu v ukázce není, ale nejedná se o žádnou vědu. Výsledný kontejner tedy spustíme takto:

#> docker run -ti -d -p 5432:5432 --name pgsql1 \
              -e POSTGRESQL_ADMIN_PASSWORD=pass postgresql

No a stejným způsobem, jako heslo pro administrátora, můžeme z proměnných prostředí nastavit při spouštění obrazu řadu věcí. Můžeme například vytvořit databázi a uživatele se zadaným heslem, který k ní bude mít přístup:

createuser "$POSTGRESQL_USER"
createdb --owner="$POSTGRESQL_USER" "$POSTGRESQL_DATABASE"
psql --command "ALTER USER \"${POSTGRESQL_USER}\" WITH ENCRYPTED PASSWORD '${POSTGRESQL_PASSWORD}';"

Konfigurace aplikačního kontejneru

A nebo můžeme umožnit uživateli nastavit některé parametry v konfiguraci serveru. Zde jen uvedu, že není nutné vytvářet podporu tímto stylem pro všechny možné volby, pokud chceme databázi používat v PaaS jako OpenShift. Stačí podorovat některé nejpoužívanější, pro ty zbylé si mohou uživatelé vytvořit vlastní minivrstvu, díky vrstvové koncepci Dockeru.

Elegantní způsob, jak vytvořit soubor, ve kterém expandujeme proměnné prostředí, je využít příkaz envsubst, třeba takto:

envsubst \
      < "/usr/share/container-scripts/postgresql/postgresql.conf.template" \
      > "/var/lib/pgsql/data/postgresql.conf"

Přičemž /usr/share/container-scripts/postgresql/postgresql.conf.template potom může obsahovat řadu voleb, pro ukázku například:

max_connections = ${POSTGRESQL_MAX_CONNECTIONS}

A jelikož pracujeme s daty, která by neměla být nikdy součástí kontejneru, který je vesměs brán jako stateless aplikace (neukládá žádná data, tedy nemění stav a tím pádem může kdykoliv skončit bez negativních důsledků), měli bychom cestu k datům nastavit jako tzv. VOLUME. Takový adresář je pak vždy oddělený i v případech, pokud uživatel nespecifikuje adresář na hostitelském stroji, kam mají být data ukládána:

VOLUME ["/var/lib/pgsql/data"]

Uživatel potom v ideálním případě spustí kontejner s parametrem -v, který bere (v tomto pořadí) adresář na hostitelském stroji, adresář uvnitř kontejneru a modifikátor :Z -- ten datům přiřadí odpovídající SELinux kontext, který neumožní pracovat se stejnými daty více kontejnerům:

docker run -v /db:/var/lib/pgsql/data:Z ...

Důležité je zde pamatovat na cesty, které mohou být v rámci obrazu téměř libovolné a záleží na volbě autora obrazu, jaké zvolí -- nicméně bychom se měli snažit použít ty cesty, které uživatelé znají z ne-kontejnerového světa, tedy například /var/lib/pgsql/data pro data PostgreSQL, /etc/my.cnf pro konfiguraci MySQL apod.

Kompletní příkaz, který uživatel na konci dne spustí, tedy bude vypadat takto:

#> docker run -d \
          -p 5432:5432 \
          -e POSTGRESQL_ADMIN_PASSWORD=secret \
          -e POSTGRESQL_MAX_CONNECTIONS=10 \
          -e POSTGRESQL_USER=guestbook \
          -e POSTGRESQL_PASSWORD=pass \
          -e POSTGRESQL_DATABASE=guestbook \
          -v /db:/var/lib/pgsql/data:Z \
          postgresql-94-centos7

To vytvoří běžící kontejner, nastaví heslo pro admina, vytvoří databázi s uživatelem a heslem pro danou aplikaci, nastaví maximální počet spojení a data bude ukládat do adresáře /db na hostovi.

Mnoho z těchto věcí je použito v trochu komplikovanějším obrazu, který je postaven na balících s PostgreSQL 9.4, dostupného jako Softwarové kolekce, na https://github.com/openshift/postgresql/tree/master/9.4. Pro další detaily si můžete projít zdrojové soubory pro vytvoření obrazu tamtéž.

V příštím díle se podíváme na vytvoření obrazu s oblíbenou aplikační platformou Python 3.4.