Use Terraform to deploy Helm charts

Archie To

We’ve talked about Terraform and Helm. Those surely are cool technologies. But you know what’s even cooler? When you combine both of those.

This article will show you how to use Terraform to deploy Helm charts, a very popular practice for our infrastructure in ARCsoft. This article requires some prerequisites: readers must read about Terraform and Helm first. So if you haven’t, go read those, I can wait.

Why use Terraform to deploy Helm charts?

You might ask: “Why don’t just use helm install? It gets the job done.” You’re not wrong, but below are the reasons why our team uses Terraform for the job.

Multiple chart deployments

If you have multiple charts describing different services, for example, a webserver, database and object store, deploying each one with helm install might take a while. For each one of those, you’d have to change directory, provide a separate values.yaml file and run helm install multiple times. Helm has a subchart feature, but this approach has similar shortcomings: we’d have to explicitly run extra commands for each subchart, for example.

Terraform allows you to deploy multiple charts with just one tfvars variable file and a single terraform apply command.

Deployments with other resources

Frequently, Helm charts are not deployed on their own. They need further infrastructure to support their existence. For example, a persistent volume or a database, a virtual machine, some networking for external access, service accounts, etc. These resources cannot be deployed using Helm alone. You could still deploy manually or write a script that does these things. However Terraform allows you to automate all these things together.

So why not just use Terraform to deploy your Helm charts anyway? One less technology to think about. Plus, you’ll get to keep all the variables in one .tfvars file, run exactly one command to deploy everything and manage all resources using only terraform CLI. You can even have the Helm chart deployed only after all its required resources are deployed to avoid messing up your workflow.

Automation

Automation is certainly a big reason why Terraform is used. Terraform is efficient and reliable in automated environments such as CI/CD and processes in your web apps (like ones in Strapper). Its capabilities in automation are supported by features such as remote state management and decomposition through modules.

Let’s try it out

In this example, we will use a Helm chart that we created in the Introduction to Helm article. To recap, here are the main components of the Helm chart:

Chart.yaml:

apiVersion: v2
name: my-web-app
description: A Helm chart for deploying a simple web application
version: 1.0.0
appVersion: 1.0.0

values.yaml:

replicaCount: 2

image:
  repository: my-web-app
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi

templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: {{ .Chart.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
          - containerPort: 80
        resources:
          limits:
            cpu: {{ .Values.resources.limits.cpu }}
            memory: {{ .Values.resources.limits.memory }}
          requests:
            cpu: {{ .Values.resources.requests.cpu }}
            memory: {{ .Values.resources.requests.memory }}

templates/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
  selector:
    app: {{ .Chart.Name }}

(This example assumes the chart is stored at ./helm.)

Specify the provider

First, we have specify which Terraform provider we want so Terraform can download that provider during terraform init. In this case, we want the hashicorp/helm provider. Create a main.tf file in the current directory:

terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.13.1"
    }
  }
}

Configure the provider

Let’s provide some necessary configuration for the Helm provider. In here, we will tell the provider which Kubernetes context we’d like Helm to work with. Add the following to main.tf:

provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"    # Assuming you have a kubeconfig file at `~/.kube/config`
    config_context = "my_context"     # Specify which context you'd like to use in the kubeconfig file
  }
}

Configure helm_release resource

hashicorp/helm provider provides a helm_release resource. Running terraform apply for this resource is the equivalent of running helm install for our Helm chart. In main.tf, add:

# Define custom values
# Specify 3 replicas and set the image tag to 2.0.0
locals {
  helm_values = yamlencode({
    replicaCount = 3
    image = {
      tag = "2.0.0"
    }
  })
}

resource "helm_release" "app" {
  name             = "myapp"
  chart            = "./helm"
  version          = "1.0.0"
  namespace        = "myapp-namespace"
  create_namespace = true
  timeout          = 180
  values           = [locals.helm_values]
}

