Skip to content
← Advanced DevOps

Practical lab guide

Lab: run the MicroBank baseline in kind

Package the existing local application as Kubernetes workloads and verify a real deposit.

Documentation reviewed2026-10-01 · 6 min read · lab time varies
On this page

Prerequisites and outcome

Complete the seven DevOps Foundations implementation labs. Keep their Accounts startup refactor, Dockerfiles, Terraform configuration, Ansible template, and Python probe. Work in that MicroBank checkout, not in the LearnWithSK repository.

Use the dedicated cluster from Architecture and kind. Follow the implementation steps below and record what happens in your environment. The deployment remains unverified until you run and check it.

Your goal is to reproduce the deposit-to-Ledger flow from Foundations. The optional Auth0 UI is outside this baseline. Backend authorization and Accounts settlement consumption remain missing from this baseline. Use only local synthetic data.

1. Select images and free the previous profile

Use the already-built images from Foundations. Check that STUDY_TAG names the actual build, then stop only that Compose project. This releases loopback ports 4566, 8000, and 8001 without deleting its data.

bash
: "${STUDY_TAG:?Select the previously built source revision}"
: "${LOCALSTACK_IMAGE:?Select your reviewed LocalStack image tag or digest}"
./scripts/study/compose.sh stop
kind load docker-image --name microbank-advanced \
  "microbank-study/accounts:$STUDY_TAG" "microbank-study/ledger:$STUDY_TAG"
mkdir -p infra/study/k8s .local/k8s-runtime .local/k8s-terraform evidence
kubectl --context kind-microbank-advanced create namespace microbank
kubectl --context kind-microbank-advanced annotate namespace microbank \
  learning.learnwithsk.dev/local-only=true

If the namespace already exists, inspect its owner and contents before continuing; do not overwrite an unrelated namespace. Keep .local/, Terraform state, runtime files, and evidence out of Git. LocalStack image support/licensing and CPU architecture must be checked as in Foundations. Do not introduce real AWS credentials.

2. Generate explicit local manifests

Save scripts/study/k8s_baseline.py. JSON is accepted as a Kubernetes manifest and avoids requiring an extra YAML package for this generator. Review the generated objects before applying them.

python
import json
import os
from pathlib import Path

ROOT = Path('infra/study/k8s')
ROOT.mkdir(parents=True, exist_ok=True)
TAG = os.environ['STUDY_TAG']
LOCALSTACK = os.environ['LOCALSTACK_IMAGE']
if not TAG or not LOCALSTACK:
    raise SystemExit('Select actual application and emulator images')


def service(name, port):
    return {'apiVersion': 'v1', 'kind': 'Service', 'metadata': {'name': name},
            'spec': {'selector': {'app': name}, 'ports': [{'port': port, 'targetPort': port}]}}


def pvc(name):
    return {'apiVersion': 'v1', 'kind': 'PersistentVolumeClaim',
            'metadata': {'name': name},
            'spec': {'accessModes': ['ReadWriteOnce'],
                     'resources': {'requests': {'storage': '1Gi'}}}}


def workload(name, image, port, request_memory, limit_memory, env=None,
             secret=None, volume=None, probe=None):
    container = {'name': name, 'image': image, 'imagePullPolicy': 'IfNotPresent',
                 'ports': [{'containerPort': port}],
                 'resources': {'requests': {'cpu': '100m', 'memory': request_memory},
                               'limits': {'memory': limit_memory}}}
    if env:
        container['env'] = [{'name': key, 'value': value} for key, value in env.items()]
    if secret:
        container['envFrom'] = [{'secretRef': {'name': secret}}]
    if probe:
        container['startupProbe'] = {**probe, 'periodSeconds': 5, 'timeoutSeconds': 3,
                                     'failureThreshold': 36}
        container['readinessProbe'] = {**probe, 'periodSeconds': 5, 'timeoutSeconds': 3}
    pod = {'automountServiceAccountToken': False, 'containers': [container]}
    if volume:
        claim, path = volume
        container['volumeMounts'] = [{'name': 'data', 'mountPath': path}]
        pod['volumes'] = [{'name': 'data', 'persistentVolumeClaim': {'claimName': claim}}]
    return {'apiVersion': 'apps/v1', 'kind': 'Deployment', 'metadata': {'name': name},
            'spec': {'replicas': 1, 'strategy': {'type': 'Recreate'},
                     'selector': {'matchLabels': {'app': name}},
                     'template': {'metadata': {'labels': {'app': name}}, 'spec': pod}}}


