How to Promote Terraform and Kubernetes Changes Together with Kargo
Jesse Suen
Written by Jesse Suen is co-founder and CTO of Akuity and a co-creator of Argo CD and Kargo.
Kargo Enterprise from Akuity connects Terraform configurations with Argo CD through promotion steps that run OpenTofu. One promotion applies the infrastructure, reads its outputs, writes them into Helm values in Git and triggers an Argo CD sync, so infrastructure and application changes move through each environment as one release.
TL;DR
GitOps works well inside the cluster, but releases often reach outside it, to Lambda functions, databases and IAM roles, and Terraform and Argo CD don't coordinate across that line.
Workarounds like GitOps Bridge and Terraform-managed Git commits pass Terraform outputs to Kubernetes but don't promote infrastructure and app changes as one release.
Akuity’s Kargo Enterprise adds Terraform promotion steps, so one promotion applies infrastructure, writes its outputs into Helm values in Git and syncs through Argo CD.
A working example walks through promoting a Kubernetes app and a Lambda function together, from dev to prod.
Automating the handoff keeps review in place for production: teams can require approval before Terraform applies, in the Kargo UI or through a pull request.
Inside the cluster, GitOps works well. Argo CD keeps Kubernetes matching what's in Git, and Kargo promotes changes from one environment to the next. We created both projects with the cluster in mind. But few products run entirely on Kubernetes. Most also depend on resources outside the cluster: managed databases, IAM roles, networking, serverless functions and VMs.
So most platform teams split deployment along a clear line. Terraform provisions the foundation and the resources outside the cluster. Argo CD deploys the applications that run on Kubernetes. Different teams own each side, and each side moves at its own pace.
That split works as long as infrastructure and applications change on separate schedules. Often, they don't. A single feature can ship as a container on Kubernetes plus a Lambda function, or need a new queue, bucket or IAM role alongside the code. App teams often own that infrastructure themselves. In those cases, the line between infrastructure and application does not sit between two teams. It runs through the middle of a single release.
Neither tool understands that release, so teams that run Terraform and Argo CD together eventually hit the same question:
How does a release made of both infrastructure and application changes move safely from dev to staging to prod? Answering it starts with a smaller problem: getting the values Terraform creates into the Kubernetes app that needs them.
What Passing Terraform Outputs to Kubernetes Looks Like in Practice
In most teams, passing Terraform outputs to Kubernetes means running terraform apply, reading the outputs and copying them into Helm values or manifests by hand, once per environment.
A clear example is an AWS Lambda function URL. AWS generates the URL when it creates the function, so the value doesn't exist until terraform apply runs, and the Kubernetes app needs it to call the function. The same pattern shows up with load balancer hostnames, VPC and subnet IDs, IAM role ARNs, database endpoints, queue URLs and other identifiers the provider generates.
So the typical workflow ends up as two steps with two tools:
Run Terraform. A pipeline or an engineer runs
terraform applyfor the environment.Copy the outputs by hand. Someone reads
terraform output, pastes the new value into the Kubernetes manifests or Helm values, opens a PR and waits for Argo CD to sync.
Step 2 is where things break down. It's slow and easy to forget, and manual copy-and-paste can be wrong. Rollbacks and audits get harder too because neither tool records that the Helm change and the Terraform apply belonged to the same release. With dev, staging and prod, you repeat both steps three times.

