Skip to main content

Writing your first Promise

This is Part 1 of a series illustrating how to write Kratix Promises.

👉🏾 Next: Using Promise Workflows

In this tutorial, you will

What is a Promise?

In Part I, you learned that Kratix is a framework used by platform teams to build platforms tailored to their organisation.

Kratix Promises are the encapsulation of platform offerings. They provide a structure to manage the complexities of providing something-as-a-service, including the definition of how a user can request a Resource, the steps to provision the requested Resource, and how to provide access to the Resource.

Promises allow platform teams to:

  • Define an API, including versioning and validation
  • Execute imperative commands per the user request
  • Use GitOps to continuously reconcile delivered services
  • Install and configure (or verify) dependencies for delivering a service
  • Manage where services are deployed across the infrastructure (on and off-Kubernetes, on Cloud providers, etc)

Promise Architecture

Before jumping into writing a Promise, here is a quick review of the Promise Architecture.

DependenciesPromiseKubernetesCluster(s)VirtualMachine(s)CloudProvider(s)ImperativePipelineEdgeCompute(s)SaaSProvider(s)DeclarativeStateStoreApplicationEngineerAPI1234
A Promise in detail

At a very high-level, a Promise is made up of four parts:

  1. The Promise API: The Promise API is what the users of the Platform will interact with when requesting a new Resource from the Promised Service
  2. The Imperative Workflow: A series of steps where Platform teams can codify all of their business requirements.
  3. The Declarative State: The Workflow executes a series of imperative steps to generate a declarative state that is then persisted to the State Store. Other systems will then converge on that state.
  4. The Dependencies: A dependency is anything that must be installed or made available on Destinations to enable the promised service to run.

As you go through building your own Promise, you will explore each of these four sections in detail.

The Promise

In this tutorial, you are going to create a Promise that delivers Apps as a service. It will bundle the Kubernetes resources needed to run an application, namely a Deployment, Service, and Ingress.

For Ingress, you will use the NGINX Ingress Controller. This is a popular Ingress Controller that is used to expose Kubernetes services to the internet.

You can start thinking about your promise by designing the Promise API.

The App Promise
The App-as-a-Service Promise

Design an API for your service

When building an API, you have a lot of design considerations to include. For example, you will want to:

  • require certain parameters
  • make some parameters dependent on values in other parameters
  • provide client-side validation
  • run server-side validation
  • ensure API versioning to manage updates

These and many other features are all provided as standard with a Kubernetes Custom Resource Definition (CRD). This is why Kratix uses CRDs as the mechanism to codify your Promise API.

info

In a Promise, the API is stored in a key called api

🤔 Want to learn more about CRDs?

A Custom Resource (CR) is an extension of the Kubernetes API that is not necessarily available in a default Kubernetes installation. It represents a customisation of a particular Kubernetes installation.

A Custom Resource Definition (CRD) is the object you create to teach your Kubernetes cluster about this new resource type.

To read more about CRDs please refer to the Kubernetes documentation.

This tutorial will not go through details on how to write a Kubernetes CRD, but you can check out the Kubernetes documentation for further details on the topic.

Start with the interface

A Kratix Promise allows platform teams to fine-tune how much application teams need to know about low-level details while still providing the levers they need to get the service they want.

You can start by defining an interface for the application teams to deploy their applications. This interface is the contract of what can be configured and within what parameters.

DependenciesPromiseImperativePipelineDeclarativeStateStoreAPI
The Promise API is where you will define the interface

When designing the API for your new Promise, you can decide how much control you want to give to the application teams.

For example, you could allow them to configure all aspects of the underlying Deployment and Ingress rules. That option provides your users with maximum autonomy and configurability. However, it may be a lot to understand and misconfiguration of services can cause both cost and security issues in the future.

On the other hand, you could go to the other end of the spectrum, hiding all the options and asking users to provide only the absolute necessary pieces of configuration. This allows for fewer configuration errors, but it also limits the flexibility of the service.

👍 maximum flexibility👎 complex👍 easy to get started👎 no configurability
The sweet spot for your organisation can be anywhere on the spectrum

While both extremes are possible using Kratix, the recommendation is to design your API with one key principle in mind: what do your users (need to) care about? Answering that question is fundamental to designing a useful API.

Define your interface

At this stage, you decided to give users a very simple API. They can define:

  • The container image of their app
  • The service.port the application is listening on

For the Ingress, you decided to hide it altogether and provide a default configuration.