infra = []
for name, database in [('accounts-db', 'accounts_dev'), ('ledger-db', 'ledger_dev')]:
    infra.extend([
        pvc(name), service(name, 5432),
        workload(name, 'postgres:15-bookworm', 5432, '128Mi', '512Mi',
                 env={'POSTGRES_DB': database, 'POSTGRES_USER': 'postgres'},
                 secret='database-fixture', volume=(name, '/var/lib/postgresql/data'),
                 probe={'exec': {'command': ['pg_isready', '-U', 'postgres', '-d', database]}})
    ])
infra.extend([
    pvc('localstack'), service('localstack', 4566),
    workload('localstack', LOCALSTACK, 4566, '512Mi', '1536Mi',
             env={'SERVICES': 'sns,sqs,s3', 'SQS_ENDPOINT_STRATEGY': 'path', 'PERSISTENCE': '1'},
             secret='localstack-runtime', volume=('localstack', '/var/lib/localstack'),
             probe={'httpGet': {'path': '/_localstack/health', 'port': 4566}})
])
apps = [
    service('accounts', 8000),
    workload('accounts', f'microbank-study/accounts:{TAG}', 8000, '256Mi', '512Mi',
             secret='microbank-runtime', probe={'httpGet': {'path': '/health', 'port': 8000}}),
    service('ledger', 8001),
    workload('ledger', f'microbank-study/ledger:{TAG}', 8001, '512Mi', '1Gi',
             env={'SPRING_JPA_HIBERNATE_DDL_AUTO': 'update',
                  'JAVA_TOOL_OPTIONS': '-XX:MaxRAMPercentage=60'},
             secret='microbank-runtime', probe={'httpGet': {'path': '/v1/health', 'port': 8001}})
]
for filename, items in [('infra.json', infra), ('apps.json', apps)]:
    ROOT.joinpath(filename).write_text(json.dumps(
        {'apiVersion': 'v1', 'kind': 'List', 'items': items}, indent=2) + '\n')

These are single-replica, Recreate lab workloads. The database fixture is not HA, and kind's default storage does not survive cluster deletion. The memory values are starting limits; measure actual usage before adjusting them. A PVC requires a working default StorageClass; inspect it before proceeding. A replacement Accounts Pod does not overlap the old publisher in this baseline.

bash
python3 scripts/study/k8s_baseline.py
kubectl --context kind-microbank-advanced get storageclass
kubectl --context kind-microbank-advanced -n microbank create secret generic database-fixture \
  --from-literal=POSTGRES_PASSWORD=study-only

Create .local/localstack.env, mode 600, containing LOCALSTACK_AUTH_TOKEN= with your token if required by the selected image. An empty value is appropriate only if that selected image/setup supports it. The secret below is local-only and never committed:

bash
chmod 600 .local/localstack.env
kubectl --context kind-microbank-advanced -n microbank create secret generic localstack-runtime \
  --from-env-file=.local/localstack.env
kubectl --context kind-microbank-advanced -n microbank apply -f infra/study/k8s/infra.json
kubectl --context kind-microbank-advanced -n microbank get pvc,pods
kubectl --context kind-microbank-advanced -n microbank rollout status deploy/accounts-db --timeout=180s
kubectl --context kind-microbank-advanced -n microbank rollout status deploy/ledger-db --timeout=180s
kubectl --context kind-microbank-advanced -n microbank rollout status deploy/localstack --timeout=180s

If a PVC remains Pending, solve the storage issue before starting the application. Do not remove persistence just to hide the failure.

3. Bootstrap the separate emulator instance

In a separate terminal, keep this loopback-only forwarding process running:

