The Painful Journey of Deploying a Helm Chart to Kubernetes with Go

Search for a command to run...

Great attempt. Wil wait for your next post.
If you have been using Terraform for more than five minutes, you would have come across variables and replacing default values for instances using a .tfvars file. Below is a snippet of replacing default values for an EC2 Instance using Terraform: ec2...

For work, I have had an interesting project where I had to pass through multiple AWS Backup Plans into a Python class to make all my Backup Plans; these backup plans had so many options that needed configuring that the most logical way of passing the...

Introduction Managing AWS resources efficiently is a critical challenge for cloud architects and developers, especially using a dynamic language like Python. While Python offers flexibility and rapid development capabilities, its lack of built-in typ...

Introduction When designing a scalable and flexible architecture on AWS, it's common to use an Application Load Balancer (ALB) to distribute traffic across multiple targets, such as EC2 instances and ECS tasks. However, a challenge arises when you mu...

How Bash Saved the Day: Deploying Helm Charts with a Bash CLI Program In the world of software development, encountering roadblocks is part of the journey. Recently, I faced a significant challenge while trying to deploy a Helm chart to a Kubernetes ...

In the fast-paced world of software development, time-sensitive projects can often lead to both breakthroughs and challenges. Recently, I embarked on a project to deploy a Helm chart to a Kubernetes cluster using Go. This was because the helm Terraform provider is currently not working as I would expect it to and does not allow you to build an Azure Kubernetes Cluster and Deploy a Helm chart in the same run.
Despite my best efforts, a critical bug in a dependency and several deployment hurdles made the journey incredibly frustrating. Here's an account of my experience and the code I used, highlighting the pain points and lessons learned.
The objective was simple: automate the deployment of a Helm chart to a Kubernetes cluster. This included setting up the Kubernetes Cert-Manager objects and leveraging the github.com/mittwald/go-helm-client package to manage Helm operations. Theoretically, it was straightforward. In practice, it proved to be anything but.
Here is the code I used for this project. Note that sensitive information has been omitted for security reasons.
package main
import (
"bytes"
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5"
certmanagerv1 "github.com/cert-manager/cert-manager"
helmclient "github.com/mittwald/go-helm-client"
"github.com/mittwald/go-helm-client/values"
"helm.sh/helm/v3/pkg/repo"
core "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
clientcmd "k8s.io/client-go/tools/clientcmd"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5"
)
func main() {
var outputBuffer bytes.Buffer
ctx := context.Background()
mycontext, _ := context.WithTimeout(ctx, 80000*time.Second)
certmanageremail := "your-email@example.com"
subid := "your-subscription-id"
rg := "your-resource-group"
cluster := "your-cluster-name"
fmt.Println("Connecting to Cluster Via Azure Call\n")
myaksconnect := connectToAks(ctx, subid, rg, cluster)
mypublicip := "0.0.0.0"
fmt.Printf("Moved %v to KubeConfig File", &myaksconnect.Name)
fmt.Println("Connecting to Cluster using KubeConfig File\n")
kubeClient := connectToK8s()
fmt.Println("Building Helm Client\n")
opt := &helmclient.Options{
Namespace: "default",
RepositoryCache: "/tmp/.helmcache",
RepositoryConfig: "/tmp/.helmrepo",
Debug: false,
Linting: true,
DebugLog: func(format string, v ...interface{}) {},
Output: &outputBuffer,
}
myHelmClient, err := helmclient.New(opt)
if err != nil {
panic(err)
}
fmt.Println("Deploying nginx-ingress\n")
nginxchartRepo := repo.Entry{
Name: "nginx-ingress",
URL: "https://kubernetes.github.io/ingress-nginx",
}
if err := myHelmClient.AddOrUpdateChartRepo(nginxchartRepo); err != nil {
log.Fatal(err)
} else {
fmt.Printf("Added Chart Repo %s\n", nginxchartRepo.Name)
}
if err := myHelmClient.UpdateChartRepos(); err != nil {
log.Fatal(err)
} else {
fmt.Printf("Updating Chart Repo\n")
}
nginxchartSpec := helmclient.ChartSpec{
ReleaseName: "nginx-ingress/nginx-ingress",
ChartName: "nginx-ingress",
Namespace: "nginx-ingress",
CreateNamespace: true,
SkipCRDs: false,
Wait: true,
ValuesOptions: values.Options{
StringValues: []string{
"rbac.create=false",
"controller.service.externalTrafficPolicy=Local",
fmt.Sprintf("controller.service.loadBalancerIP=%v", mypublicip),
"controller.replicaCount=2",
"controller.nodeSelector.kubernetes\\.io/os=linux",
"defaultBackend.nodeSelector.kubernetes\\.io/os=linux",
"controller.admissionWebhooks.patch.nodeSelector.kubernetes\\.io/os=linux",
"controller.publishService.enabled=true",
"controller.service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path=/healthz",
},
},
}
nginxInstalledHelmChart, err := myHelmClient.InstallChart(mycontext, &nginxchartSpec, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status of Chart Install %v,\n", *nginxInstalledHelmChart.Info)
fmt.Println("Deploying cert-manager\n")
certmanagerchartRepo := repo.Entry{
Name: "cert-manager",
URL: "https://charts.jetstack.io",
}
if err := myHelmClient.AddOrUpdateChartRepo(certmanagerchartRepo); err != nil {
log.Fatal(err)
} else {
fmt.Printf("Added Chart Repo %s\n", certmanagerchartRepo.Name)
}
if err := myHelmClient.UpdateChartRepos(); err != nil {
log.Fatal(err)
} else {
fmt.Printf("Updating Chart Repo\n")
}
certmanagerchartSpec := helmclient.ChartSpec{
ReleaseName: "cert-manager/cert-manager",
ChartName: "cert-manager",
Version: "v1.14.5",
Namespace: "cert-manager",
CreateNamespace: true,
ValuesOptions: values.Options{
StringValues: []string{
"extraArgs = {--dns01-recursive-nameservers=1.1.1.1:53}",
"controller.nodeSelector.kubernetes\\.io/os=linux",
},
},
SkipCRDs: false,
Wait: true,
}
certmanagermyInstalledHelmChart, err := myHelmClient.InstallChart(mycontext, &certmanagerchartSpec, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status of Chart Install %v,\n", *certmanagermyInstalledHelmChart.Info)
// Further deployments omitted for brevity...
fmt.Println("Finished Deployment\n")
}
func connectToK8s() *kubernetes.Clientset {
home, exists := os.LookupEnv("HOME")
if !exists {
home = "C:\\Users\\your-user"
}
configPath := filepath.Join(home, ".kube", "config")
config, err := clientcmd.BuildConfigFromFlags("", configPath)
if err != nil {
log.Panicln("Failed to create K8s config")
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Panicln("Failed to create K8s clientset")
}
return clientset
}
func connectToAks(ctx context.Context, subid string, rg string, aksname string) *armcontainerservice.ManagedClustersClientGetAccessProfileResponse {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
log.Fatalf("Failed to obtain a credential: %v", err)
}
clientFactory, err := armcontainerservice.NewClientFactory(subid, cred, nil)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
res, err := clientFactory.NewManagedClustersClient().GetAccessProfile(ctx, rg, aksname, "clusterUser", nil)
if err != nil {
log.Fatalf("Failed to finish the request: %v", err)
}
return &res
}
func Ptr[T any](v T) *T {
return &v
}
Dependency Bugs: The biggest hurdle was a bug in the github.com/mittwald/go-helm-client package. Despite following the documentation and best practices, the Helm charts would not deploy correctly. I reported the issue here: mittwald/go-helm-client#209. This bug made deploying a working Helm chart impossible, derailing the project timeline.
Time Sensitivity: This project was highly time-sensitive. Every minute spent troubleshooting the bug was a minute closer to the deadline, adding to the stress and frustration.
Cert-Manager Objects: Deploying Kubernetes Cert-Manager objects using the native Kubernetes Go package was another pain point. Despite their theoretical simplicity, the objects did not deploy as expected, which was a significant setback, as Cert-Manager is critical for managing SSL/TLS certificates.
Dependency Management: Always vet your dependencies thoroughly. Consider contingency plans for potential issues with third-party libraries in critical projects.
Community and Support: Don't hesitate to reach out to the community or report issues.
Time Management: Factor in potential delays when working with new or less-tested tools. Buffer time can help you better manage unexpected challenges.
While this project was fraught with challenges, it was a valuable learning experience. I learned a ton about the internals of Kubernetes and Helm, which has improved my understanding of how to build Apps for Kubernetes. I also hope it gives you an insight into how it could be done.
Note: Please don't use this code, as it currently does not work. Once I have fixed the bug in the helm package, I will write another blog post about it.