API with sidecar

A realistic deployment: API container, nginx sidecar, sealed DB password, ingress with cert-manager.

Build a production-grade deployment from scratch:

  • An API container listening on 8080
  • An nginx sidecar on 80 handling TLS termination at the service boundary and serving static assets
  • Database credentials loaded from a sealed secret
  • Ingress with automatic certificate via cert-manager
  • Horizontal pod autoscaler

The full file

# api.kdef
deployment "api" {
  namespace          = "production"
  image_pull_secrets = ["regcred"]
  service_account    = "api"

  container "api" {
    image             = image("api")
    image_pull_policy = "IfNotPresent"

    port "8080" "http" {
      health = "/health"
      ready  = "/ready"
    }

    env {
      APP_ENV      = var.environment
      DATABASE_URL = secret("db-credentials", "DATABASE_URL")
      JWT_SECRET   = secret("jwt-keys", "secret")
      LOG_LEVEL    = var.environment == "production" ? "warn" : "debug"
    }

    env_from {
      config_map = "api-config"
    }

    resources {
      cpu    = "300m..1000m"
      memory = "512Mi..1Gi"
    }

    volume "assets" {
      mount_path = "/app/public"
      empty_dir  = true
    }

    security_context {
      run_as_user     = 1000
      run_as_non_root = true
      read_only_root  = true
    }
  }

  container "nginx" {
    image = image("nginx")

    port "80" "http" {
      tcp_health = true
    }

    resources {
      cpu    = "50m..200m"
      memory = "32Mi..128Mi"
    }

    volume "nginx-conf" {
      mount_path = "/etc/nginx/conf.d/default.conf"
      sub_path   = "default.conf"
      config_map = "nginx-config"
    }

    volume "assets" {
      mount_path = "/var/www/html"
      empty_dir  = true
      read_only  = true
    }
  }

  init "warmup" {
    image   = image("api")
    command = ["/bin/sh", "-c", "php bin/console cache:warmup && cp -R public/* /shared/"]
    volumes = ["assets"]
  }

  # Pod-level declaration of the shared volume: init containers mount it at
  # this path, each container at its own mount_path above.
  volume "assets" {
    mount_path = "/shared"
    empty_dir  = true
  }

  service {
    port "80" "http" {}
  }

  ingress {
    host   = "api.example.com"
    port   = 80   # the Service port (nginx); the default is the first container port
    tls    = true
    issuer = "letsencrypt-production"

    annotations = {
      "nginx.ingress.kubernetes.io" = {
        "proxy-body-size"    = "50m"
        "proxy-read-timeout" = "120"
      }
    }
  }

  autoscale {
    min    = 2
    max    = 10
    cpu    = 70
    memory = 80
  }
}

What this generates

One kdef render --dir k8s/ produces:

  1. Deployment api with the init container, the API container and the nginx sidecar
  2. Service api on port 80 → containerPort 80 (the nginx sidecar)
  3. Ingress api for api.example.com, with TLS and the nginx annotations
  4. Certificate (via cert-manager) for the host
  5. HorizontalPodAutoscaler targeting 70% CPU / 80% memory, 2–10 replicas

Five resources. One block.

The supporting files

# vars.kdef
variable "environment" {
  type    = "enum[staging, production]"
  default = "staging"
}

ingress_defaults {
  issuer = "letsencrypt-production"
  annotations = {
    "nginx.ingress.kubernetes.io" = {
      "force-ssl-redirect" = "true"
    }
  }
}
# images.kdef
images {
  api   = "registry.example.com/my-app/api:1.4.2"
  nginx = "registry.example.com/nginx:stable-alpine"
}
# configmaps.kdef
configmap "api-config" {
  namespace = "production"
  data = {
    APP_NAME = "my-api"
    APP_URL  = "https://api.example.com"
  }
}

configmap "nginx-config" {
  namespace = "production"
  data = {
    "default.conf" = file("configs/nginx.conf")
  }
}
# secrets.kdef: committed safely; values are kubeseal-encrypted
sealedsecret "db-credentials" {
  namespace = "production"
  data = {
    DATABASE_URL = "AgBy3i4OJSWK+PiTySYZZA9rO43..."
  }
}

sealedsecret "jwt-keys" {
  namespace = "production"
  data = {
    secret = "AgCE9F2h7GKJF8mL3nP5rS7tV9xB..."
  }
}

To produce those encrypted blobs:

kdef seal --secret db-credentials --namespace production --key DATABASE_URL \
  --value "postgres://api:hunter2@db:5432/api"

See the sealed secrets guide for the full flow.

Deploy it

kdef validate --dir k8s/
kdef diff     --dir k8s/
kdef apply    --dir k8s/

Or wire it up for GitOps with Flux or ArgoCD.