Skip to content

Temporal Phase 1 Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Deploy Temporal server infrastructure and create the temporal-workers repository with a working hello-world workflow.

Architecture: Temporal server deployed via Helm (ArgoCD-managed), PostgreSQL on existing CNPG cluster, Worker Controller for rainbow deployments, workers in separate repository.

Tech Stack: Temporal 0.73.1, Python 3.12+, uv, PostgreSQL 16, ArgoCD, External Secrets, Traefik


Tracking Issue: selfhosted-cluster#548 (closed)

TaskPR/CommitNotes
Task 1: PostgreSQL DatabasePR #533CNPG Database + ExternalSecret
Task 2: Vault PolicyPR #533temporal-worker policy + K8s auth role
Task 3: Temporal ServerPR #533Helm chart via ArgoCD
Task 4: Worker ControllerPR #533TWC deployed
Task 5: temporal-workers repoRepository created
Task 6: worker-core packageHello workflow + activity
Task 7: Dockerfile + CIPR #3Multi-arch builds (ARM64/AMD64)
Task 8: K8s CRDsTemporalWorkerDeployment
Task 9: ArgoCD ApplicationPR #547, #550GHCR pull secret + main branch tracking
Task 10: VerificationAll checks passed
CheckStatusDetails
Worker pod runningv0.3.0 running, 0 restarts
HelloWorkflow execution"Hello, World!" in 230ms
Web UI accesstemporal.fzymgc.house with Authentik SSO
IssueResolution
temporal-workers#12Fixed in v0.3.0 — Dockerfile and uv workspace issues
Visibility schema not appliedCreated separate temporal_visibility database — single database caused schema version collision (default store v1.18 blocked visibility schema)

  • Access to fzymgc-house Kubernetes cluster
  • Vault access with infrastructure-developer policy
  • GitHub CLI authenticated (gh auth status)

Task 1: Create PostgreSQL Database for Temporal

Section titled “Task 1: Create PostgreSQL Database for Temporal”

Files:

  • Create: argocd/app-configs/cnpg/db-temporal.yaml
  • Create: argocd/app-configs/cnpg/users-temporal.yaml
  • Modify: argocd/app-configs/cnpg/kustomization.yaml

Step 1: Create Vault secrets for Temporal database user

Run:

Terminal window
vault kv put secret/fzymgc-house/cluster/postgres/users/main-temporal \
username=temporal \
password="$(openssl rand -base64 24)"

Expected: Success! Data written to: secret/fzymgc-house/cluster/postgres/users/main-temporal

Step 2: Create the database manifest

Create argocd/app-configs/cnpg/db-temporal.yaml:

apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: temporal
namespace: postgres
spec:
cluster:
name: main
ensure: present
name: temporal
owner: temporal

Step 3: Create the ExternalSecret for database credentials

Create argocd/app-configs/cnpg/users-temporal.yaml:

---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: main-temporal-credentials
namespace: postgres
spec:
refreshPolicy: Periodic
refreshInterval: 5m
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: main-temporal-credentials
creationPolicy: Owner
deletionPolicy: Delete
template:
metadata:
labels:
cnpg.io/reload: "true"
type: kubernetes.io/basic-auth
data:
username: "{{ .username }}"
password: "{{ .password }}"
data:
- secretKey: username
remoteRef:
key: fzymgc-house/cluster/postgres/users/main-temporal
property: username
- secretKey: password
remoteRef:
key: fzymgc-house/cluster/postgres/users/main-temporal
property: password

Step 4: Add to kustomization

Edit argocd/app-configs/cnpg/kustomization.yaml to add:

resources:
# ... existing resources ...
- db-temporal.yaml
- users-temporal.yaml

Step 5: Commit

Terminal window
git add argocd/app-configs/cnpg/db-temporal.yaml \
argocd/app-configs/cnpg/users-temporal.yaml \
argocd/app-configs/cnpg/kustomization.yaml
git commit -m "feat(cnpg): add Temporal database and user credentials"

Files:

  • Create: tf/vault/policy-temporal.tf

Step 1: Create Vault policy for Temporal workers

Create tf/vault/policy-temporal.tf:

