ARM64 procesory v Azure jsou už v General Availability a zajímalo mě, jaké to má provozní konsekvence při používání v Azure Kubernetes Service. Podívejme se tedy jak vytvářet a provozovat multi-arch image s technologiemi GitHub Actions, QEMU a buildx, Terraform a AKS.
Jako obvykle technické podklady (Terraform šablony, Kubernetes manifesty, GitHub workflow) najdete na mém GitHubu.
Architekturu ARM64 najdete u VM, které mají ve svém typu písmenko p a to buď ve variantách s lokálním diskem nebo bez (písmenko malé d) a pak v poměru CPU:RAM 1:2 (písmenko D a malé l), poměru 1:4 (D bez l) a 1:8 (E). Z pohledu výkonu je Ampere Altra opravdu velmi zajímavý, ale díky velmi odlišné architektuře se dost špatně srovnává s Intel a AMD. Pokud koukáte na výkony Intel vs. AMD, tyto procesory jsou si dost podobné. V poslední době jsou výsledky obvykle takové, že AMD je o fousek rychlejší a přitom VM v Azure levnější pro většinu běžné zátěže, ale Intel je lepší tam, kde je úloha velmi náročná na CPU a je optimalizovaná na speciální instrukce (různé výpočty - statistické modely, Monte Carlo simulace, fluidní dynamika). S ARM64 to ale tak jednoduché není. Tak například je z testů dobře vidět, že v operacích s čísly je na tom Apmere Altra zatraceně dobře - často lépe, než AMD a blízko Intelu. Jakmile ale zapojíte plovoucí desetinnou čárku je situaci úplně jiná - Ampere Altra ztrácí klidně 50% na Intelu i AMD. V porovnání s Amazon Graviton 2 se mu ale ve všech směrech velmi daří a zejména v běžných operací s čísly dá i o 70% víc než ARM od Amazonu. Nová generace Graviton 3 zdá se náskok Ampere (částečně) dorovná (zejména odstraní nesmírně slabý výkon Graviton 2 v plovoucí desetinné čárce). Závěr? Tohle si budete muset vyzkoušet pro svůj workload a změřit si sami.
Cenově je dle očekávání tato varianta nejníž (mimo jiné díky nižším nákladům na energie). Porovnejme základní srovnatelné modely s v5 generací Intel a AMD:
Proč se pokouknout po ARM64?
Nicméně s používáním ARM64 mám jeden problém - nechi mít ve svojí aplikaci závislost na této architektuře. Proč? Je pravděpodobné, že budu potřebovat stejnou aplikaci spustit i na AMD64/x86 architektuře:
S jinou architekturou mohou být spojeny potenciální operační potíže - pokud na ARM64 nemám zkušenosti, budu je muset získat a některé věci mohou být jinak (např. mohou chybět nějaké knihovny či nástroje nebo nemusí být jednoduše dostupné - např. dodavatel má ke stažení hotové balíčky, ale pro ARM64 si musíte kompilovat ze zdrojáků).
V době psaní článku byla sice ARM64 VM už v GA, ale v AKS ještě v preview, takže bylo nutné registrovat feature dle návodu. Pak už je to triviální - prostě přidáme další node pool. Tady snip z Terraformu.
// Random string
resource "random_string" "random" {
length = 12
special = false
upper = false
}
// Resource group
resource "azurerm_resource_group" "main" {
name = "d-aks-arm64"
location = "westeurope"
}
// Azure Container Registry
resource "azurerm_container_registry" "main" {
name = random_string.random.result
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
sku = "Basic"
}
// Azure Kubernetes Service
resource "azurerm_kubernetes_cluster" "main" {
name = random_string.random.result
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
dns_prefix = random_string.random.result
kubernetes_version = "1.24"
node_resource_group = "d-aks-arm64-nodes"
default_node_pool {
name = "amd64"
node_count = 1
vm_size = "Standard_B2ms"
}
identity {
type = "SystemAssigned"
}
}
// ARM64 node pool
resource "azurerm_kubernetes_cluster_node_pool" "main" {
name = "arm64"
kubernetes_cluster_id = azurerm_kubernetes_cluster.main.id
vm_size = "Standard_D2pds_v5"
node_count = 1
}
// RBAC for AKS to access ACR
resource "azurerm_role_assignment" "main" {
scope = azurerm_container_registry.main.id
role_definition_name = "AcrPull"
principal_id = azurerm_kubernetes_cluster.main.kubelet_identity[0].object_id
}
output "acr_name" {
value = azurerm_container_registry.main.name
}
output "aks_name" {
value = azurerm_kubernetes_cluster.main.name
}
output "rg_name" {
value = azurerm_resource_group.main.name
}
Celou ukázku nahazuji v GitHub Actions a tam také uvidíme jak na vybudování multi-arch image, tedy takového, který v sobě skrývá varianty pro různé architektury procesorů. Začněme tedy rozplétat GitHub Actions workflow kousek po kousku. Zdroj najdete na GitHubu.
Používám variables pro konfiguraci remote state pro Terraform a také pro přístupy do Azure. Nicméně všimněte si, že nikde není žádný secret - používám totiž GitHub workload identity federation do AAD, což je skvělá věc, o které už jsem psal. Dále vidíme definici jobu, permissions na token (to kvůli OIDC přihlášení) a definici environmentu (při federaci svého Service Principal do GitHubu jsem to kromě repozitáře použil jako identitikátor).
name: d-aks-arm64-create
on:
workflow_dispatch:
env:
TF_STATE_BLOB_ACCOUNT_NAME: tkubicastore
TF_STATE_RESOURCE_GROUP_NAME: base
TF_STATE_BLOB_CONTAINER_NAME: tfstate
TF_STATE_BLOB_FILE: d-aks-arm64.tfstate
TF_STATE_BLOB: d-aks-arm64.tfstate
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
jobs:
docker:
runs-on: ubuntu-latest
permissions:
id-token: write
environment: demo
Pojďme na jednotlivé kroky. Nejdřív stáhnem obsah Gitu a nainstalujeme QEMU. V toto chvíli totiž GitHub hosted runnery nepodporují ARM64 (protože zatím v Azure nebyly), takže než se tak stane musím buď použít self-hosted runner (na ARM64 VM v Azure například) nebo nasadit jen simulaci. To je notoricky pomalé, ale pro moje jednoduché účely naprosto dostačující.
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
Dále si připravíme buildx prostředí, protože akce běží v kontejneru a v něm už dnes není staré docker API.
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
Připojím se do Azure přes OIDC (proto žádný client secret netřeba).
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
V další akci připravím Terraform. Pozor na nastavení terraform_wrapper na false - jinak totiž nefunguje příkaz terraform output, který později používám.
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_wrapper: false
Provedeme inicializaci a napojení na backend storage. To jsem chtěl dělat přes proměnné, ale nebylo by to potřeba (pak to dám to providers.tf) a tak je předávám do CLI příkazu. Všimněte si zejména, že v providers.tf jsem zapnul podporu pro UIDC, takže jak Terraform samotný tak napojení na storage backend znovu využije federované řešení - což je skvělé, nikde žádná hesla.
- name: Terraform Init
working-directory: ./d-aks-arm64/terraform
run: |
terraform init \
-backend-config=storage_account_name=$TF_STATE_BLOB_ACCOUNT_NAME \
-backend-config=resource_group_name=$TF_STATE_RESOURCE_GROUP_NAME \
-backend-config=container_name=$TF_STATE_BLOB_CONTAINER_NAME \
-backend-config=key=$TF_STATE_BLOB \
-backend-config=client_id=$ARM_CLIENT_ID \
-backend-config=subscription_id=$ARM_SUBSCRIPTION_ID \
-backend-config=tenant_id=$ARM_TENANT_ID
A tady jsou providers v Terraformu.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3"
}
random = {
source = "hashicorp/random"
version = "~>3"
}
}
backend "azurerm" {
use_oidc = true
}
}
provider "azurerm" {
use_oidc = true
features {
resource_group {
prevent_deletion_if_contains_resources = false
}
}
}
V dalším kroku nechám Terraform nahodit infrastrukturu a následně si vezmu jeho outputs a vytáhnu si je do GitHubu (přes výpis na obrazovku v předepsaném formátu), ať s nimi mohu operovat jako output ze stepů. Možná to nějaká připravená action dělá sama, ale ta přímo od Hashicorpu ne, tak jsem to udělal takhle. Výstupy jsou jen názvy, žádné tajnosti, takže s tím netřeba dělat nějaké štráchy.
- name: Terraform apply
working-directory: ./d-aks-arm64/terraform
run: terraform apply -auto-approve
- name: Get Terraform outputs
working-directory: ./d-aks-arm64/terraform
id: tf-outputs
run: |
echo "::set-output name=acr_name::$(terraform output -raw acr_name)"
echo "::set-output name=aks_name::$(terraform output -raw aks_name)"
echo "::set-output name=rg_name::$(terraform output -raw rg_name)"
Zaloguji se do Azure Container Registry - zase bez nějakých ošklivých hesel.
- name: ACR login
working-directory: ./d-aks-arm64/terraform
run: az acr login -n ${{ steps.tf-outputs.outputs.acr_name }}
Následně použiji oficiální Action od Dockeru, která provede build mé aplikace (zdrojáky najdete v adresáři app - je to jednoduchý Hello world web v Pythonu s Gunicorn web serverem). Všimněte si, že v platforms jich můžu specifikovat víc. Proto image s názvem app-multi bude mít buildy jak pro amd64 tak arm64 zatímco image app-amd64 jen pro amd64. Tato akce i pushne kontejnery do ACR. Jako tag u image použiji SHA příslušného commitu (pro jednoduchost - nepotřebuji v takové ukázce řešit semantické verzování, ale zase nechci zaprasit s “latest”).
- name: Build and push multi-arch image
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: ./d-aks-arm64/app
file: ./d-aks-arm64/app/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tf-outputs.outputs.acr_name }}.azurecr.io/app-multi:${{ github.sha }}
- name: Build and push amd64 only image
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: ./d-aks-arm64/app
file: ./d-aks-arm64/app/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.tf-outputs.outputs.acr_name }}.azurecr.io/app-amd64:${{ github.sha }}
Následně nahodím kubectl a kustomize CLI a v existujícím kustomization.yaml změním image tak, aby mířil na správné registry a tag (SHA commitu). Pro jednoduchost jedu push metodou a soubor kustomization.yaml po úpravách nezapisuji do Gitu. To bych v praxi určitě přidal (Action udělá PR a zanese změnu do Gitu) tak, aby místo push si to mohl cluster slíznout sám přes Flux nebo ArgoCD.
- name: Install kubectl
uses: Azure/setup-kubectl@v3
- name: Install Kustomize
uses: imranismail/setup-kustomize@v1
- name: Edit and commit kustomization.yaml
working-directory: ./d-aks-arm64/kubernetes
run: |
kustomize edit set image \
registry/app-multi=${{ steps.tf-outputs.outputs.acr_name }}.azurecr.io/app-multi:${{ github.sha }} \
registry/app-amd64=${{ steps.tf-outputs.outputs.acr_name }}.azurecr.io/app-amd64:${{ github.sha }}
Pak už stačí jen natáhnout credentials do clusteru (zase nic optimálního - v praxi bych tedy jednak šel pull-based modelem a když už bych dělal push, tak přes mého Service Principal s workload identity federation) a poslat to tam.
- name: Get AKS credentials
run: az aks get-credentials -n ${{ steps.tf-outputs.outputs.aks_name }} -g ${{ steps.tf-outputs.outputs.rg_name }}
- name: Deploy Kubernetes objects
working-directory: ./d-aks-arm64/kubernetes
run: kubectl apply -k .
Co jsem chtěl ukázat je to, že image uložené jako multi-arch znamenají, že v registru je pod stejným názvem vícero verzí a cluster si dokáže sám vybrat tu správnou. Pokud tam ale je - když není, dopadne to chybou.
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
app-amd64-85b69db494-8htjb 0/1 CrashLoopBackOff 7 (4m41s ago) 15m 10.244.1.6 aks-arm64-30808820-vmss000000 <none> <none>
app-amd64-85b69db494-8jbll 1/1 Running 0 15m 10.244.0.21 aks-amd64-39196772-vmss000000 <none> <none>
app-amd64-85b69db494-n7khv 0/1 CrashLoopBackOff 7 (4m51s ago) 15m 10.244.1.10 aks-arm64-30808820-vmss000000 <none> <none>
app-amd64-85b69db494-xbrh7 1/1 Running 0 15m 10.244.0.15 aks-amd64-39196772-vmss000000 <none> <none>
app-amd64-nodeaffinity-6664b98cf6-cz9mz 1/1 Running 0 15m 10.244.0.16 aks-amd64-39196772-vmss000000 <none> <none>
app-amd64-nodeaffinity-6664b98cf6-dflnm 1/1 Running 0 15m 10.244.0.19 aks-amd64-39196772-vmss000000 <none> <none>
app-amd64-nodeaffinity-6664b98cf6-nrq8t 1/1 Running 0 15m 10.244.0.17 aks-amd64-39196772-vmss000000 <none> <none>
app-amd64-nodeaffinity-6664b98cf6-tgkns 1/1 Running 0 15m 10.244.0.18 aks-amd64-39196772-vmss000000 <none> <none>
app-multi-579746c945-2zkbf 1/1 Running 0 15m 10.244.1.7 aks-arm64-30808820-vmss000000 <none> <none>
app-multi-579746c945-8v8lz 1/1 Running 0 15m 10.244.1.8 aks-arm64-30808820-vmss000000 <none> <none>
app-multi-579746c945-f4z45 1/1 Running 0 15m 10.244.0.20 aks-amd64-39196772-vmss000000 <none> <none>
app-multi-579746c945-twd9b 1/1 Running 0 15m 10.244.1.9 aks-arm64-30808820-vmss000000 <none> <none>
Vidíte jak deployment app-multi nemá problém a běží na AMD64 i ARM64 nodech? Ale u app-amd64 máme chybu - nesedí architektura. V clusterech s různými architekturami a image, které oboje neumí, bych si na tohle měl dát pozor. Jedna ze strategií může být udělat Taint - ARM64 nodepool takhle ocejchovat, takže se s ním běžně nepočítá. Pouze aplikace připravené pro tuto architekturu na něj budou mít nastavenou toleranci. To je určitě dobré řešení pokud je ARM64 pro vás spíš takový malý experiment, drobná podmnožina clusteru. Já zvolil jinou metodu a to je expecitně přidat Node affinitu tam, kde z nějakého důvodu image není multi-arch a potřebuju ho správně nasměrovat. K tomu lze použít nativní label nodu, který se tam přidá automaticky a bude u každého clusteru fungovat stejně.
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-amd64-nodeaffinity
spec:
replicas: 4
selector:
matchLabels:
app: app-amd64-nodeaffinity
template:
metadata:
labels:
app: app-amd64-nodeaffinity
spec:
containers:
- name: app-amd64-nodeaffinity
image: registry/app-amd64:latest
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "512Mi"
cpu: "500m"
ports:
- containerPort: 8080
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- amd64
A to je vlastně všechno. Moc je zajímá, jaké budou vaše zkušenosti s ARM64 pro servery v cloudu. Na mé chytré domácnosti frčí Docker na Raspberry už dávno a mám i Surface Pro X s Woknama pro ARM64 a také je to fajn. A teď jsou na řadě i servery? Nepřeceňoval bych to, ale minimálně pro vývoj ARM64 aplikací je to pecka a umím si představit, že ve velkém se bude dát na takové architektuře i pěkně ušetřit. Ale za mě ne za cenu snížení flexibility nasadit cokoli kdekoli - proto mrkněte na multi-arch image.