Azure Kubernetes Service v kombinaci s Private Link Service - například pro privátní doručení na Front Door nebo k vašim Azure klientům

Na tomto blogu už jsem popisoval technologii Private Link Service, kdy se můžete pasovat do role providera služby tak jak to třeba děla Microsoft s Azure SQL a svým odběratelům nabídnout začlenění vaší služby přímo do jejich virtuální sítě. Stejnou technologii lze také použít pro zajištění privátního spojení POPů Azure Front Door s backendem v regionech. Oboje je skvělé a nově můžete Private Link Service v případě Kubernetu nechat Azure vytvářet automatizovaně použitím anotací. Pojďme vyzkoušet.

Kdy použít Private Link Service

Rozeberme to do většího detailu. V prvním scénáři jste poskytovatelem nějaké služby - API, platforma, software. Váš zákazník je v Azure a nelíbí se mu, že vaše služba je přistupována přes public IP a řešit to věcmi typu VPN je na dlouhé lokte a stohy bezpečnostního papírování. Tím, že jste ale oba v Azure, můžete na své straně udělat na interním load balanceru bez public IP nastavení Private Link Service a tím pádem váš zákazník si ve svém vlastním VNETu může zřídit Private Endpoint. Ve finále stejnou strategii můžete zvolit i uvnitř firmy. Protože obvykle enterprise není plně připraven na zero trust, tak stále ještě řeší koncept segmentace sítě na vnitřní s těmi hodnými a vnější s těmi zlouny a následně k tomu jiný bezpečnostní přístup. Pokud tak máte systém zpracovávající primárně data z mnoha veřejných zdrojů, může být v takové klasické infrastruktuře výhodnější ho postavit odděleně “mimo firmu”, tedy bez integrace do firemní sítě. Výsledné API přesto můžete vystavit dovnitř ve formě Private Endpoint a stále platí, že opačně to nejde (tzn. tím, že zákazník vytvoří Private Endpoint nevzniká schopnost poskytovale služby jakkoli přistupovat na služby v síti zákazníka).

Druhý scénář je akcelerace provozu na krajních bodech Microsoft sítě a to jak pro klasický statický obsah (CDN) tak pro dynamickou komunikaci (směrování na nejbližší POP, terminace TLS, optimalizace a tak podobně). Azure Front Door si ve svých POPech dokáže zřídit Private Endpoint pro vaší Private Link Service, tedy využít privátní komunikaci na backend. Není to tedy tak, že VNET roztáhnete do POPů, ale POPy si k sobě promítnou vaší službu. To je výborné řešení.

Automatizace s využitím anotací

Pokud jste používali AKS s interním LB nebyl problém k němu Private Link Service přidat, nicméně tím, že frontend se vygeneruje až v okamžiku, kdy vytvoříte Service s type LoadBalancer, automatizace je pak trochu složitější (k vytvoření PLS potřebujete znát název frontend konfigurace balanceru a tu neznáte dokud nebude Service). To není zas tak velký problém, když nám půjde o jedinou službu a to tu, na které poběží třeba NGINX Ingress. Nicméně někdy může být výhodnější publikovat přímo službu, typicky třeba u non-http protokolů jako je třeba UDP pro nějaké real-time věci.

Novinkou je automatizace vytvoření PLS přes anotace na objektu Service. Tím pro vás Kubernetes v Azure vytvoří PLS dle vašeho zadání (tedy s deklarativním jménem), na což se pak dá celkem snadno navázat, protože jméno PLS znáte dopředu.

Vytvoříme AKS a Ingress kontroler tak, aby k němu vzniklo PLS

Nejprve si vytvořím AKS a síť s pár subnety. V subnetu aks budu mít překvapivě AKS, v subnetu lb budu chtít mít interní LB a v subnetu privatelinks budou IP adresy, které představují ústí červí díry z Private Endpoint zákazníka (ten komunikuje na svůj privátní endpoint a na druhé straně vesmíru tenhle provoz vyleze z IP v mém poskytovatelském subnetu).

