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
Applicationresource is the single description of your service, and itsReadycondition aggregates the health of everything it owns.
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.
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​
| Resource | Created when |
|---|---|
Deployment | Always |
Service | Always |
ServiceAccount | Always — annotated with the workload identity client ID |
UserAssignedIdentity (Azure) | Always |
FederatedIdentityCredential (Azure) | Always |
HorizontalPodAutoscaler | autoscaling.minReplicas differs from maxReplicas |
PodDisruptionBudget | autoscaling.minReplicas is greater than 1 |
ServiceMonitor | observability.prometheusMetrics.enabled — true by default |
HTTPRoute, Ingress or GRPCRoute | ingress.host is set — which one depends on ingress.kind |
NetworkPolicy | networkPolicies is set |
CiliumNetworkPolicy | accessPolicy is set |
SecretProviderClass | azure.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:
spec: {
image: params.container_image,
port: 8080,
}
The fields teams reach for most often, in roughly the order they tend to need them:
| Field | Purpose |
|---|---|
image, port | Required. Container image and the port your application listens on |
resources | Requests and limits — override the small defaults |
autoscaling | minReplicas and maxReplicas |
ingress | host is usually all you need |
livenessProbe, readinessProbe | Standard Kubernetes probes |
env, envFrom | Environment variables |
azure.secretProviderClass | Key Vault secrets, exposed as environment variables |
networkPolicies | Raw Kubernetes network policy rules |
accessPolicy | Higher-level network intent, translated to Cilium policy |
runtime | Selects the OpenTelemetry instrumentation to inject |
observability | Metrics path and port, or turning instrumentation off |
filesFrom | Mount ConfigMaps, secrets, empty dirs or Azure file shares |
podAnnotations | Extra annotations on the pod template |
This page covers the common fields. The authoritative, always-current specification lives with the operator:
- API reference — every field, generated from the source
gap.io_applications.yaml— the CRD itself
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.
{
apiVersion: 'gap.io/v1',
kind: 'Identity',
metadata: {
name: 'my-worker',
namespace: 'team-example',
labels: labels,
},
spec: {},
}
Things worth knowing​
ingress.kinddefaults toHTTPRoute, notIngress. Setting onlyingress.hostgives you a Gateway API route through Traefik.ingress.annotationsare only applied when you explicitly setkind: Ingress, and are silently ignored otherwise.- Setting
minReplicasequal tomaxReplicasdisables the HPA entirely and pins the replica count on theDeploymentinstead. - Setting
minReplicasto 1 or 0 removes thePodDisruptionBudget, which means node maintenance can take your only pod down. - Port
8081is reserved. Gappynator always adds a container port namedmetricson 8081. Do not run your application on that port. - Defaults are not written back to the resource.
kubectl get application <name> -o yamlshows what you committed, not the effective configuration. Inspect the generatedDeploymentto 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​
- Deploying an Application — the happy path, from an empty folder to a running service
- Deploy Your First App using Gappynator and Argo CD — the surrounding CI/CD setup
- Automate App Onboarding with gap-api — have all of it generated for you
- Network Access Policies — using
accessPolicyto control traffic - Ephemeral Environments — per-pull-request deployments