Skip to content

Hello World

This is a demo application included in the digitalgedacht K8S cluster documentation.

The app is a simple python web server in a docker container serving a statically built mkdocs site (this site).

Deployment

The hello-world app uses FluxCD with ResourceSet and Kustomize for GitOps-based deployment. The ResourceSet architecture uses input providers to template Kubernetes resources dynamically.

Structure

deployment/
└── base/                    # Base configuration
    ├── kustomization.yaml   # Resource list
    └── resourceset.yaml     # FluxCD ResourceSet with OCIRepository and HelmRelease

ResourceSet Deployment

The deployment uses a single ResourceSet that defines both the OCIRepository and HelmRelease. This resource is templated using inputs from ResourceSetInputProvider resources defined in the infrastructure repository.

---
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: hello-world-deployment
spec:
  inputStrategy:
    name: Permute
  inputsFrom:
    - kind: ResourceSetInputProvider
      name: tenant-config
    - kind: ResourceSetInputProvider
      name: cluster-config

  resources:
    # OCIRepository - App team controls chart version via inputs.app.chart.version
    - apiVersion: source.toolkit.fluxcd.io/v1
      kind: OCIRepository
      metadata:
        name: <<inputs.tenant_config.app.name>>-chart
        namespace: <<inputs.tenant_config.app.name>>-<<inputs.tenant_config.tenant.customer>>
      spec:
        interval: 5m
        url: <<inputs.tenant_config.app.chart.registry>>/hello-world
        secretRef:
          name: image-pull-secret
        ref:
           semver: "<<inputs.tenant_config.app.chart.version>>"

    # HelmRelease
    - apiVersion: helm.toolkit.fluxcd.io/v2
      kind: HelmRelease
      metadata:
        name: hello-world
        namespace: <<inputs.tenant_config.app.name>>-<<inputs.tenant_config.tenant.customer>>
      spec:
        interval: 5m
        chartRef:
          kind: OCIRepository
          name: <<inputs.tenant_config.app.name>>-chart
          namespace: <<inputs.tenant_config.app.name>>-<<inputs.tenant_config.tenant.customer>>
        install:
          createNamespace: true
          remediation:
            retries: 3
        upgrade:
          remediation:
            retries: 3
        values:
          # Namespace name
          namespace: <<inputs.tenant_config.app.name>>-<<inputs.tenant_config.tenant.customer>>

          # Application name for labels
          appName: <<inputs.tenant_config.app.name>>

          image:
            name: <<inputs.tenant_config.app.name>>
            registry: <<inputs.tenant_config.app.image.registry>>
            tag: <<inputs.tenant_config.app.image.version>>
            pullPolicy: Always

          imagePullSecrets:
            - name: image-pull-secret

          replicaCount: 3

          service:
            enabled: true

          ingress:
            enabled: true
            hostname: <<inputs.tenant_config.tenant.customer>>.<<inputs.tenant_config.app.name>>.<<inputs.cluster_config.fqdn>>

          createNamespace: true

ResourceSet Input Providers

The ResourceSet uses input providers to inject configuration:

  • tenant-config: Application-specific settings (app name, customer, chart version, image version)
  • cluster-config: Cluster-wide settings (FQDN, etc.)

Template syntax: <<inputs.tenant_config.app.name>> is replaced with actual values at runtime.

Example: namespace: <<inputs.tenant_config.app.name>>-<<inputs.tenant_config.tenant.customer>> becomes hello-world-dev

Key components:

  1. OCIRepository: Fetches the Helm chart from Google Artifact Registry
  2. Polls every 5 minutes for chart updates
  3. Uses semantic versioning from input provider
  4. Chart URL: <<inputs.tenant_config.app.chart.registry>>/hello-world

  5. HelmRelease: Manages the deployment with automatic reconciliation

  6. Reconciles every 5 minutes
  7. Creates namespace if needed
  8. Retries installation/upgrade failures up to 3 times
  9. Default: 3 replicas with configurable resources per environment

App Resources

The Helm chart creates the following Kubernetes resources:

Namespace

Optional namespace creation (enabled by default):

  • Name: Templated as <<inputs.tenant_config.app.name>>-<<inputs.tenant_config.tenant.customer>> (e.g., hello-world-dev)
  • Controlled by: createNamespace: true in HelmRelease values

Deployment

Manages application pods:

  • Name: hello-world-deployment
  • Replicas: 3 (default, configurable per environment via input providers)
  • Container image: Templated as <<inputs.tenant_config.app.image.registry>>/hello-world:<<inputs.tenant_config.app.image.version>>
  • Image pull policy: Always (ensures fresh images)
  • Container port: 8000 (Python HTTP server)
  • Resource limits: Configurable per environment via input providers
  • Image pull secrets: image-pull-secret (for private registry access)

Service

Exposes the application within the cluster:

  • Type: ClusterIP
  • Port: 80 (cluster-internal)
  • Target port: 8000 (container)
  • Selector: Matches pods from Deployment

Ingress

