Alojar o Archestack no AKS
Este guia instala uma instância do Archestack no Azure Kubernetes Service (AKS) e depois mantém-na atualizada por si só. O Helm chart corre a API como várias réplicas idênticas, o builder e a aplicação publicada, e o Caddy como ponto de entrada, com certificados automáticos. O PostgreSQL corre como Azure Database for PostgreSQL Flexible Server, e as imagens vêm de um container registry próprio. Um pipeline procura todas as noites a release mais recente do Archestack, leva-a a desenvolvimento e teste, e leva essa mesma versão a aceitação e produção assim que alguém a aprovar. Num só servidor, Alojamento com Docker é a forma mais simples.
Já tem um cluster e quer lá o Archestack hoje mesmo, sob um caminho de um hostname que já serve? O Archestack no Kubernetes, a forma curta faz isso com cinco ficheiros e um pipeline, e é por onde começar.
O que corre onde
LoadBalancer, static public IP (80, 443)
|
Caddy ---- asks the API before every new certificate
+-------------+---------+---------+------------------------------+
editor.<zone> api.<zone> metrics.<zone> <workspace>.<zone>, companies' own domains
| | | |
web-dashboard api (N) grafana web-app
|
Azure Database for PostgreSQL 16, Flexible Server, private to the network
Azure Files: /files and /backups, one share every API replica mounts
omnicore-api and omnicore-web, pulled from your own registry (ACR) - O Caddy é o ponto de entrada, não um ingress controller. Os endereços dos espaços de trabalho e os domínios próprios das empresas recebem um certificado da primeira vez que alguém os visita, e o Caddy pergunta à API se um hostname pertence a um espaço de trabalho antes de pedir um. O cert-manager não consegue emitir certificados a pedido, por isso um ingress controller perderia esses endereços. Sem eles, o Archestack também pode ficar sob um caminho de um hostname que o cluster já serve.
- O endereço do visitante é preservado (
externalTrafficPolicy: Local). O limitador de pedidos conta os chamadores anónimos por endereço e, sem isto, todos os visitantes partilhariam o endereço de um nó. - Os ficheiros e os backups são uma só partilha do Azure Files que todas as réplicas
da API montam, por isso um logótipo ou um backup escrito através de uma réplica está presente em
todas elas. Os pacotes NuGet que os scripts usam são a exceção: cada pod guarda os seus no seu nó,
enquanto o pod existir (
api.nugetCache). - A API cria as suas próprias bases de dados e migra-as quando arranca, por isso só precisa de um servidor vazio. Não há nenhum job de migração separado.
Versões e tags
Cada release do Archestack tem um número de versão, major.minor.build, como
1.4.23. O número de build sobe a cada release e recomeça em 1 quando a versão major ou
minor muda, e pode faltar um número quando a build de uma release não terminou. Cada release publica
duas imagens no Docker Hub, nickpeelman/omnicore-api e
nickpeelman/omnicore-web, cada uma com três tags: a versão, o commit a partir do qual
foi construída, e latest.
Faça deploy do número de versão, nunca de latest:
- Não há rollout. Uma atualização de
latestparalatestdeixa o Deployment inalterado, por isso o Kubernetes mantém os pods que tem. - As réplicas divergem. O chart só faz pull de uma imagem quando o nó ainda não a
tem. Um nó que fez pull de
latestna semana passada continua a correr a versão da semana passada, um nó novo corre a desta semana, e uma só instância serve duas versões. - Não há volta atrás. A versão anterior não tem nenhum nome para o qual voltar.
Por isso, o pipeline abaixo transforma "a mais recente" num número uma única vez, no início de uma
execução, e todos os ambientes recebem esse número. Também pode seguir uma linha: 1.4
leva a versão 1.4.x mais recente e nunca passa para 1.5 por si só. As
alterações maiores chegam numa nova versão major ou minor, e a
extensão do VS Code avisa quando se liga a uma API de outra versão major ou
minor.
Antes de começar
- Uma subscrição do Azure e, na sua máquina, a CLI
az, okubectle o Helm 3. - Um projeto no Azure DevOps, no GitHub ou no GitLab para o pipeline, e lá um repositório feito a partir do kit inicial.
-
O Helm chart: a pasta
infra/helm/archestackdo repositório do Archestack, copiada para esse repositório no mesmo caminho. - Uma zona DNS à qual possa acrescentar registos, ou um hostname que um cluster já serve através do seu ingress controller (forma B).
O kit inicial
infra/helm/archestack/ the chart, copied from the Archestack repository
setup/ run once per environment, by a person
1-cluster.sh registry, network, Key Vault, database, cluster
2-address.sh way A: the public address for DNS
3-pipeline.sh the SMTP password, the namespace, the pipeline's identity
aks/
settings.env fill in: prefix, region, where the pipeline runs
environment.sh every Azure name, made from settings.env
resolve-version.sh newest, 1.4 or 1.4.23, made one version number
deploy.sh import into your registry, then helm upgrade
azure-pipelines.yml the pipeline, on Azure DevOps
environments/
dev/ tst/ acc/ prd/ fill in: values.yaml, and names.env for a cluster you have
.github/workflows/ the pipeline, on GitHub Actions
.gitlab-ci.yml the pipeline, on GitLab CI
Mantenha o pipeline do seu sistema de CI e apague os outros dois. grep -rn CHANGE-ME .
lista todos os valores ainda por preencher, e o deploy.sh recusa um ambiente que ainda
tenha algum. Depois, os três scripts de configuração criam cada ambiente no Azure, corridos uma única
vez por uma pessoa:
# Once per environment, after az login as someone who may create resources and assign roles
ENVIRONMENT=prd bash setup/1-cluster.sh # registry, network, Key Vault, database, cluster
ENVIRONMENT=prd bash setup/2-address.sh # way A only: the public address, then your DNS
ENVIRONMENT=prd bash setup/3-pipeline.sh # asks the SMTP password; the pipeline's identity Cada script só cria o que falta, por isso um script que tenha parado a meio pode simplesmente ser corrido de novo, e cada um termina a dizer o que vem a seguir.
1. Preencha as definições
O aks/settings.env contém o que todos os ambientes partilham:
# aks/settings.env: fill in once. No passwords here: the setup asks for them and keeps them in Key Vault.
PREFIX=CHANGE-ME # 2-10 lowercase letters or digits, unique to you, such as acme
LOCATION=westeurope # the Azure region for every environment
PIPELINE=CHANGE-ME # where the pipeline runs: azure-devops, github or gitlab
REPO= # github: owner/repo; gitlab: group/project; azure-devops: leave empty
GITLAB_URL=https://gitlab.com
Todos os nomes do Azure derivam dele em aks/environment.sh, que tanto a configuração
como o deploy.sh leem, por isso estão sempre de acordo. Os nomes que o Azure exige que
sejam únicos em todo o mundo (o registry, o vault e o servidor de base de dados) terminam no seu
prefixo:
# aks/environment.sh: one environment's Azure names, made from settings.env, and how to sign in to
# its cluster. The setup scripts and deploy.sh both read it, so they always agree.
# shellcheck source=settings.env
source "$(dirname "${BASH_SOURCE[0]}")/settings.env"
: "${ENVIRONMENT:?set ENVIRONMENT to dev, tst, acc or prd}"
[[ $PREFIX =~ ^[a-z0-9]{2,10}$ ]] || { echo "Fill in PREFIX in aks/settings.env: 2-10 lowercase letters or digits" >&2; exit 1; }
RG=rg-archestack-$ENVIRONMENT
AKS_RG=$RG
AKS=aks-archestack-$ENVIRONMENT
VNET_RG=$RG
VNET=vnet-archestack-$ENVIRONMENT
PG_SUBNET=10.20.16.0/24
PIP=pip-archestack-$ENVIRONMENT
IDENTITY=id-archestack-$ENVIRONMENT
PG=pg-$PREFIX-$ENVIRONMENT # <name>.postgres.database.azure.com, unique across Azure
KV=kv-$PREFIX-$ENVIRONMENT # unique across Azure, at most 24 characters
ACR=acr$PREFIX # one registry for every environment, unique across Azure
ACR_RG=rg-archestack-shared
NAMESPACE=archestack
# A cluster you already have, such as one whose ingress already serves the hostname Archestack
# goes under: aks/environments/<env>/names.env changes the names above (see names.env.example).
OVERRIDES="$(dirname "${BASH_SOURCE[0]}")/environments/$ENVIRONMENT/names.env"
if [ -f "$OVERRIDES" ]; then source "$OVERRIDES"; fi
# Signs kubectl and helm in to the cluster as whoever az is signed in as. A cluster with Entra ID
# sign-in needs kubelogin for that, installed into ~/.local/bin when it is missing.
kube_login() {
az aks get-credentials -g "$AKS_RG" -n "$AKS" --overwrite-existing -o none
if [ "$(az aks show -g "$AKS_RG" -n "$AKS" --query 'aadProfile != null' -o tsv)" = true ]; then
if ! command -v kubelogin > /dev/null; then
mkdir -p "$HOME/.local/bin"
az aks install-cli --install-location "$HOME/.local/bin/kubectl" \
--kubelogin-install-location "$HOME/.local/bin/kubelogin" -o none
export PATH="$HOME/.local/bin:$PATH"
fi
kubelogin convert-kubeconfig -l azurecli
fi
}
O values.yaml de cada ambiente contém os seus valores do chart e uma de duas formas de
acesso: hostnames próprios (A), ou um caminho de um hostname que o cluster já serve (B). Mantenha uma
e apague a outra.
# aks/environments/prd/values.yaml: the chart values of this environment. The chart's own values.yaml
# lists every value. The images, the database server and the public address come from deploy.sh,
# the passwords from Key Vault.
#
# Two ways in. Keep one, delete the other, and fill in every CHANGE-ME that is left:
# A. Hostnames of its own (editor., api., metrics., and <workspace>. per workspace) behind Caddy,
# with certificates it gets by itself. Needs DNS records and setup/2-address.sh.
# B. A path of a hostname the cluster's ingress already serves, such as https://apps.acme.com/archestack/.
# No new DNS record, address or certificate. No workspace addresses, own domains or Grafana.
# --- A. Hostnames of its own ---
hosts:
dashboard: editor.CHANGE-ME # editor.<your zone>, such as editor.archestack.acme.com
api: api.CHANGE-ME # api.<your zone>
grafana: metrics.CHANGE-ME # metrics.<your zone>
caddy:
acmeEmail: CHANGE-ME # Let's Encrypt writes here about certificates
persistence:
storageClass: managed-csi
# --- B. A path of a hostname the cluster already serves ---
ingress:
enabled: true
className: nginx # the ingress class that serves the hostname
host: CHANGE-ME # such as apps.acme.com
basePath: /archestack # the builder at https://<host>/archestack/
tls:
- secretName: CHANGE-ME # the hostname's certificate, as a Secret in this namespace
hosts: [CHANGE-ME] # the host again
# --- Both ---
api:
replicas: 3
# A: your zone, for workspaces at <label>.<zone> and companies' own domains, or empty for neither.
# B: empty.
tenantHostSuffix: CHANGE-ME
persistence:
files: { storageClass: azurefile-csi, accessMode: ReadWriteMany, size: 100Gi }
backups: { storageClass: azurefile-csi, accessMode: ReadWriteMany, size: 100Gi }
postgres:
username: archestack
sslMode: Require
# The mail server; its password is in Key Vault. Leave all three empty when the instance sends no mail.
smtp:
host: CHANGE-ME
username: CHANGE-ME
from: CHANGE-ME
observability:
enabled: true # B: Grafana is not routed; reach it with kubectl port-forward
instance: prd
storageClass: managed-csi -
api.replicas: qualquer número, sem sessões fixas. Infraestrutura de alojamento lista o que várias réplicas pedem à rede à sua volta. -
api.tenantHostSuffix: ativa um endereço por espaço de trabalho e os domínios próprios das empresas, apenas com a forma A. -
api.persistence: tem de serReadWriteManyassim que houver réplicas a correr em mais do que um nó. -
observability.enabled: Grafana, Prometheus, Tempo e Loki dentro do cluster. Os seus painéis de host e de containers ficam vazios no AKS; o Container Insights cobre os nós e os containers.
Sob um caminho de um hostname que já tem
A forma B coloca o Archestack sob um caminho de um hostname que o ingress controller do cluster já serve, ao lado das aplicações que já lá estão:
https://apps.acme.com/ what is already there
https://apps.acme.com/archestack/ the builder
https://apps.acme.com/archestack/app/<page> the published app
https://apps.acme.com/archestack/api the API (and /hubs, live updates) Não precisa de nenhum registo DNS novo, de nenhum endereço público nem de nenhum certificado próprio, e as pessoas continuam a usar o endereço que já conhecem. O chart deixa o Caddy de fora e gera um só Ingress para os três caminhos; o builder e a API recebem o seu prefixo, por isso todas as hiperligações que escrevem o incluem.
# --- B. A path of a hostname the cluster already serves ---
ingress:
enabled: true
className: nginx # the ingress class that serves the hostname
host: CHANGE-ME # such as apps.acme.com
basePath: /archestack # the builder at https://<host>/archestack/
tls:
- secretName: CHANGE-ME # the hostname's certificate, as a Secret in this namespace
hosts: [CHANGE-ME] # the host again - Um cluster que já tem. O hostname vive num cluster, normalmente um que já existia
antes do Archestack. Copie
names.env.exampleparaaks/environments/<env>/names.enve a configuração deixa esse cluster como está: permite ao cluster fazer pull do registry e acrescenta à sua rede uma sub-rede para a base de dados. Salte osetup/2-address.sh. -
classNameé a ingress class que serve o hostname. O chart define as anotações do ingress-nginx para uploads grandes e ligações de longa duração; outro controller precisa dos seus próprios equivalentes. - O certificado tem de ser um Secret no namespace
archestack, porque um Ingress só usa Secrets do seu próprio namespace. Copie para lá o certificado do hostname, ou faça com que o cert-manager o emita lá. - Não reescreva o caminho (sem
rewrite-target). A API precisa de ver o seu prefixo para escrever os seus próprios endereços. - Não ponha proxies de início de sessão à frente do Archestack. Um proxy que
substitua o cabeçalho
Authorizationfaz falhar todas as chamadas à API: a aplicação publicada, as chaves da Data API, os tokens de deploy, o VS Code e o MCP trazem todos o seu próprio. - O hostname é partilhado. Os tokens de início de sessão vivem no armazenamento do browser para esse endereço, que todas as aplicações nele podem ler, por isso um script injetado em qualquer uma delas chega às sessões do Archestack. Duas instâncias do Archestack num só hostname terminam a sessão dos utilizadores uma da outra.
- Não disponível desta forma: os endereços dos espaços de trabalho e os domínios
próprios das empresas, que precisam dos certificados a pedido do Caddy, e o Grafana, que o Ingress
não encaminha. Aceda ao Grafana com
kubectl port-forward.
# A cluster you already have, for one environment: copy this file to aks/environments/<env>/names.env
# and fill it in. The setup then leaves the cluster as it is, allows it to pull from the registry, and
# adds a subnet for the database to the cluster's network. Everything else is made as usual.
# The cluster, such as the one whose ingress already serves the hostname Archestack goes under.
AKS_RG=CHANGE-ME
AKS=CHANGE-ME
# Its network, which has to be one of your own (a cluster made with --vnet-subnet-id), and a free
# address range in it for the database's subnet.
VNET_RG=CHANGE-ME
VNET=CHANGE-ME
PG_SUBNET=CHANGE-ME # such as 10.30.16.0/24 2. Registry, cluster e base de dados
#!/usr/bin/env bash
# setup/1-cluster.sh: one environment's registry, network, Key Vault, database and cluster.
# Each part is made only when it is missing, so a run that stopped halfway can simply be run again,
# and a cluster you already have (aks/environments/<env>/names.env) is left as it is.
# ENVIRONMENT=prd bash setup/1-cluster.sh
# Smaller, for dev and test:
# NODE_COUNT=2 ENVIRONMENT=dev bash setup/1-cluster.sh
source "$(dirname "$0")/common.sh"
NODE_COUNT=${NODE_COUNT:-3}
NODE_SIZE=${NODE_SIZE:-Standard_D4s_v5}
PG_SKU=${PG_SKU:-Standard_D2ds_v5}
exists() { "$@" -o none 2>/dev/null; }
# One registry for every environment: a version is imported once and promoted as it is.
az group create -n "$ACR_RG" -l "$LOCATION" -o none
exists az acr show -n "$ACR" || az acr create -g "$ACR_RG" -n "$ACR" --sku Standard -o none
ACR_ID=$(az acr show -n "$ACR" --query id -o tsv)
# The network: a subnet for the cluster, and one delegated to the database.
az group create -n "$RG" -l "$LOCATION" -o none
exists az network vnet show -g "$VNET_RG" -n "$VNET" || az network vnet create -g "$VNET_RG" -n "$VNET" \
--address-prefixes 10.20.0.0/16 --subnet-name aks --subnet-prefixes 10.20.0.0/20 -o none
exists az network vnet subnet show -g "$VNET_RG" --vnet-name "$VNET" -n archestack-postgres \
|| az network vnet subnet create -g "$VNET_RG" --vnet-name "$VNET" -n archestack-postgres \
--address-prefixes "$PG_SUBNET" --delegations Microsoft.DBforPostgreSQL/flexibleServers -o none
# The vault, and the right to write to it: a vault with RBAC gives its creator no access to secrets.
exists az keyvault show -n "$KV" || az keyvault create -g "$RG" -n "$KV" -l "$LOCATION" --enable-rbac-authorization true -o none
az role assignment create --assignee "$(az ad signed-in-user show --query id -o tsv)" \
--role "Key Vault Secrets Officer" --scope "$(az keyvault show -n "$KV" --query id -o tsv)" -o none
# PostgreSQL 16, reachable from the network only. Its password is made here and goes straight
# into the vault, so nobody has to know it. No ';' in it: it goes into a connection string.
exists az postgres flexible-server show -g "$RG" -n "$PG" || {
PG_PASSWORD=$(openssl rand -base64 36 | tr -d '/+=;')
kv_set archestack-postgres-password "$PG_PASSWORD"
az postgres flexible-server create -g "$RG" -n "$PG" -l "$LOCATION" --version 16 \
--tier GeneralPurpose --sku-name "$PG_SKU" --storage-size 128 \
--admin-user archestack --admin-password "$PG_PASSWORD" \
--subnet "$(az network vnet subnet show -g "$VNET_RG" --vnet-name "$VNET" -n archestack-postgres --query id -o tsv)" \
--private-dns-zone "$PG.private.postgres.database.azure.com" --yes -o none
}
# The cluster, allowed to pull from the registry.
if exists az aks show -g "$AKS_RG" -n "$AKS"; then
az aks update -g "$AKS_RG" -n "$AKS" --attach-acr "$ACR_ID" -o none
else
az aks create -g "$AKS_RG" -n "$AKS" --tier standard \
--node-count "$NODE_COUNT" --node-vm-size "$NODE_SIZE" \
--network-plugin azure --network-plugin-mode overlay --pod-cidr 10.244.0.0/16 \
--vnet-subnet-id "$(az network vnet subnet show -g "$VNET_RG" --vnet-name "$VNET" -n aks --query id -o tsv)" \
--enable-managed-identity --generate-ssh-keys --attach-acr "$ACR_ID" -o none
fi
echo "$ENVIRONMENT: $AKS, $PG and $KV are ready."
echo "Next: ENVIRONMENT=$ENVIRONMENT bash setup/2-address.sh, or with a path of a hostname the"
echo "cluster already serves (way B in values.yaml): ENVIRONMENT=$ENVIRONMENT bash setup/3-pipeline.sh" Um só registry serve todos os ambientes, por isso uma versão é importada uma única vez e a produção corre exatamente a mesma imagem que a aceitação correu. A palavra-passe da base de dados é gerada pelo script e vai diretamente para o Key Vault: ninguém precisa de a conhecer. Capacidade e dimensionamento tem os valores medidos por trás dos tamanhos, que são um ponto de partida para produção.
Cada espaço de trabalho tem duas bases de dados. Um espaço de trabalho inativo não mantém nenhuma ligação, mas um servidor ocupado precisa de margem: o limite de 50 ligações do escalão Burstable é demasiado baixo. A API não precisa de extensões nem de um superutilizador, apenas de um utilizador admin que possa criar bases de dados.
3. Endereço público e DNS
#!/usr/bin/env bash
# setup/2-address.sh: the environment's static public address, for Caddy and your DNS. Not needed
# with a path of a hostname the cluster already serves (way B in values.yaml).
# ENVIRONMENT=prd bash setup/2-address.sh
source "$(dirname "$0")/common.sh"
# In the cluster's node resource group, because that is where its load balancer looks for it by name.
NODE_RG=$(az aks show -g "$AKS_RG" -n "$AKS" --query nodeResourceGroup -o tsv)
az network public-ip create -g "$NODE_RG" -n "$PIP" --sku Standard --allocation-method Static -o none
IP=$(az network public-ip show -g "$NODE_RG" -n "$PIP" --query ipAddress -o tsv)
echo "Point these DNS records at $IP, under the zone in aks/environments/$ENVIRONMENT/values.yaml:"
echo " editor api metrics *"
echo "Next: ENVIRONMENT=$ENVIRONMENT bash setup/3-pipeline.sh"
Apenas na forma A. Aponte editor, api e metrics sob a sua
zona para o endereço, mais um wildcard * para os endereços próprios dos espaços de
trabalho. O Caddy obtém os certificados do Let's Encrypt, por isso as portas 80 e 443 têm de estar
acessíveis a partir da internet.
4. Palavras-passe e a identidade do pipeline
#!/usr/bin/env bash
# setup/3-pipeline.sh: the SMTP password, the namespace, and the identity the pipeline deploys
# this environment as.
# ENVIRONMENT=prd bash setup/3-pipeline.sh
source "$(dirname "$0")/common.sh"
case "$PIPELINE" in
azure-devops) ;;
github | gitlab) [ -n "$REPO" ] || { echo "Fill in REPO in aks/settings.env" >&2; exit 1; } ;;
*) echo "Fill in PIPELINE in aks/settings.env: azure-devops, github or gitlab" >&2; exit 1 ;;
esac
# Asked for here, so it is never written to a file. Enter nothing to keep what the vault has,
# or when the instance sends no mail.
read -rsp "SMTP password for $ENVIRONMENT (Enter to skip): " SMTP_PASSWORD; echo
if [ -n "$SMTP_PASSWORD" ]; then kv_set archestack-smtp-password "$SMTP_PASSWORD"; fi
# The pipeline's identity, one per environment, and what it may do: sign in to the cluster,
# read the passwords, and import into the registry.
az identity create -g "$RG" -n "$IDENTITY" -l "$LOCATION" -o none
CLIENT_ID=$(az identity show -g "$RG" -n "$IDENTITY" --query clientId -o tsv)
PRINCIPAL_ID=$(az identity show -g "$RG" -n "$IDENTITY" --query principalId -o tsv)
AKS_ID=$(az aks show -g "$AKS_RG" -n "$AKS" --query id -o tsv)
grant() {
az role assignment create --assignee-object-id "$PRINCIPAL_ID" --assignee-principal-type ServicePrincipal \
--role "$1" --scope "$2" -o none
}
grant "Azure Kubernetes Service Cluster User Role" "$AKS_ID"
grant "Key Vault Secrets User" "$(az keyvault show -n "$KV" --query id -o tsv)"
grant Contributor "$(az acr show -n "$ACR" --query id -o tsv)"
# The namespace, made here by you, so the pipeline only ever needs rights inside it. A cluster
# with Entra ID sign-in checks those rights itself: Azure RBAC, or a Kubernetes RoleBinding.
kube_login
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - > /dev/null
ENTRA=$(az aks show -g "$AKS_RG" -n "$AKS" --query 'aadProfile != null' -o tsv)
AZURE_RBAC=$(az aks show -g "$AKS_RG" -n "$AKS" --query 'aadProfile.enableAzureRbac == `true`' -o tsv)
if [ "$AZURE_RBAC" = true ]; then
grant "Azure Kubernetes Service RBAC Writer" "$AKS_ID/namespaces/$NAMESPACE"
elif [ "$ENTRA" = true ]; then
kubectl create rolebinding archestack-pipeline -n "$NAMESPACE" --clusterrole=edit --user="$PRINCIPAL_ID" \
--dry-run=client -o yaml | kubectl apply -f - > /dev/null
fi
# No stored secret: the CI system hands the pipeline a token that says who it is, and the
# identity trusts only tokens with this issuer and subject.
federate() {
az identity federated-credential create -g "$RG" --identity-name "$IDENTITY" -n "$1" \
--issuer "$2" --subject "$3" --audiences "$4" -o none
}
TENANT_ID=$(az account show --query tenantId -o tsv)
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
case "$PIPELINE" in
github)
federate "github-$ENVIRONMENT" https://token.actions.githubusercontent.com \
"repo:$REPO:environment:archestack-$ENVIRONMENT" api://AzureADTokenExchange
echo "Done. In GitHub, create the environment archestack-$ENVIRONMENT, then run:"
echo " gh variable set AZURE_CLIENT_ID --env archestack-$ENVIRONMENT --body $CLIENT_ID --repo $REPO"
echo " gh variable set AZURE_TENANT_ID --body $TENANT_ID --repo $REPO"
echo " gh variable set AZURE_SUBSCRIPTION_ID --body $SUBSCRIPTION_ID --repo $REPO"
;;
gitlab)
federate "gitlab-$ENVIRONMENT" "$GITLAB_URL" \
"project_path:$REPO:ref_type:branch:ref:main" "$GITLAB_URL"
echo "Done. In GitLab, Settings, CI/CD, Variables, add:"
echo " AZURE_CLIENT_ID = $CLIENT_ID (environment scope: archestack-$ENVIRONMENT)"
echo " AZURE_TENANT_ID = $TENANT_ID (all environments)"
echo " AZURE_SUBSCRIPTION_ID = $SUBSCRIPTION_ID (all environments)"
;;
azure-devops)
if [ -n "${AZDO_ISSUER:-}" ]; then
federate "azure-devops-$ENVIRONMENT" "$AZDO_ISSUER" "$AZDO_SUBJECT" api://AzureADTokenExchange
echo "Done. sc-archestack-$ENVIRONMENT can deploy $ENVIRONMENT; verify it in Azure DevOps."
else
echo "In Azure DevOps, add a service connection sc-archestack-$ENVIRONMENT: Azure Resource Manager,"
echo "workload identity federation, set up manually, with this identity:"
echo " tenant $TENANT_ID, subscription $SUBSCRIPTION_ID, client id $CLIENT_ID"
echo "It then shows an issuer and a subject identifier. Run this again with them:"
echo " AZDO_ISSUER='<issuer>' AZDO_SUBJECT='<subject identifier>' ENVIRONMENT=$ENVIRONMENT bash setup/3-pipeline.sh"
fi
;;
esac - A palavra-passe SMTP é pedida e vai diretamente para o Key Vault. Uma instância que não envia email salta-a.
- O pipeline inicia sessão sem um segredo guardado. Cada ambiente tem uma identidade própria, que só confia no token que o seu sistema de CI entrega aos jobs desse ambiente. Pode iniciar sessão no cluster, ler as palavras-passe e importar para o registry, e nada mais.
- O namespace é criado por si, por isso o pipeline só precisa de permissões dentro dele. Um cluster com início de sessão via Entra ID verifica ele próprio essas permissões, através do Azure RBAC ou de um RoleBinding do Kubernetes; o script atribui aquele que o cluster usar.
- O script termina com o que há a definir no seu sistema de CI: variáveis do GitHub, variáveis do GitLab ou a service connection do Azure DevOps.
5. A primeira instalação
A primeira instalação é o script que o pipeline corre, iniciado à mão. Uma primeira execução do pipeline faz o mesmo.
# From the repository, after az login as the person who ran the setup.
bash aks/resolve-version.sh # writes the newest version to ./version
VERSION=$(cat version) ENVIRONMENT=prd bash aks/deploy.sh
kubectl -n archestack get deploy -o wide # every Deployment ready, and its image
# Grafana's password, generated on the first install and kept from then on
kubectl -n archestack get secret archestack-secrets -o jsonpath='{.data.admin-password}' | base64 -d Abra o builder de imediato e crie o primeiro admin. Até alguém o fazer, quem lá
chegar primeiro torna-se admin. Para não deixar intervalo nenhum, defina o primeiro admin à partida
com api.initialAdmin (ver o values.yaml do chart).
A release mais recente, automaticamente
O resolve-version.sh pede ao registry todas as tags das duas imagens e escolhe a versão
de que esta execução faz deploy:
#!/usr/bin/env bash
# aks/resolve-version.sh: turn "newest" into the version number this run deploys.
# ARCHESTACK_VERSION=newest the newest release
# ARCHESTACK_VERSION=1.4 the newest 1.4.x
# ARCHESTACK_VERSION=1.4.23 exactly that one
set -euo pipefail
export LC_ALL=C
SOURCE=${SOURCE_NAMESPACE:-nickpeelman}
WANT=${ARCHESTACK_VERSION:-newest}
OUT=${OUT_DIR:-.}
# Every major.minor.build tag of one image, from the registry API. The commit tags and
# "latest" do not match the pattern.
tags() {
local repo="$SOURCE/$1" token
token=$(curl -fsS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$repo:pull" \
| sed -nE 's/.*"token":"([^"]+)".*/\1/p')
curl -fsS -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/$repo/tags/list" \
| grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | sort -u
}
# A version counts only when both images carry it: a release build that stopped between
# the two pushes leaves one of them without it. Sorted as versions, so 1.0.10 beats 1.0.9.
versions=$(comm -12 <(tags omnicore-api) <(tags omnicore-web) | sort -V)
if [ "$WANT" = newest ]; then
pick=$(tail -n 1 <<<"$versions")
elif [[ $WANT =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
pick=$(grep -xF "$WANT" <<<"$versions" || true)
elif [[ $WANT =~ ^[0-9]+\.[0-9]+$ ]]; then
pick=$(grep -E "^${WANT//./\\.}\.[0-9]+$" <<<"$versions" | tail -n 1 || true)
else
echo "Version must be newest, major.minor or major.minor.build, not '$WANT'" >&2
exit 1
fi
[ -n "$pick" ] || { echo "No release of $SOURCE/omnicore-api and omnicore-web matches '$WANT'" >&2; exit 1; }
# The version for the next steps: a file, and whatever the CI system passes between jobs.
echo "Deploying Archestack $pick"
mkdir -p "$OUT"
echo "$pick" > "$OUT/version"
if [ -n "${TF_BUILD:-}" ]; then echo "##vso[build.updatebuildnumber]$pick"; fi
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "version=$pick" >> "$GITHUB_OUTPUT"
echo "Deploying Archestack **$pick**" >> "$GITHUB_STEP_SUMMARY"
fi
if [ -n "${GITLAB_CI:-}" ]; then echo "VERSION=$pick" > "$OUT/version.env"; fi - As versões são comparadas como números, por isso
1.0.10é mais recente do que1.0.9, e um número em falta é saltado em vez de se esperar por ele. - Só conta uma versão que as duas imagens tenham. Nunca se faz deploy de metade de uma release cuja build parou entre o push da API e o push da imagem web.
- Uma versão exata também é verificada. Uma versão que as imagens não tenham faz parar a execução antes de se tocar em alguma coisa.
- A versão segue da maneira como cada sistema de CI passa valores adiante: o Azure
DevOps dá à execução o nome da versão, o GitHub Actions torna-a um output do job e mostra-a no
resumo da execução, e o GitLab CI passa-a aos jobs seguintes como
$VERSION.
Depois, o deploy.sh leva um ambiente para essa versão:
#!/usr/bin/env bash
# aks/deploy.sh: copy one Archestack version into your registry and roll it out with Helm.
# Every pipeline runs it once per environment, signed in to Azure. By hand, after az login:
# VERSION=1.4.23 ENVIRONMENT=prd bash aks/deploy.sh
set -euo pipefail
cd "$(dirname "$0")/.."
: "${VERSION:?}" "${ENVIRONMENT:?}"
source aks/environment.sh
SOURCE=${SOURCE_NAMESPACE:-nickpeelman}
VALUES="aks/environments/$ENVIRONMENT/values.yaml"
if grep -v '^[[:space:]]*#' "$VALUES" | grep -q CHANGE-ME; then echo "Fill in every CHANGE-ME in $VALUES first" >&2; exit 1; fi
# Keeps a value out of the pipeline's log. GitLab has no such command; nothing here prints one.
mask() {
if [ -n "${TF_BUILD:-}" ]; then echo "##vso[task.setsecret]$1"; fi
if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::add-mask::$1"; fi
}
# 1. The two images, imported from Docker Hub into your registry. --force makes a rerun harmless.
for image in omnicore-api omnicore-web; do
az acr import --name "$ACR" --force \
--source "docker.io/$SOURCE/$image:$VERSION" --image "$image:$VERSION"
done
# 2. The passwords, from Key Vault into files only this job can read. They go in on every
# upgrade: the chart writes the SMTP password from the values each time.
secret() { az keyvault secret show --vault-name "$KV" -n "$1" --query value -o tsv 2>/dev/null || true; }
pg=$(secret archestack-postgres-password)
smtp=$(secret archestack-smtp-password)
[ -n "$pg" ] || { echo "Cannot read archestack-postgres-password from $KV (missing, or no Key Vault Secrets User)" >&2; exit 1; }
for value in "$pg" "$smtp"; do
if [ -n "$value" ]; then mask "$value"; fi
done
umask 077
secrets=$(mktemp -d)
trap 'rm -rf "$secrets"' EXIT
printf '%s' "$pg" > "$secrets/postgres"
printf '%s' "$smtp" > "$secrets/smtp"
# 3. The rollout, into the namespace setup/3-pipeline.sh made.
kube_login
helm upgrade --install archestack infra/helm/archestack \
--namespace "$NAMESPACE" \
-f "$VALUES" \
--set-file postgres.password="$secrets/postgres" \
--set-file smtp.password="$secrets/smtp" \
--set-string postgres.host="$PG.postgres.database.azure.com" \
--set-string 'caddy.service.annotations.service\.beta\.kubernetes\.io/azure-pip-name'="$PIP" \
--set-string api.image.repository="$ACR.azurecr.io/omnicore-api" \
--set-string api.image.tag="$VERSION" \
--set-string web.image.repository="$ACR.azurecr.io/omnicore-web" \
--set-string web.image.tag="$VERSION" \
--wait --timeout 30m - As imagens entram primeiro no seu registry. Os nós fazem pull a partir do Azure,
dentro da região, por isso acrescentar ou substituir um nó nunca fica à espera do Docker Hub nem
dos seus limites de pulls anónimos. Se uma importação for recusada por causa desses limites, dê ao
az acr importuma conta do Docker Hub com--usernamee--password. - As palavras-passe entram em cada atualização. O chart escreve a palavra-passe SMTP a partir dos valores de cada vez, por isso uma atualização sem ela deixa a instância sem conseguir enviar email. Viajam em ficheiros que só o job consegue ler, e o Azure DevOps e o GitHub Actions mascaram-nas no log.
-
--wait --timeout 30mespera até cada Deployment estar pronto. A API migra todos os espaços de trabalho antes de responder, por isso uma versão com migrações demora algum tempo numa instância com muitos espaços de trabalho. - Sem
--atomic. Um rollout falhado fica à espera de ser analisado em vez de ser desfeito, porque a nova versão pode já ter migrado as bases de dados. Entretanto, os pods antigos continuam a servir.
O pipeline
O kit tem o mesmo pipeline para três sistemas de CI. Cada um corre os dois scripts acima, pela mesma ordem:
Archestack publishes 1.4.23 omnicore-api and omnicore-web, tagged 1.4.23, <commit> and latest
03:00, the nightly run
resolve newest -> 1.4.23 the highest number both images carry
dev import 1.4.23 into your registry, helm upgrade, wait until ready
tst the same 1.4.23
a scheduled run stops here
a run started by hand, version 1.4.23
dev, tst already on 1.4.23, nothing rolls out
acc waits for its approval, then 1.4.23
prd waits for its approval, then 1.4.23 - Todas as noites, o agendamento leva a release mais recente a desenvolvimento e teste.
- Uma execução agendada termina em teste. A aceitação e a produção só avançam numa
execução iniciada à mão. Indique-lhe a versão que corre em teste, como
1.4.23, e a aceitação e a produção recebem exatamente o que foi testado. Deixada emnewest, leva o que for mais recente nesse momento. - Uma versão por execução. A versão é resolvida uma única vez, antes do primeiro ambiente, por isso uma release publicada enquanto uma execução espera por uma aprovação não se infiltra na produção.
- Um ambiente que já está na versão não faz rollout de nada. A especificação dos
pods não muda, por isso o Kubernetes deixa-os a correr. Uma alteração ao seu
values.yamlcontinua a ser aplicada.
Azure DevOps
# aks/azure-pipelines.yml: the newest Archestack release to dev and tst every night,
# to acc and prd on a run started by hand, once each is approved.
trigger: none
schedules:
- cron: "0 3 * * *" # 03:00 UTC
displayName: Nightly, newest release to dev and tst
branches:
include: [main]
always: true # a release is a new image, not a commit in this repository
parameters:
- name: version
displayName: Archestack version (newest, 1.4 for the newest 1.4.x, or 1.4.23)
type: string
default: newest
- name: environments
displayName: Environments, in order
type: object
default: [dev, tst, acc, prd]
pool:
vmImage: ubuntu-latest
stages:
# Once per run, so every environment gets the same number even when a release
# is published while the run waits for an approval.
- stage: resolve
displayName: Resolve the version
jobs:
- job: resolve
steps:
- script: bash aks/resolve-version.sh
displayName: Newest version both images carry
env:
ARCHESTACK_VERSION: ${{ parameters.version }}
OUT_DIR: $(Build.ArtifactStagingDirectory)
- publish: $(Build.ArtifactStagingDirectory)
artifact: version
- ${{ each env in parameters.environments }}:
- stage: deploy_${{ env }}
displayName: Deploy ${{ env }}
${{ if in(env, 'acc', 'prd') }}:
# A scheduled run stops after tst.
condition: and(succeeded(), ne(variables['Build.Reason'], 'Schedule'))
jobs:
- deployment: deploy
environment: archestack-${{ env }}
strategy:
runOnce:
deploy:
steps:
- checkout: self
- download: current
artifact: version
- task: AzureCLI@2
displayName: Import the images, then helm upgrade
inputs:
azureSubscription: sc-archestack-${{ env }}
scriptType: bash
scriptLocation: inlineScript
workingDirectory: $(Build.SourcesDirectory)
inlineScript: |
export VERSION=$(cat "$PIPELINE_WORKSPACE/version/version")
ENVIRONMENT=${{ env }} bash aks/deploy.sh - As service connections chamam-se
sc-archestack-<env>, uma por ambiente, cada uma com a identidade desse ambiente. Osetup/3-pipeline.shimprime o que preencher e recebe o issuer e o subject que a service connection mostra. - Aprovações. Em Pipelines, Environments, acrescente uma verificação Approvals a
archestack-accearchestack-prd, e uma verificação Exclusive lock aos quatro, para que duas execuções nunca façam rollout para o mesmo ambiente ao mesmo tempo. -
always: truefaz o agendamento correr sem um novo commit, porque uma release é uma nova imagem, não uma alteração neste repositório.
GitHub Actions
# .github/workflows/archestack.yml: the newest Archestack release to dev and tst every night,
# to acc and prd on a run started by hand, once each is approved.
name: Archestack on AKS
on:
schedule:
- cron: "0 3 * * *" # 03:00 UTC
workflow_dispatch:
inputs:
version:
description: Archestack version (newest, 1.4 for the newest 1.4.x, or 1.4.23)
default: newest
permissions:
id-token: write # sign in to Azure with a token, no stored secret
contents: read
jobs:
# Once per run, so every environment gets the same number even when a release
# is published while the run waits for an approval.
resolve:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.pick.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: pick
run: bash aks/resolve-version.sh
env:
ARCHESTACK_VERSION: ${{ inputs.version || 'newest' }}
OUT_DIR: ${{ runner.temp }}
dev:
needs: resolve
uses: ./.github/workflows/archestack-deploy.yml
with: { environment: dev, version: "${{ needs.resolve.outputs.version }}" }
tst:
needs: [resolve, dev]
uses: ./.github/workflows/archestack-deploy.yml
with: { environment: tst, version: "${{ needs.resolve.outputs.version }}" }
# A scheduled run stops after tst.
acc:
if: github.event_name != 'schedule'
needs: [resolve, tst]
uses: ./.github/workflows/archestack-deploy.yml
with: { environment: acc, version: "${{ needs.resolve.outputs.version }}" }
prd:
if: github.event_name != 'schedule'
needs: [resolve, acc]
uses: ./.github/workflows/archestack-deploy.yml
with: { environment: prd, version: "${{ needs.resolve.outputs.version }}" } O deploy de cada ambiente é feito pelo mesmo workflow reutilizável:
# .github/workflows/archestack-deploy.yml: one environment to one version, called by archestack.yml.
name: Archestack deploy
on:
workflow_call:
inputs:
environment: { type: string, required: true }
version: { type: string, required: true }
jobs:
deploy:
runs-on: ubuntu-latest
environment: archestack-${{ inputs.environment }}
concurrency: archestack-${{ inputs.environment }} # one rollout per environment at a time
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }} # this environment's identity
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- run: bash aks/deploy.sh
env:
VERSION: ${{ inputs.version }}
ENVIRONMENT: ${{ inputs.environment }} - Ambientes. Crie
archestack-dev,-tst,-acce-prdem Settings, Environments, e acrescente revisores obrigatórios à aceitação e à produção. Num repositório privado, os ambientes precisam do GitHub Pro, Team ou Enterprise. - Início de sessão. O
azure/logintroca o token do job pela identidade do ambiente, cujo client id é uma variável do ambiente para a qual osetup/3-pipeline.shimprime os comandosgh. A identidade só confia em jobs do seu próprio ambiente, por isso um job de teste nunca pode fazer deploy para produção. -
concurrencyimpede que duas execuções façam rollout para o mesmo ambiente ao mesmo tempo. - O agendamento só corre a partir da branch predefinida, e num repositório público o GitHub desativa-o ao fim de 60 dias sem atividade no repositório.
GitLab CI
# .gitlab-ci.yml: the newest Archestack release to dev and tst on the nightly schedule,
# to acc and prd from a pipeline started by hand, each when a person runs its job.
variables:
ARCHESTACK_VERSION:
value: newest
description: Archestack version (newest, 1.4 for the newest 1.4.x, or 1.4.23)
# Pipelines run on the schedule (Build, Pipeline schedules) and when started by hand, never on a push.
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"
stages: [resolve, dev, tst, acc, prd]
default:
image: mcr.microsoft.com/azure-cli:2.90.0
before_script:
- tdnf install -y curl tar gzip > /dev/null
# Once per pipeline, so every environment gets the same number. Later jobs read it as $VERSION.
resolve:
stage: resolve
script:
- bash aks/resolve-version.sh
artifacts:
reports:
dotenv: version.env
.deploy:
id_tokens:
AZURE_TOKEN:
aud: https://gitlab.com # GITLAB_URL from aks/settings.env, on a self-managed instance
before_script:
- tdnf install -y curl tar gzip > /dev/null
- curl -fsSL https://get.helm.sh/helm-v3.16.2-linux-amd64.tar.gz | tar -xz -C /tmp
- mv /tmp/linux-amd64/helm /usr/local/bin/helm
- az login --service-principal -u "$AZURE_CLIENT_ID" -t "$AZURE_TENANT_ID" --federated-token "$AZURE_TOKEN" -o none
- az account set -s "$AZURE_SUBSCRIPTION_ID"
script:
- bash aks/deploy.sh
dev:
extends: .deploy
stage: dev
environment: archestack-dev
resource_group: archestack-dev # one rollout per environment at a time
variables: { ENVIRONMENT: dev }
tst:
extends: .deploy
stage: tst
environment: archestack-tst
resource_group: archestack-tst
variables: { ENVIRONMENT: tst }
# A scheduled pipeline stops after tst. acc and prd wait until a person runs them, in order.
.by-hand:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- when: manual
allow_failure: false # prd stays blocked until acc has run
acc:
extends: [.deploy, .by-hand]
stage: acc
environment: archestack-acc
resource_group: archestack-acc
variables: { ENVIRONMENT: acc }
prd:
extends: [.deploy, .by-hand]
stage: prd
environment: archestack-prd
resource_group: archestack-prd
variables: { ENVIRONMENT: prd } - O agendamento vive no GitLab e não no ficheiro: em Build, Pipeline schedules,
acrescente um para
maincom0 3 * * *. Um push não inicia nada. - A aceitação e a produção são jobs manuais que bloqueiam o pipeline: a produção só pode ser iniciada depois de a aceitação ter corrido. No GitLab Premium, proteja também esses ambientes e exija aprovações; um job aprovado continua a ser iniciado à mão.
- Início de sessão. O ID token do job é trocado pela identidade do ambiente, cujo
client id é uma variável limitada a esse ambiente. O token do GitLab indica a branch, não o
ambiente, por isso proteja a
main. Numa instância self-managed, defina o seu endereço comoGITLAB_URLe como oauddo token. -
resource_groupimpede que dois pipelines façam rollout para o mesmo ambiente ao mesmo tempo. - A imagem está fixada, e o Helm é descarregado numa versão fixada. Atualize-os de propósito.
Atualizações sem tempo de inatividade
- Um novo pod da API de cada vez. O chart acrescenta um pod antes de remover um
(
maxSurge: 1,maxUnavailable: 0), e um pod antigo só sai quando um novo estiver pronto. - O arranque é feito à vez. As réplicas que arrancam ao mesmo tempo preparam as bases de dados uma após a outra, sob um bloqueio, por isso duas delas nunca migram ao mesmo tempo. Uma startup probe dá a cada pod 15 minutos antes de o Kubernetes desistir dele.
- As atualizações dos nós mantêm a API disponível. Um PodDisruptionBudget mantém pelo menos um pod da API a correr enquanto o AKS esvazia um nó.
- O Caddy só reinicia quando a sua própria configuração muda, como com um novo hostname. É um só pod, com os certificados num disco próprio, por isso esse reinício é uma interrupção curta. Uma atualização do Archestack não lhe toca.
Voltar atrás
Corra o pipeline com a versão anterior, como 1.4.22, ou faça rollback da release com o
Helm:
helm history archestack -n archestack # the revisions, newest last
helm rollback archestack -n archestack --wait --timeout 30m # back to the one before Em qualquer dos casos, as bases de dados ficam como a versão mais nova as deixou: migrou-as quando arrancou, e uma versão mais antiga não desfaz isso. Voltar atrás corre código mais antigo sobre um schema mais recente, por isso prefira a release seguinte que corrija o problema. Levar também o schema de volta significa restaurar o servidor com o point-in-time restore do Flexible Server.
Armadilhas
- O chart vem do seu repositório, as imagens vêm do registry. O pipeline leva as imagens mais recentes por si só, nunca um chart mais recente. Quando passar para uma nova versão major ou minor, vá buscar de novo o chart ao repositório do Archestack.
- As imagens de observabilidade não têm número de versão. Têm como tags um commit e
latest, vêm do Docker Hub e seguemobservability.imageTag. Defina-o com uma das tags de commit delas para as manter fixas. - Uma release por namespace. O Grafana e o Tempo encontram o Prometheus, o Tempo e o Loki por nomes de serviço fixos.
- Atrás do PgBouncer (a porta 6432 do Flexible Server), defina
postgres.clusterConnectionStringcom uma ligação direta na 5432. As réplicas falam entre si por uma ligação PostgreSQL que tem de ficar aberta. - Os volumes e o próprio Secret do chart sobrevivem a
helm uninstall. Apague-os à mão quando for mesmo essa a intenção.
Para onde seguir a partir daqui
Capacidade e dimensionamento tem os valores medidos para dimensionar o cluster e a base de dados. Espaços de trabalho e alojamento explica como uma instância serve muitos espaços de trabalho. Assim que a instância estiver a correr, Estratégia de deployment trata de fazer deploy da sua aplicação para os espaços de trabalho dela a partir de um repositório.