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:

https://12factor.net/pl/port-binding<br />

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 🙂

https://blog.heptio.com/on-securing-the-kubernetes-dashboard-16b09b1b7aca<br />

Reasumując:

  • defaultowy typ k8s-service (ClusterIp) jest nieprzydatny (nie realizuje dostępu do usługi z zewnątrz),
  • drugi typ (NodePort) wystawia usługi na port wzorem swarma, więc jego przydatność jest dyskusyjna,
  • nie powinno się używać k8s proxy, chyba że na chwilę albo w domowym labie,
  • w chmurach można korzystać jeszcze z typu LoadBalancer ,który wysteruje chmurowym LB, ale tu pojawiają się koszty – każdy serwice będzie chciał powoływać swój LB i po chwili namnożą się zewnętrzne IP i kwota na fakturze znacząco wzrośnie.

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ę:

  1. skoro wystawianie na porty jest passe to Ingress pewnie umie wystawiać servisy na URL’e lub na vHosty,
  2. skoro w chmurach dużo płacimy za zewnętrzny IP dla chmurowego LoadBalancera to Ingress pewnie umie wystawić wszystkie usługi k8s w jednym miejscu,
  3. Ingress pewnie dobrze, żeby był jednym z kubernetes resource i można nim było sterować via kubectl/yaml/helm czy co tam jeszcze chcemy,
  4. fajnie jakby Ingress, jakoś sam automatycznie dodawał nowe serwisy jeśli zostały odpowiednio olabelowane,
  5. Ingress powinien mieć modularną budowę – tak, aby dało się zmieniać jego silnik w zależności od tego, czy ktoś lubi traefika czy nginxa lub inne,
  6. powiniem umieć routować do wszystkich usług k8s na jednym IP (z tym routowaniem to może przesadziłem, bardziej reverse-proxy tu pasuje).

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:

  • Ingress resource (czyli kolejny k8s resource obok Deployments, Services , ReplicaSets itd),
  • Ingress controller (nginx, traefik, istio itd).

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:

  • nginx-rev-proxy,
  • backend-service (do rzucania 404 gdy nie znajdzie się dany service).

Repo projektu, a właściwie link do procedury instalacji znajduje się tutaj:

https://github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md

Instalacja polega w pierwszym kroku na wgraniu do kubernetesa Yamla z sekcji Mandatory Commands:

https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml

Czyli po kolei, co tam w środku jest robione:

  • powołanie namespace ingress-nginx,
  • deployment dla default-http-backend (tego od rzucania 404 w stronę niestosownych zapytań),
  • service dla default-http-backend,
  • 3 ConfigMapy: nginx-configuration, tcp-services, udp-services,
  • ServiceAccount nginx-ingress-serviceaccount,
  • ClusterRole nginx-ingress-clusterrole,
  • role nginx-ingress-role,
  • deployment nginx-ingress-controller.

W drugim kroku trzeba wgrać yamla dla opcji bare-metal (czyli on-premise):

https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml

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:

<code></code><code># kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default-http-backend ClusterIP 10.106.132.195 &lt;none&gt; 80/TCP 8m
ingress-nginx NodePort 10.109.196.242 &lt;none&gt; 80:32165/TCP,443:31729/TCP 7m
</code>

Aby jednak mieć cokolwiek do wystawiania i testów, trzeba powołać 2 serwisy (i leżące pod nimi 2 deploymenty):

<code>kubectl run website --image=gimboo/apacz --port=80
kubectl run forums --image=gimboo/apacz --port=80
kubectl expose deployment/website
kubectl expose deployment/forums
</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):

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-ingress
spec:
backend:
serviceName: website
servicePort: 80

Robimy kubectl apply -f powyższy_plik i testujemy konfig:

# curl 127.0.0.1:32165
website-7dcbbddc8f-cx4dc

Skalujemy backend (deploy pod spodem) do 2 i sprawdzamy czy działa Load-Balancing:

<code></code><code># kubectl scale deploy website --replicas=2
deployment.extensions "website" scaled
# curl cent401:32165
website-7dcbbddc8f-cx4dc
# curl cent401:32165
website-7dcbbddc8f-qd2np
</code>

Jest ok , jak widać działa, czas na Ingressa z rozdziałem ruchu na dwa serwisy pod spodem:

<code></code><code>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
</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:

<code></code><code># 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
</code>

Osiągneliśmy zatem vHost mapping, czas na url mapping:

<code></code><code>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
</code>

Tym razem mechanizm polega na wrzucaniu na dany serwis ruchu na podstawie url, w zapytaniu po zaaplikowaniu działa to następująco:

<code></code><code># 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
</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) .

https://github.com/hxquangnhat/kubernetes-auto-ingress

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):

  1. nowo wystawiane serwisy będą miały olabelowanie wskazujące autoingressowi jak ma parsować i dodawać do ingressa urle będzie to zrobione na labelce „auto_ingress” – dla uproszczenia na potrzeby tego LABu pakuję wszystkie 3 kluczowe informacje w jeden string,

</code>metadata: labels: run: serwis5 auto_ingress: 'serwis5_path_80'<code>

  1. autoingress będzie oglądał sobie wszystkie serwisy na k8s i dla każdego czytał dane z tego labelu i parsował to do 3 wartości service-name, service-url-path, service-port na tej podstawie będzie sterował ingressem, który z kolei będzie działał w namespace=default,
  2. autoingress będzie działał we własnym namespace=”autoingress” – tak, żeby nie przeszkadzał innym i w drugą stronę,
  3. powołany będzie dla niego ServiceAccount=autoingress-serviceaccount w przypadku naszego automatu będziemy poniższym userem:

</code>User "system:serviceaccount:autoingress:autoingress-serviceaccount" <code>

  1. powołana zostanie ClusterRole=autoingress-clusterrole, która będzie miała następujące uprawnienia:

</code><code></code><code>- apiGroups:
- ""
resources:
- services
verbs:
- list
- apiGroups:
- "extensions"
resources:
- ingresses
verbs:
- get
- list
- watch
- patch
- delete
- create
</code><code>

Te pierwsze uprawnienia, to czytanie info o serwisach – będę to robił via:

</code>kubectl -n default get svc -o jsonpath='{.items[*].metadata.labels.auto_ingress}')<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):

</code> INGRESSHOST: www.mysite.com INGRESSNAME: my-ingress INGRESSNAMESPACE: default<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:

</code><code></code><code># 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
</code><code>

To teraz pozostaje kreować serwisy i cieszyć się, że Ingress sam się rekonfiguruje.

</code><code></code><code># 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
</code><code>

Jak widać tego drugiego nie ma – trzeba go zrobić, zatem:

</code><code></code></pre>
<pre><code># 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</code></pre>
<pre><code>

Ingress już się zmienił :

</code><code></code><code># 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</code></pre>
<pre><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 🙂

Alt Text

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.

About the author

Leave a Reply