YAML, or how i like to call it: the Marmite of data-serialization.
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.
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
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.
I will translate this manifest into code -> github.com/podtato-head/podtato-head/blob/m... So we have a good reference to compare to.
Lets bring the contestants in.
Pulumi
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:
pulumi new
And chose the kubernetes-go
template. Than we can start to build our deployments inside our code.
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:
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
// 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
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:
<yourapp> install
or
<yourapp> uninstall
CDK8s
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:
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 -> github.com/dirien/no-yaml