Při vystavování služeb běžících v Kubernetes clusteru ke konzumaci okolním světem, ať už v privátní síti nebo veřejně, bývá k vidění celá řada přístupů. Ty nejčastější jsou manuální konfigurace, wildcard záznamy, API management a nebo dnešní téma - automatické vytváření záznamů v DNS mimo cluster s projektem External DNS.
Obvykle se potkávám s následujícími variantami řešení konfigurace DNS záznamů:
Projekt External DNS slouží k automatizaci DNS záznamů v externí DNS na základě poslouchání vzniku objektů Service a Ingress. Dělá to tak, že u objektu Service sleduje anotaci a u objektu Ingress kouká na políčko hostname. Díky tomu ví, jaké FQDN je požadováno. U objektu Service najde v Kubernetes databázi její přidělenou externí IP (typicky se tedy používá se service type LoadBalancer) a to bude potřebný A záznam do DNS serveru. U objektu Ingress zase najde políčko s IP adresou balanceru (což je typicky IP adresa Service vašeho Ingress kontroleru) - ne všechny kontrolery to tak dělají, ale External DNS funguje například s oblíbeným NGINX a Traefik. Jsou i tam i nějaké další varianty s konfigurací CNAME, ale to v mém scénáři nebudu potřebovat.
Z podporovaných implementací externí DNS je pro mě důležitá Azure DNS a Azure Private DNS, najdete i DNS Google a AWS, CloudFlare nebo DNS servery jako je PowerDNS, CoreDNS či Infoblox.
Pro účely externí DNS velmi doporučuji převést si své domény do Azure DNS. Je to jediná služba Azure, která má svém SLA 100% dostupnost. Jako platformní služba je také chráněna proti DDoS. Za každou zónu zaplatíte cirka 10 Kč měsíčně a za každých milion dotazů taky 10 Kč. Do Azure DNS vám bude External DNS zapisovat explicitní A záznamy s IP adresou vašeho Ingressu. Někdy můžete potřebovat nastavovat spíše CNAME a i to je s External DNS možné.
Pro vnitřní použití nasadíme variantu Azure Private DNS a i pro tu má External DNS implementaci. Doporučoval bych následující architekturu:
Následně mě napadají dvě varianty, jak se zónami pracovat:
Rozeberme scénář, kdy jde o firmu bez potřeby ruční kontroly vystavování ven, která chce řídit jak interní tak veřejné služby přímo přes Kubernetes objekty. Pokud bych chtěl použít Ingress kontroler uvnitř clusteru, zvolil bych například nginx nebo Traefik ve dvou instancích. Jednu nainstaluji tak, že má service type Load Balancer bez anotací, takže běží na veřejné IP. Druhou instanci vytvořím s jiným názvem ingress class a v anotacích specifikuji interní load balancer. Díky tomu budu umět při definici Ingress pro aplikaci jednoduše vybrat, jestli se má publikovat interně nebo veřejně.
Pokud k tomuhle přidám dvě instance External DNS, jednu napojenou na Azure Private DNS zóny a druhou na Azure DNS public zóny, dostanu řešení, které je plně automatizované použitím Kubernetes objektů.
Přesně takový scénář si teď vyzkoušíme.
V následujícím scénáři použiji externí a interní NGINX Ingress, privátní DNS zónu pro VM, privátní DNS pro aplikaci v clusteru a jednu veřejnou zónu.
Nejprve si vytvořím networking, privátní DNS zónu (tu public už mám připravenou) a AKS cluster.
# Create networking
az group create -n networking-rg -l westeurope
az network vnet create -n my-net -g networking-rg --address-prefix 10.1.0.0/16
az network vnet subnet create --vnet-name my-net -g networking-rg -n aks-subnet --address-prefixes 10.1.0.0/24
az network vnet subnet create --vnet-name my-net -g networking-rg -n vm-subnet --address-prefixes 10.1.1.0/24
# Create Private DNS zone for Kubernetes services/ingresses and for virtual machines
az network private-dns zone create -g networking-rg -n services.mydomain.cz
az network private-dns zone create -g networking-rg -n vm.mydomain.cz
az network private-dns link vnet create -n services-zone-link \
-g networking-rg \
-z services.mydomain.cz \
-e false \
-v $(az network vnet show -n my-net -g networking-rg --query id -o tsv)
az network private-dns link vnet create -n vm-zone-link \
-g networking-rg \
-z vm.mydomain.cz \
-e true \
-v $(az network vnet show -n my-net -g networking-rg --query id -o tsv)
# Create AKS
az group create -n aks-rg -l westeurope
az aks create -n aks \
-g aks-rg \
-x \
-c 1 \
-s Standard_B2ms \
--network-plugin azure \
--vnet-subnet-id $(az network vnet subnet show --vnet-name my-net -g networking-rg -n aks-subnet --query id -o tsv) \
--service-cidr 192.168.5.0/24 \
--dns-service-ip 192.168.5.100
az aks get-credentials -n aks -g aks-rg --overwrite-existing
# Grant AKS access to networking for creating internal LB
az role assignment create --role "Network Contributor" \
--assignee $(az aks show -n aks -g aks-rg --query identity.principalId -o tsv) \
-g networking-rg
Pro nasazení privátního Ingress použiji tento values soubor:
controller:
ingressClass: nginx-internal
service:
loadBalancerIP: 10.1.0.100
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
Nasadím interní Ingress.
kubectl create namespace ingress-internal
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm upgrade -i nginx-ingress-internal ingress-nginx/ingress-nginx -n ingress-internal -f ingressValues.yaml
Dále vytvoříme klasický externí NGINX Ingress.
kubectl create namespace ingress-external
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm upgrade -i nginx-ingress-external ingress-nginx/ingress-nginx -n ingress-external --set controller.ingressClass=nginx-external
V dalším kroku si připravím konfigurační soubor pro External DNS s využitím managed identity.
{
"tenantId": "mojeTenantId",
"subscriptionId": "mojeSubscriptionId",
"resourceGroup": "networking-rg",
"useManagedIdentityExtension": true
}
Nasadím RBAC pravidla a dvě instance External DNS - jednu pro interní a druhou pro externí záznamy a všimněte si filtrace podle FQDN.
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
args:
- --source=service
- --source=ingress
- --domain-filter=services.mydomain.cz
- --provider=azure-private-dns
- --azure-resource-group=networking-rg
- --azure-subscription-id=mojeSubscriptionId
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns-public
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns-public
template:
metadata:
labels:
app: external-dns-public
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
args:
- --source=service
- --source=ingress
- --domain-filter=tomaskubica.net
- --provider=azure
- --azure-resource-group=shared-services
- --azure-subscription-id=mojeSubscriptionId
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
Dám AKS identitě práva na svoje DNS zónu a pošlu nasazení External DNS do clusteru.
# Grant AKS managed identity access to DNS zones
az role assignment create --role "Private DNS Zone Contributor" \
--assignee $(az aks show -n aks -g aks-rg --query identityProfile.kubeletidentity.clientId -o tsv) \
--scope $(az network private-dns zone show -g networking-rg -n services.mydomain.cz --query id -o tsv)
az role assignment create --role "Reader" \
--assignee $(az aks show -n aks -g aks-rg --query identityProfile.kubeletidentity.clientId -o tsv) \
-g networking-rg
az role assignment create --role "Private DNS Zone Contributor" \
--assignee $(az aks show -n aks -g aks-rg --query identityProfile.kubeletidentity.clientId -o tsv) \
--scope $(az network dns zone show -g shared-services -n tomaskubica.net --query id -o tsv)
az role assignment create --role "Reader" \
--assignee $(az aks show -n aks -g aks-rg --query identityProfile.kubeletidentity.clientId -o tsv) \
-g shared-services
# Deploy External DNS
kubectl create secret generic azure-config-file --from-file=azure.json
kubectl apply -f externalDns.yaml
Nasadím teď jednoduchý webík.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myweb-deployment
spec:
replicas: 3
selector:
matchLabels:
app: myweb
template:
metadata:
labels:
app: myweb
spec:
containers:
- name: myweb
image: tkubica/web:1
env:
- name: PORT
value: "80"
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 64M
limits:
cpu: 500m
memory: 256M
Nad aplikací teď použiji na ukázku tři objekty Service. Dvě služby jsou externí z pohledu clusteru s tím, že jedna z nich je v rámci VNETu (privátní) a druhá na veřejné IP. Nepoužívám tedy Ingress (L7 balancer). Takové řešení mi může přinášet větší výkon a podporuje i non-http protokoly, třeba UDP provoz. Hostname, na který bude moje External DNS reagovat, řeknu formou anotace. Třetí služba je typu ExternalName, tedy neprovádí žádný balancing uvnitř clusteru, ale použiji jí pro ukázku vytvoření CNAME.
kind: Service
apiVersion: v1
metadata:
name: myweb-service-ext-private
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
external-dns.alpha.kubernetes.io/hostname: s1.services.mydomain.cz
spec:
selector:
app: myweb
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: myweb-service-ext-public
annotations:
external-dns.alpha.kubernetes.io/hostname: s1.tomaskubica.net
spec:
selector:
app: myweb
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: s2-to-s1-cname
annotations:
external-dns.alpha.kubernetes.io/hostname: s2.tomaskubica.net
spec:
externalName: s1.tomaskubica.net
type: ExternalName
Zkusíme současně i druhou možnost - publikaci přes Ingress. Jeden bude mířit na kontroler s privátní IP a druhý na ten s veřejnou.
kind: Service
apiVersion: v1
metadata:
name: myweb-service
spec:
selector:
app: myweb
type: ClusterIP
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: myweb-ingress-internal
annotations:
kubernetes.io/ingress.class: nginx-internal
spec:
rules:
- host: intapp.services.mydomain.cz
http:
paths:
- backend:
serviceName: myweb-service
servicePort: 80
path: /
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: myweb-ingress-external
annotations:
kubernetes.io/ingress.class: nginx-external
spec:
rules:
- host: extapp.tomaskubica.net
http:
paths:
- backend:
serviceName: myweb-service
servicePort: 80
path: /
Podívejme se na výsledek. Takhle vypadá veřejná zóna tomaskubica.net.
A tohle je privátní zóna services.mydomain.cz
Klidně si teď můžeme všechno vyzkoušet z VM v Azure.
# Test
az group create -n vm-rg -l westeurope
az vm create -n test-vm \
-g vm-rg \
--size Standard_B1ms \
--admin-username tomas \
--ssh-key-values ~/.ssh/id_rsa.pub \
--subnet $(az network vnet subnet show --vnet-name my-net -g networking-rg -n vm-subnet --query id -o tsv) \
--public-ip-address vm-ip \
--image UbuntuLTS
ssh tomas@$(az network public-ip show -n vm-ip -g vm-rg --query ipAddress -o tsv)
# Internal Ingress
dig intapp.services.mydomain.cz
;; ANSWER SECTION:
intapp.services.mydomain.cz. 300 IN A 10.1.0.100
curl intapp.services.mydomain.cz
Version 1: server id ea630f7d-d3ed-490c-adef-57b4ad68caf0
# External Ingress
dig extapp.tomaskubica.net
;; ANSWER SECTION:
extapp.tomaskubica.net. 300 IN A 51.145.177.91
curl extapp.tomaskubica.net
Version 1: server id ea630f7d-d3ed-490c-adef-57b4ad68caf0
# Internal Service
dig s1.services.mydomain.cz
;; ANSWER SECTION:
s1.services.mydomain.cz. 300 IN A 10.1.0.35
curl s1.services.mydomain.cz
Version 1: server id ea630f7d-d3ed-490c-adef-57b4ad68caf0
# External Ingress
dig s1.tomaskubica.net
;; ANSWER SECTION:
s1.tomaskubica.net. 300 IN A 20.76.9.50
curl s1.tomaskubica.net
Version 1: server id ea630f7d-d3ed-490c-adef-57b4ad68caf0
# External CNAME
dig s2.tomaskubica.net
;; ANSWER SECTION:
s2.tomaskubica.net. 300 IN CNAME s1.tomaskubica.net.
s1.tomaskubica.net. 299 IN A 20.76.9.50
curl s2.tomaskubica.net
Version 1: server id ea630f7d-d3ed-490c-adef-57b4ad68caf0
Chcete automatizovat vnitřní a vnější DNS záznamy současně s nasazováním aplikace a to oboje Kubernetes způsobem, tedy například s využitím Helm šablon a hotových konektorů v CI/CD nástrojích jako jsou GitHub Actions nebo Azure DevOps? Podívejte se na projekt External DNS a jeho schopnost ovládat DNS v Azure. Není to jediná cesta, ale pro řadu z vás určitě hodně zajímavá. Vyzkoušejte si to.