Automatyczne reverse proxy dla farmy Docker Swarm
Gdy już wkroczymy, cali na biało, w krainę automatycznych, autoskalujących się mikroserwisów które miały być dla nas ziemią obiecaną, podobnie jak w prawdziwym świecie, okaże się, że czyhają tam na nas problemy które mogą wybuchnąć w każdej chwili… Wyobraźcie sobie, taką sytuację – macie aplikację składającą się z jakiś baz danych, repozytoriów plików czy cokolwiek tam chcecie. Oczywiście macie tam również kontenery aplikacyjne z Waszym wypieszczonym programem który pozwala na wystawianie danych z rzeczonych baz poprzez cukierkowe, słodziutkie GUI. Na końcu stoi natomiast load balancer, który pilnuje, że jeżeli któryś z kontenerów zemrze to on zawsze będzie mógł pakować ruch do tego który się ostał.
Mija kilka miesięcy, aplikacja działa, kontenerki sprawują się wyśmienicie. Jednakże nadchodzi dzień sądu i Twoja firma wypuszcza nowy produkt, przychodzi okres przedświąteczny, nieważne, a usery zaczynają walić do Was drzwiami i oknami. Jesteś przygotowany na tą okazje, zakładasz spodnie rurki, przyczesujesz włos na lewo i na najnowszym iPadzie skalujesz swoją aplikację poprzez powołanie kolejnych kontenerów. Jednak ku swojemu ogromnemu zdziwieniu stwierdzasz, że coś poszło nie tak, zapomniałeś o load-balancerach, które nie wiedzą że właśnie ktoś powołał dodatkowe serwery aplikacyjne, a one bezmyślnie wciąż ładują ruch do dwóch pierwotnych. Jak to naprawić?
Inny przypadek to promocja jakiegoś pre-prod na live. Naszą najnowszą instancję z wersją 2.0 aplikacji chcemy najpierw posadowić na farmie kontenerów, gruntownie przetestować , a następnie (gdy przejdzie wszystkie testy) puścić na produkcję. Dobrze też gdyby można było jednocześnie trzymać dla bezpieczeństwa poprzednią wersję (1.9) jeszcze przez jakiś czas. W każdej chwili chcemy też mieć możliwość szybkiego powrotu do 1.9 gdyby przypadkiem wersja 2.0 okazała się zbyt nowoczesna i przez to np. nie za bardzo stabilna. Ogólnie ideałem dla nas byłby model posiadania dwóch domen „stary.sklep.pl” i „nowy.sklep.pl”, a jeszcze lepiej kolejnych N wersji.
W celu swobodnego żonglowania środowiskami na farmach dockerowych wykorzystamy mechanizm automatycznych reverse-proxy zespawany z sygnałami Docker Events. Jeśli zapniemy się do Dockera (lub całego Swarma) z poleceniem “events” to np. przy wyłączaniu jakiegoś kontenera otrzymujemy dosyć obfite komunikaty. W poniższym przykładzie zapiąłem się akurat do Swarma ale to nie robi różnicy, identycznie wygląda to gdy zapniemy się do pojedynczego węzła z silnikiem Dockera:
root@gmbUbuntu11:~# docker -H tcp://10.3.0.7:5000 events [stop jakiegoś kontenera...] 2016-04-07T07:18:24.023437507Z container kill c8c2a66e02e25a3c654cbdc80146081956d8d3f490def2414e71c06f3726ff0a (node.addr=10.3.0.9:2376, node.id=2BQM:VUX7:FDYU:7VU2:3HCL:IXJ4:ZGWT:BKHG:RBWS:RG7T:VSLL:YQ6A, node.ip=10.3.0.9, node.name=gmbUbuntu13, com.docker.swarm.constraints=["node==gmbUbuntu13"], com.docker.swarm.id=452a1b314c58bf26dbb741a40f8d54dba6309becd43b1e447d31f3d27bd08a6f, image=ubuntu_z_apache_ex8080, name=apa_exp_05)
Przy użyciu tegoż właśnie mechanizmu sterowane są te wszystkie zajebiste obrazy nginx-rev-proxy, haproxy itd. Metod na automatyczne rev-proxy jest wiele, ale omówię dziś poniższą, gdyż wydaje mi się najlepsza:
- http://anders.janmyr.com/2015/11/simple-clustering-with-docker-swarm-and.html?m=1
- https://github.com/jwilder/nginx-proxy
- http://jasonwilder.com/blog/2014/03/25/automated-nginx-reverse-proxy-for-docker/
W tym przykładzie stosowany jest image jwilder/nginx-proxy. Najpierw na węźle który ma być bramą tcp/80 ze świata do naszej infrastruktury (u mnie jest to GmbUbuntu09) startujemy nginx reverse proxy z opcją wykrywania zmian i auto-generacją-nginx-conf. Klaster użyty w przykładzie jest sterowany Swarmem znajdującym się na 10.3.0.7 na porcie 5000. Uruchomienie tego w środowisku TLS-consul-swarm z siecią typu VxLan czyli “overlay network” o nazwie “ov01” wygląda następująco:
export DOCKER_TLS_VERIFY="1" export DOCKER_CERT_PATH=/root/.docker/ export DOCKER_HOST=tcp://10.3.0.7:5000 docker run -ti --net=ov01 --name nginx_docker_rev_proxy01 -v $DOCKER_CERT_PATH:$DOCKER_CERT_PATH -p "80:80" -e "constraint:node==GmbUbuntu09" --env DOCKER_HOST --env DOCKER_CERT_PATH --env DOCKER_TLS_VERIFY jwilder/nginx-proxy
Po wywołaniu powyższego faktycznie okazuje się że nasz nowy nginx-super-proxy słucha na docker events i nawet coś wypisuje na stdoutput po dodaniu jakiegoś kontenera:
dockergen.1 | 2016/04/07 08:37:25 Received event start for container 59e4da71a795 dockergen.1 | 2016/04/07 08:37:25 Contents of /etc/nginx/conf.d/default.conf did not change. Skipping notification 'nginx -s reload'
Aby wymusić automatyczną zmianę configu rev-proxy należy uruchamiać kontenery aplikacyjne w następujący sposób (kluczowe jest tu dodanie przełącznika z VIRTUAL_HOST_:
docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=foo.bar.com -tdi -P --name=apa_exp_11 --net=ov01 ubuntu_z_apache_ex8080
Warto zauważyć że klasyczne wywołania niewiele nam się przydadzą (tzn. żadne z poniższych nie spowoduje zmian w konfigu nginx-proxy):
docker -H tcp://10.3.0.7:5000 run -tdi -p 8080:80 --name=ngi01 --net=ov01 nginx docker -H tcp://10.3.0.7:5000 run -tdi -P --name=apa_exp_11 --net=ov01 ubuntu_z_apache_ex8080 docker -H tcp://10.3.0.7:5000 run -tdi --name=apa_exp_13 --net=ov01 ubuntu_z_apache_ex8080
Tak czy siak po powołaniu kontenera foo.bar.com moduł rev-proxy samoczynnie dopisał (w środku kontenera z rev-proxy) do konfigu nginxa następujące linijki (pogrubione):
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { default $http_x_forwarded_proto; '' $scheme; } # If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any # Connection header that may have been passed to this server map $http_upgrade $proxy_connection { default upgrade; '' close; } gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; log_format vhost '$host $remote_addr - $remote_user [$time_local] ''"$request" $status $body_bytes_sent ''"$http_referer" "$http_user_agent"'; access_log off; # HTTP 1.1 support proxy_http_version 1.1; proxy_buffering off; proxy_set_header Host $http_host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $proxy_connection; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; server { server_name _; listen 80; access_log /var/log/nginx/access.log vhost; return 503; } upstream foo.bar.com { # gmbUbuntu13/apa_exp_11 server 10.3.0.9:32769; } server { server_name foo.bar.com; listen 80 ; access_log /var/log/nginx/access.log vhost; location / { proxy_pass http://foo.bar.com; } }
Zważając na to, że cały LAB jest na jakiejś chmurze mapuję port 80 maszyny na której działa nginx-rev-proxy (np. via Endpoints) na świat zewnętrzny na port 80. Po dokonaniu tego i po połączeniu się “z domu” na foo.bar.com dostajemy stronę WWW właściwą dla tego kontenera. Oczywiście aby się to wszystko wydarzyło z lokalizacji “w domu” musimy dodać foo.bar do /etc/hosts . W świecie komercyjnym zakładam że mamy wykupione nasze domeny i wtedy już nie trzeba wykonywać tego karkołomnego etapu J
A teraz sprawdźmy sobie czy faktycznie niezbędna jest flaga EXPOSE. Tworzymy testowo dwa obrazy – 01ubuntu_apache_bez_expose i 02ubuntu_apache_z_expose. Pogrubioną czcionką jest zaznaczona oczywiście różnica między nimi w Dockerfile:
FROM ubuntu:14.04 EXPOSE 80 RUN apt-get -y update && apt-get -y install apache2 CMD apachectl start && hostname > /var/www/html/index.html && /bin/bash -- FROM ubuntu:14.04 RUN apt-get -y update && apt-get -y install apache2 CMD apachectl start && hostname > /var/www/html/index.html && /bin/bash --
Jak widać po dockerfajlach nasze kontenery będą miały pewną cechę – na porcie 80 będą serwowały wybitnie biznesowy kontent – czyli swój hostname.
Odpalamy 2 kontenery:
docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=loo.bar.com -P --name=loo -tdi --net=ov01 02ubuntu_apache_z_expose docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=joo.bar.com -P --name=joo -tdi --net=ov01 01ubuntu_apache_bez_expose
Drugi docker run nie spowodował dodania wpisu do konfigu nginx na proxy – jak więc widać EXPOSE jest konieczny, a jego obecność objawia się pięknym logiem z rev-nginx-proxy:
dockergen.1 | 2016/04/08 14:06:14 Received event start for container 302eb12480ac dockergen.1 | 2016/04/08 14:06:15 Generated '/etc/nginx/conf.d/default.conf' from 13 containers dockergen.1 | 2016/04/08 14:06:15 Running 'nginx -s reload' dockergen.1 | 2016/04/08 14:06:15 Error running notify command: nginx -s reload, exit status 1
Kolejne kontenery webowe można startować następująco:
docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=goo.bar.com -P -tdi --net=ov01 nginx docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=boo.bar.com -P --name=boo -tdi --net=ov01 nginx
Lub nieco sobie uprościć tę czynność:
WEB_HOST=b.pl; docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=$WEB_HOST -P --name=$WEB_HOST -h $WEB_HOST -tdi 02ubuntu_apache_z_expose
Albo dodając nowy następny kontener – tym razem do sieci overlay:
WEB_HOST=d.pl ; docker -H tcp://10.3.0.7:5000 run -e VIRTUAL_HOST=$WEB_HOST -P --name=$WEB_HOST -h $WEB_HOST --net=ov01 -tdi 02ubuntu_apache_z_expose curl -H 'Host: d.pl 10.3.0.6
Podczas testów wygodnie jest zapiąć się do kontenera z docker-reverse-proxy poprzez docker logs, gdyż jwilder tak go przyjemnie zrobił że kontener ten wszystko co ma do powiedzenia rzuca na stdout:
docker -H tcp://10.3.0.7:5000 logs -f $(docker -H tcp://10.3.0.7:5000 ps | grep jwilder | awk '{print $1}')
… i paczcie państwo, widać już, że wszystko ładnie wyświetla się na ekranie. Jedyny problem jaki widzę na dzień dzisiejszy to to że obraz jwilder/proxy nie obsługuje za bardzo overlay networks. Z grubsza chodzi o to że do konfigu nginxa wrzucane są wpisy z normalną siecią z hardware-landu zamiast z adresacją z sieci overlay. Dla niewiernych dowód:
root@KONTENER_REV_PROXY:/app# cat /etc/nginx/conf.d/default.conf | grep server | grep -v "{" | grep -v server_name server 10.3.0.8:32777; server 10.3.0.9:32776; server 10.3.0.9:32773; server 10.3.0.8:32774; server 10.3.0.8:32778; server 10.3.0.9:32775;
Pozostaje zaczekać aż jwilder poprawi swój obraz lub ktoś inny zrobi forka lub (jak to w świecie dockera) jutro powstanie 125 kolejnych obrazów podobnych i robiących to samo ale zupełnie inaczej.
Reasumując. Automatyczne reverse proxy sterowane eventami z dockera które samoczynnie się rekonfiguruje i dodaje kolejne domeny to całkiem fajny mechanizm. Mając go w podręcznym dev-opsowym arsenale można łatwo wyobrazić sobie potencjalne zastosowania… Jednak gdyby ktoś nie umiał samodzielnie wymyśleć to na przykład:
- promocje środowisk z łańcucha Dev->Test->Q->preProd->Live
- kolejne wersje np. naszego super wydajnego API restowego.
- kolejne iteracje naszego sklepu internetowego dla kolejnych klientów lub dla kolejnego segmentu rynku w którym jesteśmy przyszłym potentatem(karma.dla.psa.pl, karma.dla.kota.pl, karma.dla.pancernika.pl)
Oczywiście jeśli zaczynamy stosować konteneryzację (zwłaszcza na produkcji) to warto wziąć na warsztat tematy bezpieczeństwa, wysokiej dostępności i skalowalności – ale to już będzie tematem następnych wpisów.
Well; a nie można Ansible to zautomatyzować? 😉
Już widzę to HAProxy (w trybie master-slave na pacemakerze) + farma kontenerów. 😉
— K.
Pewnie że można ansiblem 🙂 być może o tym będzie następny wpis 🙂