# Create AKS
az group create -n akspls -l westeurope
az network vnet create -n aksplsvnet -g akspls --address-prefixes 10.88.0.0/16 
az network vnet subnet create -n aks -g akspls --vnet-name aksplsvnet --address-prefix 10.88.0.0/22
az network vnet subnet create -n lb -g akspls --vnet-name aksplsvnet --address-prefix 10.88.4.0/24
az network vnet subnet create -n privatelinks -g akspls --vnet-name aksplsvnet --address-prefix 10.88.5.0/24
az identity create -n aksplmidentity -g akspls
az role assignment create --assignee $(az identity show -n aksplmidentity -g akspls --query principalId -o tsv) --role "Contributor" -g akspls
az aks create -n akspls -g akspls -c 1 -x -k 1.23.5 --network-plugin azure -s Standard_B2s \
    --vnet-subnet-id $(az network vnet subnet show -n aks  -g akspls --vnet-name aksplsvnet --query id -o tsv) \
    --assign-identity $(az identity show -n aksplmidentity -g akspls --query id -o tsv) \
    --assign-kubelet-identity $(az identity show -n aksplmidentity -g akspls --query id -o tsv)
az aks get-credentials --admin --overwrite-existing -n akspls -g akspls

Jako Ingress kontroler použiji klasický NGINX a upravím jeho Helm hodnoty tak, aby u jeho služby byly potřebné anotace.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm upgrade -i ingress-nginx ingress-nginx/ingress-nginx \
  --create-namespace \
  --namespace ingress \
  -f values.yaml

Takhle vypadá values.yaml

controller:
  service:
    loadBalancerIP: 10.88.4.100
    annotations:
      service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: /healthz
      service.beta.kubernetes.io/azure-load-balancer-internal: "true"
      service.beta.kubernetes.io/azure-load-balancer-internal-subnet: lb
      service.beta.kubernetes.io/azure-pls-create: "true"
      service.beta.kubernetes.io/azure-pls-name: myservice
      service.beta.kubernetes.io/azure-pls-ip-configuration-subnet: privatelinks
      service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address-count: "1"
      service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address: 10.88.5.10
      service.beta.kubernetes.io/azure-pls-proxy-protocol: "false"
    # service.beta.kubernetes.io/azure-pls-visibility: "*"
    # service.beta.kubernetes.io/azure-pls-auto-approval: "subId1"

Na tomto místě bude asi dobré vysvětlit co znamená PLS proxy protokol. Jde o to, že k jedné službě můžu vytvořit vícero Private Endpoint, takže Ingress data plane může sloužit několika mým zákazníkům. V základním stavu pakety od nich ale nerozliším - což ale často nevadí, zákazníci jsou určitě autentizovaní a tak podobně. Nicméně můžete to také přepnout na proxy protokol, ale pak ho musíte umět v ingressu rozbalovat a pracovat s tím. To jsem ještě nezkoušel, chystám se na to, ale principem je, že pak dostanete navíc metainformace o Private Endpointu, takže jste schopni zákazníky sdílející jediné PLS rozlišit mezi sebou. Protokol je postaven na standardu haproxy a co v něm dostanete najdete v dokumentaci.

Nahodíme aplikaci využívající Ingress

Výborně, rozsvítíme si aplikaci využívající tenhle Ingress a vymyslíme nějaké hostname.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: nginx
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - containerPort: 80
        volumeMounts:
        - name: content
          mountPath: "/usr/share/nginx/html/"
          readOnly: true
      volumes:
      - name: content
        configMap:
          name: content
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: content
data:
  index.html: This is my great app!
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myingress
  labels:
    name: myingress
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.demo
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: myapp
            port: 
              number: 80

Nahodíme aplikaci napřímo

Vytvořme si ještě druhou appku, která nebude za Ingress. Možná totiž budeme chtít vystavit službu přímo z nějakých důvodů, třeba protože není HTTP nebo jsou k tomu smluvní bezpečnostní důvody apod. Necháme si tedy vytvořit další PLS.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myappdirect
spec:
  selector:
    matchLabels:
      app: myappdirect
  template:
    metadata:
      labels:
        app: myappdirect
    spec:
      containers:
      - name: myappdirect
        image: nginx
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - containerPort: 80
        volumeMounts:
        - name: content
          mountPath: "/usr/share/nginx/html/"
          readOnly: true
      volumes:
      - name: content
        configMap:
          name: content
---
apiVersion: v1
kind: Service
metadata:
  name: myappdirect
  annotations:
      service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: /
      service.beta.kubernetes.io/azure-load-balancer-internal: "true"
      service.beta.kubernetes.io/azure-load-balancer-internal-subnet: lb
      service.beta.kubernetes.io/azure-pls-create: "true"
      service.beta.kubernetes.io/azure-pls-name: myservicedirect
      service.beta.kubernetes.io/azure-pls-ip-configuration-subnet: privatelinks
      service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address-count: "1"
      service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address: 10.88.5.11
      service.beta.kubernetes.io/azure-pls-proxy-protocol: "false"
spec:
  type: LoadBalancer
  selector:
    app: myappdirect
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: content
data:
  index.html: This is my great direct app!