Routes external traffic to the application:

  • Class: nginx
  • Hostname: Templated as <<inputs.tenant_config.tenant.customer>>.<<inputs.tenant_config.app.name>>.<<inputs.cluster_config.fqdn>>
  • TLS: Enabled with cert-manager
  • Certificate issuer: google-cloud-dns-cluster-issuer
  • Path: / → Service on port 80

Access Flow:

Internet → Ingress (nginx) → Service (port 80) → Pod (port 8000) → HTTP Server

Helm Chart

The app is packaged as a Helm 3 chart stored in an OCI registry.

Chart Metadata

apiVersion: v2
name: hello-world
description: A Helm chart for the hello-world demo application
type: application
version: 0.1.17
appVersion: "0.1.17"

Version Synchronization

The chart version and appVersion are automatically synced with pyproject.toml during the build process using yq. This ensures all artifacts (code, Docker image, Helm chart) share the same version.

Chart Repository

  • Registry: oci://europe-west3-docker.pkg.dev/digitalgedacht/charts
  • Chart name: hello-world
  • Versioning: Semantic versioning (e.g., 0.1.0)

Default Values

The chart provides sensible defaults that can be overridden per environment:

View Default Values

Path: charts/hello-world/values.yaml

# Default values for hello-world chart

# Namespace name (set directly, no construction)
namespace: hello-world-dev

# Application name for labels and selectors
appName: hello-world

# Image configuration
image:
  registry: europe-west3-docker.pkg.dev/digitalgedacht/docker/digitalgedacht/hello-world
  name: hello-world
  tag: latest
  pullPolicy: Always

# Image pull secrets
imagePullSecrets:
  - name: image-pull-secret

# Deployment configuration
replicaCount: 3

# Service configuration
service:
  enabled: true  # Default: true
  type: ClusterIP  # Default: ClusterIP
  port: 80  # Default: 80
  targetPort: 8000  # Default: 8000

# Ingress configuration
ingress:
  enabled: true
  className: nginx  # Default: nginx
  annotations:
    cert-manager.io/cluster-issuer: google-cloud-dns-cluster-issuer  # Default annotation
  hostname: ""  # Must be set (e.g., via FluxCD substitution)
  tls:
    enabled: true  # Default: true

# Create namespace
createNamespace: true

# Pod labels (additional labels to add to pods beyond the standard selector labels)
podLabels: {}

# Resources (uncomment to set limits)
# resources:
#   limits:
#     cpu: 100m
#     memory: 128Mi
#   requests:
#     cpu: 100m
#     memory: 128Mi

Key configuration options:

  • namespace: Target namespace (templated via ResourceSet input providers)
  • appName: Application name for labels
  • image: Docker image registry, repository, and tag
  • imagePullSecrets: List of secrets for private registry access
  • replicaCount: Number of pod replicas (3 by default)
  • service.enabled: Enable/disable Service creation
  • ingress.enabled: Enable/disable Ingress creation
  • ingress.hostname: Full hostname (templated via ResourceSet input providers)
  • createNamespace: Whether to create the namespace

Docker Image

The application runs as a containerized Python HTTP server serving static MkDocs documentation.

Dockerfile

# Use an official Python runtime as a parent image
FROM python:3.11-slim

# Set the working directory
WORKDIR /app

# Copy the script to the container
COPY site .

# Expose port 8000
EXPOSE 8000

# Run the HTTP server
CMD ["python3", "-m", "http.server", "8000"]

What It Does

  1. Base Image: python:3.11-slim (minimal Python runtime)
  2. Content: Copies the site/ directory (built MkDocs HTML)
  3. Server: Runs Python's built-in HTTP server on port 8000
  4. Purpose: Serves static documentation with zero dependencies

Image Registry

  • Repository: europe-west3-docker.pkg.dev/digitalgedacht/docker/digitalgedacht/hello-world
  • Tags: Both latest and version-specific (e.g., 0.1.0)

Build Process

Images are built via mise tasks defined in mise.toml:

# Local build (doesn't push)
mise build

# Build and push to registries
mise push

# CI/CD alias (same as push)
mise ci_release

Task dependency chain:

ci_release → push → build → docs-build

The build process:

  1. docs-build: Builds MkDocs site → site/ directory
  2. build:
  3. Syncs Chart.yaml version with pyproject.toml using yq
  4. Packages Helm chart
  5. Builds Docker image with version tags (latest + semantic version)
  6. push: Pushes Docker image and Helm chart to their respective registries

Version Tags

Every image is tagged with both latest and the semantic version from pyproject.toml, ensuring version consistency across all artifacts.

Build & Release Workflow

End-to-End Deployment Flow

graph TD
    A[Developer commits code] --> B[GitHub Actions CI]
    B --> C{Event Type?}
    C -->|Push/PR| D[Run tests only]
    C -->|Release| E[mise ci_release]
    E --> F[Build MkDocs site]
    F --> G[Sync versions with yq]
    G --> H[Build Docker image]
    G --> I[Package Helm chart]
    H --> J[Push to Docker registry]
    I --> K[Push to Helm registry]
    J --> L[FluxCD OCIRepository polls]
    K --> L
    L --> M[FluxCD HelmRelease reconciles]
    M --> N[Apply to cluster]

