Testing and releasing Helm charts with Dagger
Learn how to test, package and release Helm charts using Dagger.
⚠️ This tutorial uses Go for the demonstration.
Prerequisites
This tutorial assumes you're already familiar with the basics of Dagger.
If you're new to Dagger or need a quick refresher, we recommend starting with the Quickstart Guide.
Viewing traces on Dagger Cloud
If you wish to view traces on Dagger Cloud, make sure to log into your account on the dagger tab:
dagger login [your_org]
When running locally
You can run the tutorial locally on your machine.
Complete the following steps before proceeding:
- Make sure you have Docker installed
- Install Dagger
- Check out the demo repository:
git clone https://github.com/sagikazarmark/demo-iximiuz-dagger-helm.git cd demo-iximiuz-dagger-helm
- Make sure the application can be built:
dagger call build
Preparations
First, let's switch to the dagger tab and change the directory to demo
:
cd demo
Make sure the application can be built and functions properly:
dagger call serve up --ports 8080:80
Go to the Service tab. You should see the application running.
When running locally
The application should be accessible at http://localhost:8080
.
Go back to the dagger tab and hit q
or Ctrl+C
to stop the application.
Installing the Helm module
If you're already comfortable with Helm, you can set up your own pipelines using Dagger and the official Helm container image. However, this isn't much better than writing a vendor-specific YAML file and invoking the Helm binary directly.
Dagger’s real power lies in its modules, which provide clean, high-level APIs—removing the need to call binaries in containers.
Fortunately, there’s already a Helm module available on Daggerverse. It offers a convenient API for managing Helm charts directly in your Dagger functions:
dagger install github.com/sagikazarmark/daggerverse/helm
It's time to write some code using the newly installed Helm module.
Go to the IDE tab and open the demo/.dagger/main.go
file.
Add the following code to the main.go
file:
// Create a Helm chart object.
func (m *Tutorial) chart() *dagger.HelmChart {
chart := m.Source.Directory("deploy/charts/demo-dagger-helm")
return dag.Helm().Chart(chart)
}
The rest of the tutorial will rely on this utility function extensively.
💡 Pro tip
When using the module in production, you may want to consider pinning the Helm version to a specific release:
dag.Helm(dagger.HelmOpts{Version: "3.16.1"})
If you call dag.Helm
more than once, storing the version in a constant can help avoid duplication and improve readability.
Linting Helm charts
Part of ensuring the quality of your Helm charts is running the built-in linter that checks for common mistakes and best practices.
Staying in the IDE tab and the demo/.dagger/main.go
file, create a new Lint
function:
// Lint the Helm chart.
func (m *Tutorial) Lint(ctx context.Context) (string, error) {
return m.chart().Lint().Stdout(ctx)
}
You can now call the Lint
function on the dagger tab:
dagger call lint
Testing Helm charts
Testing Helm charts is more involved than simply linting them, especially when you want to verify that the application is both deployed correctly and fully functional. You’ll need a Kubernetes cluster for the installation process, as well as a container registry to push the application image for Kubernetes to pull from. Ideally, this registry should be local to avoid pushing images to remote registries during CI runs.
Fortunately, we can rely on existing modules from the Daggerverse to streamline this process:
dagger install github.com/marcosnils/daggerverse/k3s
dagger install github.com/sagikazarmark/daggerverse/registry
The k3s
module (lightweight Kubernetes) will run a k3s cluster in Dagger, while the registry
module provides a place to push your application image so Kubernetes can pull it as needed.
Let’s start putting the test function together. First up, build a chart package that can be deployed to Kubernetes:
// Test the Helm chart.
func (m *Tutorial) Test(ctx context.Context) error {
app := m.Build(ctx)
chart := m.chart().Package()
// ...
return nil
}
Next, set up a container registry service for the application image:
registry := dag.Registry().Service()
// Push the container image to a local registry.
_, err := dag.Container().From("quay.io/skopeo/stable").
WithServiceBinding("registry", registry).
WithMountedFile("/work/image.tar", app.AsTarball()).
WithEnvVariable("CACHE_BUSTER", time.Now().String()).
WithExec([]string{"skopeo", "copy", "--all", "--dest-tls-verify=false", "docker-archive:/work/image.tar", "docker://registry:5000/demo-dagger-helm:latest"}).
Sync(ctx)
if err != nil {
return err
}
Set up a k3s cluster with the registry acting as a mirror where Kubernetes can pull the image from:
// Configure k3s to use the local registry.
k8s := dag.K3S("test").With(func(k *dagger.K3S) *dagger.K3S {
return k.WithContainer(
k.Container().
WithEnvVariable("BUST", time.Now().String()).
WithExec([]string{"sh", "-c", `
cat <<EOF > /etc/rancher/k3s/registries.yaml
mirrors:
"registry:5000":
endpoint:
- "http://registry:5000"
EOF`}).
WithServiceBinding("registry", registry),
)
})
// Start the Kubernetes cluster.
_, err = k8s.Server().Start(ctx)
if err != nil {
return err
}
Finally, install the chart on the cluster and run the tests:
const values = `
image:
repository: registry:5000/demo-dagger-helm
tag: latest
`
_, err = chart.
WithKubeconfigFile(k8s.Config()).
Install("demo", dagger.HelmPackageInstallOpts{
Wait: true,
Values: []*dagger.File{
dag.Directory().WithNewFile("values.yaml", values).File("values.yaml"),
},
}).
Test(ctx, dagger.HelmReleaseTestOpts{
Logs: true,
})
if err != nil {
return err
}
The complete function
// Test the Helm chart.
func (m *Tutorial) Test(ctx context.Context) error {
app := m.Build(ctx)
chart := m.chart().Package()
registry := dag.Registry().Service()
// Push the container image to a local registry.
_, err := dag.Container().From("quay.io/skopeo/stable").
WithServiceBinding("registry", registry).
WithMountedFile("/work/image.tar", app.AsTarball()).
WithEnvVariable("CACHE_BUSTER", time.Now().String()).
WithExec([]string{"skopeo", "copy", "--all", "--dest-tls-verify=false", "docker-archive:/work/image.tar", "docker://registry:5000/demo-dagger-helm:latest"}).
Sync(ctx)
if err != nil {
return err
}
// Configure k3s to use the local registry.
k8s := dag.K3S("test").With(func(k *dagger.K3S) *dagger.K3S {
return k.WithContainer(
k.Container().
WithEnvVariable("BUST", time.Now().String()).
WithExec([]string{"sh", "-c", `
cat <<EOF > /etc/rancher/k3s/registries.yaml
mirrors:
"registry:5000":
endpoint:
- "http://registry:5000"
EOF`}).
WithServiceBinding("registry", registry),
)
})
// Start the Kubernetes cluster.
_, err = k8s.Server().Start(ctx)
if err != nil {
return err
}
const values = `
image:
repository: registry:5000/demo-dagger-helm
tag: latest
`
_, err = chart.
WithKubeconfigFile(k8s.Config()).
Install("demo", dagger.HelmPackageInstallOpts{
Wait: true,
Values: []*dagger.File{
dag.Directory().WithNewFile("values.yaml", values).File("values.yaml"),
},
}).
Test(ctx, dagger.HelmReleaseTestOpts{
Logs: true,
})
if err != nil {
return err
}
return nil
}
You can now call the Test
function on the dagger tab:
dagger call test
⌛ This may take a few minutes (the first time).
In the unlikely event it fails, simply run it again.
Releasing Helm charts
Pushing the chart to an OCI registry marks the final step in the development lifecycle.
The helm
module is fully equipped to handle this task:
// Package and release the Helm chart (and the application).
func (m *Tutorial) Release(ctx context.Context, version string) error {
const registry = "registry.iximiuz.com"
_, err := m.Build(ctx).
Publish(ctx, fmt.Sprintf("%s/demo-dagger-helm:%s", registry, version))
if err != nil {
return err
}
err = m.chart().
Package(dagger.HelmChartPackageOpts{
Version: strings.TrimPrefix(version, "v"),
AppVersion: version,
}).
Publish(ctx, fmt.Sprintf("oci://%s/helm-charts", registry))
if err != nil {
return err
}
return nil
}
Typically, pushing release artifacts is done by a CI/CD pipeline. However, (for the purposes of this tutorial) you can also do it manually:
dagger call release --version=v1.0.0
When running locally
registry.iximiuz.com
isn’t accessible outside this playground. Instead, use ttl.sh
as an ephemeral registry, or continue reading for instructions on using a remote registry.
💡 Pro tip
In a production environment, the container registry you're pushing to will likely require authentication.
Modify the snippet above to include credentials, as shown below:
_, err := m.Build(ctx).
WithRegistryAuth("example.com", username, password).
Publish(ctx, fmt.Sprintf("%s/demo-dagger-helm:%s", registry, version))
// ..
err = m.chart().
Package(/*...*/).
WithRegistryAuth("example.com", username, password).
Publish(ctx, fmt.Sprintf("oci://%s/helm-charts", registry))
- replace
example.com
with your container registry address username
is a plaintext stringpassword
is a Dagger secret
Summary
In this tutorial, we explored how to build a CI pipeline for Helm charts using Dagger. We walked through testing, linting, and releasing charts, highlighting real-world use cases such as pinning versions and configuring registry authentication. By following this workflow, you can easily integrate these practices into an existing Dagger pipeline or use them as the foundation for a new one.
Level up your Server Side game — Join 10,000 engineers who receive insightful learning materials straight to their inbox