Build it with your AI coding assistant
This guide is written so an assistant that has the Ankra CLI and Ankra’s agent skills can carry it out for you, one step at a time, while you review. The example below uses Claude Code; the same steps work for Cursor, Codex, Copilot, Windsurf and Gemini CLI, see Agent Skills.Install the CLI and sign in
Give the assistant the Ankra skills
ankra-* skills - platform principles, cloud clusters, stacks and add-ons, SOPS, backups, alerts, observability, domains and DNS, security - so the assistant knows the CLI, the YAML shapes and the order of operations in this guide. The hook pauses it before destructive commands and keeps it on ankra instead of kubectl or helm.Hand it this page
Review, then let it continue
ankra cluster operations list that reads up, a curl that returns the right status. Approve the draft, tell the assistant to go on, and finish with the Verify list at the end of this page.Two things it must never do on its own, and the skills tell it so: apply to the production cluster without a draft you have seen, and commit a secret in plaintext.Hub and spokes: the infrastructure cluster receives, the application clusters send
What you build
create command for Hetzner, Scaleway or any other Ankra Managed provider and the stacks are unchanged, apart from the storage class names.Prerequisites
- An Ankra organisation where you are an admin, and the CLI logged in.
- A cloud credential and an SSH key credential for the provider you build on (UpCloud credentials).
- A Git credential with write access to the GitOps repository (Git credentials).
- A domain you control, for example
example.com, and a DNS credential Ankra can publish records with (custom DNS zones covers the Avura route;ankra org dns credentials createstores an external-dns webhook credential for any other provider). - A Microsoft 365 (Entra ID) tenant for staff sign-in. Any OpenID Connect provider Kratos supports works the same way.
- A Microsoft Teams or Slack webhook for the on-call channel.
- The charts come from registries Ankra already ships in its catalogue for every organisation:
prometheus-community,grafana,opensearch-k8s-operator,fluent,oauth2-proxy,cloudnative-pg,harborandoneuptime. The one exception is Ory’s chart repository for Kratos and Hydra, connected once:
Step 1: Create the clusters
Three clusters, created with the GitOps flags so Ankra commits their generated stacks to your repository from the first minute. The control plane count is the one choice you cannot change later: three for anything that must survive losing a node.Store the credentials
Create the infrastructure cluster
--include-networking and --include-dns are on by default: the cluster arrives with Traefik behind a cloud load balancer, cert-manager, a letsencrypt-prod ClusterIssuer and external-dns for its generated subdomain under ankra.cc.Create the production cluster
Create the development cluster
Wait for online, then look at the repository
create submits and returns. Watch the cluster list until all three read online, then pull the repository. Ankra has written one directory per cluster:spec.stacks in that cluster’s YAML, with its values and manifests next to it. See GitOps for how Ankra and your commits share the directory.Step 2: Organisation-wide DNS, alerting and vaults
These are declared once and inherited by every cluster, including clusters you create next year.Serve your own zone from every cluster
ankra.cc subdomain. For example.com, store a DNS credential and declare the zone for the organisation. Ankra renders a second external-dns per cluster, scoped to exactly that zone, with its own record ownership.external-dns.alpha.kubernetes.io/hostname: metrics.example.com gets its record, and cert-manager.io/cluster-issuer: letsencrypt-prod gets its certificate.One destination, two routes
ankra alerts routes preview --kind gitops_sync_failed --severity warning --cluster-id <cluster-id> shows which destinations a notification would reach before one happens. Notification routing explains the evaluation order.One backup vault per environment
Step 3: Infrastructure cluster - storage and secrets
Two small stacks that everything else depends on. They go intoclusters/infrastructure-<short-id>/infrastructure.yaml under spec.stacks.
A storage class that keeps its disks
reclaimPolicy: Retain, and a snapshot class the backup data plane can use.provisioner, driver and parameters to that CSI driver’s values.Pre-created Secrets, sealed
ENC[...]:--all-data seals every key under data and stringData of a Secret. For a value inside Helm values, ankra cluster encrypt addon <name> --key <key> -f <cluster.yaml> does the same. SOPS Encryption covers rotation and glob keys.Step 4: Infrastructure cluster - central metrics with Prometheus, Grafana and Tempo
One Prometheus holds every cluster’s series. The hub scrapes itself; the spokes remote-write into it through a public endpoint that accepts exactly one path from exactly their addresses. Grafana queries only this Prometheus, so a dashboard with acluster drop-down shows the whole estate.
kube-prometheus-stack as the receiver
enableRemoteWriteReceiver opens /api/v1/write. The default scrapeClass stamps cluster=infrastructure on the hub’s own scrapes so they sit next to the spokes’ series, which carry their own cluster external label. group_by includes cluster so one noisy spoke does not fold into another’s alert. Every rule in this Prometheus now evaluates the spokes’ series too, so their alerts reach the channel without any Alertmanager on the spokes.Seal the webhook before committing: ankra cluster encrypt addon kube-prometheus-stack --key webhook_url -f <cluster.yaml>. Alertmanager is the one place a Slack receiver is slack_configs with api_url instead; everything else is identical.Grafana with Prometheus and Tempo data sources
auth.proxy out until that stack exists, and the local admin from grafana-admin is the only account.Tempo for traces
tempo.tempo.svc.cluster.local:4317 from inside the hub, or through an Ingress on traces-ingest.example.com built the same way as the metrics endpoint below.The metrics-ingest endpoint
ports.websecure.proxyProtocol.trustedIPs); on a provider whose load balancer passes X-Forwarded-For instead, the equivalent is forwardedHeaders.trustedIPs.A spoke’s egress address is its NAT gateway. Read it from inside the spoke with ankra cluster exec <any-pod> -n <namespace> -- curl -s https://ifconfig.me, and expect to update the list when a cluster’s network is recreated.Declare the stack
kind and name pair; the shorthand - addon: tempo is silently dropped.Make it the cluster's metrics source
metrics-query.example.com with a basicAuth middleware, register that username and password as a metrics credential in Ankra, and set each spoke’s prometheus_metrics to that endpoint with credential_name and verify_ssl: true. Prometheus covers the credential and the endpoints Ankra accepts.Step 5: Infrastructure cluster - central logs with OpenSearch
The OpenSearch operator runs a three-node cluster with Dashboards. Fluent Bit on the hub ships the hub’s own logs; the spokes ship theirs through alogs-ingest endpoint that exposes exactly /_bulk to allow-listed addresses, each spoke with its own user that can write only its own index prefix.
The OpenSearch cluster
infra-secrets instead of letting the operator generate one is what lets Fluent Bit on the hub read it from the same Secret. The operator defaults securityConfigSecret to an empty name with its own field manager, so this manifest is declared with force: true in the stack, or server-side apply conflicts on it.Retention and the application log index template
app as a flat_object, so two applications that use the same key with different types never reject each other’s documents.minIndexAge later applies to new indices on its own; existing ones keep the version they were born with until they age out.One user per sending cluster
OpenSearchUser resources whose password comes from the sealed opensearch-log-shipper-production and -development Secrets in infra-secrets; the same values are what the spokes’ shippers are given in Step 8. A 401 on a spoke means those two copies differ.The logs-ingest endpoint
insecureSkipVerify./_bulk is routed. _search, _cat, _security and the rest of the REST API are not reachable through this host at all; Dashboards on logs.example.com is the only human entrance, and it is gated in Step 6.Fluent Bit for the hub's own logs
Declare the stack and the log source
Step 6: Infrastructure cluster - a staff sign-in realm with Kratos and Hydra
Grafana, OpenSearch Dashboards, Harbor and OneUptime each have their own login. Instead of four sets of local accounts, the hub runs one staff identity realm: Ory Kratos, with Microsoft 365 as its only sign-in method, pinned to your Entra tenant; Ory Hydra, which turns a Kratos session into OpenID Connect tokens for each tool; and oauth2-proxy, a forward-auth gate that any Ingress opts into with one annotation. Disabling someone in Entra ends their next sign-in everywhere, because Kratos holds no password, passkey or code of its own that could outlive it.kubectl access is a cluster access grant. The three are separate on purpose: a person removed from one is not implicitly removed from the others, so remove them from all three.A database for the realm
cloudnative-pg operator can be shared with Harbor’s database in Step 7.Kratos: one sign-in method, pinned to your tenant
microsoft_tenant), the schema’s email pattern (which also keeps B2B guests of the tenant out), and each tool’s own domain check. The identity mapper is a small Jsonnet document that reads email, falling back to preferred_username and upn because Entra omits email for many directory members, lower-cases it, and records it as a verified address:verified_addresses is what makes the consent step emit email_verified=true; without it oauth2-proxy refuses the sign-in. Validate any change to the mapper with jsonnet before merging: Microsoft is the only method, so a mapper error is a total lockout.In Entra, register a single-tenant Web application whose redirect URI is exactly https://staff-login.example.com/self-service/methods/oidc/callback/microsoft, with delegated openid profile email and admin consent. Until that URI is registered Microsoft answers every sign-in with AADSTS50011. Seal the secret: ankra cluster encrypt addon kratos --key client_secret -f <cluster.yaml>.Hydra: tokens for the tools
iss claim and oauth2-proxy compares it byte for byte with its --oidc-issuer-url. And allow_termination_from must contain the pod CIDR, or Hydra silently emits http:// redirect and issuer URLs behind Traefik.Each tool is an OAuth2Client resource that hydra-maester registers; its secret is read from a sealed Secret in the tool’s own namespace. Keep skipConsent: false on every client: the consent app is what injects the email claim into the id_token.oauth2-proxy: the gate
.example.com, so one sign-in covers every gated host for eight hours, then Microsoft again. The gate itself is a Traefik middleware:Protect the header
X-Auth-Request-Email as the signed-in user. Only Traefik may present it, so a NetworkPolicy lets only the traefik and prometheus-stack namespaces reach the Grafana pods. Without it, any pod in the cluster could forge the header and become any user.Route the realm and declare the stack
/oauth2, /userinfo, /.well-known/openid-configuration, /.well-known/jwks.json), Kratos’s public endpoints (/self-service, /sessions, /schemas, /.well-known/ory) and the login UI (everything else) share staff-login.example.com as three Ingresses with Traefik router priorities. Neither admin API is routed: Kratos’s admin API has no authentication of its own, and exposing it is publishing the staff directory.Step 7: Infrastructure cluster - registry and uptime
Two more stacks follow the same pattern, so only what is different is shown.Harbor on CloudNativePG and Redis
core.extraEnvVars as CONFIG_OVERWRITE_JSON, read from a sealed Secret, with the Hydra client harbor registered by an OAuth2Client next to it:auth_mode, so CI pushes and Ankra’s pulls are unaffected. Once Harbor is up, register it as your application registry so Ankra mints a push robot per application.Add a PrometheusRule beside it. The infrastructure Prometheus evaluates it, so the alert reaches the same channel as everything else:OneUptime behind the gate
sso-gateway-sso@kubernetescrd middleware, and its own login is Global OIDC against the staff Hydra, so staff get in with Microsoft 365 and nobody else sees the page. Its Postgres is another CloudNativePG cluster, two instances, on the retain storage class.A status page meant for customers needs an un-gated host of its own; everything under the gated host is staff-only by construction.Step 8: Production and development clusters - send metrics and logs
The spokes carry no Grafana, no Alertmanager, no rules and no queryable Prometheus. Two small stacks make each of them a sender. Add the following toclusters/production-<short-id>/production.yaml, then repeat for development with the names swapped.
Prometheus in agent mode
agentMode on and the hub as the remote-write target. The cluster external label is what tells this cluster’s series apart in the central store; keep it equal to the Ankra cluster name.ServiceMonitor and PodMonitor an application ships is picked up (the NilUsesHelmValues: false lines), so adding an exporter to an application is enough for it to appear in Grafana on the hub.Fluent Bit shipping to the hub
infra-secrets stack in Step 3.production-YYYY.MM.DD, one per cluster per day, filtered by kubernetes.namespace_name or cluster. Structured JSON lines land under app.*; anything else stays in log.Add the spoke's egress address on the hub
metrics-ingest-allowlist and logs-ingest-allowlist in the hub’s cluster file, and apply. Until then every write from the spoke is a 403 in the agent’s and Fluent Bit’s logs.Application databases on the spokes
Step 9: Back up every stateful stack
A vault is where restore points land; a stack’s backup policy is what puts them there. Protection is a property of the stack itself: the command writes abackup block onto the stack definition, and the platform installs the backup data plane (Velero and the CloudNativePG Barman plugin) on the cluster the first time a stack on it is protected.
See what a backup would carry
Protect each stateful stack, staggered
Backup stack: installing until Velero is actually on the cluster; a capture started before then waits rather than failing. The policy lands in the cluster file as a backup block, so from here on it is reviewed and versioned like everything else:development-backups. The hub’s own stateful stacks - sso-gateway, registry, uptime - are protected the same way into a third vault, or into production-backups if you prefer fewer buckets.Read the posture
protected, unprotected (no_data_assets) for stacks with nothing to carry, and unprotected (no_policy) for the ones you still owe a policy. The last line of each row says when the last run succeeded or failed and when the next fires. A stack that holds data and reads no_policy is the thing to fix before calling the estate production.Prove a restore point can be read
get shows the manifest, the assets it carries and the run that produced it. A restore point whose status is failed carries nothing; find the run under ankra runs list --kind backup --cluster production and fix the cause before the next schedule fires. Backups and Restore Points covers restoring in place and cloning a stack with its data onto another cluster.Step 10: Security and access
Most of this is already in place from Step 1. What remains is turning it on where it is off by default, and deciding who reaches what.Security baseline on every cluster
ankra-security-baseline stack: Trivy Operator for image vulnerabilities, configuration audits and the CIS benchmark, and Pod Security Standards enforced by the API server from namespace labels the agent manages. It starts in Audit mode. Once the Security Center shows no workloads failing the baseline standard, enforce it:ankra-policy-engine profile for image-signature verification and policies of your own. Cluster Security describes what runs where.Kubernetes access through the gateway, time-boxed
ankra cluster access elevate --cluster production --role edit --expires 4h --reason "incident 4711" gives the caller themselves time-boxed access within that policy, and the grant is in the audit log.Credentials that can do one thing
- The DNS credential can edit one zone.
- The Git credential can write one repository.
- Each log shipper user can create and bulk-write one index prefix.
- The metrics-ingest endpoint can append samples and nothing else.
- Harbor robot accounts are minted per application by Ankra and can push one project.
- The backup vault keys are held by Ankra and never printed.
Everything sensitive is sealed before it is committed
ankra cluster sops-config once to confirm the organisation has its AGE key, then make a rule of it: no stringData in a commit, no webhook URL in values. The pre-push check is the simplest one there is:Verify
Run through this once at the end, and again after any change to the hub.Every stack converged
up; sso-postgres x2, hydra, kratos, the UI and oauth2-proxy are Running; three OpenSearch nodes and two Dashboards are Ready.The spokes' series have arrived
cluster label problem; the spoke’s Prometheus log shows which.The ingest endpoints refuse everyone else
403, 403, 404. The third line is the path restriction: the REST API is not reachable through that host at all.The realm answers
https://staff-login.example.com/, trailing slash included. Then in a browser, from https://metrics.example.com/: the Microsoft 365 sign-in, and Grafana with your address in the top right. Start a sign-in from a tool, never from staff-login itself.Logs are searchable from Ankra
cluster:production. Rows appear with kubernetes.namespace_name and app.* fields. In OpenSearch Dashboards the same query runs against production-*.Backups are on and recent
no_policy, every last run is succeeded, and both vaults read ready.An alert reaches the channel
Watchdog route for a minute in the hub’s values and watch Alertmanager’s own Watchdog arrive the same way - that proves the in-cluster path end to end.Troubleshooting
Remote write or log shipping returns 403
Remote write or log shipping returns 403
metrics-ingest-allowlist and logs-ingest-allowlist in the hub’s cluster file, and apply. Fluent Bit replays its filesystem buffer once the 403s stop; Prometheus’s remote-write queue holds roughly an hour before it drops samples.Log shipping returns 401
Log shipping returns 401
central-logs-opensearch Secret and the hub’s opensearch-log-shipper-<cluster> Secret differ. Decrypt both with ankra cluster decrypt manifest <name> -f <cluster.yaml> and compare. The shipper users are also briefly absent for up to 30 seconds after every security config push while the operator re-creates them; a burst of 401s right after a commit to opensearch-securityconfig is that, and clears on its own.A dependency edge was ignored and something deployed before its parent
A dependency edge was ignored and something deployed before its parent
- addon: name parses to an empty parent and is dropped without an error. Every parent is kind and name on two lines. Confirm what was stored with ankra cluster stacks list <stack> -o json, which echoes each resource’s real parents.OpenSearch Dashboards will not start
OpenSearch Dashboards will not start
staff-login.example.com is unreachable, password form included. Fix the realm first. The REST API with the admin basic credentials still works in the meantime through ankra cluster exec on an OpenSearch pod.Hydra issues http:// redirect URLs, or oauth2-proxy says the issuer does not match
Hydra issues http:// redirect URLs, or oauth2-proxy says the issuer does not match
allow_termination_from must contain the pod CIDR, or Hydra does not trust X-Forwarded-Proto from Traefik and emits http:// everywhere. And the issuer string in Hydra’s values and oauth2-proxy’s --oidc-issuer-url must be identical, trailing slash included.Microsoft answers AADSTS50011
Microsoft answers AADSTS50011
https://staff-login.example.com/self-service/methods/oidc/callback/microsoft is not registered on the Entra application, platform Web. Keep pkce: never in the Kratos provider: with PKCE on, Kratos switches to the provider-less callback path and the registered URI no longer matches.Alertmanager fires but nothing reaches the channel
Alertmanager fires but nothing reaches the channel
webhook_url was never declared in encrypted_paths and deployed as ciphertext. ankra cluster addons values kube-prometheus-stack --cluster infrastructure shows the live values; the webhook must be a real URL there. Ankra’s own notifications are a separate path: ankra alerts destinations test proves that one.A scheduled restore point fails every night
A scheduled restore point fails every night
ankra runs list --kind backup --cluster production names the run; ankra runs get <run-id> shows which asset failed. The usual causes are a VolumeSnapshotClass whose CSI sidecars cannot write snapshot status (a chart RBAC gap - upgrade the CSI add-on), and a database that is backup-policy-annotated but whose operator plugin is not yet installed (the data plane shows installing in ankra cluster backups status).A stack change deleted a volume
A stack change deleted a volume