TEMPLATE NOTE (removed before delivery): fields written as {{THIS}} are filled in by us per client; fields written as are yours to fill on your side (from the credential files delivered to your custodians, or from your own Jenkins). Secrets are never written into this document, a repository, or a pipeline file -- they are injected at run time from your Jenkins credential store.
This is the one document you need to take a Jenkins server that already reaches Omega Cloud and get it building and deploying to a Kubernetes cluster running in your tenancy. It is written as a single top-to-bottom workflow so you do not have to jump between documents to complete a task. Where a topic has a deeper reference, it is named at the end (section 11); you can finish the whole flow without opening any of them.
Two integration patterns are covered, and most teams use both:
+-------------------+ identity + API +---------------------+
| Jenkins server | ---------------------------> | Omega Cloud |
| (on your network)| TLS, application credential | (your tenancy) |
+-------------------+ +---------------------+
| |
| kubeconfig (a stored Jenkins credential) | creates
| talks to the cluster's API over its floating IP v
| +---------------------+
+------------------------------------------> | Kubernetes cluster |
kubectl / helm / build-agent pods | (control + workers)|
+---------------------+
|
LoadBalancer Service gets a
floating IP -> your app is
reachable from outside.
What each piece does:
Three accounts, and which one to use here (full detail in the Handover Pack):
| Account | Use it for |
|---|---|
{{TENANT_SHORT_NAME}}-svc |
All day-to-day automation via its |
| application credential: networks, servers, | |
| load balancers, deploying into the cluster. | |
{{TENANT_SHORT_NAME}}-cluster |
Creating and deleting Kubernetes clusters, |
| with its PASSWORD. Nothing else. | |
{{TENANT_SHORT_NAME}}-domain-admin |
Managing your own users. Identity only. |
The one rule that prevents the most common failure: cluster CREATE and DELETE run only as {{TENANT_SHORT_NAME}}-cluster with its password. They cannot be done with an application credential, so they do not belong in a pipeline. Create the cluster by hand (section 2); let pipelines DEPLOY into it (section 5).
A checklist. If any item is missing, stop here and get it before continuing.
{{TENANT_SHORT_NAME}}-svc,
`{{TENANT_SHORT_NAME}}-cluster`, and `{{TENANT_SHORT_NAME}}-domain-admin`,
delivered to your custodians.at it; you never disable certificate verification instead.
container image), plus `kubectl` and, if you use it, `helm`.
add "Kubernetes" (the cloud/agent plugin) for Pattern B.
tested this).
Cloud connection settings. Put these in a clouds.yaml on the agent (the client reads the current directory, then ~/.config/openstack/, then /etc/openstack/), and select it with export OS_CLOUD={{TENANT_SHORT_NAME}}:
clouds:
{{TENANT_SHORT_NAME}}:
auth_type: v3applicationcredential
auth:
auth_url: {{AUTH_URL}}
application_credential_id: <id from the delivered credential file>
application_credential_secret: <injected from your Jenkins secret store>
region_name: {{REGION}}
identity_api_version: 3
cacert: <path to the delivered CA bundle {{CA_BUNDLE_FILE}}>
The application credential id and secret come from the delivered credential file. The secret is injected at run time from your Jenkins credential store, not written to disk in a repository. (Full CI credential handling: the CI/Automation Integration Guide.)
You do this once, by hand, signed in as {{TENANT_SHORT_NAME}}-cluster. It is not a pipeline step (see the rule in section 0).
Prompt for the password; never read it from a file into a script, never store it in Jenkins:
export OS_AUTH_TYPE=password
export OS_AUTH_URL={{AUTH_URL}}
export OS_IDENTITY_API_VERSION=3
export OS_CACERT=<path to the delivered CA bundle>
export OS_USERNAME={{TENANT_SHORT_NAME}}-cluster
export OS_USER_DOMAIN_NAME={{TENANT_SHORT_NAME}}
export OS_PROJECT_NAME={{TENANT_SHORT_NAME}}-prod
export OS_PROJECT_DOMAIN_NAME={{TENANT_SHORT_NAME}}
read -rs -p "password for {{TENANT_SHORT_NAME}}-cluster: " OS_PASSWORD && export OS_PASSWORD
openstack token issue # smoke test: should print a token
If a template named {{TENANT_SHORT_NAME}}-k8s was delivered with your tenancy, use it and skip to 2.3:
openstack coe cluster template show {{TENANT_SHORT_NAME}}-k8s
If you need to build your own, three rules keep the create from failing:
--network-driver calico. This platform's cluster service supports calico. Do NOT use flannel: a flannel cluster never finishes coming up -- the create sits in progress and eventually fails, with the nodes never going ready. This is the single most common self-inflicted cluster failure.--public / --hidden on the command line). Publishing a template to the whole cloud is an operator-only action; setting either flag makes the entire create fail with, on the command line, "Not authorized to set public or hidden flag for cluster template", and in the dashboard, only the generic "Error: Unable to create cluster template." Templates are already usable by everyone in your project; you lose nothing by leaving these unchecked.{{TENANT_SHORT_NAME}}-cluster account and is supplied at cluster-create time, not in the template.A known-good template, mirroring the platform default:
openstack coe cluster template create <template-name> \
--coe kubernetes \
--image ubuntu-jammy-kube-v1.34.8 \
--external-network provider-ext \
--master-flavor <master-flavor> --flavor <worker-flavor> \
--network-driver calico --docker-storage-driver overlay2 \
--master-lb-enabled --floating-ip-enabled \
--fixed-network {{TENANT_SHORT_NAME}}-net \
--fixed-subnet {{TENANT_SHORT_NAME}}-subnet
Pick flavors from openstack flavor list. The delivered template's choices are a safe default; read them with the template show command above.
openstack coe cluster create <cluster-name> \
--cluster-template {{TENANT_SHORT_NAME}}-k8s \
--keypair {{TENANT_SHORT_NAME}}-key \
--master-count 1 --node-count <N>
Size <N> against TWO limits, not one:
Practical guidance: start SMALL -- 1 control node and 1 or 2 workers -- confirm the cluster comes up, then scale the worker count up if you need more and capacity allows. A large first request is the second most common cluster failure, after the flannel mistake.
openstack coe cluster show <cluster-name> -f value -c status -c status_reason # poll no faster than every 10 seconds; done when status = CREATE_COMPLETE
Cluster builds take tens of minutes -- this is normal. But if a cluster sits in CREATE_IN_PROGRESS far longer than the build should take and never completes, the usual cause is the network driver: rebuild with calico (see 2.2). Delete the stuck cluster first: openstack coe cluster delete <cluster-name>.
The kubeconfig is the file kubectl and Jenkins use to talk to your cluster's Kubernetes API. Fetch it, still signed in as {{TENANT_SHORT_NAME}}-cluster:
umask 077 openstack coe cluster config <cluster-name> --dir <secure-dir> export KUBECONFIG=<secure-dir>/config kubectl get nodes # every node should report Ready
What is inside it, and why it matters:
kubectl and your pipelines start failing to authenticate; re-fetch the kubeconfig with the same command and update the stored credential (section 4). Confirm the expiry date for your cluster -- you can read it from the certificate embedded in the kubeconfig, or ask {{ACCOUNT_CONTACT}} -- and set a renewal reminder ahead of it.Never commit the kubeconfig to a repository. It goes into the Jenkins credential store next, and nowhere else.
In Jenkins: Manage Jenkins > Credentials > (your domain) > Add Credentials.
config file you fetched in section 3.omega-k8s-kubeconfig (used by the pipeline examples below).That is the only place the kubeconfig lives inside Jenkins. Pipelines reference it by ID; the "Kubernetes CLI" plugin loads it for the duration of a block and masks it in the log. When you re-fetch the kubeconfig (expiry, or a rebuilt cluster), update this one credential and every pipeline picks up the change.
This is the main flow: a pipeline that rolls an application out to the cluster using the stored kubeconfig. Nothing here needs the cluster password; deploying into an existing cluster is ordinary automation.
Deploying pulls a container image; it does not build one. Building your app into an image and pushing it to a registry the cluster can pull from is an upstream step, and in a real pipeline it is a Build stage (and a push stage) that runs before the Deploy stage below. The examples here use the public nginx:stable image so you can prove the deploy path without a registry; swap in your own image once it is built and pushed. If your registry is private, create a Kubernetes image-pull secret in the cluster and reference it from your Deployment's imagePullSecrets, so the cluster can authenticate to pull it.
Keep your Kubernetes manifests in your application repository, for example in a k8s/ directory. A minimal Deployment plus a LoadBalancer Service:
# k8s/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
spec:
replicas: 2
selector:
matchLabels: { app: sample-app }
template:
metadata:
labels: { app: sample-app }
spec:
containers:
- name: web
image: nginx:stable
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: sample-app
spec:
type: LoadBalancer
selector: { app: sample-app }
ports:
- port: 80
targetPort: 80
When Kubernetes sees type: LoadBalancer, the cloud gives the cluster a load balancer and a floating IP for this Service. That address is how the outside world reaches your app.
// Jenkinsfile -- Pattern A: deploy into the cluster
pipeline {
agent any
options { timestamps(); timeout(time: 30, unit: 'MINUTES') }
stages {
stage('Deploy') {
steps {
// The Kubernetes CLI plugin loads the stored kubeconfig for this
// block and masks it in the log.
withKubeConfig([credentialsId: 'omega-k8s-kubeconfig']) {
// Applying the whole k8s/ directory keeps deploy and teardown
// symmetric: `kubectl delete -f k8s/` later removes everything
// this created, including the Service (and its load balancer).
sh 'kubectl apply -f k8s/'
sh 'kubectl rollout status deployment/sample-app --timeout=5m'
}
}
}
stage('Show address') {
steps {
withKubeConfig([credentialsId: 'omega-k8s-kubeconfig']) {
// The external IP appears once the load balancer finishes
// provisioning (a few minutes on the first apply).
sh 'kubectl get service sample-app -o wide'
}
}
}
}
}
Every Service of type: LoadBalancer consumes one load balancer AND one floating IP from your quota, and keeps them for as long as the Service exists. Two consequences:
Instead of (or in addition to) deploying INTO the cluster, you can have Jenkins run its BUILD jobs as short-lived pods inside the cluster. Each job gets a fresh, isolated agent that disappears when the build ends.
Setup, using the "Kubernetes" plugin:
https://<cluster-floating-ip>:6443 (the server: line in the kubeconfig from section 3).jnlp agent container plus any tool containers your builds need (for example a container with kubectl and helm). Give it a label; jobs that request that label run in these pods.Two things to keep in mind:
Pattern B does not replace Pattern A: a common setup is build agents in the cluster (B) that then deploy the built artifact to the same or another cluster (A).
This runs the whole flow once with a stock image, to prove the path before you wire in your own app. Sections in parentheses point back to the detail.
Sign in as {{TENANT_SHORT_NAME}}-cluster (2.1) and create a small cluster from the delivered template (2.3):
openstack coe cluster create demo-k8s \
--cluster-template {{TENANT_SHORT_NAME}}-k8s \
--keypair {{TENANT_SHORT_NAME}}-key \
--master-count 1 --node-count 1Wait for CREATE_COMPLETE (2.4):
openstack coe cluster show demo-k8s -f value -c status
Fetch and check the kubeconfig (3):
umask 077 openstack coe cluster config demo-k8s --dir ./demo-kube export KUBECONFIG=$PWD/demo-kube/config kubectl get nodes
Store ./demo-kube/config in Jenkins as the "Secret file" credential omega-k8s-kubeconfig (4). (For a quick local proof you can skip Jenkins and use the KUBECONFIG export above directly.)
Deploy the sample app (5.1):
kubectl apply -f k8s/ kubectl rollout status deployment/sample-app --timeout=5m
Read the external address (wait for it to move from to an IP; the load balancer takes a few minutes on first creation):
kubectl get service sample-app -o wide # then, from a host that can reach it: curl http://<external-ip>/
Tear the example down (8):
kubectl delete -f k8s/ # deletes the Service too -> releases
# the load balancer and floating IP
openstack coe cluster delete demo-k8sDelete in reverse order of creation, and delete the Service before anything else, so the load balancer and floating IP are released:
kubectl delete -f k8s/ (or kubectl delete service <name>). This is what releases the load balancer and its floating IP. Deleting the same directory you applied catches every manifest, so no Service is left stranded.{{TENANT_SHORT_NAME}}-cluster: openstack coe cluster delete <cluster-name>.Two hygiene rules that keep quota from silently filling:
post { always { ... } } block so a failed build cannot leak quota. The delivered starter-kit sweep script (ci-cleanup-sweep.sh) removes leftovers by name prefix and age; run it as a scheduled job as a backstop.Symptoms you are most likely to hit, and the fix:
"Missing value auth-url required for auth plugin password" -- the shell has no cloud environment loaded. You started a fresh shell or a fresh agent. Re-source your OS_* environment or select your clouds.yaml entry (export OS_CLOUD={{TENANT_SHORT_NAME}}) before the command.
Cluster TEMPLATE create fails; dashboard shows only "Error: Unable to create cluster template." -- the Public or Hidden flag is set. Uncheck both (2.2).
Cluster create fails, a node in ERROR with "No valid host was found. There are not enough hosts available." -- the request is too big for the free capacity right now, even if it is within quota. Use a smaller node count or a smaller flavor and retry (2.3). Raising quota will not help.
Cluster stays in CREATE_IN_PROGRESS far too long and never completes -- most often the network driver. Delete it and rebuild the template with calico; flannel is not supported (2.2).
"Keypair not found" at cluster create -- you are signed in as the wrong account. Cluster create runs as {{TENANT_SHORT_NAME}}-cluster, which owns the keypair. Check echo $OS_USERNAME first.
A pipeline tries to create a cluster and is refused -- cluster create and delete cannot use an application credential; they need the {{TENANT_SHORT_NAME}}-cluster password, so they are not pipeline steps. Create the cluster by hand (section 2); let the pipeline deploy into it.
kubectl reports "Unable to connect" or a certificate error -- the kubeconfig is wrong or its certificate has expired. Re-fetch it (3) and update the stored Jenkins credential (4). Make sure you kept the delivered CA and did not disable verification.
A LoadBalancer Service shows EXTERNAL-IP <pending> for a long time -- the load balancer is still provisioning (a few minutes is normal), or your load balancer / floating IP quota is exhausted. Check your quota usage.
{{TENANT_SHORT_NAME}}-svc account. All automation authenticates with it. It cannot create clusters.{{TENANT_SHORT_NAME}}-cluster): the only account that creates and deletes clusters, using its password. It owns the cluster keypair.provider-ext). Counts against quota even when unattached.You can complete this guide without these; open them when you want more than the workflow above.
smoke-test.sh (read-only token/catalog/quota check for the top of a pipeline), tenancy-audit.sh (read-only snapshot of what you have), acceptance-run.sh (a full build-and-teardown you can trim into a recurring smoke test), and ci-cleanup-sweep.sh (the backstop quota sweep).For anything not covered here, or a change to your tenancy, contact {{ACCOUNT_CONTACT}}.