Writing a Kratix Promise
This is Part 4 of a series illustrating how Kratix works.
👈🏾 Previous: Using multiple Kratix Promises
👉🏾 Next: Enhancing a Kratix Promise
Pre-requisites
If you completed the environment cleanup steps at the end of the previous workshop chapter your good to go! If you did not cleanup or ran into issues you can run the following from inside the Kratix repo to get a fresh environment:
./scripts/quick-start.sh --recreate
In this tutorial, you will
What's inside a Kratix Promise?
You've installed Kratix and three sample Promises. Now you'll create a Promise from scratch.
From installing a Promise, a Kratix Promise is a YAML document that defines a contract between the platform and its users. It is what allows platforms to be built incrementally.
A Promise consists of three parts:

xaasCrd
: the CRD that an application developer uses to request an instance of the Kratix Promise from the Platform Cluster.workerClusterResources
: a collection of Kubernetes resources that enable the creation of an instance and will be pre-installed in the Worker Clusters.xaasRequestPipeline
: an ordered list of docker containers that result in the creation an instance of the promised service on a Worker Cluster.
Recap: basics of getting a promised instance to your users
At a very high level
- You talk to users of your platform to find out what they're using and what they need.
- You write a Kratix Promise for a service that your users and teams need.
- In
xaasCrd
, you list what your users can configure in their request. - In
workerClusterResources
, you list what resources are required for Kratix to fulfil the Promise. - In
xaasRequestPipeline
, you list Docker images that will take the user's request and decorate it with configuration that you or the business require.
- In
- You install the Promise on your Platform Cluster, where Kratix is installed.
- Your user wants an instance of the Promise.
- Your user submit a Kratix Resource Request that lists what they want and how they want it, and this complies with the
xaasCrd
(more details on this request later). - Kratix fires off the request pipeline that you defined in
xaasRequestPipeline
and passes the Resource Request as an input. - The pipeline outputs valid Kubernetes documents that say what the user wants and what the business wants for that Promise instance.
- The Worker Cluster has what it needs based on the
workerClusterResources
and is ready to create the instance when the request comes through.
A Kratix Promise to deliver Jenkins
Imagine your platform team has received its fourth request from its fourth team for a Jenkins instance. You decide four times is too many times to manually set up Jenkins.
Now you'll write a Jenkins Promise and install it on your platform so that your four teams get Jenkins—and you get time back for more valuable work.
Writing your own Kratix Promise
This guide will follow the steps below:
Define Promise
Promise definition: xaasCrd
Promise definition: xaasRequestPipeline
- Create your Promise instance base manifest
- Build a simple request pipeline
- Package your pipeline step as a Docker image
- Test your container image
Promise definition: workerClusterResources
Test Promise
- Install your Promise
- Create and submit a Kratix Resource Request
- Review of a Kratix Promise parts (in detail)
- Summary
- Cleanup environment
Prepare your environment
Directory setup
To begin writing a Promise you will need a basic directory structure to work in. You can generate this folder structure in any local directory by running
mkdir -p jenkins-promise/{resources,request-pipeline-image}
cd jenkins-promise
Generate a Promise template
Generate a basic jenkins-promise-template.yaml
to work with
apiVersion: platform.kratix.io/v1alpha1
kind: Promise
metadata:
name: jenkins-promise
spec:
workerClusterResources:
xaasRequestPipeline:
xaasCrd:
You will fill in the fields under spec
as you progress through the tutorial.
Define your Promise API
For the purpose of this tutorial, you will create an API that accepts a single string
parameter called name
. In real world scenarios, the API can be as simple or as complex you design it to be. The Promise API is defined within the xaasCrd
of your Promise YAML.
Replace the xaasCrd
field in jenkins-promise-template.yaml
with the complete field details below. Ensure the indentation is correct (xaasCrd
is nested under spec
).
xaasCrd:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: jenkins.example.promise.syntasso.io
spec:
group: example.promise.syntasso.io
scope: Namespaced
names:
plural: jenkins
singular: jenkins
kind: jenkins
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: string
You have now defined the as-a-Service API.
Create your Resource Request Pipeline
Create your Promise instance base manifest
Next build the pipeline to use details from a Kratix Promise Resource Request into the Kubernetes resources required to create a running instance of the Jenkins service. For that, copy the YAML file below and save it in request-pipeline-image/jenkins-instance.yaml
.
CLICK HERE to expand the contents of the jenkins-instance.yaml
file.
apiVersion: jenkins.io/v1alpha2
kind: Jenkins
metadata:
name: <tbr-name>
namespace: default
spec:
configurationAsCode:
configurations: []
secret:
name: ""
groovyScripts:
configurations: []
secret:
name: ""
jenkinsAPISettings:
authorizationStrategy: createUser
master:
basePlugins:
- name: kubernetes
version: "1.31.3"
- name: workflow-job
version: "1180.v04c4e75dce43"
- name: workflow-aggregator
version: "2.7"
- name: git
version: "4.11.0"
- name: job-dsl
version: "1.79"
- name: configuration-as-code
version: "1414.v878271fc496f"
- name: kubernetes-credentials-provider
version: "0.20"
disableCSRFProtection: false
containers:
- name: jenkins-master
image: jenkins/jenkins:2.332.2-lts
imagePullPolicy: Always
livenessProbe:
failureThreshold: 12
httpGet:
path: /login
port: http
scheme: HTTP
initialDelaySeconds: 100
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
failureThreshold: 10
httpGet:
path: /login
port: http
scheme: HTTP
initialDelaySeconds: 80
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
cpu: 1500m
memory: 3Gi
requests:
cpu: "1"
memory: 500Mi
Build a simple request pipeline
Kratix takes no opinion on the tooling used within a pipeline. Kratix will pass a set of resources to the pipeline, and expect back a set of resources. What happens within the pipeline, and what tooling is used, is a decision left entirely to you. For further documentation on how pipelines work, check Kratix Pipelines
For this example, you're taking a name from the Kratix Resource Request for an instance and passing it to the Jenkins custom resource output.
To keep this transformation simple, you'll use a combination of sed
and yq
to do the work.
Create a script file in the request-pipeline-image
directory called
execute-pipeline.sh
and copy the contents below. The script will be executed
when the pipeline runs.
#!/bin/sh
set -x
#Get the name from the Promise Custom resource
instanceName=\$(yq eval '.spec.name' /input/object.yaml)
# Inject the name into the Jenkins resources
find /tmp/transfer -type f -exec sed -i \\
-e "s/<tbr-name>/\${instanceName//\//\\/}/g" \\
{} \;
cp /tmp/transfer/* /output/
Then make it executable:
chmod +x jenkins-promise/request-pipeline-image/execute-pipeline.sh
Pipelines also have the capability to write back information to the resource requester by writing to the status. See status documentation for more infoformation.
Package your pipeline step as a Docker image
Create a Dockerfile
in the request-pipeline-image
directory and copy the contents below.
FROM "mikefarah/yq:4"
RUN [ "mkdir", "/tmp/transfer" ]
ADD jenkins-instance.yaml /tmp/transfer/jenkins-instance.yaml
ADD execute-pipeline.sh execute-pipeline.sh
CMD [ "sh", "-c", "./execute-pipeline.sh"]
ENTRYPOINT []
Your file directory should now include the new file as shown below
. 📂 jenkins-promise
├── jenkins-promise-template.yaml
├── 📂 request-pipeline-image
│ ├── Dockerfile
│ ├── execute-pipeline.sh
│ └── jenkins-instance.yaml
└── 📂 resources
Next build your Docker image:
cd jenkins-promise/request-pipeline-image
docker build --tag kratix-workshop/jenkins-request-pipeline:dev .
Test your pipeline image
Test the Docker container image by supplying an input resource and examining the output resource.
Create the test input and output directories locally within the request-pipeline-image
directory:
mkdir jenkins-promise/request-pipeline-image/{input,output}
Your file directory should now include the new file as shown below
. 📂 jenkins-promise
├── jenkins-promise-template.yaml
├── 📂 request-pipeline-image
│ ├── 📂 🆕 input
│ ├── 📂 🆕 output
│ ├── Dockerfile
│ ├── execute-pipeline.sh
│ └── jenkins-instance.yaml
└── 📂 resources
The /input
directory is where your incoming Kratix Resource Request will be written when a user wants an instance.
Create a sample object.yaml
Resource Request in the /input
with the contents below
apiVersion: promise.example.com/v1
kind: jenkins
metadata:
name: my-jenkins-promise-request
spec:
name: my-amazing-jenkins
Run the container and examine the output
cd jenkins-promise/request-pipeline-image
docker run -v ${PWD}/input:/input -v ${PWD}/output:/output kratix-workshop/jenkins-request-pipeline:dev
Verify the contents of the output
directory. These will be scheduled and deployed by Kratix to a Worker Cluster once the pipeline is executed, as a response for the Resource Request. They need to be valid Kubernetes resources that can be applied to any cluster with the Promise's workerClusterResources
installed (see beneath).
Once you are satisified that your pipeline is producing the expected result, you will need to make it available to your Kubernetes cluster. You can load the it to the local KinD cache:
kind load docker-image kratix-workshop/jenkins-request-pipeline:dev --name platform
Click here if your clusters were not created with KinD
- Push the image to a Image repository (like Dockerhub), or
- Use the appropriate command to load the image (for example,
minikube cache add
if you are using minikube)
The final step of creating the xaasRequestPipeline
is to reference your docker image from the spec.xaasRequestPipeline
field in the jenkins-promise-template.yaml
.
Add the image to the array in jenkins-promise-template.yaml
.
apiVersion: platform.kratix.io/v1alpha1
kind: Promise
metadata:
name: jenkins-promise
spec:
workerClusterResources:
xaasRequestPipeline:
- kratix-workshop/jenkins-request-pipeline:dev
xaasCrd:
...
In summary, you have:
- Created a container image containing:
- A template file to be injected with per-instance details (
jenkins-instance.yaml
) - A shell script to retrieve the per-instance details from the user's request, and inject them into the template (
execute-pipeline.sh
) - A command set to the shell script
- A template file to be injected with per-instance details (
- Created a set of directories(
input
/output
) and sample user request(input/object.yaml
) - Executed the pipeline image locally as a test
- Pushed the image to the registry
- Added the image to the Promise definition in the
xaasRequestPipeline
array
Define your workerClusterResources
in your Promise definition
The workerClusterResources
describes everything required to fulfil the Promise. Kratix applies this content on all registered Worker Clusters.
For this Promise, the workerClusterResources
needs to contain the Jenkins CRD, the Jenkins Operator, and the resources the Operator requires.
Run the following command from the jenkins-promise
directory:
mkdir -p resources
curl https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/fbea1ed790e7a9deb2311e1f565ee93f07d89022/config/crd/bases/jenkins.io_jenkins.yaml --output resources/jenkins.io_jenkins.yaml --silent
curl https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/8fee7f2806c363a5ceae569a725c17ef82ff2b58/deploy/all-in-one-v1alpha2.yaml --output resources/all-in-one-v1alpha2.yaml --silent
The commands above will download the necessary files in the resources
directory. You are now ready to inject the Jenkins files into the jenkins-promise-template.yaml
.
To make this step simpler we have written a very basic tool to grab all YAML documents from all YAML files located in resources
and inject them into the workerClusterResources
field.
To use this tool, you will need to download the correct binary for your computer from GitHub releases:
- Intel Mac
- Apple Silicon Mac
- Linux ARM64
- Linux AMD64
curl -sLo worker-resource-builder https://github.com/syntasso/kratix/releases/download/v0.0.1/worker-resource-builder-v0.0.0-1-darwin-amd64
chmod +x worker-resource-builder
curl -sLo worker-resource-builder https://github.com/syntasso/kratix/releases/download/v0.0.1/worker-resource-builder-v0.0.0-1-darwin-arm64
chmod +x worker-resource-builder
curl -sLo worker-resource-builder https://github.com/syntasso/kratix/releases/download/v0.0.1/worker-resource-builder-v0.0.0-1-linux-arm64
chmod +x worker-resource-builder
curl -sLo worker-resource-builder https://github.com/syntasso/kratix/releases/download/v0.0.1/worker-resource-builder-v0.0.0-1-linux-amd64
chmod +x worker-resource-builder
Once you have downloaded the correct binary, run:
./worker-resource-builder \
-k8s-resources-directory ./resources \
-promise ./jenkins-promise-template.yaml > ./jenkins-promise.yaml
This created your finished Promise definition, jenkins-promise.yaml
.
Install your Promise
From your Promise directory, you can now install the Promise in Kratix.
At this point, your Promise directory structure should look like:
📂 jenkins-promise
├── 📂 request-pipeline-image
│ ├── 📂 input
│ │ └── object.yaml
│ ├── 📂 output
│ │ └── jenkins_instance.yaml
│ ├── Dockerfile
│ ├── execute-pipeline.sh
│ └── jenkins-instance.yaml
├── 📂 resources
│ ├── jenkins.io_jenkins.yaml
│ └── all-in-one-v1alpha2.yaml
└── jenkins-promise-template.yaml
Before installing your promise, verify that Kratix and MinIO are installed and healthy.
kubectl --context $PLATFORM get pods --namespace kratix-platform-system
You should see something similar to
NAME READY STATUS RESTARTS AGE
kratix-platform-controller-manager-769855f9bb-8srtj 2/2 Running 0 1h
minio-6f75d9fbcf-5cn7w 1/1 Running 0 1h
If that is not the case, please go back to Prepare your environment and follow the instructions.
From the jenkins-promise
directory, run:
kubectl apply --context $PLATFORM --filename jenkins-promise.yaml
Verify the Promise installed
(This may take a few minutes so --watch
will watch the command. Press Ctrl+C to stop watching)
kubectl --context $PLATFORM get crds --watch
The above command will give an output similar to
NAME CREATED AT
jenkins.example.promise.syntasso.io 2021-09-09T11:21:10Z
Verify the Jenkins Operator is running
(This may take a few minutes so --watch
will watch the command. Press Ctrl+C to stop watching)
kubectl --context $WORKER get pods --watch
The above command will give an output similar to
NAME READY STATUS RESTARTS AGE
jenkins-operator-6c89d97d4f-r474w 1/1 Running 0 1m
Create and submit a Kratix Resource Request
You can now request instances of Jenkins. Create a file in the jenkins-promise
directory called jenkins-resource-request.yaml
with the following content:
apiVersion: example.promise.syntasso.io/v1
kind: jenkins
metadata:
name: my-jenkins-promise-request
spec:
name: my-amazing-jenkins
You can now send the Resource Request to Kratix:
kubectl apply --context $PLATFORM --filename jenkins-resource-request.yaml
Applying the Kratix Promise will trigger your pipeline steps which in turn requests an instance of Jenkins from the operator. While the pipeline can run quite quickly, Jenkins requires quite a few resources to be installed including a deployment and a runner which means the full install may take a few minutes.
You can see a bit of what is happening by first looking for your pipeline completion
kubectl --context $PLATFORM get pods
This should result in something similar to
NAME READY STATUS RESTARTS AGE
request-pipeline-jenkins-promise-default-9d40b 0/1 Completed 0 1m
For more details, you can view the pipeline logs with
kubectl logs \
--context $PLATFORM \
--selector kratix-promise-id=jenkins-promise-default \
--container xaas-request-pipeline-stage-1
This should result in something like
+ yq eval .spec.name /input/object.yaml
+ instanceName=my-amazing-jenkins
+ find /tmp/transfer -type f -exec sed -i -e 's/<tbr-name>/my-amazing-jenkins/g' '{}' ';'
+ cp /tmp/transfer/jenkins-instance.yaml /output/
Then you can watch for the creation of your Jenkins instance by targeting the Worker Cluster:
(This may take a few minutes so --watch
will watch the command. Press Ctrl+C to stop watching)
kubectl --context $WORKER get pods --all-namespaces --watch
The above command will eventually give an output similar to
NAME READY STATUS RESTARTS AGE
jenkins-my-amazing-jenkins 1/1 Running 0 1m
...
For verification, access the Jenkins UI in a browser, as in previous steps.
Let's now take a look at what you have done in more details.
Review of a Kratix Promise parts (in detail)
xaasCrd
The xaasCrd
is your user-facing API for the Promise. It defines the options that users can configure when they request the Promise. The complexity of the xaasCrd
API is up to you. You can read more about writing Custom Resource Definitions in the Kubernetes docs.
workerClusterResources
The workerClusterResources
describes everything required to fulfil the Promise. Kratix applies this content on all registered Worker Clusters. For instance with the Jenkins Promise, the workerClusterResources
contains the Jenkins CRD, the Jenkins Operator, and the resources the Operator requires.
xaasRequestPipeline
The xaasRequestPipeline
defines a set of jobs to run when Kratix receives a request for an instance of one of its Promises.
The pipeline is an array of Docker images, and those images are executed in order. The pipeline enables you to write Promises with specialised images and combine those images as needed.
Each container in the xaasRequestPipeline
array should output complete, valid Kubernetes resources.
The contract with each pipeline container is simple and straightforward:
- The first container in the list receives the resource document created by the
user's request—this request will comply with the
xaasCrd
described above. The document will be always available to the pipeline in/input/object.yaml
. - The container's command then executes with the input object and fulfils its responsibilites.
- The container writes any resources to be created to
/output/
. - The resources in
/output
of the last container in thexaasRequestPipeline
array will be scheduled and applied to the appropriate Worker Clusters.
Recap
You have now authored your first promise. Congratulations 🎉
To recap the steps we took:
- ✅ Generated a Kratix Promise template
- ✅
xaasCrd
: Defined your Promise API with a X as-a-Service Custom Resource Definition - ✅ Created your Promise instance base manifest
- ✅
xaasRequestPipeline
: Built a simple request pipeline - ✅ Packaged the pipeline as a Docker image
- ✅ Tested the pipeline Docker image
- ✅
workerClusterResources
: Defined what needs to be present on your Worker Clusters to fulfil this Promise - ✅ Installed your Kratix Promise
- ✅ Created and submitted a Kratix Resource Request
- ✅ Reviewed the components of a Promise
Cleanup environment
To clean up your environment first delete the Resource Requests for the Jenkins instance
kubectl --context $PLATFORM delete --filename jenkins-resource-request.yaml
Verify the resources belonging to the Resource Requests have been deleted in the Worker Cluster
kubectl --context $WORKER get pods
Now the Resource Requests have been deleted you can delete the Promises
kubectl --context $PLATFORM delete --filename jenkins-promise.yaml
Verify the Worker Cluster Resources are deleted from the Worker Cluster
kubectl --context $WORKER get pods
🎉 Congratulations!
✅ You have written a Kratix Promise.
👉🏾 Let's see how to tailor Kratix Promises based on organisational context.