Local Development Commands

# Build documentation locally
mise docs-serve          # Serves at localhost:8080

# Build artifacts (no push)
mise build               # Builds Docker image + Helm chart

# Build and push to registries
mise push                # Requires authentication

# Version management
mise bump_version        # Increments patch version in pyproject.toml

CI/CD Pipelines

CI Workflow (.github/workflows/CI.yaml):

  • Triggers: Pull requests, pushes to develop/master
  • Actions: Build documentation in strict mode (fails on warnings), post results to PR
  • Skips: Doc-only changes

Release Workflow (.github/workflows/release.yaml):

  • Triggers: GitHub Release published
  • Actions:
  • Authenticate with GCP Artifact Registry
  • Run mise ci_release
  • Push Docker image and Helm chart

Site Deployment (.github/workflows/site.yaml):

  • Triggers: Doc changes to develop/master/production
  • Actions: Deploy versioned docs to GitHub Pages using mike

Version Management

All versioning flows from pyproject.toml:

[project]
version = "0.1.0"

Release workflow:

  1. Bump version: mise bump_version
  2. Increments patch version (0.1.0 → 0.1.1)
  3. Updates pyproject.toml and uv.lock
  4. Creates git commit automatically

  5. Create prerelease: mise release

  6. Validates no uncommitted changes
  7. Checks version tag doesn't already exist
  8. Creates and pushes git tag
  9. Creates GitHub prerelease with auto-generated notes

  10. CI/CD builds artifacts:

  11. GitHub Actions release workflow triggers automatically
  12. Builds Docker image and Helm chart
  13. Pushes to registries
  14. Attaches Helm chart and documentation as release assets

  15. Test and finalize:

  16. Test the prerelease in your environment
  17. Manually mark release as "final" (not prerelease) in GitHub UI

The release script does:

#!/usr/bin/env bash

set -euo pipefail

if [[ $(git diff --stat) != '' ]]; then
  echo "You have local uncommitted changes."
  exit 11
fi

VERSION=$(uv version --short)

git tag | grep -e "^${VERSION}$" && {
  echo "Git tag ${VERSION} already exists.  Please update the project version."
  exit 10
}

git tag "${VERSION}"
git push origin "${VERSION}"

echo "Creating prerelease."

gh release create "${VERSION}" --prerelease --generate-notes

Key safety checks:

  • Ensures no uncommitted local changes
  • Checks if version tag already exists
  • Creates git tag from pyproject.toml version
  • Pushes tag to origin
  • Creates GitHub prerelease with auto-generated notes via gh CLI

Artifacts attached to release:

  • hello-world-VERSION.tgz - Helm chart
  • site-VERSION.tar.gz - Documentation site archive

Version synchronization:

  • pyproject.toml (source of truth) → Chart.yaml (via yq) → Docker tags → Helm chart filename

FluxCD Reconciliation

FluxCD continuously monitors for changes:

  1. OCIRepository: Polls OCI registry every 5 minutes for new chart versions
  2. HelmRelease: Reconciles every 5 minutes, applying latest chart with environment-specific values
  3. Deployment: Kubernetes pulls new Docker images (pull policy: Always)

No Manual kubectl apply

FluxCD manages all deployments. Manual kubectl apply changes will be reverted during reconciliation.

Local Testing

The test/ directory provides a synthetic testing environment that simulates the infrastructure-provided InputProviders. The actual InputProviders are defined in the infrastructure repository (dg-k8s), but this test harness allows local validation without cluster access:

test/
├── namespace.yaml                 # Test namespace
├── pull-secret.yaml              # Image pull credentials
├── inputprovider-tenant.yaml     # Mock tenant-config provider
├── inputprovider-cluster.yaml    # Mock cluster-config provider
├── configmap-test.yaml           # Test configuration
├── kustomization.yaml            # Combines all resources
└── mise.toml                     # Test-specific tasks

Running Local Tests

The test directory uses Kustomize with environment variable substitution to create mock InputProviders:

# Build and validate the ResourceSet locally
cd test/
kustomize build .

# Apply to a test cluster (requires kubectl context)
kustomize build . | kubectl apply -f -

Environment variables used in test fixtures (set in test/mise.toml): - APP_NAME: Application name (e.g., "hello-world") - APP_CUSTOMER: Customer/tenant identifier (e.g., "dev") - APP_CHART_REGISTRY: OCI registry for Helm charts - APP_CHART_VERSION: Semantic version range for chart - APP_IMAGE_REGISTRY: Docker image registry - APP_IMAGE_VERSION: Docker image tag - CLUSTER_FQDN: Cluster domain name - CLUSTER_ENV: Environment name

The test kustomization includes ../deployment/base, allowing you to validate the ResourceSet locally before deploying to the cluster.

References