Assume you have an idea for a new application or service.
You write some Go to test and validate the idea - it works on your machine!
What’s the simplest way to turn the source code into an easily shareable artifact?
The answer is ko:
ko makes building Go container images easy, fast, and secure by default.
Some years ago I wrote about the ideal Dockerfile for a minimal Go container image - with ko
this is no longer necessary since it already incorporates many of the principles discussed in that post.
ko
knows how to build Go applications from source (also for multiple architectures and target platforms), package the artifact in an OCI container image and upload it to a container registry - oh, and it also has some integrations for deploying the result with Kubernetes.
In this post I describe a workflow to use ko
for fast, end-to-end prototyping on Kubernetes: from source code to deployment with one command!
Before we get started, make sure to install ko
according to the documentation - I’m using version 0.15.1
.
#
Build source code
Let’s assume we have a simple Go app like this:
1
2
| $ go mod init my-go-app
go: creating new go.mod: module my-go-app
|
main.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| package main
import (
"fmt"
"net/http"
"time"
)
// Simple HTTP server that listens on port 8080 and returns the current time as HH:MM:SS
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "It's %s.\n", time.Now().Format("15:04:05"))
})
http.ListenAndServe(":8080", nil)
}
|
With the Go toolchain we can run it locally like this:
1
2
3
| $ go run main.go &
$ curl localhost:8080
It's 15:25:41.
|
A fully “Go-native” application (without Cgo or other external dependencies) is ideal for ko
because it can rely on the standard Go build workflow.
#
Package as container image
As a Go programmer you are familiar with the go build
command: it turns the application source code into an executable binary.
The ko build
command does the same, but instead of producing a binary, it produces a container image.
One of the major benefits of OCI container images is that they are easily shareable via container registries.
Thus, log in to your favorite registry to get started:
1
2
| $ export KO_DOCKER_REPO=registry.example.com/jack
$ ko login registry.example.com --username jack --password hunter.2
|
Now let the magic begin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| $ ko build my-go-app
2023/12/14 15:34:54 Using base cgr.dev/chainguard/static:latest@sha256:6ddc52909b55f716c3154a28efab0c7ffb7ef741aeb390a7a431e9f3001ec010 for my-go-app
2023/12/14 15:34:55 Building my-go-app for linux/amd64
2023/12/14 15:34:56 Publishing registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:latest
2023/12/14 15:34:58 pushed blob: sha256:cde39bb985375cba3d3b80a0b928447709d4cfd21929e6b0b52a48680fc4106a
2023/12/14 15:34:58 pushed blob: sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542
2023/12/14 15:34:59 pushed blob: sha256:cea99c2397e0499381a4f347263688d68c29624fd2384b9a7626cb83d2814c18
2023/12/14 15:34:59 pushed blob: sha256:15cdb3441d5977fcfe316b866157555e0fb09ebe3f8bd3105d9b7a63dcc3c86f
2023/12/14 15:34:59 pushed blob: sha256:fcf8faa913eacf5240e9b089ab899671597a0bcf7a64caaa25242f1af4ebafa6
2023/12/14 15:34:59 pushed blob: sha256:2bad94c418370d034e1a4d09d58e15b0d2b6079233efb352bf89caa0eb9a7673
2023/12/14 15:34:59 registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom: digest: sha256:ccd2236c8965fa76928eda7c9b38a9c8395f473bf8d0a5e9ccc528fcbb8147b7 size: 373
2023/12/14 15:34:59 Published SBOM registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom
2023/12/14 15:34:59 registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:latest: digest: sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f size: 1209
2023/12/14 15:34:59 Published registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
|
ko
build a binary from the source (line 3), wrapped it in a container image (line 2) and automatically uploaded it to ${KO_DOCKER_REGISTRY}/<APP_NAME>-<HASH_OF_GO_IMPORT_PATH>
(line 4-10).
ko
also pushed two tags: latest
and sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom
- the first one contains the application, the second one contains the Software Bill of Materials (SBOM), i.e. all the versions and checksums of all dependencies that went into building the image (you can read more about that here).
Because the container images built by ko
are so small, all of this happens pretty fast as well (just a couple of seconds)!
Side note: in case you only want to use the image on your local machine and don’t want to upload it to a registry, you can pass the --local
flag.
#
Deploy to Kubernetes
ko
has already built the container image for us, now we need to deploy and run the application somewhere.
Because ko build
outputs a unique reference for each image it builds, we can use this to spin up a Deployment
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| $ image=$(ko build .)
2023/12/14 15:49:41 Using base cgr.dev/chainguard/static:latest@sha256:6ddc52909b55f716c3154a28efab0c7ffb7ef741aeb390a7a431e9f3001ec010 for my-go-app
2023/12/14 15:49:42 Building my-go-app for linux/amd64
2023/12/14 15:49:42 Publishing registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:latest
2023/12/14 15:49:43 existing manifest: latest@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
2023/12/14 15:49:43 existing manifest: sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom@sha256:ccd2236c8965fa76928eda7c9b38a9c8395f473bf8d0a5e9ccc528fcbb8147b7
2023/12/14 15:49:43 Published SBOM registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom
2023/12/14 15:49:43 Published registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
$ echo "$image"
registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
$ kubectl create deployment my-deploy --image=$(ko build .) --port=8080
# (ko build output omitted)
deployment.apps/my-deploy created
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
my-deploy 1/1 1 1 9s
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
my-deploy-66f999bd78-zjmtx 1/1 Running 0 23s
|
Just like that we have a Deployment
running in a Kubernetes cluster!
A similar approach can be used on OpenShift clusters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| $ oc new-app --name my-app --image=$(ko build .)
--> Found container image cea99c2 (7 days old) from registry.cern.ch for "registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f"
* An image stream tag will be created as "my-app:latest" that will track this image
--> Creating resources ...
imagestream.image.openshift.io "my-app" created
deployment.apps "my-app" created
--> Success
Run 'oc status' to view your app.
$ oc get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
my-app 1/1 1 1 8s
$ oc get pod
NAME READY STATUS RESTARTS AGE
my-app-79ff6ffc9d-nnxwz 1/1 Running 0 14s
# optional: create `Service` and `Route` (Ingress) to make the application available externally
$ oc expose deploy/my-app --port=8080
service/my-app exposed
$ oc expose svc/my-app --port=8080
$ oc get route
NAME HOST/PORT SERVICES PORT
my-app my-app-my-namespace.okd.example.com my-app 8080
$ curl my-app-my-namespace.okd.example.com
It's 15:05:59
|
#
Rapid iteration
The previous commands show the power and reusability of ko
, but they are not very useful for iterating because if we run the same command again, we get:
1
2
| $ kubectl create deployment my-deploy --image=$(ko build .) --port=8080
error: failed to create deployment: deployments.apps "my-deploy" already exists
|
We could run kubectl delete deploy/my-deploy && kubectl create deploy ...
, but there’s a better option: ko apply
.
It’s a wrapper around kubectl apply
that detects references in the form of ko://<go-import-path>
, builds the relevant image and substitutes the reference with the final path to the container image.
This means a YAML document like this (note the last line):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-app
spec:
replicas: 1
selector:
matchLabels:
app: my-go-app
template:
metadata:
labels:
app: my-go-app
spec:
containers:
- name: app
image: ko://my-go-app
|
can be deployed like this:
1
2
3
4
5
6
7
8
9
| $ ko apply -f deploy.yaml
2023/12/14 17:45:33 Using base cgr.dev/chainguard/static:latest@sha256:6ddc52909b55f716c3154a28efab0c7ffb7ef741aeb390a7a431e9f3001ec010 for my-go-app
2023/12/14 17:45:33 Building my-go-app for linux/amd64
2023/12/14 17:45:35 Publishing registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:latest
2023/12/14 17:45:38 existing manifest: latest@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
2023/12/14 17:45:38 existing manifest: sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom@sha256:ccd2236c8965fa76928eda7c9b38a9c8395f473bf8d0a5e9ccc528fcbb8147b7
2023/12/14 17:45:38 Published SBOM registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:sha256-16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f.sbom
2023/12/14 17:45:38 Published registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
deployment.apps/my-go-app created
|
Hint: instead of directly applying the manifests to the cluster, ko resolve
can be used to print them to stdout
.
Now we can update our source code, change the deployment and even add additional resources to the manifest, e.g. to expose the application externally:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # add a the bottom of deploy.yaml
---
apiVersion: v1
kind: Service
metadata:
name: my-go-app
spec:
type: ClusterIP
selector:
app: my-go-app
ports:
- port: 8080
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-go-app
spec:
rules:
- host: my-go-app.cluster.example.com
http:
paths:
- backend:
service:
name: my-go-app
port:
number: 8080
path: /
pathType: Exact
|
And simply run the same command again to update everything:
1
2
3
4
5
6
7
8
9
| $ ko apply -f deploy.yaml --sbom=none
2023/12/14 17:51:42 Using base cgr.dev/chainguard/static:latest@sha256:6ddc52909b55f716c3154a28efab0c7ffb7ef741aeb390a7a431e9f3001ec010 for my-go-app
2023/12/14 17:51:42 Building my-go-app for linux/amd64
2023/12/14 17:51:43 Publishing registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737:latest
2023/12/14 17:51:45 existing manifest: latest@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
2023/12/14 17:51:45 Published registry.example.com/jack/my-go-app-a9f6a17535407a54e1bf94b032dcd737@sha256:16a75df4106093942d012409deb68fe347d5b618295e9161edbb0a61c65e087f
deployment.apps/my-go-app configured
service/my-go-app created
ingress.networking.k8s.io/my-go-app created
|
This is really neat because it allows quickly iterating on application source code and deployment manifests:
- add a new environment variable in the code → specify the right value in the
Depoyment
; - load configuration from a file → add a
ConfigMap
or Secret
to the YAML manifests and mount them into the Deployment
; - add a sidecar container for authentication / reverse proxying → update the
Service
definition;
I have found this to be an incredibly productive workflow and will surely be using it more often in the future.
Check out all of ko
’s configuration options and have a look at the frequently asked questions.
Happy building!