As previously mentioned, the Kratix Promise API is defined via a Kubernetes CRD. A deep dive on CRDs is out of scope for this tutorial, but you can read more about them in the Kubernetes documentation.

To build the Promise, you can either write it yourself or bootstrap it with the Kratix CLI. In this workshop, you will use the CLI.

To keep the promise all in one place, create a directory called app-promise and run the init promise command inside it:

mkdir -p app-promise && cd app-promise
kratix init promise app --group workshop.kratix.io --version v1 --kind App

The command above should generate a promise.yaml file in the current directory. You can check it out by running:

cat promise.yaml

Now, run the update api command to include the API fields you defined above:

kratix update api --property image:string --property service.port:integer

If you inspect the promise.yaml file again, you should see the API fields you defined as a CRD:

cat promise.yaml | yq '.spec.api'
Click here to see the Promise so far
app-promise/promise.yaml
apiVersion: platform.kratix.io/v1alpha1
kind: Promise
metadata:
name: app
spec:
api:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: apps.workshop.kratix.io
spec:
group: workshop.kratix.io
names:
kind: App
plural: apps
singular: apps
shortNames:
- app
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
service:
type: object
properties:
port:
type: integer
image:
type: string

Great! Your Promise now has an API. You can move on to the next step: defining the dependencies.

Define the dependencies

DependenciesPromiseImperativePipelineDeclarativeStateStoreAPI
The Promise API is where you will define the interface

The next step is to define the dependencies of your Promise. This is where you will define the Kubernetes resources that will be created when the Promise is deployed.

As previously mentioned, this Promise will create a Deployment, a Service, and an Ingress. Deployments and Services are built in to Kubernetes, so you can use them directly. However, to use the NGINX Ingress Controller, you need to have it installed on your worker cluster Destination prior to any user deploying their applications.

That's exactly what Kratix dependencies are for: to define the resources that need to be created when the Promise is installed.

If you follow the NGINX Ingress Controller installation instructions, you will find that you need the resources defined in this file installed on the cluster.

Create a subdirectory in your app-promise directory called dependencies. Then, create a file called dependencies.yaml and copy the contents of the file above into it.

mkdir -p dependencies
curl -o dependencies/dependencies.yaml --silent https://raw.githubusercontent.com/syntasso/kratix-docs/main/docs/workshop/part-ii/_partials/nginx-patched.yaml

Once stored locally, you will need to add these Dependencies to the Promise file. The Dependencies are added as a list under dependencies which can be tricky to format and requires some subtle whitespace changes.

Inject Dependencies into the Promise

You can use the update dependencies command on the Kratix CLI to add Dependencies to an existing Promise. Run:

warning

If you are using Instruqt, please make sure the promise.yaml file is closed in the editor tab before running the command below. Once you execute the command, make sure to click the Reload button on the editor tab.

kratix update dependencies ./dependencies/

If everything goes well, the command above should output a confirmation message. You can verify that the dependencies were added by checking the contents of the promise.yaml file. You should now see the contents of the dependencies.yaml file under the dependencies key.

info

You may notice that promise file is now getting quite big. Further down in this series, you will learn how you can use Promise Workflows to reduce the size of the Promise file.

Amazing! You are now one step away from having a working Promise. The next step is to define the Promise Workflow, that is, what must happen when a user requests a new app deployment.

Define the Workflow

DependenciesPromiseImperativePipelineDeclarativeStateStoreAPI
The Promise API is where you will define the interface

Kratix provides several hooks for managing and customising the Promise's lifecycle. Those hooks are defined in the Promise workflows key. The minimum you need to define is what must happen when the platform receives a request for a new resource from the promised service. Refer to the Workflows reference documentation for further details.

Kratix ships with a builtin Workflow kind, Pipeline, which provides a straightforward way to define Workflows. You could, however, use other tools (like Tekton) in your Workflows.

In a nutshell, a Kratix Pipeline is a series of containers that will be executed, in order. In this section, you will write and configure a Kratix Pipeline to be executed as part of the resource.configure Workflow.

When a user requests a new App deployment, the resource.configure Workflow will be executed. This workflow will query the user's input and declare the resources that must be created.

We will once again use the Kratix CLI to create the necessary files. Run the following command to bootstrap the skeleton of your Workflow:

kratix add container resource/configure/mypipeline --image kratix-workshop/app-pipeline-image:v1.0.0

The command above should have created a directory structure that looks like the following:

.
├── README.md
├── dependencies
│   └── dependencies.yaml
├── example-resource.yaml
├── promise.yaml
└── workflows
└── resource
└── configure
└── mypipeline
└── kratix-workshop-app-pipeline-image
├── Dockerfile
├── resources
└── scripts
└── pipeline.sh

The CLI will create a basic Dockerfile and a pipeline.sh script. We will customise these files to fit our needs as we progress through the workshop. Feel free to explore them.

The generated Dockerfile uses the alpine image as a base image. As you progress through the Workshop, you will install the tools you need to execute the steps. You may also notice that image will execute a script called pipeline.sh when the container starts.

As we will use this same image to build several stages in the pipeline, let's rename the pipeline.sh to resource-configure to better reflect its purpose.

mv workflows/resource/configure/mypipeline/kratix-workshop-app-pipeline-image/scripts/{pipeline.sh,resource-configure}

Make sure to update the Dockerfile to reflect the new script name. Remove the lines marked in red and add the lines marked in green:

app-promise/workflows/resource/configure/mypipeline/kratix-workshop-app-pipeline-image/Dockerfile
- RUN apk update && apk add --no-cache yq
+ RUN apk update && apk add --no-cache yq kubectl

- ADD scripts/pipeline.sh /usr/bin/pipeline.sh
+ COPY scripts/* /usr/bin/

- RUN chmod +x /usr/bin/pipeline.sh
+ RUN chmod +x /usr/bin/*

- CMD [ "sh", "-c", "pipeline.sh"]

Replace the contents of the resource-configure script with the following:

app-promise/workflows/resource/configure/mypipeline/kratix-workshop-app-pipeline-image/scripts/resource-configure
#!/bin/sh

set -eux

# Get the user's input
image=$(yq '.spec.image' /kratix/input/object.yaml)
service_port=$(yq '.spec.service.port' /kratix/input/object.yaml)

# Get the request name and namespace
name=$(yq '.metadata.name' /kratix/input/object.yaml)
namespace=$(yq '.metadata.namespace' /kratix/input/object.yaml)

# Create a deployment
kubectl create deployment ${name} \
--namespace=${namespace} \
--replicas=1 \
--image=${image} \
--dry-run=client \
--output yaml > /kratix/output/deployment.yaml

# Create a service
kubectl create service nodeport ${name} \
--namespace=${namespace} \
--tcp=${service_port}\
--dry-run=client \
--output yaml > /kratix/output/service.yaml

# Create an ingress
kubectl create ingress ${name} \
--namespace=${namespace} \
--class="nginx" \
--rule="${name}.local.gd/*=${name}:${service_port}" \
--dry-run=client \
--output yaml > /kratix/output/ingress.yaml

As you can see, the script above uses the kubectl command to create a Deployment, a Service, and an Ingress. The script uses the yq command to extract the user's input from the /kratix/input/object.yaml file. The object.yaml file contains the input the user provided when requesting the new resource.

You may also notice that the script uses the /kratix/output directory to store the resources that must be created. At the end of the workflow, Kratix will deploy the resources stored in the /kratix/output directory to one of the available Destinations.

Finally, you can see this pipeline script is written in shell script. Kratix is unopinionated about the language you use to write your Pipelines. You can use any language you want as long as you provide a script that can be executed in a container.

info

It's straightforward to test your pipeline using docker run (or equivalent). You will learn how to do that in a later stage.

When you ran the kratix add container command, the CLI automatically included the workflow key into the Promise, with the resource.configure Workflow defined. As you will add more scripts to the same image, open the promise.yaml and set the command key to the script you just created.

app-promise/promise.yaml
apiVersion: platform.kratix.io/v1alpha1
kind: Promise
metadata:
name: app
spec:
api: # ...
dependencies: # ...
workflows:
resource:
configure:
- apiVersion: platform.kratix.io/v1alpha1
kind: Pipeline
metadata:
name: resource-configure
spec:
containers:
- name: create-resources
image: kratix-workshop/app-pipeline-image:v1.0.0
command: [ resource-configure ]

The final step is to actually build the pipeline image and make it available in the container registry. Since you are using kind clusters, you can build and load the image into the cluster directly by running the following commands:

from the app-promise directory
docker build --tag kratix-workshop/app-pipeline-image:v1.0.0 workflows/resource/configure/mypipeline/kratix-workshop-app-pipeline-image
kind load docker-image kratix-workshop/app-pipeline-image:v1.0.0 --name platform

And that's it! You have now defined a Promise that can be used to deploy a new App.

Click here for the full promise.yaml file.

app-promise/promise.yaml
apiVersion: platform.kratix.io/v1alpha1
kind: Promise
metadata:
name: app
spec:
api:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: apps.workshop.kratix.io
spec:
group: workshop.kratix.io
names:
kind: App
plural: apps
singular: apps
shortNames:
- app
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
service:
type: object
properties:
port:
type: integer
image:
type: string
dependencies:
- apiVersion: v1
kind: Namespace
metadata:
labels:
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
name: ingress-nginx
- apiVersion: v1
automountServiceAccountToken: true
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx
namespace: ingress-nginx
- apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission
namespace: ingress-nginx
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx
namespace: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- endpoints
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses/status
verbs:
- update
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- coordination.k8s.io
resourceNames:
- ingress-nginx-leader
resources:
- leases
verbs:
- get
- update
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- create
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- list
- watch
- get
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission
namespace: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- create
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx
namespace: default
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
- namespaces
verbs:
- list
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- networking.k8s.io
resources:
- ingresses/status
verbs:
- update
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- list
- watch
- get
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission
namespace: default
rules:
- apiGroups:
- admissionregistration.k8s.io
resources:
- validatingwebhookconfigurations
verbs:
- get
- update
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx
namespace: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ingress-nginx
subjects:
- kind: ServiceAccount
name: ingress-nginx
namespace: ingress-nginx
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission
namespace: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ingress-nginx-admission
subjects:
- kind: ServiceAccount
name: ingress-nginx-admission
namespace: ingress-nginx
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: ingress-nginx
subjects:
- kind: ServiceAccount
name: ingress-nginx
namespace: ingress-nginx
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: ingress-nginx-admission
subjects:
- kind: ServiceAccount
name: ingress-nginx-admission
namespace: ingress-nginx
- apiVersion: v1
data:
allow-snippet-annotations: "true"
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-controller
namespace: ingress-nginx
- apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
externalTrafficPolicy: Local
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: http
nodePort: 31338
port: 80
protocol: TCP
targetPort: 80
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
type: LoadBalancer
- apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-controller-admission
namespace: ingress-nginx
spec:
ports:
- appProtocol: https
name: https-webhook
port: 443
targetPort: webhook
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
type: ClusterIP
- apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
minReadySeconds: 0
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
spec:
containers:
- args:
- /nginx-ingress-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
- --election-id=ingress-nginx-leader
- --controller-class=k8s.io/ingress-nginx
- --ingress-class=nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: LD_PRELOAD
value: /usr/local/lib/libmimalloc.so
image: registry.k8s.io/ingress-nginx/controller:v1.8.1@sha256:e5c4824e7375fcf2a393e1c03c293b69759af37a9ca6abdb91b13d78a93da8bd
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /wait-shutdown
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
name: controller
ports:
- containerPort: 80
name: http
protocol: TCP
- containerPort: 443
name: https
protocol: TCP
- containerPort: 8443
name: webhook
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources:
requests:
cpu: 100m
memory: 90Mi
securityContext:
allowPrivilegeEscalation: true
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
runAsUser: 101
volumeMounts:
- mountPath: /usr/local/certificates/
name: webhook-cert
readOnly: true
dnsPolicy: ClusterFirst
nodeSelector:
kubernetes.io/os: linux
serviceAccountName: ingress-nginx
terminationGracePeriodSeconds: 300
volumes:
- name: webhook-cert
secret:
secretName: ingress-nginx-admission
- apiVersion: batch/v1
kind: Job
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission-create
namespace: ingress-nginx
spec:
template:
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission-create
spec:
containers:
- args:
- create
- --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc
- --namespace=$(POD_NAMESPACE)
- --secret-name=ingress-nginx-admission
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20230407@sha256:543c40fd093964bc9ab509d3e791f9989963021f1e9e4c9c7b6700b02bfb227b
imagePullPolicy: IfNotPresent
name: create
securityContext:
allowPrivilegeEscalation: false
nodeSelector:
kubernetes.io/os: linux
restartPolicy: OnFailure
securityContext:
fsGroup: 2000
runAsNonRoot: true
runAsUser: 2000
serviceAccountName: ingress-nginx-admission
- apiVersion: batch/v1
kind: Job
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission-patch
namespace: ingress-nginx
spec:
template:
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission-patch
spec:
containers:
- args:
- patch
- --webhook-name=ingress-nginx-admission
- --namespace=$(POD_NAMESPACE)
- --patch-mutating=false
- --secret-name=ingress-nginx-admission
- --patch-failure-policy=Fail
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20230407@sha256:543c40fd093964bc9ab509d3e791f9989963021f1e9e4c9c7b6700b02bfb227b
imagePullPolicy: IfNotPresent
name: patch
securityContext:
allowPrivilegeEscalation: false
nodeSelector:
kubernetes.io/os: linux
restartPolicy: OnFailure
securityContext:
fsGroup: 2000
runAsNonRoot: true
runAsUser: 2000
serviceAccountName: ingress-nginx-admission
- apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: nginx
namespace: default
spec:
controller: k8s.io/ingress-nginx
- apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
labels:
app.kubernetes.io/component: admission-webhook
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-admission
namespace: default
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: ingress-nginx-controller-admission
namespace: ingress-nginx
path: /networking/v1/ingresses
failurePolicy: Fail
matchPolicy: Equivalent
name: validate.nginx.ingress.kubernetes.io
rules:
- apiGroups:
- networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- ingresses
sideEffects: None
workflows:
resource:
configure:
- apiVersion: platform.kratix.io/v1alpha1
kind: Pipeline
metadata:
name: resource-configure
spec:
containers:
- name: create-resources
image: kratix-workshop/app-pipeline-image:v1.0.0
command: [ resource-configure ]

Install the Promise

Go ahead and install the Promise in the platform:

kubectl --context $PLATFORM apply --filename promise.yaml

As part of the Promise dependencies, you have included the NGINX Ingress Controller. You can check that it got installed in the worker cluster Destination correctly by running the following command:

kubectl --context $WORKER get deployments --namespace ingress-nginx

It may take a couple of minutes, but you will eventually see an output similar to:

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx-controller 1/1 1 1 2m8s

You can switch hats now and assume the role of an App developer trying to deploy their application.

Clusters with a Promise
installed
Dependencies being scheduled to the Destination

Using the App Promise

As a developer, you can check the available promises by running the following command:

kubectl --context $PLATFORM get promises

The above command should output the following:

NAME   STATUS      KIND   API VERSION             VERSION
app Available App workshop.kratix.io/v1

You can see that the app Promise is available. Great! You can deploy your app by providing an image and a service.port.

The CLI should've created an example-resource.yaml file in the app-promise directory. You can use this file to request a new App deployment. If the file is not there, you can create it by running the following command:

touch example-resource.yaml

Open the file in your editor and add the following content:

app-promise/example-resource.yaml
apiVersion: workshop.kratix.io/v1
kind: App
metadata:
name: todo
namespace: default
spec:
image: syntasso/sample-todo:v0.1.0
service:
port: 8080

Then, submit the request by running the following command:

kubectl --context $PLATFORM apply --filename example-resource.yaml

Kratix will receive the request and execute the workflow defined in the Promise. You can verify the workflow by running the following command:

kubectl --context $PLATFORM get pods

The above command should output something similar to:

NAME                                       READY   STATUS      RESTARTS      AGE
kratix-app-todo-resource-configure-029c3-5tmbq 0/1 Completed 0 1m

It may take a few seconds for the pod to appear, but it will eventually show as Completed. At this stage, Kratix will schedule the workflow outputs to the worker cluster. You should see the App getting deployed on the Worker with the following command:

kubectl --context $WORKER get deployments

The above command should output something similar to (it may take a couple of minutes):

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
todo 1/1 1 1 16s

Once the deployment is completed, you can access the App in your browser, at http://todo.local.gd:31338.

Clusters with a Promise
installed
Todo app getting deployed

Bonus Challenge

Kubernetes CRDs provide many features that you can use to make your Promise API more user-friendly. For example, you can set the image property to be required, ensure service.port is within a range of ports and set it to be 8080 by default. Try adding some of those validations to your Promise API. Check the Kubernetes documentation for reference. Once you add the validations, try sending invalid resource requests and see how Kubernetes responds.

🎉   Congratulations!

You just now authored your first Promise and delivered an App-as-a-Service. Well done!

To recap what you achieved:

  1. ✅  api: Defined your Promise API as a Custom Resource Definition
  2. ✅  dependencies: Defined what needs to be present on your Destinations to fulfil this Promise
  3. ✅  workflows: Built a simple pipeline step for Resource configuration
  4. ✅  Installed your Kratix Promise
  5. ✅  Created and submitted a request for the promised Resource

👉🏾   Next, let's go over promise workflows.