# Kubernetes: No YAML, please!?

**YAML**, or how i like to call it: the Marmite of data-serialization.

![grafik.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633895753798/8HQalgvE3.png)

Marmites slogan "You either love it or hate it", fits so perfectly to our relationship with **YAML**. Particular in the Kubernetes world, where all the Kubernetes resource manifest are written in YAML. Whatever your position is, I thought about what other possibities we have to describe our workload for Kubernetes.

I want to see how it is to express the abstraction’s API using strong-typed data types and do all that stuff that we are used to do.

I looked around and found three approaches to not use **YAML**. All of them with their own twist.

- [Pulumi](https://www.pulumi.com/) - 10k Github-⭐ 
- [naml](https://github.com/kris-nova/naml) - 972 Github-⭐
- [CDK8s](https://cdk8s.io/) - 2.5k Github-⭐

I will use only `Golang` as the language in the demo code. But expect of  `naml` you can choose also choose the development language of your preference.

# Podtato-head

We will deploy the 📨🚚 CNCF App Delivery SIG Demo [podtato-head](https://github.com/podtato-head/podtato-head) 

Podtato-head demonstrates cloud-native application delivery scenarios using many different tools and services. It is intended to help application delivery support teams test and decide which mechanism(s) to use.

![grafik.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633896307123/4eZMQKX1Ay.png)

I will translate this manifest into code -> https://github.com/podtato-head/podtato-head/blob/main/delivery/kubectl/manifest.yaml. So we have a good reference to compare to.

Lets bring the contestants in.

![grafik.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633898729446/iyyblxGd0.png)

# Pulumi

![grafik.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633896465413/Jm66NMgF6.png)

Pulumi is an open source infrastructure as code tool for creating, deploying, and managing cloud infrastructure. Pulumi works with traditional infrastructure like VMs, networks, and databases, in addition to modern architectures, including containers, Kubernetes clusters, and serverless functions. 

Starting with Pulumi is so easy, we just need to download the CLI and run following commands:

```bash
pulumi new
```
And chose the `kubernetes-go` template. Than we can start to build our deployments inside our code. 


```go
package main

import (
	"fmt"
	appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/apps/v1"
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/core/v1"
	metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/meta/v1"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func buildPodtatoHeadComponent(ctx *pulumi.Context, ns *corev1.Namespace, appName pulumi.StringMap, componentName,
	imageVersion string, servicePort int, serviceType string) error {

	componentLabel := pulumi.StringMap{
		"component": pulumi.String(componentName),
	}

	_, err := appsv1.NewDeployment(ctx, componentName, &appsv1.DeploymentArgs{
		Metadata: &metav1.ObjectMetaArgs{
			Name:      pulumi.String(componentName),
			Namespace: ns.Metadata.Name(),
			Labels:    appName,
		},
		Spec: appsv1.DeploymentSpecArgs{
			Selector: &metav1.LabelSelectorArgs{
				MatchLabels: componentLabel,
			},
			Template: &corev1.PodTemplateSpecArgs{
				Metadata: &metav1.ObjectMetaArgs{
					Labels: componentLabel,
				},
				Spec: &corev1.PodSpecArgs{
					TerminationGracePeriodSeconds: pulumi.Int(5),
					Containers: &corev1.ContainerArray{
						&corev1.ContainerArgs{
							Name:            pulumi.String("server"),
							Image:           pulumi.String(fmt.Sprintf("ghcr.io/podtato-head/%s:%s", componentName, imageVersion)),
							ImagePullPolicy: pulumi.String("Always"),
							Ports: &corev1.ContainerPortArray{
								&corev1.ContainerPortArgs{
									ContainerPort: pulumi.Int(9000),
								},
							},
							Env: &corev1.EnvVarArray{
								&corev1.EnvVarArgs{
									Name:  pulumi.String("PORT"),
									Value: pulumi.String("9000"),
								},
							},
						}},
				},
			},
		},
	})
	if err != nil {
		return err
	}

	_, err = corev1.NewService(ctx, componentName, &corev1.ServiceArgs{
		Metadata: &metav1.ObjectMetaArgs{
			Name:      pulumi.String(componentName),
			Namespace: ns.Metadata.Name(),
			Labels:    appName,
		},
		Spec: &corev1.ServiceSpecArgs{
			Selector: componentLabel,
			Ports: &corev1.ServicePortArray{
				&corev1.ServicePortArgs{
					Name:       pulumi.String("http"),
					Port:       pulumi.Int(servicePort),
					Protocol:   pulumi.String("TCP"),
					TargetPort: pulumi.Int(9000),
				},
			},
			Type: pulumi.String(serviceType),
		},
	})
	if err != nil {
		return err
	}
	return nil
}

<snip ...>

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		namespace, err := corev1.NewNamespace(ctx, "podtato-kubectl", &corev1.NamespaceArgs{
			Metadata: &metav1.ObjectMetaArgs{
				Name: pulumi.String("podtato-kubectl"),
			},
		})
		if err != nil {
			return err
		}
		appLabels := pulumi.StringMap{
			"app": pulumi.String("podtato-head"),
		}

		for _, podtatoPart := range podtatoHead.PodtatoParts {
			err = buildPodtatoHeadComponent(ctx, namespace, appLabels, podtatoPart.PartName,
				podtatoPart.ImageVersion, podtatoPart.ServicePort, podtatoPart.ServiceType)
			if err != nil {
				return err
			}
		}
		return nil
	})
}
```

After you wrote your Kubernetes deployment you can "deploy" it with just typing:

```bash
pulumi up
```
Pulumi will compare your deployment now against the Kubernetes cluster and will print you out everything what will be modified. This is a huge ddvantage of `pulumi`. 

You get a State handling and can detect any configuration drifts and can re-apply the deployment.

On top has pulumi "CrossGuard" which is their take on the Policy as Code. With CrossGuard you can set guardrails to enforce compliance for resources so developers within your organization can deploy their apps while sticking to best practices and security compliance.

# naml

Not Another Markup Language or short `naml` is a Go library and command line tool that can be used as a framework to develop and deploy Kubernetes applications.

All you need to do is create a go project and add the `naml` libaries to your `go.mod`. Plus you need to implement the `Deployable` Interface

```go
// Deployable is an interface that can be implemented
// for deployable applications.
type Deployable interface {

    // Install will attempt to install in Kubernetes
    Install(client *kubernetes.Clientset) error

    // Uninstall will attempt to uninstall in Kubernetes
    Uninstall(client *kubernetes.Clientset) error

    // Meta returns the Kubernetes native ObjectMeta which is used to manage applications with naml.
    Meta() *metav1.ObjectMeta

    // Description returns the application description
    Description() string

    // Objects will return the runtime objects defined for each application
    Objects() []runtime.Object
}
```

In our example the `podtato app` will look like this in `naml`

```go
package podtato

import (
	"context"
	"fmt"
	appsv1 "k8s.io/api/apps/v1"
	v1 "k8s.io/api/apps/v1"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/client-go/kubernetes"
)

var Version = "0.0.1"

type PodtatoHeadApp struct {
	name         string
	description  string
	objects      []runtime.Object
	podtatoParts []podtatoParts
}

type podtatoParts struct {
	PartName     string
	ImageVersion string
	ServicePort  int
	ServiceType  apiv1.ServiceType
}

func NewPodtatoHeadApp() *PodtatoHeadApp {
	return &PodtatoHeadApp{
		name:        "podtato-kubectl",
		description: "📨🚚 CNCF App Delivery SIG Demo",
		... snip
		},
	}
}

func (p *PodtatoHeadApp) Install(client *kubernetes.Clientset) error {
	ctx := context.Background()
	namespace := &apiv1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: p.name,
		},
	}
	ns, err := client.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("unable to install namespace in Kubernetes: %v", err)
	}
	p.objects = append(p.objects, ns)
	appLabels := map[string]string{"app": "podtato-head"}

	err = p.buildPodtatoHeadComponent(ctx, client, appLabels)
	if err != nil {
		return err
	}

	return nil
}

func (p *PodtatoHeadApp) Uninstall(client *kubernetes.Clientset) error {
	ctx := context.Background()
	err := client.CoreV1().Namespaces().Delete(ctx, p.name, metav1.DeleteOptions{})
	if err != nil {
		return err
	}
	return nil
}

func (p *PodtatoHeadApp) Meta() *metav1.ObjectMeta {
	return &metav1.ObjectMeta{
		Name: p.name,
	}
}

func (p *PodtatoHeadApp) Description() string {
	return p.description
}

func (p *PodtatoHeadApp) Objects() []runtime.Object {
	return p.objects
}

func (p *PodtatoHeadApp) buildPodtatoHeadComponent(ctx context.Context, client *kubernetes.Clientset, appName map[string]string) error {

	for _, podtatoPart := range p.podtatoParts {
		componentLables := map[string]string{"component": podtatoPart.PartName}
		terminationGracePeriodSeconds := int64(5)
		deployment := &v1.Deployment{
			ObjectMeta: metav1.ObjectMeta{
				Name:      podtatoPart.PartName,
				Namespace: p.name,
				Labels:    appName,
			},
			Spec: appsv1.DeploymentSpec{
				Selector: &metav1.LabelSelector{
					MatchLabels: componentLables,
				},
				Template: apiv1.PodTemplateSpec{
					ObjectMeta: metav1.ObjectMeta{
						Labels: componentLables,
					},
					Spec: apiv1.PodSpec{
						TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
						Containers: []apiv1.Container{
							{
								Name:            "server",
								Image:           fmt.Sprintf("ghcr.io/podtato-head/%s:%s", podtatoPart.PartName, podtatoPart.ImageVersion),
								ImagePullPolicy: apiv1.PullAlways,
								Ports: []apiv1.ContainerPort{
									{
										ContainerPort: 9000,
									},
								},
								Env: []apiv1.EnvVar{
									{
										Name:  "PORT",
										Value: "9000",
									},
								},
							},
						},
					},
				},
			},
		}
		_, err := client.AppsV1().Deployments(p.name).Create(ctx, deployment, metav1.CreateOptions{})
		if err != nil {
			return fmt.Errorf("unable to install deployment in Kubernetes: %v", err)
		}
		p.objects = append(p.objects, deployment)

		service := &apiv1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name:      podtatoPart.PartName,
				Namespace: p.name,
				Labels:    appName,
			},
			Spec: apiv1.ServiceSpec{
				Selector: componentLables,
				Ports: []apiv1.ServicePort{
					{
						Name:       "http",
						Port:       int32(podtatoPart.ServicePort),
						Protocol:   apiv1.ProtocolTCP,
						TargetPort: intstr.FromInt(9000),
					},
				},
				Type: podtatoPart.ServiceType,
			},
		}
		_, err = client.CoreV1().Services(p.name).Create(ctx, service, metav1.CreateOptions{})
		if err != nil {
			return fmt.Errorf("unable to install service in Kubernetes: %v", err)
		}
		p.objects = append(p.objects, service)
	}
	return nil
}
```
Now you can compile this in to binary and deploy / undeploy it with the cli command:

```bash
<yourapp> install

or

<yourapp> uninstall
```

# CDK8s

![grafik.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633897596710/GmwUD0iuQ.png)

cdk8s is an open-source software development framework for defining Kubernetes applications and reusable abstractions using familiar programming languages and rich object-oriented APIs. 

And now comes the fun part: cdk8s apps synthesize into standard Kubernetes manifests which can be applied to any Kubernetes cluster.

To get started you just need to install the cli

```
npm install -g cdk8s-cli #for the 1.0.0-beta

brew install cdk8s # for the 0.33.0
```

And then create your project with the language template you want to use. At the momend `CDK8s` supports following languages:

- TypeScript
- Python
- Java
- Go

```
mkdir podtato-head-cdk8s
cd podtato-head-cdk8s
cdk8s init go-app
cdk8s import
cdk8s synth
kubectl apply -f dist/podtato-head-cdk8s.k8s.yaml 
```

One importanc concept of `CDK8s` is the abstraction through constructs

Constructs are the basic building block of `CDK8s`. They are the instrument that enables composition and creation of higher-level abstractions through normal object-oriented classes.

So with this in mind, my `CDK8s` go project looks like this:

```go
package podtato

import (
	"fmt"
	"github.com/aws/constructs-go/constructs/v3"
	"github.com/aws/jsii-runtime-go"
	"github.com/cdk8s-team/cdk8s-core-go/cdk8s"
	"github.com/dirien/podtato-head-cdk8s/imports/k8s"
)

type PodtatoHeadProps struct {
	cdk8s.ChartProps
	PodtatoParts []PodtatoParts
}

type PodtatoParts struct {
	PartName     string
	ImageVersion string
	ServicePort  int
	ServiceType  string
}

func buildPodtatoHeadComponent(scope constructs.Construct, ns k8s.KubeNamespace, appName map[string]*string, componentName,
	imageVersion string, servicePort int, serviceType string) {
	componentLabel := map[string]*string{"component": jsii.String(componentName)}

	k8s.NewKubeDeployment(scope, jsii.String(fmt.Sprintf("%s-depl", componentName)), &k8s.KubeDeploymentProps{
		Metadata: &k8s.ObjectMeta{
			Name:      jsii.String(componentName),
			Namespace: ns.Metadata().Name(),
			Labels:    &appName,
		},
		Spec: &k8s.DeploymentSpec{
			Selector: &k8s.LabelSelector{
				MatchLabels: &componentLabel,
			},
			Template: &k8s.PodTemplateSpec{
				Metadata: &k8s.ObjectMeta{
					Labels: &componentLabel,
				},
				Spec: &k8s.PodSpec{
					TerminationGracePeriodSeconds: jsii.Number(5),
					Containers: &[]*k8s.Container{
						{
							Name:            jsii.String("server"),
							Image:           jsii.String(fmt.Sprintf("ghcr.io/podtato-head/%s:%s", componentName, imageVersion)),
							ImagePullPolicy: jsii.String("Always"),
							Ports: &[]*k8s.ContainerPort{
								{
									ContainerPort: jsii.Number(9000),
								},
							},
							Env: &[]*k8s.EnvVar{
								{
									Name:  jsii.String("PORT"),
									Value: jsii.String("9000"),
								},
							},
						},
					},
				},
			},
		},
	})

	k8s.NewKubeService(scope, jsii.String(fmt.Sprintf("%s-svc", componentName)), &k8s.KubeServiceProps{
		Metadata: &k8s.ObjectMeta{
			Name:      jsii.String(componentName),
			Namespace: ns.Metadata().Name(),
			Labels:    &appName,
		},
		Spec: &k8s.ServiceSpec{
			Selector: &componentLabel,
			Ports: &[]*k8s.ServicePort{
				{
					Name:       jsii.String("http"),
					Port:       jsii.Number(float64(servicePort)),
					Protocol:   jsii.String("TCP"),
					TargetPort: k8s.IntOrString_FromNumber(jsii.Number(float64(9000))),
				},
			},
			Type: jsii.String(serviceType),
		},
	})
}

func PodtatoHeadChart(scope constructs.Construct, id string, props *PodtatoHeadProps) cdk8s.Chart {
	var cprops cdk8s.ChartProps
	if props != nil {
		cprops = props.ChartProps
	}
	chart := cdk8s.NewChart(scope, jsii.String(id), &cprops)

	appLabels := map[string]*string{"app": jsii.String("podtato-head")}

	namespace := k8s.NewKubeNamespace(chart, jsii.String("podtato-kubectl"), &k8s.KubeNamespaceProps{
		Metadata: &k8s.ObjectMeta{
			Name: jsii.String("podtato-kubectl"),
		},
	})
	for _, podtatoPart := range props.PodtatoParts {
		buildPodtatoHeadComponent(chart, namespace, appLabels, podtatoPart.PartName,
			podtatoPart.ImageVersion, podtatoPart.ServicePort, podtatoPart.ServiceType)
	}

	return chart
}
```

# TL;DR

I would give the whole `no-yaml` way of defining our workload a try. Find out what fits the best inside your team/project or even whole company. It helps us to accelerate the whole shift left movement in embeding this subjects into a domain we are already very well in. 

Le code -> https://github.com/dirien/no-yaml
