Install on a cloud cluster¶
A cloud installation is the same set of components as the local platform, applied with a production
kustomize overlay that changes only what must differ from a laptop. The ankka repository carries an
example production overlay in kustomization/overlays/cloud; copy it, set every value marked SET for
your installation, and apply it by hand after the three controllers whose resources it uses.
deploy-local.sh is not used here. It refuses any kubectl context that is not the local kind cluster,
on purpose: a script that deploys to whatever context happens to be current is a script that eventually
deploys to the wrong one.
Prerequisites¶
- Kubernetes 1.32 or later. Envoy Gateway's custom resource definitions use a validation rule an older API server rejects.
- A network plugin that enforces
NetworkPolicy. The platform restricts who may connect to every workload with network policies, and a network that stores them without enforcing them refuses nothing. GKE Dataplane V2, Calico, Cilium and k3s's built-in controller enforce them. See Check that the network enforces policy. - A domain you control, with DNS at a provider cert-manager can write to. The platform serves every
exposed service under one wildcard certificate,
*.<base domain>, and a wildcard can only be issued over the ACME DNS-01 challenge, which writes a TXT record into your zone. HTTP-01 cannot issue wildcards. cert-manager has built-in DNS-01 solvers for Cloudflare, Route53, Google Cloud DNS, Azure DNS, DigitalOcean, Akamai, ACME-DNS and RFC-2136 servers; other providers need a webhook solver. - A load balancer your cluster can provision for a
Serviceof typeLoadBalancer, and a DNS record pointing*.<base domain>at its address. - A container registry the cluster can pull from, holding the platform's images: the operator, the
control plane, the sidecar, the console and, if you want the sample, the shopping cart. Every image is
published for
linux/amd64andlinux/arm64under one tag, so the nodes may be of either architecture, or both.
What a production overlay changes¶
The overlay lists the same components as the local one, and patches or replaces five things:
| Local overlay | Production overlay | |
|---|---|---|
| Gateway exposure | node ports 30080/30443, published by kind on 8080/8443 | a LoadBalancer service, optionally with a pinned address |
| Wildcard certificate | a certificate authority created in the cluster, as a namespaced Issuer |
an ACME issuer, such as Let's Encrypt, over DNS-01 |
| Base domain and HTTPS port | 127.0.0.1.sslip.io, port 8443 |
your domain, port 443 |
| Identity provider admin | a development secret, admin/admin |
the secret deleted; you create one out of band |
| Images | unqualified names loaded into the node | your registry, at a pinned release tag |
| The broker's size | one node, 2Gi, a 512MB heap | the values you set in broker-size.yaml, and three nodes with every topic copied three times when you list the broker-three-nodes component |
| The model server | not listed, unless deployed with ANKKA_LOCAL_MODELS=on |
listed when the overlay names the models component, at the size, placement and storage you set in models-size.yaml |
| Topic defaults and bounds | seven days, one copy, any retention allowed | the ANKKA_TOPIC_* variables you set on the control plane before the first topic is declared |
Both overlays install the same two authorities the platform issues workload certificates from — one for
traffic between a service's own instances, one for HTTP between services and from the gateway — as
self-signed roots in cert-manager's namespace, and trust-manager to share the second one's root with every
ankka namespace. They are private on purpose: no public authority issues an ankka:// identity or a
cluster-internal name, and nothing outside the cluster is asked to trust them. An installation that wants
its authorities to come from elsewhere — a corporate PKI, a cloud certificate service — replaces the two
ClusterIssuers ankka-cluster and ankka-service in kustomization/components/pki and nothing else;
the operator asks for them by name. The root must not be an intermediate, because cluster remoting
refuses a certificate issued by one.
The base domain¶
The base domain and HTTPS port are written once, in the overlay's ankka-platform ConfigMap, and
kustomize replacements copy them into every place that must agree: the wildcard certificate, the
gateway's listener, the control plane's and identity provider's routes, Keycloak's own hostname, and the
ANKKA_BASE_DOMAIN of the operator and the control plane.
apiVersion: v1
kind: ConfigMap
metadata:
name: ankka-platform
namespace: ankka-gateway
data:
baseDomain: example.com
httpsPort: "443"
The control plane answers at https://api.example.com, the identity provider at
https://auth.example.com, and exposed services at https://<service>-<project>.example.com.
The certificate¶
The gateway's HTTPS listener serves a Secret named ankka-wildcard-tls in the ankka-gateway namespace,
holding a certificate for *.<base domain>. That Secret is the whole contract. The example overlay
produces it with a cert-manager Certificate from an ACME ClusterIssuer; any issuer that produces it
will do, and so will a bought certificate placed in that Secret.
Two details decide whether issuance works. Use the ACME provider's staging endpoint until issuance
succeeds, because the production endpoint rate-limits failures. And mind where cert-manager looks for
secrets: a ClusterIssuer reads the secrets it references from cert-manager's own namespace, and a
namespaced Issuer from its own namespace. Put the DNS provider's credential where the issuer you choose
will look for it. A webhook solver may add its own rule: the example overlay's webhook reads its token
with its own permissions in cert-manager's namespace, which is why it uses a ClusterIssuer.
Custom hostnames¶
A service can answer at a name under a domain its owner brings (see Serve a service at your own
domain). Each such name gets a certificate of its own, since the wildcard
covers only the base domain. The example overlay names a second ClusterIssuer, letsencrypt-hostnames,
in hostnameIssuer on the ankka-platform ConfigMap: ACME over HTTP-01, answered through the gateway's
port-80 listener, because the installation cannot write to another domain's zone. The load balancer must
therefore pass port 80 as well as 443. The components already carry what that needs: cert-manager's
controller runs with --enable-gateway-api, and the Gateway admits listener sets and challenge routes
from project namespaces. Leave hostnameIssuer empty to refuse every custom hostname.
Set gatewayAddress on the same ConfigMap to the load balancer's address so the control plane can tell a
member the A record for an apex name, which cannot be a CNAME. Left empty, an apex is told that this
installation has published no address.
The identity provider's administrator¶
The local platform's Keycloak administrator is admin/admin, in a Secret that is public in the ankka
repository. A production overlay deletes that Secret rather than overriding it, so the real password
never enters git, and Keycloak cannot start until you create the Secret yourself:
kubectl -n ankka-auth create secret generic ankka-keycloak-admin \
--from-literal=username=admin --from-literal=password="$(openssl rand -base64 32)"
An identity provider that refuses to start is a loud and immediate failure. One on the internet with a published administrator password is neither. The realm contains no users at all; add them in the console. See Identity and machine accounts.
The object store's secrets¶
The example overlay deletes the object store's two development Secrets as it deletes the identity provider's, and the store and the operator cannot start until you create them. The administrator token is written twice, once for the store and once for the operator:
TOKEN="$(openssl rand -base64 32)"
kubectl -n garage-system create secret generic garage-secrets \
--from-literal=rpc-secret="$(openssl rand -hex 32)" --from-literal=admin-token="$TOKEN"
kubectl -n ankka-operator create secret generic ankka-object-store-admin --from-literal=token="$TOKEN"
See Object storage.
The console¶
The overlay's ankka-platform ConfigMap names the console's address as
consoleAuthority: console.<base domain>, with :<port> when HTTPS is not on 443. The overlay deletes the
console's development Secret and removes the development secret from its realm client, so Keycloak generates
one. Once Keycloak is up, create the Secret from that client secret and a session secret of your own; see
Install and configure the console. Until the Secret exists
the console does not start, and nothing else waits on it. An installation that does not want a console
leaves the component out; see Install and configure the console.
Images¶
Every platform manifest names an unqualified image, such as ankka-controlplane:latest, with
imagePullPolicy: IfNotPresent. That suits a node the image was loaded onto and is useless for a
cluster that pulls. Every release publishes the images publicly on GitHub Container Registry, tagged with
the release's version: ghcr.io/thinkmorestupidless/ankka-operator, ankka-controlplane,
ankka-sidecar, ankka-console and sample-shopping-cart. A cluster can pull those directly, through a pull-through
cache of ghcr.io in its own cloud, or from a registry of your own that you build them into by setting
DOCKER_REPOSITORY:
DOCKER_REPOSITORY=registry.example.com/ankka sbt docker:publish
A cache in the cluster's own region keeps pulls local and keeps serving a version it has already
fetched when ghcr.io is unavailable. Whichever you choose, rename the images in the overlay with an
images: block, at the release you run:
images:
- name: ankka-controlplane
newName: registry.example.com/ankka/ankka-controlplane
newTag: "0.16.1"
- name: ankka-operator
newName: registry.example.com/ankka/ankka-operator
newTag: "0.16.1"
The sidecar needs one more step. It appears in no manifest, because the operator injects it beside every
Python service, so the images: block never sees it. The operator learns its name from the
ANKKA_SIDECAR_IMAGE variable on its own Deployment; patch that to the same registry and tag. Unset, a
Python service fails with operator has no sidecar image rather than run a sidecar of the wrong version.
The proxy the operator runs beside every web-hosted service is the same: no manifest names it, and the
operator reads it from ANKKA_PROXY_IMAGE on its own Deployment, which the example overlay patches to the
registry and tag. Unset, a web-hosted service fails with operator has no proxy image. The operator also
reads ANKKA_HTTPS_PORT, the port the gateway is reached on, which the overlays set from the same value as
the control plane's.
The model server's keeper is ankka-model-keeper, named by the models component's manifests, so an
overlay that lists the component renames it in the same images: block. The component's other image,
Ollama's, is already pinned to a version and pulled from Docker Hub.
The keyring is ankka-keyring, named by the keyring component's manifests and published with every
release beside the others; an overlay that lists the component renames it in the same images: block.
Pin a release tag rather than latest. A cluster should run a version that was built, tested and
published as one.
The model server¶
The installation's model server is the models component: Ollama and a keeper that serves the models
its administrator declares, to the installation's services only (see
Models served by the installation). An overlay that wants one lists the
component after the control plane and patches it with models-size.yaml, where the node it runs on,
its toleration, its memory, CPU and accelerator, the number of calls it answers at once and its storage
class and size are marked SET. The component's own values suit a laptop: one small model on a
processor. Size the storage for every declared model together, since each is kept on the volume. An
installation that wants no model server leaves the component out, and its services name a model
provider with an address of its own.
The broker¶
The installation's broker is the broker component: Strimzi and one Kafka, which every project's
services use (see The installation's broker). The example overlay patches its node pool
with broker-size.yaml, where the node count, each node's storage, the heap and the memory and CPU are
marked SET; the component's own values suit a laptop. Strimzi's images come from quay.io, so a cluster
that pulls only through a cache of its own needs that cache to mirror quay.io/strimzi too. An
installation that keeps a Kafka of its own leaves the component out, and its services name that Kafka in
their descriptors.
Apply it, in order¶
kubectl config current-context # confirm it is the cluster you mean
kubectl apply -k kustomization/components/cnpg --server-side --force-conflicts
kubectl apply -k kustomization/components/certmanager --server-side --force-conflicts
kubectl apply -k kustomization/components/envoy-gateway --server-side --force-conflicts
kubectl apply -f kustomization/components/broker/namespace.yaml --server-side
kubectl apply -k kustomization/components/broker/strimzi --server-side --force-conflicts
# wait for the four controllers to be ready; install a DNS-01 webhook solver here if yours needs one
kubectl apply -k kustomization/components/trust-manager --server-side --force-conflicts
kubectl -n cert-manager rollout status deployment/trust-manager
kubectl apply -k kustomization/components/keycloak-operator --server-side --force-conflicts
kubectl create namespace ankka-controlplane
kubectl create namespace ankka-auth
kubectl -n ankka-auth create secret generic ankka-keycloak-admin ... # see above
kubectl apply -k kustomization/overlays/<your-overlay> --server-side --force-conflicts
The overlay carries the cloud-provider component and the installation's cloud settings in its
ankka-platform ConfigMap, with no provider named. To name one, set cloudProvider, cloudAccount and
cloudLocation there, and install the provider's own Deployment into ankka-cloud-provider after the
overlay; see The cloud provider.
An installation whose buckets are in Google Cloud Storage names its object store there too
(objectStoreBackend, objectStorePrefix, objectStoreSoftDeleteDays); see Install on GKE.
An overlay that lists the models component needs nothing applied before it: the component brings no
custom resource definitions, and its certificate is issued by an authority the overlay already carries.
Every service already running restarts once when it is added.
The controllers go first because each brings custom resource definitions that the overlay's resources
are instances of, and a single kubectl apply -k gives no ordering guarantee between a definition and a
resource of that kind. Apply with --server-side: CloudNativePG's definitions are too large for
client-side apply's annotation.
The overlay carries everything else the platform needs, so the same apply works by hand and from a
GitOps tool such as Flux. The control plane's database schema is a ConfigMap the overlay generates from
the schema's single copy, created in the same apply as the Postgres cluster that runs it once at bootstrap.
The realm is a KeycloakRealmImport in the overlay, which the Keycloak operator imports once Keycloak is
ready.
kubectl apply of many documents applies everything it can and exits non-zero if anything failed.
Check the exit code, not the count of lines that say applied.
The realm import is one-shot: it creates the ankka realm if it does not exist and never updates it,
so a later change to kustomization/components/keycloak/realm-import.json is made in Keycloak's console
on an installation that already has the realm. Create the first users there too; the realm holds none.
A cluster of your own machines¶
Nothing above assumes a cloud. A cluster you built yourself — k3s on a few machines, say — runs the platform the same way once it provides three things a cloud would otherwise hand you:
- An address for the gateway. The gateway is a
Serviceof typeLoadBalancer, and something must give it an address: k3s's built-in ServiceLB gives it a node's, which is enough for one network; MetalLB gives it one from a pool you reserve. Point*.<base domain>,api.<base domain>andauth.<base domain>at that address. A private address behind a public name is ordinary: DNS-01 issues the wildcard certificate by writing a record in your zone and never connects to the cluster, so nothing need be reachable from the internet. - A default storage class. Every database the platform provisions asks the cluster for a volume of
its default class and names none. k3s ships
local-path, which keeps a volume on the node that first mounted it; that is fine for a lab and means a database follows its node. - Nodes of either architecture. amd64 or arm64, or a mix: every image of the platform's is published for both, and nothing in the platform pins a workload to one. Your own services' images are yours to build for the nodes you have; see Build an image.
Memory is the constraint such a cluster usually meets first. The control plane, the operator, the
identity provider, the gateway, the certificate manager and the database operator sit beside every
service you deploy, and each service is a JVM. Leave out the broker, garage and telemetry-store
components until something needs them.
Check that the network enforces policy¶
Prove it once per cluster, with a pod that a deny-all policy should cut off:
kubectl create namespace netpol-check
kubectl -n netpol-check run server --image=busybox:1.36 --labels=probe=server --command -- httpd -f -p 8080
kubectl -n netpol-check run client --image=busybox:1.36 --command -- sleep 600
kubectl -n netpol-check wait --for=condition=Ready pod/server pod/client
IP=$(kubectl -n netpol-check get pod server -o jsonpath='{.status.podIP}')
kubectl -n netpol-check exec client -- sh -c "echo | nc -w 3 $IP 8080" && echo reachable
kubectl -n netpol-check apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: deny-all }
spec: { podSelector: { matchLabels: { probe: server } }, policyTypes: [Ingress] }
EOF
kubectl -n netpol-check exec client -- sh -c "echo | nc -w 3 $IP 8080" && echo "NOT ENFORCED"
kubectl delete namespace netpol-check
The first connection must succeed and the second must fail. On a cluster that prints NOT ENFORCED,
every connection is still mutual TLS and every caller still named, but nothing is refused before the
handshake: a workload of another project can reach a service's cluster ports and its database's port, and
is stopped only by lacking a certificate.
After installing¶
ankka config set url https://api.example.com
ankka login
The console is at https://console.example.com. A publicly trusted certificate needs no config set ca. Grant the first administrator the
platform-admin realm role in Keycloak's console, then create organizations and projects. See
Organizations, projects and members.
Upgrading the installation is the same apply with new image tags. The operator, control plane, sidecar, console and CLI are released together as one version. Let the control plane's rollout finish before any service is deployed again: a service's runtime records each read of its secrets with the control plane, and refuses the read when the control plane does not yet know how.
Where secrets are kept is secretBackend on the ankka-platform ConfigMap, with cloudProvider,
cloudAccount and cloudLocation for Google Secret Manager; the record of reads is kept in a database of
its own, ankka-secret-reads-db, which every overlay installs beside the control plane's. See
Secrets on the platform for each setting, what Google Cloud needs before
an installation uses Secret Manager, and how an installation moves to it.
A spoke installation¶
Several installations can share one set of users. One of them, the hub, runs Keycloak as described on this page. The others, the spokes, run the control plane and the operator with no identity provider of their own, and trust the hub's realm. A person registered once at the hub logs in to every spoke with the same account.
A spoke's overlay is a copy of the production overlay with these differences:
- It leaves out the
keycloak-operatorandkeycloakcomponents, and the patch that deletes the Keycloak administrator Secret, since there is no Keycloak to administer. - It sets two variables on the control plane Deployment, both naming the hub's realm by its public address:
- name: ANKKA_AUTH_ISSUER
value: https://auth.example.com/realms/ankka
- name: ANKKA_AUTH_JWKS_URL
value: https://auth.example.com/realms/ankka/protocol/openid-connect/certs
- It empties
ANKKA_AUTH_JWKS_CAon the same Deployment. Thecontrolplanecomponent sets it to the cluster's own service authority, which verifies the in-cluster Keycloak of a hub. The hub's public address has a certificate from a public issuer, so a spoke that keeps that setting refuses every token. Empty means the JVM's trust store:
- name: ANKKA_AUTH_JWKS_CA
value: ""
- It keeps its own base domain, for example
caladan.example.com, so its control plane answers atapi.caladan.example.comand its exposed services have hostnames under that domain. - Its console, at
console.caladan.example.com, signs in at the hub. It reads the issuer from its own control plane, which names the hub's realm. Empty the two settings that point the console at an in-cluster Keycloak, so it calls the hub at its public address and verifies it by the system's trust store:
- name: ANKKA_CONSOLE_AUTH_BACKCHANNEL_URL
value: ""
- name: ANKKA_CONSOLE_AUTH_CA
value: ""
In the hub's realm, add https://console.caladan.example.com/* to the ankka-console client's valid
redirect URIs. Give the spoke's ankka-console-secrets Secret that client's secret and a session secret
of the spoke's own; see Install and configure the console.
The spoke accepts only tokens whose issuer is the hub's; a token issued anywhere else is refused with
401. Its login discovery advertises the hub's issuer, so ankka login against the spoke's address opens
the hub's sign-in page. A platform-admin in the hub's realm is a platform administrator on every
installation that trusts it. See Identity and machine accounts.