W niniejszym artykule poruszę temat wystawiania mikroserwisów na zewnątrz klastrów z kontenerami, będzie też trochę o tym, czy i jak zautomatyzować proces zarządzania kubernetesowym ingressem i jak z grubsza ogarnąć temat takiej automatyzacji w środowisku RBAC k8s. Zrobimy też sobie automat do dynamicznej rekonfiguracji ingressa i nawet dla potomnych wrzucimy go na najnowszy nabytek microsoftu.
Najpierw jednak tytułem wstępu i zbudowania jakichkolwiek podwalin teoretycznych prześledzimy ogólne zagadnienie wystawiania usług kontenerowych na zewnątrz.
Zacznijmy może od tego, co tam słychać w 12-factorapp na ten temat – a konkretnie tu:
[gdlr_core_code style=”light” ]
https://12factor.net/pl/port-binding
[/gdlr_core_code]
a że 12factor ma dużo do powiedzenia na każdy temat to i tu jest nie inaczej – w rozdziale 7 pojawia się zjawisko Port bindingu, cytuję:
“Udostępniaj usługi przez przydzielanie portów […] W przypadku aplikacji wdrożonej w środowisku produkcyjnym zapytania do udostępnionej publicznie nazwy hosta, są obsługiwane przez warstwę nawigacji. Kierowane są one później do procesu sieciowego udostępnionego na danym porcie.”
O ile cenię 12factor za upraszczanie życia i mądre wytyczne to ten rozdział jakoś do mnie nie przemawia i widzę w nim pewne niebezpieczeństwo nadinterpretacji. Wydaje mi się, że dotknięto tu tematu pojedynczych mikroserwisów, a nie całych usług. Spotkałem się kilka razy z interpretacją tego rozdziału, która prowadziła wprost do wystawiania usług na portach i kombinowania potem jak przemapować nazwy na porty albo urle na porty.
Tu warto od razu wspomnieć o metodzie wystawiania usług, jaka została zaimplementowana na początku istnienia docker swarma, czyli mapowanie całych usług na porty. I znowu kombinowanie z mapowaniem itd. – na pewno poprawiono to później i dodano wszystkie inne metody co przyczynia się do stale rosnącej popularności swarma. A nie, czekajcie…
Z kolei jak wspomnimy o dockerowym EXPOSE w Dockerfile i opcjach “-p” lub “-P” to jakoś nagle można zdać sobie sprawę, że zagadnienie jest przesuwane z miejsca w miejsce i dopiero profesjonalne orkiestratory jakoś zaczęły się tym zajmować i to ogarniać (w miarę).
Mesos/Marathon np. radzi sobie z tym tak, że owszem inwentaryzuje exposowane porty, ale potem i tak trzeba dorzucać sobie zewnętrzny api gateway, który robi vhost mapping lub uri mapping, można też korzystać z marathon load-balancera lub pakować wszystko do consula i czytać z consula. Słabe i masa dłubania, z drugiej strony tu jest lepsze load-balancowanie na L7 niż jak w swarm na IPVS.
A Kubernetes jak to z nim zwykle bywa dostarcza nam kilka metod a jak nie dostarcza takiej funkcjonalności, jaką chcemy to można korzystać z zasady “battery included but removable” – obecnie community k8s oferuje liczne warianty.
Co do dostępnych opcji w środowisku k8s on premise zakładam, że wszyscy wiedzą co i jak – ale jak nie wiedzą to service może być typu ClusterIp lub NodePort. Typ ClusterIp jest defaultowy i ogranicza wystawienie VIPa w środku klastra więc wiele nam nie pomoże, z kolei NodePort jest z grubsza tożsamy ze swarmowym expose service, czyli każdy węzeł wystawia port dla usługi (nawet jak nie ma jej kontenerów u siebie) i wewnętrznym IPVSem i iptablesem kieruje ruch i od razu robi load-balancing. Porty trzeba jakoś inwentaryzować, można nakłonić developerów, żeby sobie z tym radzili, ja jednak zwykle spotykałem się z panicznym oporem z ich strony, co więcej równie często spotykałem się z oporem przed uri-mapping i parcie w stronę vhost mapping, ale to już inna historia.
Tu warto dodać, że istnieją pewne opracowania sugerujące dostęp do usług via kubernetes-proxy, ale wyłącznie na chwilę i dla środowisk domowych/developerskich – o tym jak poważny problem można sobie wygenerować używając na stałe na produkcji rozwiązań przeznaczonych do domowego labu przekonała się niedawno Tesla, której infrastruktura kubernetesa, zamiast pracować dla Tesli zaczęła kopać bitcoiny jakiemuś nieznanemu podmiotowi 🙂
[gdlr_core_code style=”light” ]
https://blog.heptio.com/on-securing-the-kubernetes-dashboard-16b09b1b7aca
[/gdlr_core_code]
Reasumując:
Aby zaadresować powyższe problemy w k8s wymyślono Ingress i niejako z delegowano na community proces wytwórczy ingress controllera.
Czym jest Ingress? Biorąc pod uwagę, że z zasady rozwiązuje wszystkie wcześniej omawiane problemy, można śmiało samemu zdefiniować jego architekturę:
Jak łatwo się domyślić prawie wszystkie powyższe punkty są spełnione i Ingress z powodzeniem realizuje te założenia. Poza jednym punktem, który jest mocno dyskusyjny – czyli 4.
No właśnie – co z tym punktem o automatyce? Osobiście tak do końca nie mogę rekomendować czy lepiej dać swobodę developerom, czy jednak manualnie kontrolować co nasz klaster wystawia na zewnątrz. Bardzo dużo zależy od środowiska, ludzi, standaryzacji, strefy wpływu bezpieczników w firmie itd. itp. Różnie bywało i w niektórych projektach stosowałem ręczny proces kontroli, w innych robiłem automaty, które wystawiały usługi na zewnątrz bez udziału adminów. Zawsze gdzieś tam górę brało podejście oparte na pełnej automatyce i co najwyżej nadzorowania/auditingu, przy 10 deploymentach na godzinę średnio chciałoby mi się modyfikować jakieś wpisy czy rekonfigurować cokolwiek. To ogólnie temat na głębszą dyskusję, ale fakt faktem zawsze lepiej mieć automat niż go nie mieć, najwyżej nie będzie uruchomiony – dlatego w dalszej części go sobie zbudujemy i wspawamy w infrastrukturę kubernetesa.
Wracając do Ingressa – twór ten podzielony jest na 2 komponenty:
Z racji długoletniego używania nginx do realizacji load-balancingu dla kontenerów zdecydowałem się na zastosowanie jako controllera właśnie tego silnika. Słyszałem też dużo dobrych opinii o traefiku, ale niestety równie dużo wzmianek, że nie nadaje się jeszcze na produkcję. Jednakże nie testowałem, nie wiem, nie znam, mam go na roadmapie, jak coś będę wiedział dam znać.
Wracając do nginxa – realizowany w oparciu o niego Ingress Controller oparty jest o dwa komponenty:
Repo projektu, a właściwie link do procedury instalacji znajduje się tutaj:
[gdlr_core_code style="light" ] https://github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md [/gdlr_core_code]
Instalacja polega w pierwszym kroku na wgraniu do kubernetesa Yamla z sekcji Mandatory Commands:
[gdlr_core_code style="light" ] https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml [/gdlr_core_code]
Czyli po kolei, co tam w środku jest robione:
W drugim kroku trzeba wgrać yamla dla opcji bare-metal (czyli on-premise):
[gdlr_core_code style="light" ] https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml [/gdlr_core_code]
Tutaj z kolei mamy utworzenie Service typu NodePort dla ingress-nginx (nasz Ingress controller będzie dostępny, na każdym węźle na konkretnym porcie)
Wykonanie powyższych kroków powoduje, że na klastrze k8s mamy Ingress Controller – pozostaje jeszcze dorzeźbić sam IngressResource.
Przy okazji – nasz ingress controller słucha na każdym węźle na porcie 32165:
[gdlr_core_code style="light" ]# kubectl get svc -n ingress-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE default-http-backend ClusterIP 10.106.132.195 <none> 80/TCP 8m ingress-nginx NodePort 10.109.196.242 <none> 80:32165/TCP,443:31729/TCP 7m
[/gdlr_core_code]
Aby jednak mieć cokolwiek do wystawiania i testów, trzeba powołać 2 serwisy (i leżące pod nimi 2 deploymenty):
[gdlr_core_code style="light" ]
kubectl run website --image=gimboo/apacz --port=80
kubectl run forums --image=gimboo/apacz --port=80
kubectl expose deployment/website
kubectl expose deployment/forums
[/gdlr_core_code]
Obraz gimboo/apacz to testowy obraz występujący dosyć często w tej serii blogpostów 🙂 Jego jedyną funkcjonalnością jest wystawianie HOSTNAME kontenera na porcie 80 – do testów wystarczy jak znalazł.
Pozostaje teraz stworzyć IngressResource.
Zaczynamy od najprostrzego Ingressa bez żadnych reguł (taki Ingress wrzuca wszystko jak leci na 1 serwis):
[gdlr_core_code style="light" ] apiVersion: extensions/v1beta1 kind: Ingress metadata: name: my-ingress spec: backend: serviceName: website servicePort: 80 [/gdlr_core_code]
Robimy kubectl apply -f powyższy_plik i testujemy konfig:
[gdlr_core_code style="light" ] # curl 127.0.0.1:32165 website-7dcbbddc8f-cx4dc [/gdlr_core_code]
Skalujemy backend (deploy pod spodem) do 2 i sprawdzamy czy działa Load-Balancing:
[gdlr_core_code style="light" ]# kubectl scale deploy website --replicas=2 deployment.extensions "website" scaled # curl cent401:32165 website-7dcbbddc8f-cx4dc # curl cent401:32165 website-7dcbbddc8f-qd2np
[/gdlr_core_code]
Jest ok , jak widać działa, czas na Ingressa z rozdziałem ruchu na dwa serwisy pod spodem:
[gdlr_core_code style="light" ]apiVersion: extensions/v1beta1 kind: Ingress metadata: name: my-ingress spec: rules: - host: www.mysite.com http: paths: - backend: serviceName: website servicePort: 80 - host: forums.mysite.com http: paths: - path: backend: serviceName: forums servicePort: 80
[/gdlr_core_code]
Czyli jak wpadnie na hosta www.mysite.com to przerzuci na service=website, a jak na forums.mysite.com to na service=forums.
Po zaaplikowaniu powyższego tak oto wygląda działanie:
[gdlr_core_code style="light" ]# curl -H 'Host:www.mysite.com' 127.0.0.1:32165 website-7dcbbddc8f-cx4dc # curl -H 'Host:forums.mysite.com' 127.0.0.1:32165 forums-6d4b76fd95-vtrcw # curl -H 'Host:fake.mysite.com' 127.0.0.1:32165 default backend - 404
[/gdlr_core_code]
Osiągneliśmy zatem vHost mapping, czas na url mapping:
[gdlr_core_code style="light" ]apiVersion: extensions/v1beta1 kind: Ingress metadata: name: my-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: www.mysite.com http: paths: - path: /website backend: serviceName: website servicePort: 80 - path: /forums backend: serviceName: forums servicePort: 80
[/gdlr_core_code]
Tym razem mechanizm polega na wrzucaniu na dany serwis ruchu na podstawie url, w zapytaniu po zaaplikowaniu działa to następująco:
[gdlr_core_code style="light" ]# curl -H 'Host:www.mysite.com' 127.0.0.1:32165/forums forums-6d4b76fd95-vtrcw # curl -H 'Host:www.mysite.com' 127.0.0.1:32165/website website-7dcbbddc8f-cx4dc # curl -H 'Host:www.mysite.com' 127.0.0.1:32165/website website-7dcbbddc8f-qd2np # curl -H 'Host:www.mysite.com' 127.0.0.1:32165/fakeurl default backend - 404
[/gdlr_core_code]
Ogólnie jak widać działa i daje nawet jakieś niezerowe możliwości konfiguracji – jeden jedyny problem, jaki mnie uwiera to ta nieszczęsna konieczność ręcznego dostawiania wpisów do Ingressa dla nowo powołanych serwisów – sprawdzałem pobieżnie, czy nie ma jakiegoś gotowego rozwiązania i w sumie znalazłem jeden projekt, ale nie daje on oznak życia (w całym repo zarejestrowane jedno (!) issue, zresztą przeze mnie i na chwilę pisania niniejszego tekstu bez jakiejkolwiek odpowiedzi) .
[gdlr_core_code style="light" ] https://github.com/hxquangnhat/kubernetes-auto-ingress [/gdlr_core_code]
Z drugiej strony jak się dobrze zastanowić, to zrobienie automatu nie będzie zbyt trudne i większość walki będzie z k8s RBAC, niż z samym mechanizmem automatycznego update. A przy okazji możemy się czegoś nowego nauczyć 🙂
A zatem czas stworzyć nasz własny testowy autoingress, oto założenia (przypominamy że od 1.10 mamy default=RBAC):
[gdlr_core_code style="light" ]
metadata: labels: run: serwis5 auto_ingress: 'serwis5_path_80'[/gdlr_core_code]
[gdlr_core_code style="light" ]
User "system:serviceaccount:autoingress:autoingress-serviceaccount"[/gdlr_core_code]
[gdlr_core_code style="light" ]
- apiGroups: - " resources: - services verbs: - list - apiGroups: - "extensions" resources: - ingresses verbs: - get - list - watch - patch - delete - create
[/gdlr_core_code]
Te pierwsze uprawnienia, to czytanie info o serwisach – będę to robił via:
[gdlr_core_code style="light" ]
kubectl -n default get svc -o jsonpath='{.items[*].metadata.labels.auto_ingress}')[/gdlr_core_code]
Te drugie uprawnienia, to update ingressa, nie za bardzo chciało mi się wnikać więc dałem dosyć szeroko.
6. ServiceAccount autoingressa będzie zbindowany z rolą autoingress-clusterrole,
7. powołana będzie ConfigMapa=autoingress-configuration, z której autoingress będzie czytał konfig – a zasadniczo 3 dane (tu moje deafulty):
[gdlr_core_code style="light" ]
INGRESSHOST: www.mysite.com INGRESSNAME: my-ingress INGRESSNAMESPACE: default[/gdlr_core_code]
8. finalnie deployment=autoingress korzystający z gotowego obrazu “demona” gimboo/autoingress:1.0 (z tym demonem to bym może nie przesadzał, napisałem to na potrzeby tego LABU w shellu)
to teraz te punkty 1-7 trzeba wpakować do yamla
Finalnie jak się komuś nie chce nad tym wszystkim zastanawiać, to można na skróty jednym poleceniem wykonać deploy na czystym klastrze k8s:
[gdlr_core_code style="light" ]
# kubectl apply -f https://raw.githubusercontent.com/slawekgh/autoingress/master/autoingress.yaml namespace "autoingress" created serviceaccount "autoingress-serviceaccount" created clusterrole.rbac.authorization.k8s.io "autoingress-clusterrole" created clusterrolebinding.rbac.authorization.k8s.io "autoingress-clusterrole-binding" created configmap "autoingress-configuration" created deployment.extensions "autoingress" created
[/gdlr_core_code]
To teraz pozostaje kreować serwisy i cieszyć się, że Ingress sam się rekonfiguruje.
[gdlr_core_code style="light" ]
# cat service1.yaml apiVersion: v1 kind: Service metadata: labels: run: serwis1 auto_ingress: 'serwis1_serwis1_80' name: serwis1 spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: run: website # kubectl apply -f service1.yaml service "serwis1" created # kubectl get ing -o yaml [...] spec: rules: - host: www.mysite.com http: paths: - backend: serviceName: serwis1 servicePort: 80 path: /serwis1 # curl -H 'Host:www.mysite.com' 127.0.0.1:32165/serwis1 website-7dcbbddc8f-qd2np # curl -H 'Host:www.mysite.com' 127.0.0.1:32165/serwis2 default backend - 404
[/gdlr_core_code]
Jak widać tego drugiego nie ma – trzeba go zrobić, zatem:
[gdlr_core_code style="light" ]
# cat service2.yaml
apiVersion: v1
kind: Service
metadata:
labels:
run: serwis2
auto_ingress: 'serwis2_serwis2_80'
name: serwis2
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: website
# kubectl apply -f service2.yaml
service "serwis2" created
[/gdlr_core_code]
Ingress już się zmienił :
[gdlr_core_code style="light" ]
# kubectl get ing -o yaml spec: rules: - host: www.mysite.com http: paths: - backend: serviceName: serwis1 servicePort: 80 path: /serwis1 - backend: serviceName: serwis2 servicePort: 80 path: /serwis2 # curl -H 'Host:www.mysite.com' 127.0.0.1:32165/serwis2 website-7dcbbddc8f-qd2np
[/gdlr_core_code]
Od tej pory jeśli chcemy, żeby nasze serwisy wystawiały się automatycznie na ingresie, trzeba im dodać label auto_ingress i gotowe. Wszystko dzieje się automatycznie i samo za nas 🙂
To by było na tyle, pozostaje jeszcze dyskusja o komunikacji inter-kontenerowej, czy narzucić obowiązek rozmawiania zawsze przez Ingressa, czy zezwolić na odwoływanie się po nazwach serwisów wewnątrz namespace – na chwilę obecną nie mam gotowej recepty.
Pierwszy wariant wnosi troche porządku i standaryzacji, ale z drugiej strony rezygnujemy wtedy z funkcjonalności wbudowanych w k8s, z przyjemnością poznałbym opinie innych użytkowników kubernetesa.