Dynamic Helm Charts in GitLab CI
When deploying applications to Kubernetes, managing Helm charts across multiple repositories can become a maintenance burden. Today, I'll share an elegant solution I've implemented that generates Helm charts on the fly during CI/CD pipelines using Helm starters.
The problem
Typically, when using Helm for Kubernetes deployments, you'll find:
Helm charts duplicated across multiple repositories
Lots of boilerplate YAML that needs maintenance
Challenges in maintaining consistency across different services
Enter Helm Starters
Helm starters are templates for new charts. Think of them as blueprints that can be used to generate new charts with predefined configurations. By hosting starter templates in a central repository, you can ensure consistent deployments across all our services.
The commands
The main Helm commands you need to run:
# Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Install Helm starter plugin - this adds support for "helm starter fetch"
helm plugin install https://github.com/salesforce/helm-starter.git
# Fetch the starter you need - I use one with support for setting envs
helm starter fetch https://github.com/andiradulescu/helm-starter-envs.git
# Create your Helm chart from the starter - $NAME sets both the chart folder and name in Chart.yaml
helm create $NAME --starter=helm-starter-envs
# Helm upgrade takes care of the (re)deployment
helm upgrade $NAME $NAME --install
--set image.repository=$REPOSITORY,image.tag=$CI_COMMIT_SHORT_SHA
--set service.port=3000
# extra functionality added by "helm-starter-envs"
--set-string env[0].name=NODE_ENV,env[0].value=$NODE_ENV
--set-string env[1].name=DEPLOYMENT,env[1].value=$NODE_ENV
If you don’t need a custom “helm-starter”, everything becomes as simple as:
# Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Create your Helm chart from the starter - $NAME sets both the chart folder and name in Chart.yaml
helm create $NAME
# Helm upgrade takes care of the (re)deployment
helm upgrade $NAME $NAME --install
--set image.repository=$REPOSITORY,image.tag=$CI_COMMIT_SHORT_SHA
--set service.port=3000
The actual implementation
As for the actual implementation that you can use in your .gitlab-ci.yml
:
build-and-deploy:
only:
- "dev"
- "main"
before_script:
# Install doctl (DigitalOcean command line interface)
- curl -sL https://github.com/digitalocean/doctl/releases/download/v1.102.0/doctl-1.102.0-linux-amd64.tar.gz | tar -xzv && mv doctl /usr/local/bin
# Install kubectl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# Install Helm
- curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | VERIFY_CHECKSUM=false sh
# Install Helm starter plugin
- helm plugin install https://github.com/salesforce/helm-starter.git
# Fetch the starter you need
- helm starter fetch https://github.com/andiradulescu/helm-starter-envs.git
script:
# doctl auth, login to registry and save the temporary kubeconfig file
- doctl auth init -t $DO_API_TOKEN
- doctl registry login --expiry-seconds 1200
- doctl kubernetes cluster kubeconfig save --expiry-seconds 1200 $DO_CLUSTER_NAME
# Set any ENVs you might need
- NODE_ENV=$([ "$CI_COMMIT_REF_NAME" == "main" ] && echo "production" || echo "dev")
- HOST=$([ "$CI_COMMIT_REF_NAME" == "main" ] && echo "api.your-app.com" || echo "dev-api.your-app.com")
- REPLICA_COUNT=$([ "$CI_COMMIT_REF_NAME" == "main" ] && echo "2" || echo "1")
- REPOSITORY=$DO_REGISTRY_NAME/$CI_PROJECT_NAME
- RELEASE=$CI_PROJECT_NAME-$NODE_ENV
# Build the Docker image
- docker build --pull -t $REPOSITORY:$NODE_ENV -t $REPOSITORY:$CI_COMMIT_SHORT_SHA .
# Push the Docker image
- docker push -a $REPOSITORY
# Create your Helm chart
- helm create $RELEASE --starter=helm-starter-envs
# Deploy your Helm chart
- helm upgrade $RELEASE $RELEASE --install
--set image.repository=$REPOSITORY,image.tag=$CI_COMMIT_SHORT_SHA
--set replicaCount=$REPLICA_COUNT
--set service.port=3000 # depends on your container
--set serviceAccount.create=false
# ingress (this works if you have an ingress installed e.g. NGINX Ingress Controller)
--set ingress.enabled=true
--set ingress.className=nginx
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod
--set ingress.hosts[0].host=$HOST,ingress.hosts[0].paths[0].path=/,ingress.hosts[0].paths[0].pathType=ImplementationSpecific
--set ingress.tls[0].hosts[0]=$HOST,ingress.tls[0].secretName=$RELEASE-tls
# envs
--set-string env[0].name=NODE_ENV,env[0].value=$NODE_ENV
--set-string env[1].name=DEPLOYMENT,env[1].value=$NODE_ENV
This is configured with DigitalOcean Kubernetes (DOKS) but can be adapted for any Kubernetes cluster. The parameters are set just for reference. You might need something simpler or more complex, feel free to remove or add whatever you need.
As for GitLab CI/CD Variables:
# These need to be set to support DigitalOcean
DO_API_TOKEN="***" # replace with your API token - https://cloud.digitalocean.com/account/api/tokens
DO_CLUSTER_NAME="k8s-1-32-1-do-0-fra1-1739277198510" # replace with your cluster name - http://cloud.digitalocean.com/kubernetes/clusters
DO_REGISTRY_NAME="registry.digitalocean.com/your-do-registry-name" # replace with your registry endpoint - https://cloud.digitalocean.com/registry
# These are set by GitLab automatically - https://docs.gitlab.com/ee/ci/variables/#list-all-variables
CI_COMMIT_REF_NAME="main" # branch name
CI_PROJECT_NAME="gitlab" # project name
CI_COMMIT_SHORT_SHA="1ecfd275" # short commit hash
# again, don't set these, they are set by GitLab automatically
If you want to test this setup out with a new DigitalOcean account, you can use this link to get $200 in credit over 60 days on DigitalOcean. Using this link will give me $25 credit, once you spend $25.