# Temporal worker Vault policy
# Allows workers to read secrets for Discord, GitHub, S3, etc.
resource "vault_policy" "temporal_worker" {
name = "temporal-worker"
policy = <<-EOT
# Read Temporal-specific secrets
path "secret/data/fzymgc-house/cluster/temporal/*" {
capabilities = ["read"]
}
# Read Discord bot credentials
path "secret/data/fzymgc-house/cluster/discord/*" {
capabilities = ["read"]
}
# Read GitHub tokens
path "secret/data/fzymgc-house/cluster/github/*" {
capabilities = ["read"]
}
# Read S3/R2 credentials
path "secret/data/fzymgc-house/cluster/cloudflare/r2/*" {
capabilities = ["read"]
}
EOT
}
# Kubernetes auth role for Temporal workers
resource "vault_kubernetes_auth_backend_role" "temporal_worker" {
backend = vault_auth_backend.kubernetes.path
role_name = "temporal-worker"
bound_service_account_names = ["temporal-worker"]
bound_service_account_namespaces = ["temporal"]
token_policies = [vault_policy.temporal_worker.name]
token_ttl = 3600
}

Step 2: Apply Terraform

Run:

Terminal window
cd tf/vault
terraform plan -out=tfplan
terraform apply tfplan

Expected: Apply complete! Resources: 2 added

Step 3: Commit

Terminal window
git add tf/vault/policy-temporal.tf
git commit -m "feat(vault): add Temporal worker policy and Kubernetes auth role"

Task 3: Create Temporal Server ArgoCD Application

Section titled “Task 3: Create Temporal Server ArgoCD Application”

Files:

  • Create: argocd/app-configs/temporal-server/namespace.yaml
  • Create: argocd/app-configs/temporal-server/secrets.yaml
  • Create: argocd/app-configs/temporal-server/certificate.yaml
  • Create: argocd/app-configs/temporal-server/ingress.yaml
  • Create: argocd/app-configs/temporal-server/kustomization.yaml
  • Create: argocd/cluster-app/templates/temporal-server.yaml

Step 1: Create namespace manifest

Create argocd/app-configs/temporal-server/namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
name: temporal

Step 2: Create ExternalSecret for database connection

Create argocd/app-configs/temporal-server/secrets.yaml:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: temporal-db-secret
namespace: temporal
spec:
refreshPolicy: Periodic
refreshInterval: 5m
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: temporal-db-secret
creationPolicy: Owner
deletionPolicy: Delete
data:
- secretKey: password
remoteRef:
key: fzymgc-house/cluster/postgres/users/main-temporal
property: password

Step 3: Create TLS certificate

Create argocd/app-configs/temporal-server/certificate.yaml:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: temporal-tls
namespace: temporal
spec:
secretName: temporal-tls
issuerRef:
name: cloudflare-acme-issuer
kind: ClusterIssuer
commonName: temporal.fzymgc.house
dnsNames:
- temporal.fzymgc.house
usages:
- server auth

Step 4: Create ingress for Web UI

Create argocd/app-configs/temporal-server/ingress.yaml:

---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: temporal-auth
namespace: temporal
spec:
forwardAuth:
address: http://authentik-server.authentik.svc/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
- Remote-Email
- Remote-Name
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: temporal-web
namespace: temporal
annotations:
router-hosts.fzymgc.house/enabled: "true"
spec:
entryPoints:
- websecure
routes:
- match: Host(`temporal.fzymgc.house`)
kind: Rule
middlewares:
- name: temporal-auth
services:
- name: temporal-web
port: 8080
tls:
secretName: temporal-tls

Step 5: Create kustomization

Create argocd/app-configs/temporal-server/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- secrets.yaml
- certificate.yaml
- ingress.yaml

Step 6: Create ArgoCD Application

Create argocd/cluster-app/templates/temporal-server.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: temporal-server
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: core-services
sources:
- chart: temporal
repoURL: https://go.temporal.io/helm-charts
targetRevision: "0.73.1"
helm:
releaseName: temporal
valuesObject:
server:
replicaCount:
frontend: 1
history: 1
matching: 1
worker: 1
config:
persistence:
defaultStore: default
visibilityStore: visibility
numHistoryShards: 128
datastores:
default:
sql:
pluginName: postgres12
driverName: postgres12
databaseName: temporal
connectAddr: "main-rw.postgres.svc.cluster.local:5432"
connectProtocol: tcp
user: temporal
existingSecret: temporal-db-secret
secretKey: password
maxConns: 20
maxIdleConns: 20
maxConnLifetime: "1h"
visibility:
sql:
pluginName: postgres12
driverName: postgres12
databaseName: temporal
connectAddr: "main-rw.postgres.svc.cluster.local:5432"
connectProtocol: tcp
user: temporal
existingSecret: temporal-db-secret
secretKey: password
maxConns: 10
maxIdleConns: 10
maxConnLifetime: "1h"
web:
enabled: true
replicaCount: 1
admintools:
enabled: true
# Disable sub-charts - we use external PostgreSQL
cassandra:
enabled: false
mysql:
enabled: false
postgresql:
enabled: false
elasticsearch:
enabled: false
# Prometheus metrics
prometheus:
enabled: true
- repoURL: https://github.com/fzymgc-house/selfhosted-cluster
targetRevision: HEAD
path: argocd/app-configs/temporal-server
destination:
server: https://kubernetes.default.svc
namespace: temporal
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=true
- CreateNamespace=true
- RespectIgnoreDifferences=true
ignoreDifferences:
- kind: ExternalSecret
group: external-secrets.io
jqPathExpressions:
- .spec.data[].remoteRef.conversionStrategy
- .spec.data[].remoteRef.decodingStrategy
- .spec.data[].remoteRef.metadataPolicy

Step 7: Commit

Terminal window
git add argocd/app-configs/temporal-server/ \
argocd/cluster-app/templates/temporal-server.yaml
git commit -m "feat(temporal): add Temporal server ArgoCD application"

Files:

  • Create: argocd/cluster-app/templates/temporal-worker-controller.yaml

Step 1: Create Worker Controller ArgoCD Application

Create argocd/cluster-app/templates/temporal-worker-controller.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: temporal-worker-controller
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: core-services
source:
chart: temporal-worker-controller
repoURL: oci://docker.io/temporalio/helm-charts
targetRevision: "1.0.0"
helm:
releaseName: temporal-worker-controller
valuesObject:
# Controller connects to Temporal frontend
temporal:
address: temporal-frontend.temporal.svc.cluster.local:7233
namespace: workflows
destination:
server: https://kubernetes.default.svc
namespace: temporal
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=true

Step 2: Commit

Terminal window
git add argocd/cluster-app/templates/temporal-worker-controller.yaml
git commit -m "feat(temporal): add Worker Controller for rainbow deployments"

Task 5: Create temporal-workers Repository

Section titled “Task 5: Create temporal-workers Repository”

Step 1: Create the repository on GitHub

Run:

Terminal window
gh repo create fzymgc-house/temporal-workers \
--private \
--description "Temporal workflow workers for fzymgc-house automation" \
--clone

Expected: Created repository fzymgc-house/temporal-workers

Step 2: Initialize uv workspace

Run:

Terminal window
cd temporal-workers
uv init --no-readme

Step 3: Create workspace pyproject.toml

Replace pyproject.toml:

