Skip to main content

Gappynator

Gappynator is GAP's own Kubernetes operator. It lets you describe your application as a single Application resource, and takes care of generating and maintaining the dozen or so Kubernetes and Azure resources that a production-ready workload in GAP actually needs.

It is the recommended way to deploy applications to GAP. 🚀

Why we built it​

Running a service in GAP correctly used to mean hand-writing a Deployment, Service, HorizontalPodAutoscaler, PodDisruptionBudget, ServiceAccount, Ingress, NetworkPolicy, ServiceMonitor and SecretProviderClass — and then keeping all of them consistent across every environment and every app.

The security hardening, the availability guarantees, the Azure identity plumbing and the observability wiring are the same for almost every application. Gappynator moves that knowledge into the platform:

  • Less to write. A working application is around 10 lines of specification instead of several hundred.
  • Secure and compliant by default. The generated workload satisfies our AKS cluster policies and the hardening described in Security in GAP without you configuring anything.
  • Improvements are rolled out for you. When the platform team changes a default, every application picks it up on the next reconcile — no PR in 70 manifest repositories.
  • One place to look. The Application resource is the single description of your service, and its Ready condition aggregates the health of everything it owns.
Read more

Gappynator is open to everyone in the organisation at github.com/gjensidige/gappynator.

How it works​

Gappynator is a standard Kubernetes operator. It watches Application resources and continuously reconciles the cluster towards the state they describe. You commit one resource; the operator owns everything downstream of it.

flowchart LR cr["Application (gap.io/v1)"] subgraph gappynator["Gappynator reconcile loop"] direction TB rec["Sub-controllers run in order"] end subgraph workload["Workload"] direction TB deploy["Deployment"] svc["Service"] hpa["HorizontalPodAutoscaler"] pdb["PodDisruptionBudget"] end subgraph routing["Routing and network"] direction TB route["HTTPRoute / Ingress / GRPCRoute"] netpol["NetworkPolicy / CiliumNetworkPolicy"] end subgraph identity["Azure Workload Identity"] direction TB uai["UserAssignedIdentity"] fic["FederatedIdentityCredential"] sa["ServiceAccount"] spc["SecretProviderClass"] end subgraph obs["Observability"] direction TB sm["ServiceMonitor"] otel["OpenTelemetry injection annotations"] end cr --> rec rec --> workload rec --> routing rec --> identity rec --> obs uai --> fic uai --> sa sa --> spc

Two details are worth knowing:

The Azure identity chain is a gate. Gappynator first asks Azure Service Operator to create a managed identity for your application. Only once Azure reports it as ready does it create the federated credential, the annotated ServiceAccount and any SecretProviderClass. This is why a brand new application can take a minute or two before its pods start.

Everything is owned by the Application. All generated resources carry an owner reference, so deleting the Application cleans up after itself, and editing a generated resource by hand will simply be reverted on the next reconcile.

What it generates​

ResourceCreated when
DeploymentAlways
ServiceAlways
ServiceAccountAlways — annotated with the workload identity client ID
UserAssignedIdentity (Azure)Always
FederatedIdentityCredential (Azure)Always
HorizontalPodAutoscalerautoscaling.minReplicas differs from maxReplicas
PodDisruptionBudgetautoscaling.minReplicas is greater than 1
ServiceMonitorobservability.prometheusMetrics.enabled — true by default
HTTPRoute, Ingress or GRPCRouteingress.host is set — which one depends on ingress.kind
NetworkPolicynetworkPolicies is set
CiliumNetworkPolicyaccessPolicy is set
SecretProviderClassazure.secretProviderClass is set

What you get by default​

You do not need to ask for any of the following — it is applied unless you explicitly override it.

Security hardening

The pod runs as UID 10001, non-root, with a read-only root filesystem, privilege escalation disabled, all Linux capabilities dropped and the RuntimeDefault seccomp profile. The service account token is not mounted. A writable emptyDir is provided at /tmp so that a read-only root filesystem does not break your application.

Together this satisfies the Gatekeeper policies described in Cluster Policies. See Security in GAP for the full picture.

