Si bien es cierto que en la documentación oficial se cuenta la teoría de cómo puedes integrar API Management con AKS, en el artículo de hoy quiero compartir contigo la práctica 😊. He creado un script para desplegar un clúster con una API de ejemplo y un API Management que será el encargado de exponerla al mundo exterior.
Configurar las variables
Para poder montar este entorno de pruebas, necesitas al menos configurar estas variables:
RESOURCE_GROUP="aks-and-apim-demo"
LOCATION="westeurope"
PRIVATE_DNS_ZONE_NAME="tour-of-heroes.internal"
VNET_NAME="aks-and-apim-vnet"
AKS_SUBNET_NAME="aks-subnet"
APIM_SUBNET_NAME="apim-subnet"
AKS_NAME="aks-cluster"
APIM_NAME="apim-demo-${RANDOM}"
# Colors for the output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo -e "${GREEN}Variables set 👍${NC}"
Como te puedes imaginar, vamos a crear: un grupo de recursos, una zona privada de Azure DNS, una red virtual donde vivirán todos los servicios, un clúster de AKS y una instancia de API Management.
Crear una zona de Azure Private DNS y la red virtual
En este ejemplo se crea la red en la cuál se alojará tu clúster de AKS, y posteriormente la instancia de API Management. Para que API Management pueda referenciar las APIs dentro del clúster, voy a utilizar ExternalDNS con Azure Private DNS para que esta comunicación se haga a través de un dominio y no directamente a las IPs asociadas a los servicios:
echo -e "${GREEN}Creating resource group ${RESOURCE_GROUP} in ${LOCATION}..."
RESOURCE_GROUP_ID=$(az group create --name ${RESOURCE_GROUP} --location ${LOCATION} --query id -o tsv)
echo -e "${GREEN}Creating vnet ${VNET_NAME} in ${RESOURCE_GROUP}..."
az network vnet create \
--resource-group ${RESOURCE_GROUP} \
--name ${VNET_NAME} \
--address-prefixes 192.168.0.0/16 \
--subnet-name ${AKS_SUBNET_NAME} \
--subnet-prefix 192.168.1.0/24
echo -e "${GREEN}Creating apim subnet ${APIM_SUBNET_NAME} in ${RESOURCE_GROUP}..."
# But https://learn.microsoft.com/en-us/azure/api-management/virtual-network-concepts?tabs=stv2#examples
az network vnet subnet create \
--resource-group ${RESOURCE_GROUP} \
--vnet-name ${VNET_NAME} \
--name ${APIM_SUBNET_NAME} \
--address-prefixes 192.168.2.0/28
echo -e "${GREEN}Create private DNS zone $PRIVATE_DNS_ZONE_NAME...${NC}"
PRIVATE_DNS_ZONE_ID=$(az network private-dns zone create -g $RESOURCE_GROUP -n $PRIVATE_DNS_ZONE_NAME --query id -o tsv)
# Link the private DNS zone to the virtual network
echo -e "${GREEN}Link the private DNS zone to the virtual network...${NC}"
az network private-dns link vnet create \
--resource-group $RESOURCE_GROUP \
--zone-name $PRIVATE_DNS_ZONE_NAME \
--name $PRIVATE_DNS_ZONE_NAME \
--virtual-network $VNET_NAME \
--registration-enabled false
Crear el clúster de AKS y desplegar ExternalDNS
Ahora que ya tienes la red y tu private DNS creados y conectados, lo siguiente que vamos a hacer es crear un clúster de AKS y desplegar en este ExternalDNS para que nos facilite el trabajo de registrar los servicios que den acceso a las APIs:
echo -e "${GREEN}Creating an identity for AKS..."
az identity create \
--resource-group ${RESOURCE_GROUP} \
--name ${AKS_NAME}-identity
echo -e "${GREEN}Waiting 60 seconds for the identity..."
sleep 60
IDENTITY_ID=$(az identity show --name $AKS_NAME-identity --resource-group $RESOURCE_GROUP --query id -o tsv)
IDENTITY_CLIENT_ID=$(az identity show --name $AKS_NAME-identity --resource-group $RESOURCE_GROUP --query clientId -o tsv)
# Get VNET id
VNET_ID=$(az network vnet show --resource-group $RESOURCE_GROUP --name $VNET_NAME --query id -o tsv)
# Assign Network Contributor role to the user identity
echo -e "${GREEN}Assign roles to the identity..."
az role assignment create --assignee $IDENTITY_CLIENT_ID --scope $VNET_ID --role "Network Contributor"
# Permission granted to your cluster's managed identity used by Azure may take up 60 minutes to populate.
# Get roles assigned to the user identity
az role assignment list --assignee $IDENTITY_CLIENT_ID --all -o table
AKS_SUBNET_ID=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --name $AKS_SUBNET_NAME --query id -o tsv)
echo -e "${GREEN}Creating AKS cluster ${AKS_NAME} in ${RESOURCE_GROUP}..."
time az aks create \
--resource-group ${RESOURCE_GROUP} \
--name ${AKS_NAME} \
--node-vm-size Standard_B4ms \
--node-count 1 \
--enable-managed-identity \
--vnet-subnet-id $AKS_SUBNET_ID \
--assign-identity $IDENTITY_ID \
--enable-addons monitoring \
--generate-ssh-keys
echo -e "${GREEN}Getting AKS credentials..."
az aks get-credentials --resource-group ${RESOURCE_GROUP} --name ${AKS_NAME} --overwrite-existing
echo -e "${GREEN}Fetching the kubelet identity...${NC}"
KUBELET_IDENTITY_CLIENT_ID=$(az aks show --resource-group $RESOURCE_GROUP --name $AKS_NAME --query "identityProfile.kubeletidentity.objectId" --output tsv)
echo -e "${GREEN}Assign the 'Reader' and 'DNS Zone Contributor' role...${NC}"
az role assignment create --role "Reader" --assignee $KUBELET_IDENTITY_CLIENT_ID --scope $RESOURCE_GROUP_ID
az role assignment create --role "Private DNS Zone Contributor" --assignee $KUBELET_IDENTITY_CLIENT_ID --scope $PRIVATE_DNS_ZONE_ID
echo -e "${GREEN}Create a configuration file for the identity...${NC}"
cat <<EOF > azure.json
{
"tenantId": "$(az account show --query tenantId -o tsv)",
"subscriptionId": "$(az account show --query id -o tsv)",
"resourceGroup": "$RESOURCE_GROUP",
"useManagedIdentityExtension": true
}
EOF
echo -e "${GREEN}Create a secret for the identity...${NC}"
kubectl create secret generic azure-config-file --from-file=azure.json
echo -e "${GREEN}Deploy ExternalDNS for Azure Private DNS..."
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns-private
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods", "nodes"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer-private
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns-private
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns-private
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns-private
template:
metadata:
labels:
app: external-dns-private
spec:
serviceAccountName: external-dns-private
containers:
- name: external-dns-private
image: registry.k8s.io/external-dns/external-dns:v0.13.4
args:
- --source=service
- --source=ingress
# - --domain-filter=$PUBLIC_DNS_ZONE_NAME # (optional)
- --provider=azure-private-dns
- --azure-resource-group=$RESOURCE_GROUP # (optional)
- --txt-prefix=externaldns-
- --publish-internal-services
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
EOF
# Wait for external-dns to be ready
kubectl wait --for=condition=available --timeout=600s deployment/external-dns-private
Puedes leer más sobre ExternalDNS en este otro artículo.
Crear API Management
Como ya te mostré en el artículo anterior, lo siguiente que necesitas es crear una instancia de API Management. Para este entorno de pruebas puedes utilizar el tier Developer, el cual te permite integración con la red que acabas de crear:
echo "${GREEN}Creating API Management instance..."
time az apim create \
--resource-group ${RESOURCE_GROUP} \
--name ${APIM_NAME} \
--location ${LOCATION} \
--publisher-email "your@email.com" \
--publisher-name "return(GiS);" \
--sku-name Developer
echo -e "${GREEN}API Management instance created${NC}"
Este proceso puede tardar hasta una hora en completarse.
Inyectar la instancia de API Management en la red
Si quieres que la comunicación entre API Management y el clúster de AKS sea interna, necesitas que API Management viva en la misma red, o al menos en una que tenga peering con esta. En este ejemplo se despliega API Management en la misma, de la forma que ya te conté ayer:
echo -e "${GREEN} Connecting API Management to the virtual network ${NC}"
# Get API Management resource id
APIM_ID=$(az apim show -n ${APIM_NAME} -g ${RESOURCE_GROUP} --query "id")
APIM_SUBNET_ID=$(az network vnet subnet show -g ${RESOURCE_GROUP} -n $APIM_SUBNET_NAME --vnet-name $VNET_NAME --query "id")
time az resource update -n ${APIM_NAME} -g ${RESOURCE_GROUP} \
--resource-type Microsoft.ApiManagement/service \
--set properties.virtualNetworkType=External properties.virtualNetworkConfiguration.subnetResourceId=$APIM_SUBNET_ID
Nota: Es buena práctica asociar NSG a las subnets de una red, si no las generamos nosotros. Es por ello que en nuestras suscripciones internas se nos aplican como política. Por esta razón en mi repo hay dos scripts adicionales (04.1-fix-apim-subnet-nsg.sh y 06.1-fix-aks-subnet-nsg.sh) en los que valido si hay alguna NSG asociada, para habilitar ciertos puertos y que el entorno sea funcional.
Ahora ya tienes la infraestructura necesaria para que API Management haga de punto de entrada de tus APIs en AKS. Ahora solo falta probarlo 😃
Desplegar API de ejemplo en AKS
Para comprobar que todo funciona correctamente, tienes los manifiestos de la API de mi ejemplo Tour Of Heroes en el repositorio. Para crearla y probar que funciona correctamente puedes lanzar este otro script:
echo -e "${GREEN}Deploying sample API to AKS cluster ${AKS_NAME} in ${RESOURCE_GROUP}..."
kubectl apply -f manifests --recursive
echo -e "${GREEN}Waiting for sample API to be ready..."
kubectl wait --for=condition=available --timeout=600s deployment/tour-of-heroes-sql
echo -e "${GREEN}Done 👍🏻"
echo -e "${GREEN} Get internal IP for tour-of-heroes-api service ${NC}"
INTERNAL_IP_API=$(kubectl get service tour-of-heroes-api -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo -e "${GREEN} Test tour of heroes API ${NC}"
kubectl run -it --rm test --image=debian --restart=Never -- bash -c "apt-get update && apt-get install curl -y && curl -k -v http://tour-of-heroes-api/api/hero"
kubectl run -it --rm test --image=debian --restart=Never -- bash -c "apt-get update && apt-get install curl -y && curl -k -v http://${INTERNAL_IP_API}/api/hero"
Dar de alta la API en API Management
Como cualquier otra API, debes dar de alta la misma en tu instancia de API Management. A modo de prueba podemos crear la misma, y un par de operaciones, con estos comandos:
echo -e "${GREEN} Create Tour of Heroes API in API Management ${NC}"
az apim api create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id tour-of-heroes-api \
--path /tour-of-heroes-api \
--display-name "Tour of Heroes API" \
--service-url http://api.tour-of-heroes.internal/api/hero \
--protocols http
echo -e "${GREEN} Add GET operation to the API ${NC}"
az apim api operation create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id tour-of-heroes-api \
--url-template / \
--method GET \
--display-name "Get all heroes"
echo -e "${GREEN} Add POST operation to the API ${NC}"
az apim api operation create \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--api-id tour-of-heroes-api \
--url-template / \
--method POST \
--display-name "Add hero"
Al usar ExternalDNS con Azure Private DNS puedo utilizar el hostname asignado al servicio de mi API en lugar de utilizar la IP privada asignada a este recurso en mi subnet.
Probar a llamar a la API desde API Management
Para finalizar con la prueba, el último paso es asociar esta API a un producto y recuperar la API Key asociada. Con ello podremos hacer llamadas a las operaciones dadas de alta en nuestro API Management:
# Add api to a starter product
echo -e "${GREEN} Add the API to the Starter product ${NC}"
az apim product api add \
--resource-group ${RESOURCE_GROUP} \
--service-name ${APIM_NAME} \
--product-id Starter \
--api-id tour-of-heroes-api
# Get subscription id
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Get starter API Key
API_KEY=$(az rest --method get \
--uri "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.ApiManagement/service/${APIM_NAME}/subscriptions?api-version=2018-01-01" \
| jq '.value[] | select(.properties.productId | endswith("starter")) | .properties.primaryKey')
echo -e "${GREEN} Test tour of heroes API ${NC}"
echo -e "${GREEN} Get heroes ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "http://${APIM_NAME}.azure-api.net/tour-of-heroes-api/" | jq
echo -e "${GREEN} Add hero ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-X POST \
-d '{"name": "Arrow", "alterEgo": "Oliver Queen", "description": "Multimillonario playboy Oliver Queen (Stephen Amell), quien, cinco años después de estar varado en una isla hostil, regresa a casa para luchar contra el crimen y la corrupción como un vigilante secreto cuya arma de elección es un arco y flechas." }' \
"http://${APIM_NAME}.azure-api.net/tour-of-heroes-api/"
echo -e "${GREEN} Get heroes ${NC}"
curl -H "Ocp-Apim-Subscription-Key: ${API_KEY}" "http://${APIM_NAME}.azure-api.net/tour-of-heroes-api/" | jq
El ejemplo completo lo tienes en mi GitHub.
¡Saludos!