Skip to main content

kubernetes

Deploy to Kubernetes with Dagger

caution

This is documentation for Dagger 0.1, which is no longer actively maintained.

For up-to-date documentation, see the latest version (0.2).

This tutorial illustrates how to use Dagger to build, push and deploy Docker images to Kubernetes.

Prerequisites

For this tutorial, you will need a Kubernetes cluster.

Kind is a tool for running local Kubernetes clusters using Docker.

1. Install kind

Follow these instructions to install Kind.

Alternatively, on macOS using homebrew:

brew install kind

2. Start a local registry

docker run -d -p 5000:5000 --name registry registry:2

3. Create a cluster with the local registry enabled in containerd

cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
endpoint = ["http://registry:5000"]
EOF

4. Connect the registry to the cluster network

docker network connect kind registry

Initialize a Dagger Project and Environment

(optional) Setup example app

You will need the local copy of the Dagger examples repository used in previous guides

git clone https://github.com/dagger/examples

Make sure that all commands are run from the todoapp directory:

cd examples/todoapp

Organize your package

Let's create a new directory for our Cue package:

mkdir kube

Deploy using Kubectl

Kubernetes objects are located inside the k8s folder:

ls -l k8s
# k8s
# ├── deployment.yaml
# └── service.yaml

# 0 directories, 2 files

As a starting point, let's deploy them manually with kubectl:

kubectl apply -f k8s/
# deployment.apps/todoapp created
# service/todoapp-service created

Verify that the deployment worked:

kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# todoapp 1/1 1 1 10m

kubectl get service
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# todoapp-service NodePort 10.96.225.114 <none> 80:32658/TCP 11m

The next step is to transpose it in Cue. Before continuing, clean everything:

kubectl delete -f k8s/
# deployment.apps "todoapp" deleted
# service "todoapp-service" deleted

Create a basic plan

Create a file named todoapp.cue and add the following configuration to it.

todoapp/kube/todoapp.cue
package main

import (
"alpha.dagger.io/dagger"
"alpha.dagger.io/kubernetes"
)

// input: kubernetes objects directory to deploy to
// set with `dagger input dir manifest ./k8s -e kube`
manifest: dagger.#Artifact & dagger.#Input

// Deploy the manifest to a kubernetes cluster
todoApp: kubernetes.#Resources & {
"kubeconfig": kubeconfig
source: manifest
}

This defines a todoApp variable containing the Kubernetes objects used to create a todoapp deployment. It also references a kubeconfig value defined below:

The following config.cue defines:

  • kubeconfig a generic value created to embed this string kubeconfig value
todoapp/kube/config.cue
package main

import (
"alpha.dagger.io/dagger"
)

// set with `dagger input text kubeconfig -f "$HOME"/.kube/config -e kube`
kubeconfig: string & dagger.#Input

Setup the environment

Create a new environment

Let's create a project:

dagger init

Let's create an environment to run it:

dagger new 'kube' -p kube

Configure the environment

Before we can bring up the deployment, we need to provide the kubeconfig input declared in the configuration. Otherwise, Dagger will complain about a missing input:

dagger up -e kube
# 5:05PM ERR system | required input is missing input=kubeconfig
# 5:05PM ERR system | required input is missing input=manifest
# 5:05PM FTL system | some required inputs are not set, please re-run with `--force` if you think it's a mistake missing=0s

You can inspect the list of inputs (both required and optional) using dagger input list:

dagger input list -e kube
# Input Value Set by user Description
# kubeconfig string false set with `dagger input text kubeconfig -f "$HOME"/.kube/config -e kube`
# manifest dagger.#Artifact false input: source code repository, must contain a Dockerfile set with `dagger input dir manifest ./k8s -e kube`
# todoApp.namespace *"default" | string false Kubernetes Namespace to deploy to
# todoApp.version *"v1.19.9" | string false Version of kubectl client

Let's provide the missing inputs:

# we'll use the "$HOME"/.kube/config created by `kind`
dagger input text kubeconfig -f "$HOME"/.kube/config -e kube

# Add as an artifact the k8s folder
dagger input dir manifest ./k8s -e kube

Deploying

Now is time to deploy to Kubernetes.

dagger up -e kube
# deploy | computing
# deploy | #26 0.700 deployment.apps/todoapp created
# deploy | #27 0.705 service/todoapp-service created
# deploy | completed duration=1.405s

