ko
is a tool for building and deploying Golang applications to Kubernetes.
ko
can be installed and upgraded from our
releases by choosing a particular
version (the example shows v0.7.0 for x86_64) from that list and then running:
Note: Golang version 1.14.0
or higher is required.
curl -L https://github.com/google/ko/releases/download/v0.7.0/ko_0.7.0_Linux_x86_64.tar.gz | tar xzf - ko
chmod +x ./ko
The ko
CLI makes extensive use of the container registry as a ubiquitous and
standard object store. However, the typical model for authenticating with a
container registry is via docker login
, and ko
does not require users to
install docker
locally. To facilitate logging in without docker
we expose:
ko login my.registry.io -u username --password-stdin
ko
is built around a very simple extension to Go's model for expressing
dependencies using import paths.
In Go, dependencies are expressed via blocks like:
import (
"github.com/google/foo/pkg/hello"
"github.com/google/bar/pkg/world"
)
Similarly (as you can see above), Go binaries can be referenced via import paths
like github.com/google/ko/cmd
.
One of the goals of ko
is to make containers invisible infrastructure.
Simply replace image references in your Kubernetes yaml with the import path for
your Go binary prefixed with ko://
(e.g. ko://github.com/google/ko/test
),
and ko
will handle containerizing and publishing that container image as
needed.
For example, you might use the following in a Kubernetes Deployment
resource:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-world
spec:
selector:
matchLabels:
foo: bar
replicas: 1
template:
metadata:
labels:
foo: bar
spec:
containers:
- name: hello-world
# This is the import path for the Go binary to build and run.
image: ko://github.com/mattmoor/examples/http/cmd/helloworld
ports:
- containerPort: 8080
ko
will attempt to containerize and build any string within the yaml prefixed
with ko://
.
Employing this convention enables ko
to have effectively zero configuration
and enables very fast development iteration. For
warm-image, ko
is able to build,
containerize, and redeploy a non-trivial Kubernetes controller app in seconds
(dominated by two go build
s).
$ ko apply -f config/
2018/07/19 14:56:41 Using base gcr.io/distroless/static:nonroot for github.com/mattmoor/warm-image/cmd/sleeper
2018/07/19 14:56:42 Publishing us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37:latest
2018/07/19 14:56:43 mounted blob: sha256:57752e7f9593cbfb7101af994b136a369ecc8174332866622db32a264f3fbefd
2018/07/19 14:56:43 mounted blob: sha256:59df9d5b488aea2753ab7774ae41a9a3e96903f87ac699f3505960e744f36f7d
2018/07/19 14:56:43 mounted blob: sha256:739b3deec2edb17c512f507894c55c2681f9724191d820cdc01f668330724ca7
2018/07/19 14:56:44 us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37:latest: digest: sha256:6c7b96a294cad3ce613aac23c8aca5f9dd12a894354ab276c157fb5c1c2e3326 size: 592
2018/07/19 14:56:44 Published us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37@sha256:6c7b96a294cad3ce613aac23c8aca5f9dd12a894354ab276c157fb5c1c2e3326
2018/07/19 14:56:45 Using base gcr.io/distroless/static:nonroot for github.com/mattmoor/warm-image/cmd/controller
2018/07/19 14:56:46 Publishing us.gcr.io/my-project/controller-9e91872fd7c48124dbe6ea83944b87e9:latest
2018/07/19 14:56:46 mounted blob: sha256:007782ba6738188a59bf21b4d8e974f218615ee948c6357535d07e7248b2a560
2018/07/19 14:56:46 mounted blob: sha256:57752e7f9593cbfb7101af994b136a369ecc8174332866622db32a264f3fbefd
2018/07/19 14:56:46 mounted blob: sha256:7fec050f965d7fba3de4bd19739746dce5a5125331b7845bf02185ff5d4cc374
2018/07/19 14:56:47 us.gcr.io/my-project/controller-9e91872fd7c48124dbe6ea83944b87e9:latest: digest: sha256:5a81029bb0cfd519c321aeeea2bc1b7dc6488b6c72003d3613442b4d5e4ed14d size: 593
2018/07/19 14:56:47 Published us.gcr.io/my-project/controller-9e91872fd7c48124dbe6ea83944b87e9@sha256:5a81029bb0cfd519c321aeeea2bc1b7dc6488b6c72003d3613442b4d5e4ed14d
namespace/warmimage-system configured
clusterrolebinding.rbac.authorization.k8s.io/warmimage-controller-admin configured
deployment.apps/warmimage-controller unchanged
serviceaccount/warmimage-controller unchanged
customresourcedefinition.apiextensions.k8s.io/warmimages.mattmoor.io configured
ko
has four commands, most of which build and publish images as part of their
execution. By default, ko
publishes images to a Docker Registry specified via
KO_DOCKER_REPO
.
Note: You'll need to be authenticated with your KO_DOCKER_REPO
before
pushing images. Run gcloud auth configure-docker
if you are using Google
Container Registry or docker login
if you are using Docker Hub.
However, these same commands can be directed to operate locally as well via the
--local
or -L
command (or setting KO_DOCKER_REPO=ko.local
). See the
minikube
section for more detail.
ko
can also be used with kind
directly by setting
KO_DOCKER_REPO=kind.local
. See the relevant section
for more detail.
ko publish
simply builds and publishes images for each import path passed as
an argument. It prints the images' published digests after each image is
published.
$ ko publish github.com/mattmoor/warm-image/cmd/sleeper
2018/07/19 14:57:34 Using base gcr.io/distroless/static:nonroot for github.com/mattmoor/warm-image/cmd/sleeper
2018/07/19 14:57:35 Publishing us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37:latest
2018/07/19 14:57:35 mounted blob: sha256:739b3deec2edb17c512f507894c55c2681f9724191d820cdc01f668330724ca7
2018/07/19 14:57:35 mounted blob: sha256:57752e7f9593cbfb7101af994b136a369ecc8174332866622db32a264f3fbefd
2018/07/19 14:57:35 mounted blob: sha256:59df9d5b488aea2753ab7774ae41a9a3e96903f87ac699f3505960e744f36f7d
2018/07/19 14:57:36 us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37:latest: digest: sha256:6c7b96a294cad3ce613aac23c8aca5f9dd12a894354ab276c157fb5c1c2e3326 size: 592
2018/07/19 14:57:36 Published us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37@sha256:6c7b96a294cad3ce613aac23c8aca5f9dd12a894354ab276c157fb5c1c2e3326
ko publish
also supports relative import paths, when in the context of a repo
on GOPATH
.
$ ko publish ./cmd/sleeper
2018/07/19 14:58:16 Using base gcr.io/distroless/static:nonroot for github.com/mattmoor/warm-image/cmd/sleeper
2018/07/19 14:58:16 Publishing us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37:latest
2018/07/19 14:58:17 mounted blob: sha256:59df9d5b488aea2753ab7774ae41a9a3e96903f87ac699f3505960e744f36f7d
2018/07/19 14:58:17 mounted blob: sha256:739b3deec2edb17c512f507894c55c2681f9724191d820cdc01f668330724ca7
2018/07/19 14:58:17 mounted blob: sha256:57752e7f9593cbfb7101af994b136a369ecc8174332866622db32a264f3fbefd
2018/07/19 14:58:18 us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37:latest: digest: sha256:6c7b96a294cad3ce613aac23c8aca5f9dd12a894354ab276c157fb5c1c2e3326 size: 592
2018/07/19 14:58:18 Published us.gcr.io/my-project/sleeper-ebdb8b8b13d4bbe1d3592de055016d37@sha256:6c7b96a294cad3ce613aac23c8aca5f9dd12a894354ab276c157fb5c1c2e3326
ko resolve
takes Kubernetes yaml files in the style of kubectl apply
and
(based on the model above) determines the set of Go import
paths to build, containerize, and publish.
The output of ko resolve
is the concatenated yaml with import paths replaced
with published image digests. Following the example above, this would be:
# Command
export PROJECT_ID=$(gcloud config get-value core/project)
export KO_DOCKER_REPO="gcr.io/${PROJECT_ID}"
ko resolve -f deployment.yaml
# Output
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-world
spec:
replicas: 1
template:
spec:
containers:
- name: hello-world
# This is the digest of the published image containing the go binary.
image: gcr.io/your-project/helloworld-badf00d@sha256:deadbeef
ports:
- containerPort: 8080
Some Docker Registries (e.g. gcr.io) support multi-level repository names. For
these registries, it is often useful for discoverability and provenance to
preserve the full import path, for this we expose --preserve-import-paths
, or
-P
for short.
# Command
export PROJECT_ID=$(gcloud config get-value core/project)
export KO_DOCKER_REPO="gcr.io/${PROJECT_ID}"
ko resolve -P -f deployment.yaml
# Output
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-world
spec:
replicas: 1
template:
spec:
containers:
- name: hello-world
# This is the digest of the published image containing the go binary
# at the embedded import path.
image: gcr.io/your-project/github.com/mattmoor/examples/http/cmd/helloworld@sha256:deadbeef
ports:
- containerPort: 8080
It is notable that this is not the default (anymore) because certain popular registries (including Docker Hub) do not support multi-level repository names.
ko resolve
, ko apply
, and ko create
accept an optional --selector
or
-l
flag, similar to kubectl
, which can be used to filter the resources from
the input Kubernetes YAMLs by their metadata.labels
.
In the case of ko resolve
, --selector
will render only the resources that
are selected by the provided selector.
See the documentation on Kubernetes selectors for more information on using label selectors.
ko apply
is intended to parallel kubectl apply
, but acts on the same
resolved output as ko resolve
emits. It is expected that ko apply
will act
as the vehicle for rapid iteration during development. As changes are made to a
particular application, you can run: ko apply -f unit.yaml
to rapidly rebuild,
repush, and redeploy their changes.
ko apply
will invoke kubectl apply
under the hood, and therefore apply to
whatever kubectl
context is active.
The --watch
flag (-W
for short) does an initial apply
as above, but as it
does, it builds up a dependency graph of your program and starts to continuously
monitor the filesystem for changes. When a file changes, it re-applies any yamls
that are affected.
For example, if I edit github.com/foo/bar/pkg/baz/blah.go
, the tool sees that
the github.com/foo/bar/pkg/baz
package has changed, and perhaps both
github.com/foo/bar/cmd/one
and github.com/foo/bar/cmd/two
consume that
library and were referenced by config/one-deploy.yaml
and
config/two-deploy.yaml
. The edit would effectively result in a re-application
of:
ko apply -f config/one-deploy.yaml -f config/two-deploy.yaml
This flag is still experimental, and feedback is very welcome.
ko delete
simply passes through to kubectl delete
. It is exposed purely out
of convenience for cleaning up resources created through ko apply
.
ko version
prints version of ko. For not released binaries it will print hash
of latest commit in current git tree.
You can use ko
with minikube
via a Docker Registry, but this involves
publishing images only to pull them back down to your machine again. To avoid
this, ko
exposes --local
or -L
options to instead publish the images to
the local machine's Docker daemon.
This would look something like:
# Use the minikube docker daemon.
eval $(minikube docker-env)
# Make sure minikube is the current kubectl context.
kubectl config use-context minikube
# Deploy to minikube w/o registry.
ko apply -L -f config/
# This is the same as above.
KO_DOCKER_REPO=ko.local ko apply -f config/
A caveat of this approach is that it will not work if your container is
configured with imagePullPolicy: Always
because despite having the image
locally, a pull is performed to ensure we have the latest version, it still
exists, and that access hasn't been revoked. A workaround for this is to use
imagePullPolicy: IfNotPresent
, which should work well with ko
in all
contexts.
Images will appear in the Docker daemon as
ko.local/import.path.com/foo/cmd/bar
. With --local
import paths are always
preserved (see --preserve-import-paths
).
Likewise, you can use ko
with kind
to aid in rapid local iteration both locally and in small CI environments. To
instruct ko
to publish images into your kind
cluster, the KO_DOCKER_REPO
variable must be set to kind.local
.
This would look something like:
# Create a kind cluster
kind create cluster
# Deploy to kind w/o registry.
KO_DOCKER_REPO=kind.local ko apply -f config/
If you want to create a kind
cluster with a non default name, you can set the
KIND_CLUSTER_NAME
variable to the respective name (which is also supported by
kind
itself).
Like with minikube
above, a caveat of this approach is that it will not work
if your container is configured with imagePullPolicy: Always
because despite
having the image locally, a pull is performed to ensure we have the latest
version, it still exists, and that access hasn't been revoked. A workaround for
this is to use imagePullPolicy: IfNotPresent
, which should work well with ko
in all contexts.
Note that images will not appear in the Docker daemon running kind
as the
cluster itself is running in a container that is running containerd
inside.
The images are loaded into the respective containerd
daemon.
While ko
aims to have zero configuration, there are certain scenarios where
you will want to override ko
's default behavior. This is done via .ko.yaml
.
.ko.yaml
is put into the directory from which ko
will be invoked. One can
override the directory with the KO_CONFIG_PATH
environment variable.
If neither is present, then ko
will rely on its default behaviors.
By default, ko
makes use of gcr.io/distroless/static:nonroot
as the base
image for containers. There are a wide array of scenarios in which overriding
this makes sense, for example:
- Pinning to a particular digest of this image for repeatable builds,
- Replacing this streamlined base image with another with better debugging
tools (e.g. a shell, like
docker.io/library/ubuntu
).
The default base image ko
uses can be changed by simply adding the following
line to .ko.yaml
:
defaultBaseImage: gcr.io/another-project/another-image@sha256:deadbeef
Some of your binaries may have requirements that are a more unique, and you may
want to direct ko
to use a particular base image for just those binaries.
The base image ko
uses can be changed by adding the following to .ko.yaml
:
baseImageOverrides:
github.com/my-org/my-repo/path/to/binary: docker.io/another/base:latest
Once introduced to .ko.yaml
, you may find yourself wondering: Why does it not
hold the value of $KO_DOCKER_REPO
?
The answer is that .ko.yaml
is expected to sit in the root of a repository,
and get checked in and versioned alongside your source code. This also means
that the configured values will be shared across developers on a project, which
for KO_DOCKER_REPO
is actually undesirable because each developer is (likely)
using their own docker repository and cluster.
A question that often comes up after using ko
for a while is: "How do I
include static assets in images produced with ko
?".
For this, ko
builds around an idiom similar to go test
and testdata/
. ko
will include all of the data under <import path>/kodata/...
in the images it
produces.
These files are placed under /var/run/ko/...
, but the appropriate mechanism
for referencing them should be through the KO_DATA_PATH
environment variable.
The intent of this is to enable users to test things outside of ko
as follows:
KO_DATA_PATH=$PWD/test/kodata go run ./test/*.go
2018/07/19 23:35:20 Hello there
This produces identical output to being run within the container locally:
ko publish -L ./test
2018/07/19 23:36:11 Using base gcr.io/distroless/static:nonroot for github.com/google/ko/test
2018/07/19 23:36:12 Loading ko.local/github.com/google/ko/test:703c205bf2f405af520b40536b87aafadcf181562b8faa6690fd2992084c8577
2018/07/19 23:36:13 Loaded ko.local/github.com/google/ko/test:703c205bf2f405af520b40536b87aafadcf181562b8faa6690fd2992084c8577
docker run -ti --rm ko.local/github.com/google/ko/test:703c205bf2f405af520b40536b87aafadcf181562b8faa6690fd2992084c8577
2018/07/19 23:36:25 Hello there
... or on cluster:
ko apply -f test/test.yaml
2018/07/19 23:38:24 Using base gcr.io/distroless/static:nonroot for github.com/google/ko/test
2018/07/19 23:38:25 Publishing us.gcr.io/my-project/test-294a7bdc57d85dc6ddeef5ba38a59fe9:latest
2018/07/19 23:38:26 mounted blob: sha256:988abcba36b5948da8baa1e3616b94c0b56da814b8f6ff3ae3ac316e375e093a
2018/07/19 23:38:26 mounted blob: sha256:57752e7f9593cbfb7101af994b136a369ecc8174332866622db32a264f3fbefd
2018/07/19 23:38:26 mounted blob: sha256:f24d43c24e22298ed99ea125af6c1b828ae07716968f78cb6d09d4291a13f2d3
2018/07/19 23:38:26 mounted blob: sha256:7a7bafbc2ae1bf844c47b33025dd459913a3fece0a94b1f3ced860675be2b79c
2018/07/19 23:38:27 us.gcr.io/my-project/test-294a7bdc57d85dc6ddeef5ba38a59fe9:latest: digest: sha256:703c205bf2f405af520b40536b87aafadcf181562b8faa6690fd2992084c8577 size: 751
2018/07/19 23:38:27 Published us.gcr.io/my-project/test-294a7bdc57d85dc6ddeef5ba38a59fe9@sha256:703c205bf2f405af520b40536b87aafadcf181562b8faa6690fd2992084c8577
pod/kodata created
kubectl logs kodata
2018/07/19 23:38:29 Hello there
If ko
is invoked with --platform=all
, for any image that it builds that is
based on a multi-architecture image (e.g., the default
gcr.io/distroless/static:nonroot
, busybox
, alpine
, etc.), ko
will
attempt to build the Go binary using Go's cross-compilation support and produce
a multi-architecture
image index
(aka "manifest list"), with support for each OS and architecture pair supported
by the base image.
If ko
is invoked with --platform=<some-OS>/<some-arch>
(e.g.,
--platform=linux/amd64
or --platform=linux/arm64
), then it will attempt to
build an image for that OS and architecture only, assuming the base image
supports it.
If the base image is a manifest list with more platforms than you want to build,
invoking ko
with comma-separated list of platforms (e.g.
--platform=linux/amd64,linux/arm/v6
) will produce a manifest list containing
only the provided platforms. Note that if the base image does not contain
platforms that are provided by this flag, ko
will be unable to build a
corresponding image, and this is not an error. The resulting artifact will be a
multi-platform image containing the intersection of platforms from the base
image and the --platform
flag. This is especially relevant for projects that
use multiple base images, as you must ensure that every base image contains all
the platforms that you'd like to build.
When --platform
is not provided, if both GOOS
and GOARCH
environment
variables are set, ko
will build an image for ${GOOS}/${GOARCH}[/v${GOARM}]
,
otherwise ko
will build a linux/amd64
image.
Note that using both --platform
and GOOS/GOARCH will return an error, as it's
unclear what platform should be used.
To generate an bash completion script, you can run:
ko completion
To use the completion script, you can copy the script in your bash_completion directory (e.g. /usr/local/etc/bash_completion.d/):
ko completion > /usr/local/etc/bash_completion.d/ko
or source it in your shell by running:
source <(ko completion)
ko
is also useful for helping manage releases. For example, if your project
periodically releases a set of images and configuration to launch those images
on a Kubernetes cluster, release binaries may be published and the configuration
generated via:
export PROJECT_ID=<YOUR RELEASE PROJECT>
export KO_DOCKER_REPO="gcr.io/${PROJECT_ID}"
ko resolve -f config/ > release.yaml
Note that in this context it is recommended that you also provide
-P
, if supported by your Docker registry. This improves users' ability to tie release binaries back to their source.
This will publish all of the binary components as container images to
gcr.io/my-releases/...
and create a release.yaml
file containing all of the
configuration for your application with inlined image references.
This resulting configuration may then be installed onto Kubernetes clusters via:
kubectl apply -f release.yaml
Using -ldflags
is a common way to embed version info in go binaries. (In fact,
we do this for ko
.)
Unforunately, because ko
wraps go build
, it's not possible to use this flag
directly; however, you can use the GOFLAGS
environment variable instead:
GOFLAGS="-ldflags=-X=main.version=1.2.3" ko publish .
In order to support reproducible builds, ko
doesn't embed timestamps in the images it produces by default; however, ko
does respect the
SOURCE_DATE_EPOCH
environment variable.
For example, you can set this to the current timestamp by executing:
export SOURCE_DATE_EPOCH=$(date +%s)
or to the latest git commit's timestamp with:
export SOURCE_DATE_EPOCH=$(git log -1 --format='%ct')
Over time, we will add new functionality under experimental environment variables listed here.
Env Var | Value(s) | What is does |
---|---|---|
GGCR_EXPERIMENT_ESTARGZ |
"1" |
When enabled this experiment will direct ko to emit estargz compatible layers, which enable them to be lazily loaded by an appropriately configured containerd. |
This work is based heavily on learnings from having built the Docker and Kubernetes support for Bazel. That work was presented here.