Availability and rollout behaviour

Two replicas by default, spread across nodes and availability zones with topology spread constraints. A PodDisruptionBudget keeping at least one pod available, which protects you during the node image upgrades described in Cluster Operations. A rolling update strategy with 25% max surge and 25% max unavailable, a 600 second progress deadline and a 300 second termination grace period.

Autoscaling

A HorizontalPodAutoscaler between 2 and 4 replicas, scaling on 75% average CPU utilisation.

Observability

A ServiceMonitor scraping /actuator/prometheus on the metrics port, so your application appears in Metrics automatically. OpenTelemetry auto-instrumentation is enabled, and Gappynator adds the correct instrumentation.opentelemetry.io/inject-<runtime> annotation for your runtime. See the Observability Stack Overview.

A container port named metrics on 8081 is always added alongside your application port.

Azure Workload Identity

A user-assigned managed identity, a federated credential trusting your namespace and service account, and a ServiceAccount annotated with the resulting client ID. Your application can authenticate to Azure without any secret. If you declare Key Vault secrets, the matching SecretProviderClass is created and the secrets are synced and rotated for you — the manual setup in the Secrets Store CSI Driver guide is no longer needed.

The permissions granted to that identity are still managed in terraform-aks, not by Gappynator.

Resource requests and limits

Requests of 100m CPU and 128Mi memory, limits of 200m CPU and 256Mi memory. These are deliberately small — almost every application should set its own.

The Application resource​

The API group is gap.io/v1 and the kind is Application. Only image and port are required:

the minimal spec
spec: {
image: params.container_image,
port: 8080,
}

The fields teams reach for most often, in roughly the order they tend to need them:

FieldPurpose
image, portRequired. Container image and the port your application listens on
resourcesRequests and limits — override the small defaults
autoscalingminReplicas and maxReplicas
ingresshost is usually all you need
livenessProbe, readinessProbeStandard Kubernetes probes
env, envFromEnvironment variables
azure.secretProviderClassKey Vault secrets, exposed as environment variables
networkPoliciesRaw Kubernetes network policy rules
accessPolicyHigher-level network intent, translated to Cilium policy
runtimeSelects the OpenTelemetry instrumentation to inject
observabilityMetrics path and port, or turning instrumentation off
filesFromMount ConfigMaps, secrets, empty dirs or Azure file shares
podAnnotationsExtra annotations on the pod template
Full specification :bulb:

This page covers the common fields. The authoritative, always-current specification lives with the operator:

The Identity resource​

Gappynator also provides an Identity resource. It creates the same Azure Workload Identity chain — managed identity, federated credential and annotated service account — but without a workload attached. Use it when something other than a Gappynator Application needs an Azure identity in your namespace, for example a CronJob or a Kafka consumer deployed by other means.

identity.jsonnet
{
apiVersion: 'gap.io/v1',
kind: 'Identity',
metadata: {
name: 'my-worker',
namespace: 'team-example',
labels: labels,
},
spec: {},
}

Things worth knowing​

Behaviour that surprises people
  • ingress.kind defaults to HTTPRoute, not Ingress. Setting only ingress.host gives you a Gateway API route through Traefik. ingress.annotations are only applied when you explicitly set kind: Ingress, and are silently ignored otherwise.
  • Setting minReplicas equal to maxReplicas disables the HPA entirely and pins the replica count on the Deployment instead.
  • Setting minReplicas to 1 or 0 removes the PodDisruptionBudget, which means node maintenance can take your only pod down.
  • Port 8081 is reserved. Gappynator always adds a container port named metrics on 8081. Do not run your application on that port.
  • Defaults are not written back to the resource. kubectl get application <name> -o yaml shows what you committed, not the effective configuration. Inspect the generated Deployment to see what actually applied.
  • Referenced ConfigMaps, secrets and PVCs must already exist. Gappynator mounts them, it does not create them.

Your namespace must be onboarded before Gappynator can provision an Azure identity for your applications. See Namespace Onboarding.

Next steps​