Let's verify if the deployment worked:

kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# todoapp 1/1 1 1 1m

Before continuing, cleanup deployment:

kubectl delete -f k8s/
# deployment.apps "todoapp" deleted
# service "todoapp-service" deleted

Building, pushing, and deploying Docker images

Rather than deploying an existing (todoapp) image, we're going to build a Docker image from the source, push it to a registry, and update the Kubernetes configuration.

Update the plan

Let's see how to deploy an image locally and push it to the local cluster

kube/todoapp.cue faces these changes:

  • repository, source code of the app to build. It needs to have a Dockerfile
  • registry, URI of the registry to push to
  • image, build of the image
  • remoteImage, push an image to the registry
  • kustomization, apply kustomization to image
todoapp/kube/todoapp.cue
package main

import (
"encoding/yaml"

"alpha.dagger.io/dagger"
"alpha.dagger.io/docker"
"alpha.dagger.io/kubernetes"
"alpha.dagger.io/kubernetes/kustomize"
)

// input: source code repository, must contain a Dockerfile
// set with `dagger input dir repository . -e kube`
repository: dagger.#Artifact & dagger.#Input

// Registry to push images to
registry: string & dagger.#Input
tag: "test-kind"

// input: kubernetes objects directory to deploy to
// set with `dagger input dir manifest ./k8s -e kube`
manifest: dagger.#Artifact & dagger.#Input

// Todoapp deployment pipeline
todoApp: {
// Build the image from repository artifact
image: docker.#Build & {
source: repository
}

// Push image to registry
remoteImage: docker.#Push & {
target: "\(registry):\(tag)"
source: image
}

// Update the image from manifest to use the deployed one
kustomization: kustomize.#Kustomize & {
source: manifest

// Convert CUE to YAML.
kustomization: yaml.Marshal({
resources: ["deployment.yaml", "service.yaml"]

images: [{
name: "public.ecr.aws/j7f8d3t2/todoapp"
newName: remoteImage.ref
}]
})
}

// Deploy the customized manifest to a kubernetes cluster
kubeSrc: kubernetes.#Resources & {
"kubeconfig": kubeconfig
source: kustomization
}
}

Connect the Inputs

Next, we'll provide the two new inputs, repository and registry.

# A name after `localhost:5000/` is required to avoid error on push to the local registry
dagger input text registry "localhost:5000/kind" -e kube

# Add todoapp (current folder) to repository value
dagger input dir repository . -e kube

Bring up the changes

dagger up -e kube
# 4:09AM INF manifest | computing
# 4:09AM INF repository | computing
# ...
# 4:09AM INF todoApp.kubeSrc | #37 0.858 service/todoapp-service created
# 4:09AM INF todoApp.kubeSrc | #37 0.879 deployment.apps/todoapp created
# Output Value Description
# todoApp.remoteImage.ref "localhost:5000/kind:test-kind@sha256:cb8d92518b876a3fe15a23f7c071290dfbad50283ad976f3f5b93e9f20cefee6" Image ref
# todoApp.remoteImage.digest "sha256:cb8d92518b876a3fe15a23f7c071290dfbad50283ad976f3f5b93e9f20cefee6" Image digest

Let's verify if the deployment worked:

kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# todoapp 1/1 1 1 50s

Before continuing, cleanup deployment:

kubectl delete -f k8s/
# deployment.apps "todoapp" deleted
# service "todoapp-service" deleted

CUE Kubernetes manifest

This section will convert Kubernetes YAML manifest from k8s directory to CUE to take advantage of the language features.

For a more advanced example, see the official CUE Kubernetes tutorial

Convert Kubernetes objects to CUE

First, let's create re-usable definitions for the deployment and the service to remove a lot of boilerplate and repetition.

Let's define a re-usable #Deployment definition in kube/deployment.cue.

todoapp/kube/deployment.cue
package main

// Deployment template containing all the common boilerplate shared by
// deployments of this application.
#Deployment: {
// Name of the deployment. This will be used to label resources automatically
// and generate selectors.
name: string

// Container image.
image: string

// 80 is the default port.
port: *80 | int

// 1 is the default, but we allow any number.
replicas: *1 | int

