How to Integrate Terraform with Argo CD (Without Breaking GitOps)

Blake Pettersson

Kargo Custom Steps
Kargo Custom Steps

Terraform is the go-to tool for provisioning cloud infrastructure—things like Kubernetes clusters, databases, and identity or access controls (aka “cloudy stuff”). But integrating those Terraform-managed resources with Argo CD in a way that actually sticks to GitOps principles? That’s where things get tricky.

In this guide, we’ll walk through a better, more GitOps-friendly way to coordinate Terraform and Argo CD—one that’s declarative, simple, and doesn’t rely on plugins or fragile workflows.

The GitOps Bridge Pattern (And Its Drawbacks)

One of the more common approaches to integrating Terraform with Argo CD is the GitOps Bridge Pattern. It’s gained traction (even with AWS backing), and it works—but it introduces real-world trade-offs.

Here’s the basic idea: Terraform exports the values your Argo CD Application needs and injects them into a cluster secret as labels or annotations. Argo CD then consumes that metadata using an ApplicationSet with a cluster generator.

A simplified version might look like this:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: external-dns
  namespace: argocd
spec:
  goTemplate: true
  generators:
  - clusters: {}
  template:
    metadata:
      name: "{{.name}}-external-dns"
    spec:
      destination:
        namespace: external-dns
        server: '{{.server}}'
      project: 'default'
      sources:
        - helm:
            releaseName: external-dns
            # Any value from the cluster secret can be passed inline into the Helm chart.
            values: |
              annotations:
                  eks.amazonaws.com/role-arn: arn:aws:iam::{{ index .metadata.labels "aws-account-id" }}:role/
              {{ index .metadata.annotations "external-dns-iam-role-name" }}
              txtOwnerId: {{.name}}

Limitations of the GitOps Bridge Pattern

  • Requires ApplicationSets This pattern only works with ApplicationSets using the cluster generator. That’s fine if you're managing dozens of clusters, but not ideal if you want to use a basic Application or a different generator.

  • Breaks GitOps as a Source of Truth Metadata used by Argo CD comes from cluster secrets—not Git. That means you now have to check both Git and your cluster to understand what’s actually being deployed.

  • Bloated Inline Helm Values Terraform exports values directly into Helm charts via inline configuration, which can quickly balloon into dozens of parameters. It gets unwieldy fast.

  • Not Compatible with Kustomize This pattern is tightly coupled to Helm and doesn’t support Kustomize, limiting flexibility in how you define your applications.

Step-by-Step: How to Connect Terraform and Argo CD the GitOps Way

Since the GitOps Bridge pattern was conceived, there have been a few developments. One of those being the advent of multi-source applications in Argo CD. This feature lets you pull your Helm chart from one source (like a Helm repo) and your values from another (like a Git repo). No umbrella charts. No inline value overload. Just clean, flexible config separation.

Here’s where it gets even better: Terraform’s GitHub provider now supports creating and updating files directly in Git. It can even open pull requests.

With that, we can use Terraform to write Helm values (or Kustomize overlays) directly into Git. Argo CD then picks them up and deploys—keeping Git as the single source of truth. No plugins, no secret injection workarounds, and no breaking GitOps principles.

This walkthrough uses GitHub and the GitHub Terraform Provider. It should work similarly with other providers (like GitLab), but that hasn’t been tested here—so results may vary.

What You’ll Need Before You Start

  • Terraform installed

  • A GitHub repository where Terraform can commit files

  • A GitHub Personal Access Token with repo scope

  • The GitHub Terraform Provider configured

  • (Optional) A multi-source Argo CD Application already set up. This guide uses Helm, but the same approach works with umbrella charts or Kustomize too.

Step 1: Define Helm Values in Terraform

Start by declaring your values in Terraform using a local block. These will later be templated into your Helm deployment.

locals {
  helm_values = {
    foo = "bar"
    bla = {
      one   = "dddddd"
      two   = "adddddfsdfaa"
      three = "sdfffffs"
      four  = "fff"
      five  = "ffffff"
    }
  }
}

Step 2: Commit Helm Values to Git Using Terraform

The secret here is that the GitHub provider provideshas a resource called github_repository_file, which lets you create or update files in a GitHub repo directly from Terraform—so long as the content is a string.

Here’s an example:

resource "github_repository_file" "helm-values-example" {
  repository          = "tf-test"
  branch              = "main"
  file                = "main/values.yaml"
  content             = yamlencode(local.helm_values)
  commit_message      = "Managed by Terraform"
  commit_author       = "Terraform User"
  commit_email        = "terraform@example.com"
  overwrite_on_create = true
}

Let’s do terraform apply. This will have made a commit to the tf-test GitHub repository with the values which have been templated from Terraform. When doing this within a GitHub organization, you will need to prefix the repository with your GitHub organization.

Step 3: Reference the Values in Argo CD

Now that the values file is in Git, you can reference it in an Argo CD multi-source Application. Here’s what that setup looks like:

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: default
  project: default
  sources:
    - repoURL: 'https://prometheus-community.github.io/helm-charts'
      chart: prometheus
      targetRevision: 15.7.1
      helm:
        valueFiles:
          - $values/main/values.yaml
    - repoURL: 'https://github.com/blakepettersson/tf-test.git'
      targetRevision: main
      ref

Once applied, the Argo CD Application will make use of the values that have been templated from Terraform 🎉

Step 4: Create a Dynamic Feature Branch for Safer Changes

So far, Terraform is committing changes directly to main. But in most cases, you probably want a more controlled workflow—especially in production.

To make that happen, we can have Terraform generate a feature branch any time Helm values change. This allows us to open a pull request for review before merging anything into main.

To do that, we’ll use the github_branch resource to create a uniquely named branch based on a hash of the values content. It’s not pretty, but it works.

resource "github_branch" "pr" {
  repository = "tf-test"
  branch     = "terraform-branch-${substr(sha256(jsonencode(local.helm_values)), 0, 7)}"
}

Then update your github_repository_file to commit to that branch instead:

resource "github_repository_file" "helm-values-example" {
  repository          = "tf-test"
  branch              = github_branch.pr.branch
  file                = "main/values.yaml"
  content             = yamlencode(local.helm_values)
  commit_message      = "Managed by Terraform"
  commit_author       = "Terraform User"
  commit_email        = "terraform@example.com"
  overwrite_on_create = true
}

Step 5: Automate Pull Request for Review and Merge

Finally, use the github_repository_pull_request resource, which opens a PR whenever the contents of the Helm values change.

resource "github_repository_pull_request" "helm-example-pr" {
  base_repository = github_repository_file.helm-values-example.repository
  base_ref        = "main"
  head_ref        = github_repository_file.helm-values-example.branch
  title           = "My newest feature"
  body            = "This will change everything"
}

Now, when you run terraform apply, Terraform will:

  • Generate values

  • Create a new feature branch

  • Commit the values to that branch

  • Open a PR

Why This Terraform–Argo CD Setup Beats the Old Way

This workflow keeps things simple and true to GitOps:

  • You don’t need plugins, bridge patterns, or secret injection hacks.

  • Git stays the single source of truth.

  • Changes go through pull requests, not straight to production.

  • It works just as well with Helm as it does with Kustomize.

How to Use the Same Approach Kustomize

The good news is that with this variant of the GitOps Bridge pattern, we can also support Kustomize. Since we can commit any file to Git using Terraform, we can template any file the way we like. Let’s say we want to have a Kustomize overlay, and we would like to template some variables from Terraform. Here’s how we’d do it:

Step 1: Define the Kustomize Overlay in Terraform

Just like with Helm values, you’ll define your overlay as a Terraform object:

kustomize = {
    resources : ["../../base"]
    namePrefix : "staging-"
    configMapGenerator : [
      {
        name : "special-config-2"
        literals : [
          "ENV_VAR_ONE=${local.some_env_var}",
          "ENV_VAR_TWO=${local.some_other_env_var}"
        ]
      }
    ]
  }
}

Step 2: Generate a Dynamic Branch (Like Before)

You’ll want to create a unique branch based on the hash of the overlay content:

resource "github_branch" "pr" {
  repository = "tf-test"
  branch     = "terraform-branch-${substr(sha256(jsonencode(local.kustomize)), 0, 7)}"
}

Step 3: Commit the Overlay to Git

Encode the overlay into YAML and commit it to the correct path in your repo:

resource "github_repository_file" "kustomize-example" {
  repository          = "tf-test"
  branch              = github_branch.pr.branch
  file                = "kustomize/overlays/dev/kustomization.yaml"
  content             = yamlencode(local.kustomize)
  commit_message      = "Managed by Terraform"
  commit_author       = "Terraform User"
  commit_email        = "terraform@example.com"
  overwrite_on_create = true
}

Step 4: Open a Pull Request

Just like you did with Helm:

resource "github_repository_pull_request" "helm-example-pr" {
  base_repository = github_repository_file.helm-values-example.repository
  base_ref        = "main"
  head_ref        = github_repository_file.helm-values-example.branch
  title           = "My newest feature"
  body            = "This will change everything"
}
resource "github_repository_pull_request" "helm-example-pr" {
  base_repository = github_repository_file.helm-values-example.repository
  base_ref        = "main"
  head_ref        = github_repository_file.helm-values-example.branch
  title           = "My newest feature"
  body            = "This will change everything"
}
resource "github_repository_pull_request" "helm-example-pr" {
  base_repository = github_repository_file.helm-values-example.repository
  base_ref        = "main"
  head_ref        = github_repository_file.helm-values-example.branch
  title           = "My newest feature"
  body            = "This will change everything"
}

Step 5: Reference It in Argo CD

Once the PR is merged, you can reference the overlay just like you would any other Kustomize path in your Argo CD Application:

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  sources:
    - repoURL: 'https://github.com/my-git-org/my-repo.git'
      targetRevision: main
      path

This is a very simple example, you can apply the exact same methodology to Kustomize patches (JSON or merge patches) - the sky's the limit! There are a whole lot of ways to get creative while using Terraform, but hopefully these tidbits can show you how this is useful in practice. 

A Modern GitOps Workflow with Terraform and Argo CD

This approach isn’t just a workaround—it’s a cleaner evolution of the GitOps Bridge Pattern. By using Terraform to write directly to Git, you keep Git as your single source of truth while avoiding plugin overhead, secret gymnastics, or bloated inline configs.

We’ve seen this exact setup working in production environments, and it’s flexible enough to support both Helm and Kustomize—all while staying true to GitOps principles.

If you have any questions feel free to hit me up on the CNCF Slack or on the Akuity Community Discord (you’ll find me as Blake Pettersson there). Also, feel free to play around and fork the repo using the examples above.

Additional Resources

If you enjoyed this blog, be sure to check out our other great content on Argo CD:

Ready to simplify delivery with Akuity?

Deploy, promote, and operate applications reliably, powered by OSS you trust and Intelligence you control.

Ready to simplify delivery with Akuity?

Deploy, promote, and operate applications reliably, powered by OSS you trust and Intelligence you control.

Ready to simplify delivery with Akuity?

Deploy, promote, and operate applications reliably, powered by OSS you trust and Intelligence you control.

Sign Up for Akuity Updates

Practical guidance on MTTR reduction, GitOps at scale, and safe automation, with product updates from the Argo CD and Kargo team.

@2026 Akuity Inc. All rights reserved.

Akuity Inc. 440 N. Wolfe Road, Sunnyvale, CA 94085-3869 US +1-510-771-7837

SOC2 Type 2 Compliant

Sign Up for Akuity Updates

Practical guidance on MTTR reduction, GitOps at scale, and safe automation, with product updates from the Argo CD and Kargo team.

@2026 Akuity Inc. All rights reserved.

Akuity Inc. 440 N. Wolfe Road, Sunnyvale, CA 94085-3869 US +1-510-771-7837

SOC2 Type 2 Compliant

Sign Up for Akuity Updates

Practical guidance on MTTR reduction, GitOps at scale, and safe automation, with product updates from the Argo CD and Kargo team.

@2026 Akuity Inc. All rights reserved.

Akuity Inc. 440 N. Wolfe Road, Sunnyvale, CA 94085-3869 US +1-510-771-7837

SOC2 Type 2 Compliant