Cordium documentation · Latest

CI/CD Pipelines

Cordium Workspaces make capable CI/CD execution environments: each job runs in a fresh, ephemeral sandbox on your own infrastructure, with as much CPU and memory as it needs, shared caches, and secretless access to the private resources that integration tests and deployments need, such as databases, internal APIs and Kubernetes clusters. Since deployments run with the identity of the job's Workspace, there is no kubeconfig, cloud credential or database password to store in your CI system.

This example builds a test-and-deploy pipeline for a Go service that can be driven by any CI system, such as GitHub Actions, GitLab CI, Jenkins or Buildkite.

The Test Template

The ci Template checks out the commit to test when the Workspace is created, and reuses the Go caches across runs via a shared Volume (read more about variables here and about caches here):

spec: vars: - name: REF value: main image: registry: url: golang:1.26-bookworm repository: url: https://github.com/acme-corp/payments-api cloneOptions: branch: main disableLazyUnshallow: true authentication: http: username: x-access-token password: fromSecret: github-read-token.payments.cordium runtime: volumeMounts: - volumeRef: name: go-cache mountPath: /cache envVars: - key: GOMODCACHE value: /cache/mod - key: GOCACHE value: /cache/build tasks: - name: checkout type: ON_CREATE workingDir: /workspace/repo onFailure: ON_FAILURE_ABORT run: | git fetch -q --depth 1 origin "${{ vars.REF }}" git checkout -q FETCH_HEAD git log -1 --oneline limit: cpu: millicores: 8000 memory: megabytes: 16384 storage: megabytes: 30000
cordium create volume go-cache.payments.cordium --size 30000 --shared cordium create template ci.payments.cordium --file ci.yaml

REF can be a branch, a tag or a commit SHA, since the checkout task fetches it explicitly.

Running Jobs from Any CI System

The following Python script is all that a CI job needs to run. It starts an ephemeral Workspace for the commit, runs the pipeline's steps in it via exec while streaming their output to the CI job's log, stops at the first failing step with its exit code, and always deletes the Workspace (read more about authenticating CI jobs here and here):

import sys from cordium import Cordium STEPS = [ "go vet ./...", "go test -race -count 1 ./...", "go build -o /tmp/payments-api ./cmd/server", ] def main(ref: str) -> int: with Cordium() as client: workspace = client.workspaces.run( template="ci.payments.cordium", ephemeral=True, display_name=f"CI {ref[:12]}", vars={"REF": ref}, timeout=900, ) try: for step in STEPS: print(f"::group::{step}", flush=True) with workspace.exec_stream( step, cwd="/workspace/repo", interactive=False, timeout=3600 ) as session: for chunk in session: out = sys.stdout if chunk.stream == "stdout" else sys.stderr out.buffer.write(chunk.data) out.flush() result = session.wait() print("::endgroup::", flush=True) if result.exit_code != 0: print(f"{step} failed with exit code {result.exit_code}", file=sys.stderr) return result.exit_code return 0 finally: workspace.delete() if __name__ == "__main__": sys.exit(main(sys.argv[1]))
python ci_run.py "$GIT_COMMIT_SHA"

Since each step is a separate exec session, you get the exact exit code and output of every step, while the Workspace's CPU, memory, caches and network access are shared by all of them. The ::group:: lines fold the output of each step in GitHub Actions and are harmless in other CI systems.

Secretless Deployments

Deployment jobs need access to production-like infrastructure, which is exactly where long-lived CI credentials are the most dangerous. Instead, a Cluster administrator exposes the staging Kubernetes cluster as an Octelium KUBERNETES Service named k8s-staging (read more here), and only allows the Workspaces of the deploy Template to use it, and only within the payments Kubernetes namespace:

kind: Policy metadata: name: deploy-to-staging spec: rules: - effect: ALLOW condition: all: of: - match: ctx.service.metadata.name == "k8s-staging.default" - match: '"ext" in ctx.session.status && "cordium" in ctx.session.status.ext' - match: ctx.session.status.ext.cordium.templateRef.name == "deploy.payments.cordium" - match: ctx.request.kubernetes.namespace == "payments"

The deploy Template, which uses Cordium's default Ubuntu-based image, then rolls out a new image with kubectl, which is authenticated by Octelium on a per-request basis. Since it is an auto-stopping POST_START task with ON_FAILURE_ABORT, the run fails if the rollout fails:

spec: vars: - name: IMAGE value: ghcr.io/acme-corp/payments-api:latest runtime: autoStop: true tasks: - name: install-kubectl type: ON_CREATE runAsRoot: true onFailure: ON_FAILURE_ABORT run: | VERSION="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" curl -fsSLo /usr/local/bin/kubectl "https://dl.k8s.io/release/$VERSION/bin/linux/$(dpkg --print-architecture)/kubectl" chmod +x /usr/local/bin/kubectl - name: deploy type: POST_START onFailure: ON_FAILURE_ABORT run: | set -e timeout 120 sh -c 'until getent hosts k8s-staging > /dev/null; do sleep 2; done' octelium cfg k8s-staging export KUBECONFIG="$HOME/.kube/k8s-staging.$OCTELIUM_DOMAIN" kubectl -n payments set image deployment/payments-api server="${{ vars.IMAGE }}" kubectl -n payments rollout status deployment/payments-api --timeout 10m
cordium create ws --template deploy.payments.cordium --ephemeral --start \ --var IMAGE=ghcr.io/acme-corp/payments-api:3f9c2e71

Every kubectl request made by the job, including the image that was rolled out, appears in Octelium's access logs along with the identity of the Workspace and of the User who triggered it (read more here).