// Deployment manifest. Uses the name, image, port and replicas above to
// generate the resource manifest.
manifest: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
"name": name
labels: app: name
}
spec: {
"replicas": replicas
selector: matchLabels: app: name
template: {
metadata: labels: app: name
spec: containers: [{
"name": name
"image": image
ports: [{
containerPort: port
}]
}]
}
}
}
}

Indeed, let's also define a re-usable #Service definition in kube/service.cue.

todoapp/kube/service.cue
package main

// Service template containing all the common boilerplate shared by
// services of this application.
#Service: {
// Name of the service. This will be used to label resources automatically
// and generate selector.
name: string

// NodePort is the default service type.
type: *"NodePort" | "LoadBalancer" | "ClusterIP" | "ExternalName"

// Ports where the service should listen
ports: [string]: number

// Service manifest. Uses the name, type and ports above to
// generate the resource manifest.
manifest: {
apiVersion: "v1"
kind: "Service"
metadata: {
"name": "\(name)-service"
labels: app: name
}
spec: {
"type": type
"ports": [
for k, v in ports {
name: k
port: v
},
]
selector: app: name
}
}
}

Generate Kubernetes manifest

Now that you have generic definitions for your Kubernetes objects. You can use them to get back your YAML definition without having boilerplate nor repetition.

Create a new definition named #AppManifest that will generate the YAML in kube/manifest.cue.

todoapp/kube/manifest.cue
package main

import (
"encoding/yaml"
)

// Define and generate kubernetes deployment to deploy to kubernetes cluster
#AppManifest: {
// Name of the application
name: string

// Image to deploy to
image: string

// Define a kubernetes deployment object
deployment: #Deployment & {
"name": name
"image": image
}

// Define a kubernetes service object
service: #Service & {
"name": name
ports: http: deployment.port
}

// Merge definitions and convert them back from CUE to YAML
manifest: yaml.MarshalStream([deployment.manifest, service.manifest])
}

Update manifest

You can now remove the manifest input in kube/todoapp.cue and instead use the manifest created by #AppManifest.

kube/todoapp.cue configuration has following changes:

  • removal of unused imported encoding/yaml and kustomize packages.
  • removal of manifest input that is doesn't need anymore.
  • removal of kustomization to replace it with #AppManifest definition.
  • Update kubeSrc to use manifest field instead of source because we don't send Kubernetes manifest of dagger.#Artifact type anymore.
todoapp/kube/todoapp.cue
package main

import (
"alpha.dagger.io/dagger"
"alpha.dagger.io/docker"
"alpha.dagger.io/kubernetes"
)

// input: source code repository, must contain a Dockerfile
// set with `dagger input dir repository . -e kube`
repository: dagger.#Artifact & dagger.#Input

// Registry to push images to
registry: string & dagger.#Input
tag: "test-kind"

// Todoapp deployment pipeline
todoApp: {
// Build the image from repositoru artifact
image: docker.#Build & {
source: repository
}

// Push image to registry
remoteImage: docker.#Push & {
target: "\(registry):\(tag)"
source: image
}

// Generate deployment manifest
deployment: #AppManifest & {
name: "todoapp"
image: remoteImage.ref
}

// Deploy the customized manifest to a kubernetes cluster
kubeSrc: kubernetes.#Resources & {
"kubeconfig": kubeconfig
manifest: deployment.manifest
}
}

Remove unused input

Now that we manage our Kubernetes manifest in CUE, we don't need manifest anymore.

# Remove `manifest` input
dagger input unset manifest -e kube

Deployment

dagger up -e kube
# 4:09AM INF manifest | computing
# 4:09AM INF repository | computing
# ...
# 4:09AM INF todoApp.kubeSrc | #37 0.858 service/todoapp-service created
# 4:09AM INF todoApp.kubeSrc | #37 0.879 deployment.apps/todoapp created
# Output Value Description
# todoApp.remoteImage.ref "localhost:5000/kind:test-kind@sha256:cb8d91518b076a3fe15a33f7c171290dfbad50283ad976f3f5b93e9f33cefag7" Image ref
# todoApp.remoteImage.digest "sha256:cb8d91518b076a3fe15a33f7c171290dfbad50283ad976f3f5b93e9f33cefag7" Image digest

Let's verify that the deployment worked:

kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# todoapp 1/1 1 1 37s

Next Steps

Integrate Helm with Dagger: