Building GitOps CI/CD Pipelines: Terraform, FluxCD, Docker, Kubernetes, Helm & Kustomize - Part 2
This tutorial continues the series on building GitOps pipelines with Terraform, FluxCD, Docker and Kubernetes.
Tutorial series outline:
- Terraform to provision infra (Google Kubernetes Engine, Cloud SQL) - Part 1
- FluxCD to automate application deployment from a git repository - Part 2 (You are here)
- GitHub Actions for building and publishing Docker images
- Helm & Kustomize to manage app deployments
In part one of this series, we provisioned our infrastructure, primarily Google Kubernetes Engine (GKE) and Cloud SQL, with rotatable secrets already baked in. (Rotating the SQL user password is as simple as bumping the local.postgres_user_db_password_version in the sql.tf file.)
Before we move forward, let’s configure access to our Google Kubernetes cluster using the gcloud CLI, as we will need this later on. I’ve also recorded a short video showing how to set up your CLI. If you’re already configured, feel free to skip ahead to the next section.
Setup google kubernetes engine for gcloud cli
Now that we've finished infra setup, let's setup and configure FluxCD.
What is FluxCD?
FluxCD is a GitOps tool for Kubernetes. It watches your Git repository and automatically keeps your cluster in sync with the configs you define there. Anytime you update code or infrastructure in Git, FluxCD applies those changes to Kubernetes making deployments consistent, automated, and traceable.
FluxCD is open-source and free to use, and it’s one of the key tools for enabling pull-based continuous delivery in Kubernetes. You might be wondering: why pull-based CD matter? Well here's quick summary and difference between push-based CD and pull-based CD
Aspect | Push-based CD (Traditional) | Pull-based CD (GitOps: FluxCD) |
---|---|---|
How it works | CI/CD pipeline pushes changes into the cluster | In-cluster agent pulls desired state from Git |
Initiator | External CI/CD system | Kubernetes controller inside the cluster |
Flow | Code → CI builds → CI connects to cluster → kubectl apply |
Code → CI builds → Pushes Docker Images → FLux montiors Docker images → Updates Git → Flux syncs state |
Security | CI needs direct access to cluster (creds, RBAC) | Cluster fetches from Git, no external access needed |
Drift handling | Only applied during pipeline run | Continuous reconciliation, fixes drift automatically |
Simplicity | Easier initial setup | Slightly more advanced setup, but scales better |
Best use case | Small/simple projects, lower security concerns | Large teams, production, compliance & security needs |
👉 In short: Push means you tell the cluster what to do. Pull means the cluster keeps itself in sync with Git, the single source of truth.
Today, we’ll use Terraform to set up and manage FluxCD in our Kubernetes cluster. While you could optionally use the CLI for the FluxCD installation (see here), I prefer Terraform because it makes managing upgrades, patches, and ongoing maintenance to FluxCD much easier when everything is defined as code. It also allows Terraform to both provision and tear down the infrastructure automatically, without requiring a human in the loop.
In the GitHub repo I shared in Part 1 (see link), you’ll find a folder named part-2 containing replacement files and instructions. Here’s what to do:
- Copy the file named
part-1/terraform/init/terraform.tfvars
=>part-2/terraform/init/terraform.tfvars
(you should have created this file in Part 1), and change the line withgithub_working_directory = part-2/terraform/infrastructure
notice the change from part-1 to part-2 🙂. - Next, review the file
part-2/terraform/infrastructure/variables.tf
and ensure all settings are valid and consistent with what you configured in Part 1 of this tutorial series. For brevity, I’ve included the relevant section below.
The most important variable to double-check is flux_repository, as it must exactly match your setup.
############################################
# Added for Part 2
############################################
variable "github_organization" {
description = "Github organization name, use this to specify the github organization name to use for terraform cloud this can be either github individual or a team org account"
type = string
}
variable "flux_cluster_name" {
description = "FluxCD cluster name"
type = string
default = "primary-cluster"
}
variable "flux_repository" {
description = "FluxCD Github repository name, use this to specify the github repository used for fluxcd to manage the cluster state"
type = string
default = "tutorial-gitops-fluxcd" # Change this to wwhatever you named your repository in part-1 -> provision the infra -> step 6
}
variable "flux_repository_branch" {
description = "FluxCD git repository branch to use"
type = string
default = "main"
}
variable "github_token" {
description = "GitHub personal access token for API access"
type = string
sensitive = true
}
part-2/terraform/infrastructure/variables.tf
- Now switch to the
part-2/terraform/init
folder and runterraform plan
review the proposed changes carefully, and once you’re satisfied, apply them with:terrform apply
. This updates your Terraform Cloud deployment to begin watching Part 2 for changes, since we’ll be installing Flux from this section. - Finally, switch to the
part-2/terraform/infrastructure
folder andterraform init
This is required since here are new changes in theproviders.tf
file in this part of series, and they need to be re-initialised. Once re-initialised, run aterraform plan
to review summary of changes. Now add a whitespace to any file, git commit and push to trigger the terraform cloud run
All of the changes in this step include installing FluxCD and configuring a Cloud NAT router. The NAT router enables our GKE cluster to access the internet, which is required for pulling container images from GitHub’s registry where the FluxCD controllers are hosted.
For brevity, I’ve included the FluxCD configuration file below, with inline comments to explain what each section is doing.
############################################
# 1) Google Client Config (current gcloud context)
############################################
# This data source fetches details from your currently authenticated
# gcloud user or service account. It provides things like:
# - The active project ID
# - The active region/zone
# - An OAuth2 access token (used by providers like Kubernetes/Flux)
#
# Terraform uses this so it knows *who* you are in Google Cloud
# and can reuse your credentials for API calls.
data "google_client_config" "default" {}
############################################
# 2) Kubernetes Provider (how Terraform talks to your cluster)
############################################
# Terraform needs cluster connection details to install Flux CRDs/resources.
# These values typically come from the GKE data source and your gcloud auth.
provider "kubernetes" {
# API server endpoint for your GKE cluster
host = "https://${google_container_cluster.primary.endpoint}"
# OAuth2 access token from your local gcloud context / service account
token = data.google_client_config.default.access_token
# The cluster CA cert is base64-encoded by GKE; decode for the provider
cluster_ca_certificate = base64decode(google_container_cluster.primary.master_auth.0.cluster_ca_certificate)
}
############################################
# 3) Flux Provider (lets Terraform configure Flux itself)
############################################
# The Flux provider needs BOTH:
# - Kubernetes connection (same cluster details as above)
# - Git connection (where your cluster config lives)
provider "flux" {
kubernetes = {
# Same cluster endpoint and token used above
host = "https://${google_container_cluster.primary.endpoint}"
token = data.google_client_config.default.access_token
cluster_ca_certificate = base64decode(google_container_cluster.primary.master_auth.0.cluster_ca_certificate)
}
git = {
# Https URL to your GitHub repo (org + repo provided via variables created from part-1)
url = "https://github.com/${var.github_organization}/${var.flux_repository}.git"
# Branch Flux should reconcile from (e.g., "main")
branch = var.flux_repository_branch
http = {
username = "git"
password = var.github_token
}
}
}
############################################
# 4) Bootstrap Flux against the Git repo
############################################
# This installs Flux into the cluster and points it at your Git repo/path.
# Flux will reconcile whatever manifests live under the specified path.
resource "flux_bootstrap_git" "this" {
# Path in the repo where cluster config lives, e.g., clusters/prod
path = "clusters/${var.flux_cluster_name}"
# Include extra controllers for image automation (optional but common)
components_extra = ["image-reflector-controller", "image-automation-controller"]
embedded_manifests = true
# Make sure the Google Kubernetes Engine cluster exists before bootstrapping
depends_on = [google_container_cluster.primary, google_compute_router_nat.main]
}
part-2/terraform/infrastructure/flux.tf
You can now monitor progress in the Terraform Cloud dashboard. If everything has run successfully, both the plan and apply should complete without errors. Next, verify the following:
✅ Terraform Cloud Dashboard
- The plan and apply have both completed successfully without errors.
✅ Flux GitHub Repository
- The repository is now populated with Flux manifest files.
- This confirms that Flux has write access to the repo.
✅ Google Kubernetes Cluster
- A new namespace (usually flux-system) has been created.
- All Flux controller pods (source-controller, kustomize-controller, helm-controller, etc.) are running.
📌 If any of these checks fail, tear everything down and redeploy to ensure a clean setup.
See the images below for reference, your setup should look the same.



Alternatively you can use the following command in the cli to confirm the pods are running:
kubectl -n flux-system get pods
NAME READY STATUS RESTARTS AGE
helm-controller-5c898f4887-sh8n9 1/1 Running 0 37m
image-automation-controller-cdfcd6bd7-7ldzl 1/1 Running 0 37m
image-reflector-controller-6cc7b7669c-sf4qv 1/1 Running 0 37m
kustomize-controller-7bcf986f97-jw8g4 1/1 Running 0 37m
notification-controller-5f66f99d4d-ssdn6 1/1 Running 0 37m
source-controller-54bc45dc6-sccxd 1/1 Running 0 37m
🎉 Congratulations! If you’ve made it this far, you’ve successfully set up FluxCD on your GKE cluster using Terraform.
When you have some time, go back and review the inline comments in the files. They’re there to guide you and provide extra context on what’s happening under the hood. If you’d like to dive deeper, you can also explore the official FluxCD documentation.
👉 In the next tutorial, we’ll set up a GitHub Actions pipeline to prepare for deploying applications using FluxCD.
Thanks for reading!