Nais Platform Skill
Patterns and procedures for deploying, configuring, and troubleshooting applications on Nais (Kubernetes on GCP).
When to Use
- Creating or modifying Nais manifests
- Adding GCP resources (PostgreSQL, Kafka, buckets)
- Configuring access policies and ingress
- Troubleshooting pod startup failures or crashes
- Understanding pod lifecycle and graceful shutdown
Commands
# Check pod status
kubectl get pods -n <namespace> -l app=<app-name>
# View pod logs (add -f to follow)
kubectl logs -n <namespace> -l app=<app-name> --tail=100 -f
# Describe pod (events, errors)
kubectl describe pod -n <namespace> <pod-name>
# View Nais app status
kubectl get app -n <namespace> <app-name> -o yaml
# Restart deployment (rolling)
kubectl rollout restart deployment/<app-name> -n <namespace>
# Port-forward for local debugging
kubectl port-forward -n <namespace> svc/<app-name> 8080:80
Nais Manifest Structure
Every Nais application requires:
apiVersion: nais.io/v1alpha1
kind: Application
metadata:
name: app-name
namespace: team-namespace
labels:
team: team-namespace
spec:
image: {{ image }} # Replaced by CI/CD
port: 8080
# Observability (required)
prometheus:
enabled: true
path: /metrics
# Health checks (required)
liveness:
path: /isalive
initialDelay: 5
readiness:
path: /isready
initialDelay: 5
# Resources (required)
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
memory: 512Mi
Pod Lifecycle and Graceful Shutdown
When Kubernetes terminates a pod on NAIS:
- K8s notifies load balancer and pod simultaneously — LB starts draining connections
- NAIS preStop hook runs
sleep 5— gives LB time to stop routing new traffic - App receives SIGTERM — no new requests arrive from LB
- App drains in-flight requests and exits
- After
terminationGracePeriodSeconds(default 30s): SIGKILL
Key insight: Readiness probes are NOT involved in shutdown. Your app just needs to handle SIGTERM, finish in-flight requests, and exit cleanly.
Common anti-patterns:
- ❌ Setting readiness to
falseon SIGTERM — unnecessary - ❌
terminationGracePeriodSecondstoo low — must be > 5s (preStop) + drain time - ❌ Adding a
preStophook with extra sleep — NAIS already injectssleep 5
Common Tasks
Adding PostgreSQL Database
gcp:
sqlInstances:
- type: POSTGRES_15
databases:
- name: myapp-db
envVarPrefix: DB
Application receives: DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
Configuring Kafka
kafka:
pool: nav-dev # or nav-prod
Azure AD Authentication
azure:
application:
enabled: true
tenant: nav.no
TokenX for Service-to-Service
tokenx:
enabled: true
accessPolicy:
inbound:
rules:
- application: calling-app
namespace: calling-namespace
outbound:
rules:
- application: downstream-app
namespace: downstream-namespace
Ingress Configuration
ingresses:
- https://myapp.intern.dev.nav.no # Internal dev
- https://myapp.dev.nav.no # External dev
Scaling
replicas:
min: 2
max: 4
cpuThresholdPercentage: 80
Resource Recommendations
| Size | CPU request | Memory request | Memory limit |
|---|---|---|---|
| Small | 50m | 256Mi | 512Mi |
| Medium | 100m | 512Mi | 1Gi |
| Large | 200m | 1Gi | 2Gi |
Never set CPU limits — causes throttling. Use requests only.
Troubleshooting
Pod Not Starting
- Check logs:
kubectl logs -n namespace pod-name - Check events:
kubectl describe pod -n namespace pod-name - Verify health endpoints return 200 OK
- Check resource limits (memory/CPU)
Database Connection Issues
- Verify database exists in GCP Console
- Check environment variables are injected
- Verify Cloud SQL Proxy is running
- Check network policies allow connection
Kafka Connection Issues
- Verify
kafka.poolis correct (nav-dev/nav-prod) - Check Kafka credentials are injected
- Verify SSL configuration
- Check topic exists and permissions are correct
Deployment Workflow
- Create
.nais/app.yamlmanifest - Implement health endpoints (
/isalive,/isready,/metrics) - Test locally with Docker
- Deploy to dev environment
- Verify metrics in Grafana
- Check logs in Loki
- Create prod manifest (
.nais/app-prod.yaml) - Deploy to production
Gotchas
accessPolicydefaults to deny-all — you must explicitly allow traffic- Don't set CPU limits — only requests (limits cause throttling)
- Memory limits are mandatory — missing limits cause OOM cluster issues
- NAIS injects
preStop: sleep 5— don't add your own {{ image }}in manifest is replaced by CI/CD — don't hardcode images- Environment-specific manifests (
app-dev.yaml,app-prod.yaml) are the norm
Boundaries
✅ Always
- Include liveness, readiness, and metrics endpoints
- Set memory limits
- Define explicit
accessPolicyfor network traffic - Use environment-specific manifests
- Run
kubectl get app <name> -o yamlto verify deployment
⚠️ Ask First
- Changing production resource limits or replicas
- Adding new GCP resources (cost implications)
- Modifying network policies (
accessPolicy) - Changing Kafka topic configurations
- Adding new ingress domains
🚫 Never
- Store secrets in Git (use Azure Key Vault or Kubernetes secrets)
- Deploy directly without CI/CD pipeline
- Skip health endpoints
- Set CPU limits
- Remove memory limits