Figure 1: Without Kargo, a person carries Terraform outputs to the Kubernetes side by hand, once per environment.
Common Ways to Connect Terraform and Argo CD, and Their Trade-offs
#1: The GitOps Bridge pattern. Terraform writes its outputs as labels or annotations on the Argo CD cluster Secret, and an ApplicationSet cluster generator (an Argo CD resource that creates one Application per registered cluster) templates them into Helm values. AWS uses this pattern in EKS Blueprints.
The cost: deployment configuration ends up split between Git and live cluster state, and values reach the cluster with no review step. The pattern also depends on ApplicationSets and fits cluster-level values better than per-release ones. It suits values set once per cluster, like the VPC ID or IAM role used to bootstrap cluster add-ons, more than values that change with each release.
#2: Terraform writes to Git. Terraform's GitHub provider commits the generated Helm values or Kustomize overlays to the GitOps repo and can open the pull request. This keeps the values in Git and adds review. The cost: Terraform now owns files in your GitOps repo, so the next apply overwrites manual edits. Terraform also has to manage branches and pull requests from its state. Read How to Integrate Terraform with Argo CD (Without Breaking GitOps) to learn more.
#3: Glue CI pipelines or in-cluster controllers. Some teams write a pipeline that runs Terraform, parses the outputs and pushes a commit. Others move Terraform into the cluster with an operator, or adopt Crossplane. Pipelines mean custom scripts each team maintains outside the promotion process. Controllers mean installing a new control plane and rewriting modules as CRDs, which is a large commitment when the goal is to pass one URL from one tool to another.
How Should Infrastructure and App Changes Move Through Environments Together?
All three approaches move data from one tool into the other, but none of them moves the release. Infrastructure and app changes still travel through dev, staging and prod as separate deployments, and nothing keeps them in step.
Moving them together takes a layer above both tools that runs the same sequence in every environment: apply the infrastructure, capture its outputs, feed them into the app config and deploy. Kargo Enterprise provides that layer.
How Kargo's Terraform Promotion Steps Work (Using OpenTofu)
Kargo Enterprise is Akuity’s enterprise edition of Kargo, available as SaaS on the Akuity Platform or self-hosted. It adds four promotion steps: hcl-update edits HCL files, and tf-plan, tf-apply and tf-output run OpenTofu inside a Kargo promotion
Together they let one promotion apply infrastructure and pass its outputs to the Kubernetes side of the same release.
Where Argo CD keeps a cluster matching Git, Kargo decides what goes into Git next. It watches artifact sources (container registries, Git repos and Helm charts), bundles new versions into Freight (an immutable set of exact artifact versions, and the unit of promotion) and promotes that Freight through a pipeline of Stages (one per environment, such as dev, staging and prod).
Each promotion runs a sequence of promotion steps, such as git-clone, yaml-update, git-commit, git-push and argocd-update, and records its changes as Git commits.
Until now, those promotion steps covered what runs in the cluster: they update manifests and Helm values in Git and tell Argo CD to sync. The parts of a release outside the cluster, like a Lambda function or the IAM role it needs, sat outside the promotion, and teams handled them in a separate pipeline. Kargo Enterprise’s Terraform and OpenTofu steps bring those parts into the same promotion. Kargo plans and applies the infrastructure for each Stage, reads the outputs and passes them to the steps that update the Kubernetes side:
Step | What it does |
| Edits values in HCL files such as |
| Runs a plan and captures the result, so reviewers can see the infrastructure changes before the apply runs, in a pull request or the Kargo UI. |
| Applies the configuration for the Stage being promoted. |
| Reads outputs from state after an apply and makes them available to later steps. |
Each step can pass its results to the steps after it, so a Terraform output can go straight into your Helm values. The handoff that used to be manual becomes a tf-output step and one yaml-update entry.
Example: Deploying a Kubernetes App and AWS Lambda in One Kargo Promotion
This example promotes a Kubernetes app and an AWS Lambda function as one release. In each environment, Kargo applies the Lambda's Terraform, reads the generated function URL and writes it into the app's Helm values before Argo CD syncs. The full example is in https://github.com/akuity/kargo-multi-target.