Otestujeme z pohledu zákazníka

Vyzkoušejme si teď nahodit VM v jiném VNETu (a potenciálně i v jiném tenantu i v jiném regionu). Vytvoříme Private Endpointy jak pro službu co jde napřímo, tak pro tu přes NGINX a vyzkoušíme. Možná vás bude mást jedna věc - zákazník používá stejné IP adresní rozsahy. To není proto, že bych to chtěl komplikovat, ale proto, aby bylo vidět, že sítě spolu napřímo nijak nekomunikují - není třeba dohadovat se na IP adresách ani nic takového, sítě nejsou nijak přímo propojené, klidně ať se překrývají.

# Create VNET and VM (no I am on purpose use the same ip addresing to demonstrate there is no need to coordinate on ranges or anything)
az group create -n client -l westeurope
az network vnet create -n clientvnet -g client --address-prefixes 10.88.0.0/16 
az network vnet subnet create -n vm -g client --vnet-name clientvnet --address-prefix 10.88.0.0/24
az storage account create -n tomasclientstorage123 -g client
az vm create -n clientvm \
  -g client \
  --size Standard_B1s \
  --image UbuntuLTS \
  --vnet-name clientvnet \
  --subnet vm \
  --authentication-type password \
  --admin-username tomas \
  --admin-password Azure12345678 \
  --nsg "" \
  --public-ip-address "" \
  --boot-diagnostics-storage tomasclientstorage123

# Create private endpoint for Ingress
az network private-endpoint create \
    --connection-name myConnection \
    -n myPrivateEndpoint \
    --private-connection-resource-id $(az network private-link-service show -g MC_akspls_akspls_westeurope -n myservice --query id -o tsv) \
    -g client \
    --subnet vm \
    --vnet-name clientvnet  

# Create private endpoint for direct service
az network private-endpoint create \
    --connection-name myConnectionDirect \
    -n myPrivateEndpointDirect \
    --private-connection-resource-id $(az network private-link-service show -g MC_akspls_akspls_westeurope -n myservicedirect --query id -o tsv) \
    -g client \
    --subnet vm \
    --vnet-name clientvnet  

# Get Private Endpoint IP for Ingress 
az network nic show \
  --ids $(az network private-endpoint show -n myPrivateEndpoint -g client --query networkInterfaces[0].id -o tsv) \
  --query ipConfigurations[0].privateIpAddress -o tsv

# Get Private Endpoint IP for Direct service 
az network nic show \
  --ids $(az network private-endpoint show -n myPrivateEndpointDirect -g client --query networkInterfaces[0].id -o tsv) \
  --query ipConfigurations[0].privateIpAddress -o tsv

# Test access from VM
az serial-console connect -n clientvm -g client 
curl 10.88.0.5 
curl 10.88.0.6 -H 'Host:myapp.demo'

Použití s Azure Front Door

Druhý scénář je když chceme vystavit aplikaci přes Front Door. Ano, ještě než to řeknete, i já bych úplně nejraději, kdyby tohle zašlo ještě o krok dál a Front Door se privátně napojoval tak, že to jde přes Ingress implementaci (podobně jak to je pro Azure Application Gateway), ale to zatím není. Nicméně i přesto je konfigurace PLS přes anotace výhodná, protože, jak už jsem vlastně říkal, vytváří predikovatelná jména. Udělám si Front Door s jedním endpointem a dva origin - jeden půjde do služby co je napřímo a druhý do té co je za Ingress (tam musím samozřejmě přepsat host header na myapp.demo, podle čehož pak Ingress může odlišovat různé služby apod.). Trochu jsem nejdřív tápal co při definici origin zadávat jako host name, ale je to IP adresa toho endpointu u mě (tedy to co najdete jako externí IP v kubectl get service). Je to zkrátka jiný scénář, než ten předchozí.

# Create Front Foor
az afd profile create --profile-name tomasafd -g akspls --sku Premium_AzureFrontDoor

# Create endpoint
az afd endpoint create -g akspls \
    --endpoint-name tomasafd \
    --profile-name tomasafd

# Create Origin group for direct service
az afd origin-group create -g akspls  \
  --origin-group-name originsDirect \
  --profile-name tomasafd \
  --probe-request-type GET \
  --probe-protocol Http \
  --probe-interval-in-seconds 120 \
  --probe-path / \
  --sample-size 4 \
  --successful-samples-required 3 \
  --additional-latency-in-milliseconds 50

