Tutorial  on  CI/CDKubernetes

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:

  1. Make sure you have Docker installed
  2. Install Dagger
  3. Check out the demo repository:
    git clone https://github.com/sagikazarmark/demo-iximiuz-dagger-helm.git
    cd demo-iximiuz-dagger-helm
    
  4. 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 string
  • password 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.

Discussion:  Discord
Categories: CI/CDKubernetes

Level up your Server Side game — Join 10,000 engineers who receive insightful learning materials straight to their inbox