Od czerwca zeszłego roku kiedy to pojawiła się wersja 1.12 minęło już sporo czasu, więc ekipa z Dockera postanowiła wydać kolejną już wersję - 1.13
W niniejszym artykule będę opisywał zarówno 1.13 (styczeń 2017) jak i nowość z lutego 1.13.1 która wprowadziła kolejne zmiany.
Warto od razu zaznaczyć że w momencie pisania tego tekstu dokonano podziału dockera na enterprise edition i community edition. Dodatkowo wraz z tym podziałem powołano nowy schemat nazewnictwa oparty o miesiąc/rok tak więc obecnie mamy już 17.03 (CE realesowane miesięcznie zaś EE kwartalnie).
O różnicach między 17.03 a 1.13 nie będę pisał bo po dogłębnym przetestowaniu 17.03 mogę powiedzieć że ich po prostu nie ma. Na moje oko wersja 17.03 to z grubsza (z technicznego punktu widzenia) coś w stylu 1.13.2 - zawiera bugfixy do 1.13 i 1.13.1, jej API jest zgodne z API z 1.13 , ma zaimplementowane wszystkie nowe polecenia z 1.13 i zachowuje się identycznie.
Wróćmy zatem do zmian technicznych w 1.13 bo te interesują nas najbardziej. Aby korzystać z ficzerów jakie opisuję bardzo często trzeba włączyć tryb experimental dockera. Warto od razu zauważyć że od 1.13 docker wbudował obydwie wersje (stable i experimental) w jedną binarkę
Aby nie dzielić nowości z 1.13 na experimental i stable od razu zmieniamy wywołanie docker demona:
# cat /usr/lib/systemd/system/docker.service | grep -i execstart
ExecStart=/usr/bin/dockerd --experimental=true
# sudo systemctl daemon-reload
# sudo systemctl restart docker
# docker version
Client:
Version: 1.13.1
API version: 1.26
Go version: go1.7.5
Git commit: 092cba3
Built: Wed Feb 8 06:38:28 2017
OS/Arch: linux/amd64
Server:
Version: 1.13.1
API version: 1.26 (minimum version 1.12)
Go version: go1.7.5
Git commit: 092cba3
Built: Wed Feb 8 06:38:28 2017
OS/Arch: linux/amd64
Experimental: true
To jedna z pierwszych nowości jakie omówimy , czy jej wprowadzenie było niezbędne można dyskutować - zawsze można było zainstalować przecież logspouta w trybie swarm global osiągając ten sam efekt. Docker słynie jednak z zastępowania rozwiązań firm trzecich swoimi własnymi odpowiednikami. Podobnie zresztą będzie przy metrics które robią to samo co cAdvisor.
Aby skorzystać z docker service logs tworzymy 4 kontenerowy service którego zadaniem jest logowanie hostname na wyjście:
# docker service create --name w8 --replicas=4 alpine /bin/sh -c "while [ 1 ] ; do hostname ; sleep 2 ; done "
idcgz0xw4qefhvi5h41zyerrv
jak widać swarm uruchomił 4 kontenery:
# docker service ps w8
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
meb3inrben6p w8.1 alpine:latest cent302 Running Running less than a second ago
resh1ax75psj w8.2 alpine:latest cent301 Running Running 3 seconds ago
o9qr3re09hwd w8.3 alpine:latest cent302 Running Running less than a second ago
uv9ki5h7pngc w8.4 alpine:latest cent301 Running Running 3 seconds ago
sprawdźmy ID kontenerów na lokalnym węźle:
# docker ps | grep w8
46f49b352012 alpine@sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8 "/bin/sh -c 'while..."
About a minute ago Up About a minute w8.4.uv9ki5h7pngc582wnun98ks05
f30d0c5e9956 alpine@sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8 "/bin/sh -c 'while..."
About a minute ago Up About a minute w8.2.resh1ax75psjvrr3f97s348kw
kążdy z nich loguje na stdout swój hostname:
# docker logs -f 46f49b352012 | head -3
46f49b352012
46f49b352012
46f49b352012
# docker logs -f f30d0c5e9956 | head -3
f30d0c5e9956
f30d0c5e9956
f30d0c5e9956
pozostaje zbieranie logów z całego service - do tego służy nowe polecenie:
# docker service logs -f w8
w8.1.meb3inrben6p@cent302 | 00a9d341b302
w8.2.resh1ax75psj@cent301 | f30d0c5e9956
w8.3.o9qr3re09hwd@cent302 | 6c093ff89396
w8.4.uv9ki5h7pngc@cent301 | 46f49b352012
w8.1.meb3inrben6p@cent302 | 00a9d341b302
w8.2.resh1ax75psj@cent301 | f30d0c5e9956
w8.3.o9qr3re09hwd@cent302 | 6c093ff89396
w8.4.uv9ki5h7pngc@cent301 | 46f49b352012
Sprawdźmy wywołanego nieco wcześniej do tablicy logspouta. Skoro teoretycznie potrafi routować wszystkie logi na zewnątrz to powinien poradzić sobie również z docker service logs.
Uruchamiamy logspouta:
docker run -d -v /var/run/docker.sock:/tmp/docker.sock --name logspout -e LOGSPOUT=ignore -e DEBUG=true --publish=8000:80
gliderlabs/logspout:master syslog://172.17.0.1:5000
Obserwujemy listing zdarzeń jakie via REST wystawia kontener logspouta na TCP/8000
curl 0:8000/logs
Na drugim terminalu tworzymy 4 kontenerowy serwis (w5) którego jedyną funkcją jest pisanie na STDOUT :
# docker service ps w5
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
mxcem2viifbw w5.1 alpine:latest cent503 Running Running 7 seconds ago
bl4s0ux9f30a w5.2 alpine:latest cent503 Running Running 7 seconds ago
qcw3pcux3iyc w5.3 alpine:latest cent502 Running Running 8 seconds ago
yvbebbsmkdew w5.4 alpine:latest cent501 Running Running 8 seconds ago
niestety logspout uruchomiony na node=cent501 zbiera logi tylko z kontenera uruchomionego na cent501:
w5.4.yvbebbsmkdewlafbf9s05knsz|7ba88f114552
w5.4.yvbebbsmkdewlafbf9s05knsz|7ba88f114552
w5.4.yvbebbsmkdewlafbf9s05knsz|7ba88f114552
w5.4.yvbebbsmkdewlafbf9s05knsz|7ba88f114552
równocześnie inny logspout odpalony na node=cent503 (gdzie są 2 kontenery) pokazuje logi tylko z tych 2 kontenerów
Niestety Issue z pytaniem czy logspout planuje fixa aby zbierać logi ze swarm service logs: gliderlabs/logspout#271
zamknęli z komentarzem: No. It only reads logs from the local machine. You'll need to run logspout on every system in your swarm cluster.
Oczywiście logspout ma konkurencję , nie wróżę świetlanej przyszłości przy takim podejściu, była kiedyś taka marka SAAB :-) ...
##Docker squash image layers
Historycznie przypomnę pewien wątek ("Can't stack more than 42 aufs layers") moby/moby#1171
Niezależnie od tego czy problemy z ilością warstw dla różnych driverów są faktycznie problemami czy sztuczną barierą którą można zaniedbać mimo wszystko ekipa Dockera postanowiła dodać funkcjonalność spłaszczania obrazów.
Pokażmy najpierw klasyczne piętrowe obrazy na przykładzie - tworzymy Dockerfile który na gołym IMG=alpine buduje 3 warstwy:
# cat Dockerfile
FROM alpine
RUN echo 1 > /plik
RUN echo 2 >> /plik
RUN echo 3 >> /plik
budujemy z tego arcydzieła obraz :
# docker build -t test_img_02 .
jak widać nie jest on zbyt płaski i zawiera każdą sekcję RUN w postaci 1 extra warstwy
# docker history test_img_02
IMAGE CREATED CREATED BY SIZE COMMENT
dcefa0bda16e 46 seconds ago /bin/sh -c echo 3 >> /plik 6 B
258404afe6f5 47 seconds ago /bin/sh -c echo 2 >> /plik 4 B
fc9880909684 48 seconds ago /bin/sh -c echo 1 > /plik 2 B
88e169ea8f46 8 weeks ago /bin/sh -c #(nop) ADD file:92ab746eb22dd3e... 3.98 MB
inspect na obrazie również pokazuje 4 warstwy
# docker inspect test_img_02 | tail -10
"Type": "layers",
"Layers": [
"sha256:60ab55d3379d47c1ba6b6225d59d10e1f52096ee9d5c816e42c635ccc57a5a2b",
"sha256:8f6a439091db9c676e931bf8da05add5fc01b644ad3325db5421c64a98574995",
"sha256:8f9aee109583484f37647d88277f4967514a52c3eef10c61a8fc5e154cb93df6",
"sha256:7b23033eb2828a85c392d5a59006c209ebc2b4fd63b2d61c31819493798feac4"
]
}
}
]
Jak widać każda linijka z Dockerfile jest reprezentowana przez oddzielną warstwę. Ten pierwszy layer ("sha256:60ab55d3379d47c1ba6b6225d59d10e1f52096ee9d5c816e42c635ccc57a5a2b") to oczywiście alpine:
# docker inspect alpine | tail -10
}
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:60ab55d3379d47c1ba6b6225d59d10e1f52096ee9d5c816e42c635ccc57a5a2b"
W 1.13.x aby nie robić wielu warstw wprowadzono opcję squash dla IMG:
# docker build --squash -t test_img_03 .
tym razem docker history pokazuje że warstw mamy dużo mniej:
# docker history test_img_03
IMAGE CREATED CREATED BY SIZE COMMENT
2b4cfdfb143f 45 seconds ago 6 B merge
sha256:dcefa0bda16ef2140bb8cd2a4b1a5eb2d5592d0e8ef3cc80693fe7447ac78103 to
sha256:88e169ea8f46ff0d0df784b1b254a15ecfaf045aee1856dca1ec242fdd231ddd
<missing> 6 minutes ago /bin/sh -c echo 3 >> /plik 0 B
<missing> 6 minutes ago /bin/sh -c echo 2 >> /plik 0 B
<missing> 6 minutes ago /bin/sh -c echo 1 > /plik 0 B
<missing> 8 weeks ago /bin/sh -c #(nop) ADD file:92ab746eb22dd3e... 3.98 MB
docker inspect również:
# docker inspect test_img_03 | tail -10
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:60ab55d3379d47c1ba6b6225d59d10e1f52096ee9d5c816e42c635ccc57a5a2b",
"sha256:7b23033eb2828a85c392d5a59006c209ebc2b4fd63b2d61c31819493798feac4"
]
}
Jak widać powyżej obraz zbudowany z opcją squash składa się już tylko z 2 warstw (pierwsza to parent image, druga to skompresowane następne 3 warstwy z deltą zmian - skompresowane do jednej)
(ten missing to zupełnie normalna sprawa)
##Docker metrics in Prometheus format
TL;DR: zamiast używać cadvisora można teraz wykorzystać mechanizmy wbudowane w docker engine.
Prometeusz czyli celebryta wśród systemów monitoringu robi zawrotną karierę i każdy dzisiaj chce mieć co najmniej jednego exportera. Obecna lista konektorów przysparza o ból głowy i wkrótce zapewne lodówki, pralki czy drzwi od stodoły będą miały URI="/metrics". Na dzień dzisiejszy rozpoznanie nowego softu można ograniczyć do sprawdzenia resta jakiego wystawia, można również dowolne rozwiązania od razu skreślić i skrytykować za brak exportera.
Jak wiadomo już wieki temu do exportowania metryk w świecie dockera służył tandem cAdvisor + node exporter. Oczywiście żaden z nich nie pochodzi z fabryki Docker więc koniecznie_musiano dostarczyć "markowy" odpowiednik.
Do systemd exec startu silnika poza flagą experimental dodajemy --metrics:
# cat /usr/lib/systemd/system/docker.service | grep ExecStart
ExecStart=/usr/bin/dockerd --experimental=true --metrics-addr=0.0.0.0:4999
Od tej pory na TCP/4999 wystawiany jest rest typu "prometeuszowy metrics" na którym mamy kilkadziesiąt statsów wprost z wnętrza silnika:
# curl 0:4999/metrics 2>&1 | head
[...]
# HELP engine_daemon_container_actions_seconds
The number of seconds it takes to process each container action
# TYPE engine_daemon_container_actions_seconds histogram
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.005"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.01"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.025"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.05"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.1"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.25"} 1
Czy to zagrozi cAdvisorovi ? No nie sądzę, zwłaszcza jak sie popatrzy kim jest jego ojciec i że ten ojciec z dockerem się chyba nie lubi lub za chwilę pokaże konkurencyjne kontenery :-)
Dbając o czystość, spójność i przejrzystość dokonano kluczowych zmian w API. Z grubsza chodzi o to żeby wprowadzić w miarę takie same polecenia do kontenerów jak i obrazów itd. Mi tam akurat nie przeszkadzało że nie ma spójnej wizji i jest ogólny chlew (co nawet miało swój urok) no ale ktoś zdecydował że trzeba zrobić porządek. Ja natomiast omówię tu nowe przydatne API - docker system
docker system:
Commands:
df Show docker disk usage
events Get real time events from the server
info Display system-wide information
prune Remove unused data
zacznijmy od "df", odpalamy kontener:
docker run -tdi ubuntu:14.04
potem kontener z volumenem, w środku generacja danych 4 MB :
docker run -ti -v /dane --name=test_alpine01 alpine sh
/ # dd if=/dev/zero of=/dane/PLIK bs=4k count=1000
docker system df pokaże nam wszystko co narobiliśmy:
# docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 2 2 191.9 MB 0 B (0%)
Containers 2 2 384 B 384 B (100%)
Local Volumes 1 1 4.096 MB 0 B (0%)
wersja bardziej szczegółowa:
# docker system df -v
Images space usage:
REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE
SIZE CONTAINERS
ubuntu 14.04 b969ab9f929b 5 weeks ago 187.9 MB 0 B 187.9
MB 1
alpine latest 88e169ea8f46 2 months ago 3.98 MB 0 B 3.98 MB
1
Containers space usage:
CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED STATUS
NAMES
e9945a3a28cd alpine "sh" 1 96 B 33 seconds ago Up 31
seconds test_alpine01
4639e2890fb2 ubuntu:14.04 "/bin/bash" 0 0 B 33 seconds ago Up 31
seconds cranky_clarke
Local Volumes space usage:
VOLUME NAME LINKS SIZE
f08a3b5b8b0ac37b8301eabfe7a3c26cfb63f783fb13df46b086165d19292c31 1 4.096 MB
stopujemy obydwa kontenery i wykonujemy docker system prune
# docker system prune
WARNING! This will remove:
- all stopped containers
- all volumes not used by at least one container
- all networks not used by at least one container
- all dangling images
Are you sure you want to continue? [y/N] y
Deleted Containers:
e9945a3a28cd6f088fe8d9ed3e30cb3225f3440dbf13434605fcf42ef959ba4f
4639e2890fb2d708447b317830247e48a8ca5f80d5521835d587ea5c84ebc437
Deleted Volumes:
f08a3b5b8b0ac37b8301eabfe7a3c26cfb63f783fb13df46b086165d19292c31
Total reclaimed space: 4.096 MB
Poza szeroko działającym system prune są jeszcze 3 polecenia bardziej szczegółowe:
# docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0 B
# docker volume prune
WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0 B
# docker image prune
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0 B
TL;DR: Wersja 1.13.x wprowadza wsparcie formatu docker-compose.yml do budowania stacków (już bez DAB)
A oto szczegóły:
Jak pamiętamy docker-compose mógł obsługiwać swarma ale tylko do wersji 1.11.
Wraz z nadejściem swarm-mode (swarm wbudowany, od wersji 1.12) docker-compose przestał być wspieraną formą deploymentu usług na swarm i wg niektórych źródeł zaczał mieć nawet status depreciated. Jako następca i alternatywa dla docker-compose w środowisku swarm zaproponowano format DAB (“Distributed Application Bundle”)
Jednocześnie wprowadzono pojęcie swarm docker stack. Celem usystematyzowania przypomnijmy jego definicję i funkcję:
- Stack to zbiór usług swarma składających się na środowisko aplikacyjne.
- Stack umożliwia automatyzację instalacji wielu powiązanych ze sobą (zależnościami) serwisów swarma.
W hierarachi od dołu najpierw zatem mamy kontenery, potem swarm-service (jako zbiór N kontenerów rozpędzonych z jednego obrazu) a następnie na szczycie jest stack który z kolei jest zbiorem serwisów.
I teraz najważniejsze - Docker w 1.13 znacząco uprościł deployment wielowarstowych środowisk - teraz już nie trzeba używać do tego formatu DAB (który nie do końca wiadomo jaką ma przyszłość) ale wystarczy do tego celu używać starego i znanego docker-compose.
Jest to jeden z najważniejszych i kluczowych ficzerów w 1.13. Dla niektórych wręcz jest to triumfalny i wyczekiwany powrót starego i lubianego formatu docker-compose.yml. Niektórzy idą jeszcze dalej ze spekulacjami i wieszczą naturalną śmierć formatu DAB.
Celem przeprowadzenia LABu tworzymy obraz z którego będziemy rozpędzać kontenery serwujące swój numer rewizji i hostname na porcie 80.
oto Dockerfile:
FROM ubuntu:14.04
RUN apt-get -y update && apt-get install -y apache2 && apt-get install -y dnsutils && apt-get install -y curl
CMD apachectl start && echo 1 > /var/www/html/index.html && hostname >> /var/www/html/index.html ; tail -f /dev/null
budujemy:
docker build -t apacz01 .
tak zbudowany obraz dystrybuujemy na wszystkie węzły swarma (np via docker save , scp , docker load)
Oto bardzo prosty docker-compose-v3 który uruchomi swarm service oparty o nasz IMG:
# cat docker-compose.yml
version: "3"
services:
web04:
image: apacz01
ports:
- "9004:80"
deploy:
replicas: 2
robimy deploy stacka nową metodą - czyli wprost z pliku docker-compose:
# docker stack deploy --compose-file docker-compose.yml stack_01
Creating network stack_01_default
Creating service stack_01_web04
jak widać nasza 2 kontenerowa usługa wstała :
# docker service ls
ID NAME MODE REPLICAS IMAGE
yjzaowkmrnbb stack_01_web04 replicated 2/2 apacz01
# docker service ps stack_01_web04
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
twjbod862ybu stack_01_web04.1 apacz01 cent503 Running Running 8 seconds ago
vh5w1guhkgl8 stack_01_web04.2 apacz01 cent501 Running Running 9 seconds ago
a nawet działa ingresowo-ipvs'owy load-balancing (zwraca raz jeden raz drugi kontener) :
# curl 0:9004
534c4c112613
# curl 0:9004
27d441d1a697
# curl 0:9004
534c4c112613
Listing stacków pracujących na swarmie:
# docker stack ls
NAME SERVICES
stack_01 1
Listing kontenerów pracujących w stacku:
# docker stack ps stack_01
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
twjbod862ybu stack_01_web04.1 apacz01 cent503 Running Running 3 minutes ago
vh5w1guhkgl8 stack_01_web04.2 apacz01 cent501 Running Running 3 minutes ago
Listing serviców pracujących w stacku:
# docker stack services stack_01
ID NAME MODE REPLICAS IMAGE
yjzaowkmrnbb stack_01_web04 replicated 2/2 apacz01
warto zauważyć że sieć typu overlay (o nazwie stack_01_default) została dociągnięta TYLKO do tych węzłów swarma na których są kontenery usługi (jak to w SDN zwykło działać)
Na koniec usuwamy stack :
# docker stack rm stack_01
Removing service stack_01_web04
Removing network stack_01_default
Wprowadzamy
- rolling update (a la Blue-Green deployment)
- update parallelism (czyli update nie na wszystkich kontenerach na raz ale w paczkach po 2 sztuki)
- dependency między serwisami swarma w naszym stacku (najpierw registry, potem usługa web)
- private registry jako swarm service - ale na wskazanym node (master)
- celem uniknięcia używania flagi --insecure-registry przy ExecStart dla demona dockera będziemy świadczyc repo na każdym węźle swarma - ale będzie to jedno repo, tyle że mapowane po portach
- Nasz stack zdefiniuje 2 usługi swarma, będą one komunikować się w jednej sieci typu overlay (prywatnej)
Podczas testów wygodnie obserwować zachowanie swarma za pomocą mano-marks-swarm-visualizera - na masterze uruchamiamy zatem:
docker run -it -d -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock manomarks/visualizer
I wchodzimy przeglądarką na 8080 na swarm-master.
A oto plik docker-compose.yml w drugiej odsłonie:
version: "3"
services:
www01:
image: apacz01
ports:
- "9004:80"
depends_on:
- registry
deploy:
replicas: 4
update_config:
parallelism: 1
delay: 5s
registry:
image: registry:2
ports:
- "5000:5000"
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
Jak widać nowe argumenty (te do swarma) pojawiają się głównie w sekcji deploy (ogólnie ta sekcja może być uznana za pojemnik na opcje swarma). Na dole pliku YML pojawia się czasem lista sieci dla stacku - jeśli ich nie podamy swarm zbuduje sieć defaultową (nazwa-stacka_default).
Wykonujemy deployment stacka poleceniem:
# docker stack deploy --compose-file ./docker-compose.yml stack_03
Creating network stack_03_default
Creating service stack_03_www01
Creating service stack_03_registry
weryfikacja że wszystko uruchomiło się poprawnie :
# docker stack ls
NAME SERVICES
stack_03 2
# docker stack ps stack_03
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
z9u4jlzd35rc stack_03_registry.1 registry:2 cent501 Running Running 7 minutes ago
nlhwvig7f7ha stack_03_www01.1 apacz01 cent501 Running Running 7 minutes ago
dmuwnkfdxlku stack_03_www01.2 apacz01 cent503 Running Running 7 minutes ago
vl51n9mu5cnr stack_03_www01.3 apacz01 cent503 Running Running 7 minutes ago
iuxb28u32i56 stack_03_www01.4 apacz01 cent502 Running Running 7 minutes ago
# docker service ls
ID NAME MODE REPLICAS IMAGE
vv69mtd0iif8 stack_03_registry replicated 1/1 registry:2
ytcdlg22ghtq stack_03_www01 replicated 4/4 apacz01
# docker service ps stack_03_www01
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
nlhwvig7f7ha stack_03_www01.1 apacz01 cent501 Running Running 7 minutes ago
dmuwnkfdxlku stack_03_www01.2 apacz01 cent503 Running Running 7 minutes ago
vl51n9mu5cnr stack_03_www01.3 apacz01 cent503 Running Running 7 minutes ago
iuxb28u32i56 stack_03_www01.4 apacz01 cent502 Running Running 7 minutes ago
Powstał 1 stack składający się z 2 usług swarma - jak widać Swarm zrobił dystrybucję usługi webowej na 3 węzły zaś Registry trafiło tak jak chcieliśmy na node-master.
Zaraz po starcie powyższego stacka tworzymy (na swarm-master lub dowolnym innym węźle) 2 nowe obrazy zwracające poza hostname kolejne rewizje (2 i 3) Tym razem z racji posiadania centralnego registry nie musimy ich już dystrybuować na pozostałe węzły via scp itd.
FROM ubuntu:14.04
RUN apt-get -y update && apt-get install -y apache2 && apt-get install -y dnsutils && apt-get install -y curl
CMD apachectl start && echo 2 > /var/www/html/index.html && hostname >> /var/www/html/index.html ; tail -f /dev/null
docker build -t apacz02 .
FROM ubuntu:14.04
RUN apt-get -y update && apt-get install -y apache2 && apt-get install -y dnsutils && apt-get install -y curl
CMD apachectl start && echo 3 > /var/www/html/index.html && hostname >> /var/www/html/index.html ; tail -f /dev/null
docker build -t apacz03 .
tagujemy wszystkie 3 obrazy (apacz01 , 02 i 03)
docker tag apacz01 127.0.0.1:5000/apacz01
docker tag apacz02 127.0.0.1:5000/apacz02
docker tag apacz03 127.0.0.1:5000/apacz03
Po postawieniu stack_03 mamy już registry więc można wykonać push (x 3) obrazów - i to w dodatku na dowolnym węźle tam gdzie mamy je w lokalnym repo obrazów. Registry słucha na 5000 na każdym węźle więc nie musimy używać flagi --insecure-registry przy starcie demona dockera:
docker push 127.0.0.1:5000/apacz01
docker push 127.0.0.1:5000/apacz02
docker push 127.0.0.1:5000/apacz03
pozostaje wykonać update jednej z usług stacka nowym obrazem
W tym celu sprawdzamy na dowolnym węźle że działa load-balancing i zwracana jest via WWW rewizja = 1
curl 0:9004
Następnie wykonujemy update stacka nowym obrazem:
a) modyfikacja w pliku docker-compose.yml - w sekcji image zmieniamy IMG i replicas:
[...]
www01:
image: 127.0.0.1:5000/apacz02
ports:
- "9004:80"
[...]
b) stack update:
# docker stack deploy --compose-file docker-compose.yml stack_03
Updating service stack_03_web04 (id: 8l7l2ap8iiswpg4oz97t5f11c)
Updating service stack_03_registry (id: ny0wlhndgy3chreskkpscezmk)
Wykonujemy ponownie test via curl 0:9004 - działa przez pewien czas i stara i nowa usługa, po pewnym czasie już tylko nowa.
Jak widać z outputu zwracanego przez curla i na GUI mano-marks usługi zmieniają obraz w sposób w jaki zdefiniowaliśmy - czyli nie wszystkie naraz i z zadanym przez nas odstępem. Najpierw update wykonywany jest na pierwszym kontenerze, dopiero jak się skończy to na kolejnym. Dzięki temu nasza usługa biznesowa cały czas jest świadczona. Takie zachowanie wymuszane jest poprzez parametry update parallelism = 1 (czyli update na tylko jednym kontenerze z 4 pracujących) oraz z odstępem delay = 5s. Dla przypomnienia parametry te pochodzą z pliku docker-compose.yml dla naszego stacka
Na koniec wykonujemy listing kontenerów pracujących w stacku:
# docker stack ps stack_03
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
PORTS
trw6gnedkc0y stack_03_registry.1 registry:2 cent501 Running Running 2 minutes ago
q2vlrrk63e8g stack_03_www01.1 127.0.0.1:5000/apacz02:latest cent501 Running Running 26 seconds ago
ss128knv97rv stack_03_www01.1 apacz01 cent501 Shutdown Shutdown 27 seconds ago
i1p1t0btoadq stack_03_www01.3 127.0.0.1:5000/apacz02:latest cent503 Running Running 9 seconds ago
x8vn0zx5yc46 \_ stack_03_www01.3 apacz01 cent503 Shutdown Shutdown 10 seconds ago
y3wiimgv5fd0 stack_03_www01.2 127.0.0.1:5000/apacz02:latest cent503 Running Running 43 seconds ago
jfxc5wisyit0 \_ stack_03_www01.2 apacz01 cent503 Shutdown Shutdown 48 seconds ago
y5lzv3dpa40i stack_03_www01.4 127.0.0.1:5000/apacz02:latest cent502 Running Running about a minute ago
2i4u5rje4wvi \_ stack_03_www01.4 apacz01 cent502 Shutdown Shutdown about a minute ago
Jak widać swarm podmienił wszystkie obrazy pracujące w stacku dla service www01
A zatem zróbmy kolejną podmianę IMG i deploy:
# cat docker-compose.yml
[...]
www01:
image: 127.0.0.1:5000/apacz03
docker stack deploy --compose-file docker-compose.yml stack_03
weryfikacja:
# docker stack ps stack_03
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
PORTS
trw6gnedkc0y stack_03_registry.1 registry:2 cent501 Running Running 10 minutes ago
ksdm2ssq2rcj stack_03_www01.1 127.0.0.1:5000/apacz03:latest cent501 Running Running 52 seconds ago
q2vlrrk63e8g \_ stack_03_www01.1 127.0.0.1:5000/apacz02:latest cent501 Shutdown Shutdown 52 seconds ago
kdlxek58ehpa stack_03_www01.2 127.0.0.1:5000/apacz03:latest cent503 Running Running 35 seconds ago
y3wiimgv5fd0 \_ stack_03_www01.2 127.0.0.1:5000/apacz02:latest cent503 Shutdown Shutdown 35 seconds ago
ztcv0i8p8e5u stack_03_www01.3 127.0.0.1:5000/apacz03:latest cent503 Running Running about a minute ago
i1p1t0btoadq \_ stack_03_www01.3 127.0.0.1:5000/apacz02:latest cent503 Shutdown Shutdown about a minute ago
wbx7qfy0ortl stack_03_www01.4 127.0.0.1:5000/apacz03:latest cent502 Running Running 18 seconds ago
y5lzv3dpa40i \_ stack_03_www01.4 127.0.0.1:5000/apacz02:latest cent502 Shutdown Shutdown 18 seconds ago
Jak widać wszystko zgodnie z naszymi założeniami - swarm podmienił IMG dla service z rewizji 2 na rewizję 3. Podczas update można było zaoserwować że update wykonywany jest porcjami (po 1 sztuce).
Secrets to wrażliwe dane konfiguracyjne takie jak m.in.:
- hasła do baz danych
- klucze SSH
- certyfikaty TLS
- tokeny do REST-API
- hasła do wszelakiej komunikacji międzysystemowej
- być może nawet hasła do SSH
- itd itp
w skrócie i dla uproszczenia możemy wszystkie powyższe dane traktować jako hasła i nazywać je hasłami.
Oto wszystkie znane mi metody trzymania danych wrażliwych w świecie dockera:
niebezpiecznie prosty ale dosyć słaby pomysł:
- zmiana hasła oznacza zmianę pracującego obrazu (czyli wymianę kontenerów)
- nie da się opublikowac obrazu (bo w środku jest nasze ważne hasło)
- nie da się opublikować Dockerfile (bo widać nasze hasło)
zaletą jest zgodność z 12-factors app ale niestety hasła widać w docker ps i docker inspect dostęp do zmiennych ENV mają kontenery zlinkowane i child-procesy itd itp
Niestety tutaj trzeba mieć na każdym węźle swarma przygotowane aktualne dane w filesystemie "/hasła". Trzeba mieć zatem jakiś mechanizm dbający o aktualność i synchronizację danych w tym katalogu (scaleio? rexray? gpfs? gluster? nfs? świętej pamięci flocker? ) . Zarządzanie zawartością katalogu "/hasła" może (ale nie musi) okazać się trudnym procesem
Rozwiązanie działa tylko wtedy gdy procesy z sekcji CMD Dockerfile umieją działać bez haseł. Mówiąc prościej - trudno wymagać od mysql aby wystartował i trzymał swoim procesem (mysqld) swój kontener przy życiu skoro hasło otrzyma od nas dopiero na etapie późniejszym. To rozwiązanie jest zatem nieco egzotyczne i sprawdza się tylko w niektórych przypadkach ale jak już się sprawdzi to jest w miarę ok.
W sumie mimo wielu rekomendacji na rynku nie zdążyłem zabrać się za Vaulta bo nagle pojawił się docker secrets :-)
No i tu dochodzimy do sedna - Docker 1.13 wprowadza jedną z najbardziej wyczekiwanych funkcjonalności - zarządzanie hasłami (secrets Management) . Docker miłościwie pozwala nam trzymać nie tylko hasła ale i dane binarne (do 500kb) oraz różne stringi konfiguracyjne.
Mechanizm Docker secrets jest jak na razie dostępny tylko w Swarmie więc jesli chcemy uruchamiać zwykłe kontenery trzeba je przekonwertować na usługi swarma.
A teraz w wielkim skrócie jak to działa - jeśli jakiemuś serwisowi nadamy dostęp do hasła to jego niezaszyfrowana postać jest dostępna w każdym kontenerze w pliku /run/secrets/[secret_name].
Warto podkreślić że /run/secrets to filesystem tmpfsowy, rezyduje w RAM (co będzie ważne przy usuwaniu haseł) .
Najlepiej zaczać od realnego przykładu - tym razem LAB skonstruowany będzie na stacku Wordpressa (wiem że wordpress to mało oryginalny pomysł ale to pierwsze co przyszło mi do głowy jak zacząłem szukać środowiska które potrzebuje haseł do DB) :-)
Wordpress to jak wiadomo baza i aplikacja - 2 warstwy, późne lata 90'te.
Klasyczne uruchomienie via generic containers (dla pogrążenia tej metody tutaj jeszcze z zakazaną opcją --link):
docker run --name my-mysql -e MYSQL_ROOT_PASSWORD=haslo123 -d mysql
docker run --name my-wordpress --link my-mysql:mysql -e WORDPRESS_DB_PASSWORD=haslo123 -p 8080:80 -d wordpress
niestety hasła nieco widać:
# docker inspect my-mysql | grep haslo
"MYSQL_ROOT_PASSWORD=haslo123",
# docker inspect my-wordpress | grep haslo
"WORDPRESS_DB_PASSWORD=haslo123",
Podobnie jest gdy wystartujemy stack wordpressa w formie swarm services:
docker service create --name my_mysql --network=my_net -e MYSQL_ROOT_PASSWORD=haslo123 --replicas=1 mysql
docker service create --name my_wordpress --network my_net -e WORDPRESS_DB_PASSWORD=haslo123 -e WORDPRESS_DB_HOST=my_mysql --publish 8090:80 --replicas=1 wordpress
tu również (niezależnie na które węzły trafiły kontenery poszczególnych usług) widać hasła w docker inspect:
# docker inspect e504b9f4c5fd | grep haslo
"WORDPRESS_DB_PASSWORD=haslo123",
# docker inspect 37f8df6c1126 | grep haslo
"MYSQL_ROOT_PASSWORD=haslo123",
Zmieniamy zatem stack na nowy czyli z docker secrets:
# docker service rm my_mysql my_wordpress
Tworzymy secret:
# echo "haslo123456" | docker secret create wordpress_secret -
i9qnntxll4kl93k78qsx6zqke
# docker secret ls
ID NAME CREATED UPDATED
i9qnntxll4kl93k78qsx6zqke wordpress_secret 8 seconds ago 8 seconds ago
Tworzymy ponownie serwisy ale dodając im --secret oraz opcje czytania haseł do mysql z plików:
docker service create --name my_mysql --secret="wordpress_secret" --network=my_net -e MYSQL_ROOT_PASSWORD_FILE="/run/secrets/wordpress_secret" --replicas=1 mysql
docker service create --name my_wordpress --secret="wordpress_secret" --network my_net -e WORDPRESS_DB_PASSWORD_FILE="/run/secrets/wordpress_secret" -e WORDPRESS_DB_HOST=my_mysql --publish 8090:80 --replicas=1 wordpress
I nagle docker inspect na kontenerach nie pokazuje już haseł :-)
Co tu zaszło ? (teraz będzie nudna teoria)
Gdy dodamy secret do swarma jest on wysyłany via TLS do swarm-managera który następnie zachowuje go w szyfrowanym logu RAFT . Log ten jest replikowany na pozostałe węzły swarma więc już po chwili wszyscy którzy powinni mają do niego dostęp - czyli głównie kontenery należące do serwisu który ma wykorzystywać nowy secret. Tak jak już wcześniej padło każdy potrzebujący kontener otrzymuje deszyfrowaną postać secretu w postaci pliku /run/secrets/[secret_name].
Co ważne kiedy kontener przestaje być potrzebny (i ginie) wtedy zdeszyfrowane hasła są odmontowywane i usuwane z pamięci węzła.
Na koniec przykład rotacji (wymiany) hasła via docker service update:
docker service update --secret-rm stare_haslo --secret-add source=nowe_haslo,target=password NAZWA_SERVICE
To w sumie by było na tyle, jeszcze 2 uwagi z relase notes ktore IMHO wymagają osobnego podkreślenia:
w 1.13.0 Deprecate MAINTAINER w Dockerfile
nie wiem po co ta zmiana i komu to przeszkadzało :-)
w 1.13.1
docker info | grep -i storage
Storage Driver: overlay
powyższe zostawiam bez komentarza, osobiście często brakuje mi sił do warstw storage w świecie dockera
Wiele rewolucji nie było (nie to co przepaść między 1.11 a 1.12) , raczej ewolucja i facelifting. Cieszy fakt że wyłapano dużo braków funkcjonalnych i je załatano, dobrze też że liczył się bardzo głos użytkowników. Zmiany tym razem raczej wyłącznie na lepsze ale równie dobrze nowa wersja 1.13 mogłaby się nazywać 1.12.10