bash
kubectl --context kind-microbank-advanced -n microbank port-forward \
  --address 127.0.0.1 service/localstack 4566:4566

Reuse the reviewed Foundations Terraform configuration, but not its state: the Kubernetes emulator is a different instance. These commands assume the Foundations provider lock file already exists.

bash
cp infra/study/terraform/main.tf .local/k8s-terraform/main.tf
cp infra/study/terraform/.terraform.lock.hcl .local/k8s-terraform/.terraform.lock.hcl
terraform -chdir=.local/k8s-terraform init
terraform -chdir=.local/k8s-terraform plan -out=local.tfplan
terraform -chdir=.local/k8s-terraform apply local.tfplan
terraform -chdir=.local/k8s-terraform output -json runtime > .local/k8s-runtime/runtime.json
ansible-playbook -i localhost, infra/study/ansible/configure.yml \
  -e "study_dir=$PWD/.local/k8s-runtime"
kubectl --context kind-microbank-advanced -n microbank create secret generic microbank-runtime \
  --from-env-file=.local/k8s-runtime/runtime.env

The existing template works because the Kubernetes Services deliberately retain accounts-db, ledger-db, and localstack names. SNS/SQS names and the path-style queue URL contract also remain the same. Only the disposable runtime instance and its Terraform state differ. Keep the generated files private.

4. Start and verify the real applications

bash
kubectl --context kind-microbank-advanced -n microbank apply -f infra/study/k8s/apps.json
kubectl --context kind-microbank-advanced -n microbank rollout status deploy/accounts --timeout=180s
kubectl --context kind-microbank-advanced -n microbank rollout status deploy/ledger --timeout=180s

Keep each forwarding command in its own terminal, then run the unchanged Foundations probe from another terminal:

bash
kubectl --context kind-microbank-advanced -n microbank port-forward \
  --address 127.0.0.1 service/accounts 8000:8000
bash
kubectl --context kind-microbank-advanced -n microbank port-forward \
  --address 127.0.0.1 service/ledger 8001:8001
bash
python3 scripts/study/probe.py --create --case-file evidence/kind-case.json
python3 scripts/study/probe.py --verify evidence/kind-case.json
kubectl --context kind-microbank-advanced -n microbank get pods \
  -o custom-columns=NAME:.metadata.name,IMAGE:.status.containerStatuses[*].imageID

Acceptance is a real ledger_verified: true result with one matching entry and a 1,000-cent balance for the fresh account. Accounts may still report pending. Port-forwards select a Pod and can terminate when it is replaced; restart them after rollouts. Do not treat a stale forwarding process as proof the Service is broken.

Checkpoint, recovery, and cleanup

Keep versions, rendered manifests, image IDs, resource observations, case file, and the actual result. If any step fails, stop and use the troubleshooting bite. Do not mark the lab verified from YAML parsing alone.

Continue using this baseline for later modules. When finished with the entire track, stop scheduled checks and optional tools, destroy these emulator resources through this separate Terraform directory while forwarding still works, then remove only the named kind cluster. Cluster deletion destroys its local volumes. Preserve any needed evidence first; do not copy the lost emulator's state into a new instance.

Read the generator as a design you now understand

Before running it, find the same links you practised in classroom: each Service selector matches Pod labels, target ports match listeners, and the Pod template receives configuration. The extra pieces are persistent claims, private runtime inputs, and dependencies that must exist first. infra.json supplies databases/emulator; apps.json supplies the APIs. Keeping them separate is what makes the later GitOps ownership exercise understandable.

Treat each wait as a local checkpoint. PVCs Bound and processes Ready permit the next inspection; they do not prove the final deposit. If rollout passes but the probe fails, follow the transaction across Accounts, outbox, queue, and Ledger using the earlier design rather than rerunning the entire setup. Save the same-case result for GitOps and observability.

Sources

MicroBank baseline source↗, kind image loading↗, PVC behavior↗, port forwarding↗, and LocalStack SQS URL strategy↗.

Your notes and evidence

Record observations, questions, or links to your work. Keep credentials out of your notes.

Loading saved progress…

Back up or restore this path

Progress and notes stay in this browser. A backup contains only this learning path.