If you want to avoid specifying sensitive Helm values in main.tf, you can also provide the values as follows:

resource "helm_release" "app" {
  ...
  values           = [
    "${file("myapp_values.yaml")}",
  ]
}

In myapp_values.yaml:

replicaCount: 3
image:
  tag: 2.0.0

And make sure you don’t track this file in Git 😉.

Deploy Helm chart with a database

Looking at the example above, you might ask: “Why did I have to go through all that just to deploy a Helm chart? Could I just run helm install?”

Yes, you could. But here is an example why Terraform should be used. Most apps need a database to do its job, right? Let’s say you want to deploy a PostgreSQL database for myapp. Here is where Terraform shines. You can deploy the database and your app at once. Better yet, you can modify the deployment so that your app is only deployed after the database.

Note: This example assumes that you’ve made necessary changes in your Helm chart to configure a database. Here is the updated values.yaml:

replicaCount: 2

image:
  repository: my-web-app
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi

database:
  host: myhost.example.org
  user: postgres
  database: mydb
  password: passw0rd
  port: 5432
  sslmode: none

Here is the updated version of main.tf to create the database and connect that database to myapp:

terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.13.1"
    },
    # Add postgres provider
    postgresql = {
      source = "cyrilgdn/postgresql"
      version = "1.22.0"
    }
  }
}

# Define a variable to configure Postgres server
variable "postgres" {
  description = "Postgres server definition"
  type = object({
    host = string
    port = optional(number, 5432)
    database = string
    username = string
    password = string
  })
  nullable = false
}

# Define a variable to specify Postgres user credentials for "myapp"
variable "myapp" {
  description = "myapp deployment specifications"
  type        = object({
    dbname = optional(string, "myapp")
    dbuser = optional(string, "myapp")
    dbpass = string
  })
  nullable    = false
}


# Configure postgres provider
provider "postgresql" {
  host            = var.postgres.host
  port            = var.postgres.port
  database        = var.postgres.database
  username        = var.postgres.username
  password        = var.postgres.password
  sslmode         = "require"
}

# Provision Postgres roles and database
resource "postgresql_role" "myapp" {
  name     = var.myapp.dbuser
  password = var.myapp.dbpass
  login    = true
}

# Only create the database after creating the role
resource "postgresql_database" "myapp" {
  name              = var.myapp.dbname
  owner             = postgresql_role.myapp.name
}

provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"    # Assuming you have a kubeconfig file at `~/.kube/config`
    config_context = "my_context"     # Specify which context you'd like to use in the kubeconfig file
  }
}

# These values are only fully specified after the database is created
locals {
  myapp_yaml_overrides = {
    "database" = {
      host = var.postgres.host
      port = var.postgres.port
      database = postgresql_database.myapp.name
      user = postgresql_database.myapp.owner
      password = var.myapp.dbpass
    }
  }
}

# This Helm release is only deployed after the database is created
resource "helm_release" "myapp" {
  name             = "myapp"
  chart            = "./helm"
  version          = "1.0.0"
  namespace        = "myapp-namespace"
  create_namespace = true
  timeout          = 180
  values           = [
    "${file("myapp_values.yaml")}",
    yamlencode(local.myapp_yaml_overrides)
  ]
}

Create a terraform.tfvars in the same directory:

postgres = {
  host = "database_host.postgres.example.org"
  database = "postgres"
  username = "superuser"
  password = "superuserpassword"
}

myapp = {
  dbpass = "anypasswordyouwant"
}

One great thing is that the configuration above will deploy the database first, then the app: because Terraform does a decent job with dependencies, and the app depends on the local variables which depend on information about the deployed database.

Simply run terraform apply, you will have a database created that is dedicated to myapp, and the Helm chart will have the database credentials to hook them up with your deployment.

Conclusion

Terraform provides extensive automation to your workflow with Helm. It’s best used when you need to deploy multiple Helm charts or deploy Helm charts with other resources.