# Create origin with private link for direct service
az afd origin create -g akspls  \
    --host-name 10.88.4.4 \
    --profile-name tomasafd \
    --origin-group-name originsDirect \
    --origin-name myappdirect \
    --origin-host-header 10.88.4.4 \
    --enabled-state Enabled \
    --enable-private-link true \
    --private-link-location westeurope \
    --private-link-resource $(az network private-link-service show -g MC_akspls_akspls_westeurope -n myservicedirect --query id -o tsv) \
    --private-link-request-message "myrequest1"

# Approve private connection for direct service
az network private-endpoint-connection approve --id $(
  az network private-endpoint-connection list \
    --id $(az network private-link-service show -g MC_akspls_akspls_westeurope -n myservicedirect --query id -o tsv) \
    --query "[?properties.privateLinkServiceConnectionState.description == 'myrequest1'].id" -o tsv)

# Create routing rule
az afd route create -g akspls \
  --endpoint-name tomasafd \
  --profile-name tomasafd \
  --supported-protocols Https \
  --forwarding-protocol HttpOnly \
  --route-name myappdirect \
  --https-redirect Enabled \
  --patterns-to-match  "/direct/*" \
  --origin-path "/" \
  --origin-group originsDirect \
  --link-to-default-domain Enabled

# Test connection
export url=https://$(az afd endpoint show -g akspls --endpoint-name tomasafd --profile-name tomasafd --query hostName -o tsv)
curl $url/direct/

# Create Origin group for ingress
az afd origin-group create -g akspls  \
  --origin-group-name originsIngress \
  --profile-name tomasafd \
  --probe-request-type GET \
  --probe-protocol Http \
  --probe-interval-in-seconds 120 \
  --probe-path / \
  --sample-size 4 \
  --successful-samples-required 3 \
  --additional-latency-in-milliseconds 50

# Create origin with private link for ingress
az afd origin create -g akspls  \
    --host-name 10.88.4.4 \
    --profile-name tomasafd \
    --origin-group-name originsIngress \
    --origin-name myappingress \
    --origin-host-header myapp.demo \
    --enabled-state Enabled \
    --enable-private-link true \
    --private-link-location westeurope \
    --private-link-resource $(az network private-link-service show -g MC_akspls_akspls_westeurope -n myservice --query id -o tsv) \
    --private-link-request-message "myrequest1"

# Approve private connection for ingress
az network private-endpoint-connection approve --id $(
  az network private-endpoint-connection list \
    --id $(az network private-link-service show -g MC_akspls_akspls_westeurope -n myservice --query id -o tsv) \
    --query "[?properties.privateLinkServiceConnectionState.description == 'myrequest1'].id" -o tsv)

# Create routing rule
az afd route create -g akspls \
  --endpoint-name tomasafd \
  --profile-name tomasafd \
  --supported-protocols Https \
  --forwarding-protocol HttpOnly \
  --route-name myappingress \
  --https-redirect Enabled \
  --patterns-to-match  "/ingress/*" \
  --origin-path "/" \
  --origin-group originsIngress \
  --link-to-default-domain Enabled

# Test connection
export url=https://$(az afd endpoint show -g akspls --endpoint-name tomasafd --profile-name tomasafd --query hostName -o tsv)
curl $url/ingress/

Kdo z vás je defacto SaaS provider, mrkněte na to - použití Private Link Service je určitě skvělý způsob jak uspokojit enterprise požadavky zákazníků co mají Azure a anotace v AKS zjednodušují automatizaci toho všeho. Pokud sice SaaS nejste, ale ocitnete se v situaci, kdy může být výhodné se tak vzhledem ke zbytku firmy cítit, může být tahle technologie dobrý způsob jak získat síťovou svobodu pro projekt, který nepotřebuje nijak sahat do backendu ve vnitřní síti, a přitom výsledné rozhranní publikovat do vnitřní sítě jako Private Endpoint. No a pokud chcete moderně distribuovat aplikace k uživatelům, na což je Front Door skvělý, použitím řešení přes Private Link Service možná odstraníte některé bezpečnostní překážky, které vám budou bezpečáci (a často velmi oprávněně) klást a uspokojení takových potřeb bude jednodušší.



Hybridní DNS jako služba v Azure Networking
Nativní Azure Monitor a Microsoft Sentinel nově umí levnější logy a zabudovanou levnější archivaci - praxe (část 2) Kubernetes
Nativní Azure Monitor a Microsoft Sentinel nově umí levnější logy a zabudovanou levnější archivaci - analýza nákladů (část 1) Kubernetes
Federace vnitřních Kubernetes identit s Azure Active Directory pro přístup k cloudovým službám bez hesel Kubernetes
Použití lokální storage v Azure Kubernetes Service Kubernetes