Figure 2: With Kargo, one promotion applies the infrastructure, passes its outputs to Helm and syncs Argo CD, then repeats as the same Freight moves to staging and prod.
Bundling a Container Image and Lambda Function into One Kargo Freight
The application has two deployable artifacts, built and published independently:
A container image for the Kubernetes app,
otel-app, in GitHub Container RegistryAn AWS Lambda function, packaged as an OCI image in Amazon ECR
A Kargo Warehouse watches artifact sources for new versions. This one subscribes to both artifacts:
When a new version of either artifact appears, the Warehouse creates a new piece of Freight, for example, otel-app:v1.4.0 together with lambda-app:v2.1.0. Freight is what you promote from dev to staging to prod, so the Kubernetes app and the Lambda function move together, as a pair.
Without Freight, the two deployments have separate version histories, and someone has to track which otel-app release goes with which Lambda release.
With Freight:
Staging and prod get the combination you tested. Promoting Freight to prod deploys the same container image and the same Lambda image that passed in staging, so a new app version doesn't land next to an old function.
Rollback covers both artifacts. Promoting an earlier Freight redeploys the earlier container image and re-applies Terraform with the earlier Lambda image. Rolling back stateful infrastructure, such as a database schema or networking change, still takes the usual care.
Promotion steps can read each artifact in the bundle. The PromotionTask below (a reusable set of promotion steps that Stages share by name) pulls the Lambda version for Terraform and the container version for Helm from the same Freight.
Structuring the GitOps Repo for Helm and Terraform
Each environment has its own directory, and each directory holds both the Kubernetes and the Terraform side of that environment:
Argo CD deploys helm/ for each environment. Kargo runs Terraform against terraform/.
The demo stores Terraform state in Git to keep the example self-contained. For real environments, use a remote backend: state files can contain sensitive values, and Git provides no state locking when promotions run at the same time. Kargo's Terraform steps work with your existing backend, as long as OpenTofu supports it.
The Kargo PromotionTask: From Terraform Apply to Argo CD Sync
Every Stage shares this PromotionTask. Values in ${{ }} are expressions that Kargo evaluates during the promotion. For example, ctx.stage is the name of the Stage being promoted, so one task serves dev, staging and prod. The comments describe each step:
The key line is this one:
The as: tf-output field on step 4 names that step's results so later steps can read them through task.outputs. tf-output reads the lambda_url output from the state tf-apply just wrote, and yaml-update puts it into the Helm values for the same environment. The URL moves from Terraform to Helm without a manual copy. Because Kargo runs the apply, the output read and the Helm update in one promotion, in that order, the Kubernetes change ships with the infrastructure it depends on.
The example passes static AWS credentials from a Kubernetes Secret to keep setup short. For production, use IRSA instead of static keys, as shown in the Kargo documentation. What One Combined Promotion Produces
When a promotion to a Stage finishes:
One commit contains the release. The new otel-app image tag, the new Lambda image in
env.auto.tfvars, the generated Lambda URL invalues.yamland the updated state file land together. The commit message includes theterraform applyresult, so the Git history shows what changed in the infrastructure as well as in the manifests.Argo CD syncs to that commit. The otel-app pods restart with
LAMBDA_URLset to the URL Terraform created.
Adding Approval Gates for Production Terraform Changes
Automatic apply works well for dev, and most teams want a review step before Terraform touches production. Kargo supports two: a wait-for-approval step that pauses the promotion for sign-off in the Kargo UI, or a pull request that reviewers merge after reading the Terraform plan. Either way, the output handoff after the apply stays automatic. Our post on approval gates walks through both options with examples.
Why Use Kargo to Coordinate Terraform and Argo CD
Kargo keeps Terraform outputs in Git, adds review where teams need it and promotes infrastructure and application changes together, without changes to existing Terraform modules. Here's how that compares with the workarounds covered earlier:
GitOps Bridge | Terraform writes to Git | Glue pipelines or controllers | Kargo Enterprise | |
Where outputs live | Annotations on the Argo CD cluster Secret | Git | Git or cluster resources, depending on the approach | Git, in Helm values |
Review before deploy | No | Yes, through a PR | Custom, if built | Yes, in the Kargo UI or a PR |
Who writes GitOps repo files | N/A (values stay outside Git) | Terraform | Custom scripts | Kargo promotion steps |
Changes to Terraform code | Outputs written to the cluster Secret | GitHub provider resources for files, branches and PRs | None for pipelines; CRD rewrites for controllers | None |
Config tool support | Requires ApplicationSets | Helm or Kustomize | Varies | Plain Applications or ApplicationSets, Helm or Kustomize |
Moves infra and app through environments together | No | No | Only with custom scripting | Yes, as one Freight |
The result is a clear division of labor: Terraform provisions infrastructure, Argo CD syncs manifests and Kargo handles the ordering and the data that flows between them.
Beyond Kubernetes: Kargo as a Promotion Layer for Infrastructure and Apps
Terraform support is part of a larger goal: making Kargo a promotion layer for more than Kubernetes.
GitOps worked for Kubernetes because it made the desired state declarative, versioned in Git and continuously reconciled. Argo CD put those principles to work inside the cluster. Kargo added what the principles don't cover: safe, progressive promotion from one environment to the next.
The Terraform promotion steps carry GitOps principles and progressive promotion past the cluster boundary, to the infrastructure and services a release depends on, whether that's a Lambda function, a managed database or a VM. As the line between infrastructure and application runs through more releases, the promotion layer has to cover both sides.
If your team runs Terraform in one pipeline and Argo CD in another, with a person copying values between them, try running both as one promotion.
Get started:
Browse the full example: https://github.com/akuity/kargo-multi-target
Learn more about Kargo: kargo.io
Start a free trial or book a demo today.
Frequently Asked Questions
How do I pass Terraform outputs to Argo CD?
Argo CD can’t read Terraform outputs directly, so teams either copy them by hand, write them to the Argo CD cluster Secret (the GitOps Bridge pattern), have Terraform commit them to Git, or script it in CI. Kargo Enterprise automates the handoff: its tf-output step reads outputs after an apply, and a yaml-update step writes them into Helm values in Git before Argo CD syncs.
Can Argo CD run Terraform?
No. Argo CD syncs Kubernetes manifests from Git into a cluster and doesn’t provision infrastructure outside it. To run Terraform as part of a GitOps release, teams pair Argo CD with a separate tool, such as a CI pipeline, Crossplane, or Kargo Enterprise’s Terraform promotion steps.
What’s the difference between Terraform, Argo CD and Kargo?
Terraform provisions infrastructure such as databases, IAM roles and Lambda functions. Argo CD keeps Kubernetes clusters in sync with Git. Kargo promotes changes from one environment to the next, and with Kargo Enterprise it can order a release that spans both: apply the infrastructure, pass its outputs to the app config, then trigger the Argo CD sync.
Does Kargo support Terraform or OpenTofu?
Yes, it does. Kargo Enterprise adds four promotion steps: hcl-update, tf-plan, tf-apply and tf-output. The steps run OpenTofu, which handles configurations written for Terraform 1.6 and earlier. Kargo doesn’t bundle the terraform CLI because of Terraform’s Business Source License (BSL).
Are the Terraform steps available in open-source Kargo?
No. The Terraform and OpenTofu steps are part of Kargo Enterprise on the Akuity Platform. Open-source Kargo includes the standard promotion steps, such as git-clone, yaml-update and argocd-update.
Do I need to rewrite my Terraform modules to use Kargo?
No. Kargo runs your existing Terraform configuration with OpenTofu, so modules written for Terraform 1.6 and earlier work without changes. Kargo edits input values in files like env.auto.tfvars and reads outputs after the apply.
What is the GitOps Bridge pattern?
GitOps Bridge is a way to pass Terraform outputs to Argo CD by writing them as labels or annotations on the Argo CD cluster Secret, where an ApplicationSet cluster generator templates them into Helm values. AWS uses it in EKS Blueprints. It works well for values set once per cluster, like a VPC ID, but the values live outside Git and reach the cluster without review.

