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:
- OCIRepository: Fetches the Helm chart from Google Artifact Registry
- Polls every 5 minutes for chart updates
- Uses semantic versioning from input provider
-
Chart URL:
<<inputs.tenant_config.app.chart.registry>>/hello-world -
HelmRelease: Manages the deployment with automatic reconciliation
- Reconciles every 5 minutes
- Creates namespace if needed
- Retries installation/upgrade failures up to 3 times
- 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: truein 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:
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 labelsimage: Docker image registry, repository, and tagimagePullSecrets: List of secrets for private registry accessreplicaCount: Number of pod replicas (3 by default)service.enabled: Enable/disable Service creationingress.enabled: Enable/disable Ingress creationingress.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
- Base Image:
python:3.11-slim(minimal Python runtime) - Content: Copies the
site/directory (built MkDocs HTML) - Server: Runs Python's built-in HTTP server on port 8000
- Purpose: Serves static documentation with zero dependencies
Image Registry
- Repository:
europe-west3-docker.pkg.dev/digitalgedacht/docker/digitalgedacht/hello-world - Tags: Both
latestand 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:
The build process:
docs-build: Builds MkDocs site →site/directorybuild:- Syncs Chart.yaml version with
pyproject.tomlusingyq - Packages Helm chart
- Builds Docker image with version tags (
latest+ semantic version) 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:
Release workflow:
- Bump version:
mise bump_version - Increments patch version (0.1.0 → 0.1.1)
- Updates
pyproject.tomlanduv.lock -
Creates git commit automatically
-
Create prerelease:
mise release - Validates no uncommitted changes
- Checks version tag doesn't already exist
- Creates and pushes git tag
-
Creates GitHub prerelease with auto-generated notes
-
CI/CD builds artifacts:
- GitHub Actions release workflow triggers automatically
- Builds Docker image and Helm chart
- Pushes to registries
-
Attaches Helm chart and documentation as release assets
-
Test and finalize:
- Test the prerelease in your environment
- 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.tomlversion - Pushes tag to origin
- Creates GitHub prerelease with auto-generated notes via
ghCLI
Artifacts attached to release:
hello-world-VERSION.tgz- Helm chartsite-VERSION.tar.gz- Documentation site archive
Version synchronization:
pyproject.toml(source of truth) →Chart.yaml(viayq) → Docker tags → Helm chart filename
FluxCD Reconciliation
FluxCD continuously monitors for changes:
- OCIRepository: Polls OCI registry every 5 minutes for new chart versions
- HelmRelease: Reconciles every 5 minutes, applying latest chart with environment-specific values
- 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.