[project]
name = "temporal-workers"
version = "0.1.0"
description = "Temporal workflow workers for fzymgc-house automation"
readme = "README.md"
requires-python = ">=3.12"
[tool.uv.workspace]
members = ["packages/*"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

Step 4: Create directory structure

Run:

Terminal window
mkdir -p packages/common/src/temporal_common
mkdir -p packages/worker-core/src/worker_core/activities
mkdir -p packages/worker-core/src/worker_core/workflows
mkdir -p tests
mkdir -p docker
mkdir -p k8s/base k8s/crds
mkdir -p .github/workflows

Step 5: Create common package pyproject.toml

Create packages/common/pyproject.toml:

[project]
name = "temporal-common"
version = "0.1.0"
description = "Shared utilities for Temporal workers"
requires-python = ">=3.12"
dependencies = [
"temporalio>=1.9.0",
"pydantic>=2.0",
"pydantic-settings>=2.0",
"hvac>=2.0",
"opentelemetry-api>=1.20",
"opentelemetry-sdk>=1.20",
"opentelemetry-exporter-otlp>=1.20",
"opentelemetry-instrumentation>=0.40",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/temporal_common"]

Step 6: Create common/init.py

Create packages/common/src/temporal_common/__init__.py:

"""Shared utilities for Temporal workers."""
from temporal_common.client import get_temporal_client
from temporal_common.settings import Settings
__all__ = ["get_temporal_client", "Settings"]

Step 7: Create settings module

Create packages/common/src/temporal_common/settings.py:

"""Pydantic settings for worker configuration."""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Worker configuration loaded from environment."""
model_config = SettingsConfigDict(env_prefix="TEMPORAL_")
# Temporal connection
host: str = "localhost:7233"
namespace: str = "workflows"
task_queue: str = "default"
# Observability
otel_endpoint: str = "http://localhost:4317"
otel_service_name: str = "temporal-worker"
# Vault (optional, for dynamic secrets)
vault_addr: str = ""
vault_role: str = "temporal-worker"

Step 8: Create client module

Create packages/common/src/temporal_common/client.py:

"""Temporal client factory with OpenTelemetry instrumentation."""
from temporalio.client import Client
from temporalio.contrib.opentelemetry import TracingInterceptor
from temporal_common.observability import setup_telemetry
from temporal_common.settings import Settings
async def get_temporal_client(settings: Settings | None = None) -> Client:
"""Create a Temporal client with tracing enabled."""
if settings is None:
settings = Settings()
interceptor = setup_telemetry(settings)
return await Client.connect(
settings.host,
namespace=settings.namespace,
interceptors=[interceptor] if interceptor else [],
)

Step 9: Create observability module

Create packages/common/src/temporal_common/observability.py:

"""OpenTelemetry setup for Temporal workers."""
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from temporalio.contrib.opentelemetry import TracingInterceptor
from temporal_common.settings import Settings
def setup_telemetry(settings: Settings) -> TracingInterceptor | None:
"""Configure OpenTelemetry and return Temporal interceptor."""
if not settings.otel_endpoint:
return None
resource = Resource.create({"service.name": settings.otel_service_name})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(endpoint=settings.otel_endpoint, insecure=True)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return TracingInterceptor()

Step 10: Commit common package

Terminal window
git add packages/common/
git commit -m "feat(common): add shared utilities package"

Step 1: Create worker-core pyproject.toml

Create packages/worker-core/pyproject.toml:

[project]
name = "worker-core"
version = "0.1.0"
description = "Core Temporal worker with basic activities"
requires-python = ">=3.12"
dependencies = [
"temporal-common",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/worker_core"]

Step 2: Create worker-core init.py

Create packages/worker-core/src/worker_core/__init__.py:

"""Core Temporal worker."""

Step 3: Create hello world activity

Create packages/worker-core/src/worker_core/activities/__init__.py:

"""Core activities."""
from worker_core.activities.hello import say_hello
__all__ = ["say_hello"]

Create packages/worker-core/src/worker_core/activities/hello.py:

"""Hello world activity for testing."""
from temporalio import activity
@activity.defn
async def say_hello(name: str) -> str:
"""Simple hello world activity."""
activity.logger.info(f"Saying hello to {name}")
return f"Hello, {name}!"

Step 4: Create hello world workflow

Create packages/worker-core/src/worker_core/workflows/__init__.py:

"""Core workflows."""
from worker_core.workflows.hello import HelloWorkflow
__all__ = ["HelloWorkflow"]

Create packages/worker-core/src/worker_core/workflows/hello.py:

"""Hello world workflow for testing."""
from datetime import timedelta
from temporalio import workflow
with workflow.unsafe.imports_passed_through():
from worker_core.activities import say_hello
@workflow.defn
class HelloWorkflow:
"""Simple hello world workflow."""
@workflow.run
async def run(self, name: str) -> str:
"""Execute the workflow."""
return await workflow.execute_activity(
say_hello,
name,
start_to_close_timeout=timedelta(seconds=10),
)

Step 5: Create worker main entry point

Create packages/worker-core/src/worker_core/main.py:

"""Worker entry point."""
import asyncio
import logging
from temporalio.worker import Worker
from temporal_common import Settings, get_temporal_client
from worker_core.activities import say_hello
from worker_core.workflows import HelloWorkflow
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def main() -> None:
"""Run the worker."""
settings = Settings()
logger.info(f"Starting worker, connecting to {settings.host}")
client = await get_temporal_client(settings)
worker = Worker(
client,
task_queue=settings.task_queue,
workflows=[HelloWorkflow],
activities=[say_hello],
)
logger.info(f"Worker started on task queue: {settings.task_queue}")
await worker.run()
if __name__ == "__main__":
asyncio.run(main())

Step 6: Commit worker-core package

Terminal window
git add packages/worker-core/
git commit -m "feat(worker-core): add hello world workflow and activity"

Task 7: Create Dockerfile and GitHub Actions

Section titled “Task 7: Create Dockerfile and GitHub Actions”

Step 1: Create Dockerfile for worker-core

Create docker/Dockerfile.core:

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
# Copy workspace files
COPY pyproject.toml uv.lock ./
COPY packages/common packages/common
COPY packages/worker-core packages/worker-core
# Install dependencies
RUN uv sync --frozen --no-dev
FROM python:3.12-slim
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /app/.venv /app/.venv
# Set PATH to use venv
ENV PATH="/app/.venv/bin:$PATH"
# Copy application code
COPY packages/common/src packages/common/src
COPY packages/worker-core/src packages/worker-core/src
# Run worker
CMD ["python", "-m", "worker_core.main"]

Step 2: Create GitHub Actions workflow

Create .github/workflows/build.yaml:

name: Build and Push Workers
on:
push:
branches: [main]
paths:
- 'packages/**'
- 'docker/**'
- '.github/workflows/build.yaml'
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
worker: [core]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_PREFIX }}/${{ matrix.worker }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.${{ matrix.worker }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

Step 3: Create .gitignore

Create .gitignore:

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
.uv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build
dist/
build/
*.egg-info/

Step 4: Create README

Create README.md:

# Temporal Workers
Temporal workflow workers for fzymgc-house automation.
## Development
```bash
# Install dependencies
uv sync
# Run worker locally (requires local Temporal server)
temporal server start-dev &
uv run python -m worker_core.main
  • common - Shared utilities (client, settings, observability)
  • worker-core - Core activities (Discord, GitHub, S3)
  • worker-terraform - Terraform operations (planned)
  • worker-llm - LLM/Agent activities (planned)
**Step 5: Sync and commit**
```bash
uv sync
git add docker/ .github/ .gitignore README.md uv.lock
git commit -m "feat: add Dockerfile and GitHub Actions workflow"

Step 6: Push to GitHub

Terminal window
git push -u origin main

Task 8: Create TemporalWorkerDeployment CRD

Section titled “Task 8: Create TemporalWorkerDeployment CRD”

Files:

  • Create: k8s/base/serviceaccount.yaml
  • Create: k8s/crds/worker-core.yaml
  • Create: k8s/base/kustomization.yaml
  • Create: k8s/crds/kustomization.yaml

Step 1: Create service account

Create k8s/base/serviceaccount.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
name: temporal-worker
namespace: temporal

Step 2: Create worker CRD

Create k8s/crds/worker-core.yaml:

apiVersion: temporal.io/v1alpha1
kind: TemporalWorkerDeployment
metadata:
name: worker-core
namespace: temporal
spec:
deploymentName: worker-core
image: ghcr.io/fzymgc-house/temporal-workers/core:latest
temporalNamespace: workflows
taskQueues:
- default
serviceAccountName: temporal-worker
env:
- name: TEMPORAL_HOST
value: temporal-frontend.temporal.svc.cluster.local:7233
- name: TEMPORAL_NAMESPACE
value: workflows
- name: TEMPORAL_TASK_QUEUE
value: default
- name: TEMPORAL_OTEL_ENDPOINT
value: alloy.alloy.svc.cluster.local:4317
resources:
limits:
memory: 512Mi
requests:
memory: 256Mi
cpu: 100m
rollout:
strategy: Progressive
steps:
- rampPercentage: 50
pauseDuration: 1m
scaling:
minReplicas: 1
maxReplicas: 3

Step 3: Create kustomization files

Create k8s/base/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml

Create k8s/crds/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
- worker-core.yaml

Step 4: Commit and push

Terminal window
git add k8s/
git commit -m "feat(k8s): add TemporalWorkerDeployment CRD for worker-core"
git push

Task 9: Create ArgoCD Application for Workers

Section titled “Task 9: Create ArgoCD Application for Workers”

Files (in selfhosted-cluster repo):

  • Create: argocd/app-configs/temporal-workers/secrets.yaml
  • Create: argocd/app-configs/temporal-workers/kustomization.yaml
  • Create: argocd/cluster-app/templates/temporal-workers.yaml

Step 1: Switch to selfhosted-cluster repo

Run:

Terminal window
cd /Volumes/Code/github.com/fzymgc-house/selfhosted-cluster

Step 2: Create worker secrets ExternalSecret

Create argocd/app-configs/temporal-workers/secrets.yaml:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: temporal-worker-secrets
namespace: temporal
spec:
refreshPolicy: Periodic
refreshInterval: 5m
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: temporal-worker-secrets
creationPolicy: Owner
deletionPolicy: Delete
data:
- secretKey: DISCORD_BOT_TOKEN
remoteRef:
key: fzymgc-house/cluster/discord/bot
property: token
- secretKey: GITHUB_TOKEN
remoteRef:
key: fzymgc-house/cluster/github/api
property: token

Step 3: Create kustomization

Create argocd/app-configs/temporal-workers/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml

Step 4: Create ArgoCD Application

Create argocd/cluster-app/templates/temporal-workers.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: temporal-workers
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "3"
spec:
project: core-services
sources:
# Worker CRDs from temporal-workers repo
- repoURL: https://github.com/fzymgc-house/temporal-workers
targetRevision: HEAD
path: k8s/crds
# Secrets from selfhosted-cluster
- repoURL: https://github.com/fzymgc-house/selfhosted-cluster
targetRevision: HEAD
path: argocd/app-configs/temporal-workers
destination:
server: https://kubernetes.default.svc
namespace: temporal
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=true
ignoreDifferences:
- kind: ExternalSecret
group: external-secrets.io
jqPathExpressions:
- .spec.data[].remoteRef.conversionStrategy
- .spec.data[].remoteRef.decodingStrategy
- .spec.data[].remoteRef.metadataPolicy

Step 5: Commit

Terminal window
git add argocd/app-configs/temporal-workers/ \
argocd/cluster-app/templates/temporal-workers.yaml
git commit -m "feat(temporal): add ArgoCD application for Temporal workers"

Task 10: Create Temporal Namespace and Test

Section titled “Task 10: Create Temporal Namespace and Test”

Step 1: Push all changes

Run:

Terminal window
git push

Step 2: Wait for ArgoCD sync

Run:

Terminal window
kubectl --context fzymgc-house get applications -n argocd | grep temporal

Expected: Applications syncing/synced

Step 3: Create workflows namespace in Temporal

Run:

Terminal window
kubectl --context fzymgc-house exec -it -n temporal deploy/temporal-admintools -- \
temporal operator namespace create --namespace workflows

Expected: Namespace workflows successfully registered.

Step 4: Run hello world workflow

Run:

Terminal window
kubectl --context fzymgc-house exec -it -n temporal deploy/temporal-admintools -- \
temporal workflow execute \
--namespace workflows \
--task-queue default \
--type HelloWorkflow \
--input '"World"'

Expected: Result: "Hello, World!"

Step 5: Verify in Web UI

Open: https://temporal.fzymgc.house

Expected: See completed HelloWorkflow execution


  • Database created in CNPG
  • Vault policy applied
  • Temporal server pods running
  • Worker Controller pod running
  • temporal-workers repo created with CI
  • Worker image pushed to GHCR (multi-arch)
  • Worker pod running via Controller (v0.3.0)
  • HelloWorkflow executes successfully ("Hello, World!")
  • Web UI accessible with auth (Authentik SSO)

With Phase 1 complete, potential next steps include:

  1. Add real workflows — Discord notifications, GitHub automation, S3 operations
  2. Create Authentik application — Restrict Web UI access to specific groups (tf/authentik/temporal.tf)
  3. Add observability — Temporal metrics to Grafana dashboards
  4. Worker scaling — Configure HPA based on task queue depth
  5. Documentation — Add Temporal operations guide to docs/operations/temporal.md