Skip to main content

Running Rewrite on an Infrastructure-as-Code project

Infrastructure-as-Code (IaC) repositories typically have no pom.xml or build.gradle. That makes them awkward to run OpenRewrite against, because the rewrite-maven-plugin and rewrite-gradle-plugin are — by design — Maven and Gradle plugins, and need a host build to be invoked from.

OpenRewrite ships standalone parsers for the file types these repositories contain, so the recipes themselves can run; the only question is how to invoke them without a Java build. This guide uses Terraform as the running example because it has the richest ecosystem of OpenRewrite recipes and community tooling today, but the same three approaches work unchanged for any source that has a standalone parser:

SourceParser moduleNotes
Terraform / OpenTofu / HCL (.tf, .tfvars, .tofu, .hcl)rewrite-hclUsed as the running example below. See the HCL recipe catalog.
Kubernetes manifests, Helm chartsrewrite-yaml
Dockerfiles, Containerfilesrewrite-docker
GitHub Actions, GitLab CI workflowsrewrite-yaml + rewrite-github-actions / rewrite-gitlab
Protobuf (.proto)rewrite-protobufSchema, not IaC, but the same pattern applies.

Wherever the examples below reference rewrite-hcl, HclParser, or .tf, substitute the equivalent module, parser, and file extensions for your source type.

This guide covers three ways to apply recipes to such a repository, ordered from easiest to most flexible:

  1. Use the Moderne CLI — least setup, no Java build knowledge required.
  2. Use a host Gradle project — pure OSS, the approach demonstrated in the referenced community write-up.
  3. Embed recipe execution in a standalone runner — for users who want to integrate recipe execution into their own tooling.

Option 1: Moderne CLI

The Moderne CLI (mod) is the easiest way to run OpenRewrite recipes against repositories that have no Java build. It builds a Lossless Semantic Tree (LST) from the repository's source files — for an IaC-only repository, no Java toolchain or build file is required — and then applies recipes against that LST.

note

The Moderne CLI is free to use against public repositories, but requires a Moderne license to run recipes against private repositories. If your Terraform repo is private, choose Option 2 or 3 below, or contact Moderne about a license.

# Build an LST for the Terraform repo
mod build path/to/terraform-repo

# Apply a recipe
mod run path/to/terraform-repo --recipe org.openrewrite.hcl.ReplaceLegacyAttributeIndexSyntax

See the Moderne CLI documentation for installation and the full set of subcommands.

Option 2: Host Gradle project

If the Moderne CLI is not (yet) an option for you, the established OSS pattern is to create a host Gradle project that applies the OpenRewrite plugin, and to clone target Terraform repositories into a subdirectory of that host project. The Gradle plugin then walks the workspace and the HclParser picks up every .hcl, .tf, .tfvars, and .tofu file beneath it. The target Terraform repository itself remains untouched — it never gets a build.gradle of its own.

For a complete working setup, see infra-at-scale/avm-openrewrite-migrations and the accompanying write-up Refactoring HCL organization-wide with OpenRewrite, which walks through the host build.gradle.kts, the projects/ layout, and the workaround needed for the plugin to see .tf files past the host's .gitignore and the cloned repos' nested .git/ directories.

Step-by-step walkthrough

Step 1: Create the host project

Create a new directory with a Gradle wrapper and the following build.gradle.kts:

build.gradle.kts
plugins {
id("java-library")
id("org.openrewrite.rewrite") version "latest.release"
}

repositories {
mavenCentral()
}

dependencies {
// Recipes that operate on HCL/Terraform — replace with whichever
// recipe artifacts you intend to run.
rewrite("org.openrewrite:rewrite-hcl:latest.release")
}
settings.gradle.kts
rootProject.name = "hcl-migrations"

Step 2: Clone target repositories into projects/

mkdir -p projects
git clone https://github.com/your-org/some-terraform-repo.git projects/some-terraform-repo

You can clone any number of repositories into projects/. The plugin will process all of them in a single run.

Step 3: Work around .gitignore and nested .git

Once you git clone a repo into projects/, two things on disk get in the plugin's way — independent of what the host repo tracks:

  • The cloned repos each contain a real .git/ directory. JGit treats any directory containing .git/ as a separate repository and skips its contents during source discovery, so the .tf files inside never reach HclParser.
  • The host project's own root .gitignore almost certainly lists projects/ (or build outputs, *.tf, etc.) to keep cloned repos out of the host's history. The OpenRewrite plugin respects .gitignore via QuarkParser and turns every matching file into an unmodifiable Quark, so recipes see placeholders instead of HCL LSTs.

Add the following workaround to your build.gradle.kts so the plugin temporarily hides the host's root .gitignore and the nested .git/ directories during the task, then restores them afterwards. .gitignore files inside the cloned repos are left alone — they typically ignore .tfstate and .terraform/, which you do want quarked.

build.gradle.kts (continued)
val hideGitignore = { file: File -> file.renameTo(File(file.parentFile, ".gitignore-backup")) }
val restoreGitignore = { file: File -> file.renameTo(File(file.parentFile, ".gitignore")) }

val hideGitDirs = { dir: String ->
file(dir).walk().filter { it.isDirectory && it.name == ".git" }
.forEach { it.renameTo(File(it.parentFile, ".git-backup")) }
}
val restoreGitDirs = { dir: String ->
file(dir).walk().filter { it.isDirectory && it.name == ".git-backup" }
.forEach { it.renameTo(File(it.parentFile, ".git")) }
}

listOf("rewriteRun", "rewriteDryRun").forEach { taskName ->
tasks.named(taskName) {
doFirst {
hideGitDirs("projects")
file(".gitignore").takeIf { it.exists() }?.let { hideGitignore(it) }
}
doLast {
file(".gitignore-backup").takeIf { it.exists() }?.let { restoreGitignore(it) }
restoreGitDirs("projects")
}
}
}

Step 4: Preview and apply recipes

# Preview without modifying any files
./gradlew rewriteDryRun -Drewrite.activeRecipes=org.openrewrite.hcl.ReplaceLegacyAttributeIndexSyntax

# Inspect the proposed changes
cat build/reports/rewrite/rewrite.patch

# Apply the changes
./gradlew rewriteRun -Drewrite.activeRecipes=org.openrewrite.hcl.ReplaceLegacyAttributeIndexSyntax

Step 5: Commit the changes in each target repository

Because the target repositories are independent git checkouts under projects/, commit the resulting changes from inside each one:

cd projects/some-terraform-repo
git add .
git commit -m "Apply OpenRewrite HCL recipe"

Option 3: Standalone runner

If you need to embed recipe execution in your own tooling — a CI worker, a custom CLI, or a service — you can call OpenRewrite directly as a library. The HCL parser is self-contained and needs no build metadata. The example below requires Java 17 or later (it uses Stream.toList()), which is also the recommended baseline for running modern OpenRewrite recipes.

warning

The OpenRewrite parser and recipe-execution APIs used below are not guaranteed to be stable across releases. They may change without notice — use at your own risk, and prefer Options 1 or 2 if you don't need to embed execution yourself.

Example standalone runner
import org.openrewrite.*;
import org.openrewrite.config.Environment;
import org.openrewrite.hcl.HclParser;
import org.openrewrite.tree.ParseError;

import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;

public class HclRecipeRunner {
public static void main(String[] args) throws Exception {
Path dir = Paths.get(args[0]);
String activeRecipe = args[1];

HclParser parser = HclParser.builder().build();
ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);

List<Parser.Input> inputs;
try (Stream<Path> paths = Files.walk(dir)) {
inputs = paths
.filter(Files::isRegularFile)
.filter(parser::accept)
.map(p -> new Parser.Input(p, () -> {
try { return Files.newInputStream(p); }
catch (Exception e) { throw new RuntimeException(e); }
}))
.toList();
}

List<SourceFile> parsed = parser.parseInputs(inputs, dir, ctx)
.filter(sf -> !(sf instanceof ParseError))
.toList();

Recipe recipe = Environment.builder()
.scanRuntimeClasspath()
.build()
.activateRecipes(activeRecipe);

RecipeRun run = recipe.run(new InMemoryLargeSourceSet(parsed), ctx);
for (Result result : run.getChangeset().getAllResults()) {
if (result.getAfter() == null) {
Files.deleteIfExists(result.getBefore().getSourcePath());
} else {
Files.writeString(dir.resolve(result.getAfter().getSourcePath()),
result.getAfter().printAll());
}
}
}
}

Package this as an executable jar with rewrite-hcl and any recipe artifacts on the classpath, and you have a self-contained command-line tool that applies OpenRewrite HCL recipes against any directory. Swap HclParser for YamlParser, DockerParser, or ProtoParser (and the corresponding rewrite-* artifact) to do the same against the other source types listed at the top of this guide.

Authoring recipes for IaC sources

Writing custom recipes against any of the source types listed at the top of this guide follows the same patterns as Java recipes — see Types of recipes. Each language module is laid out the same way: the LST definition lives in src/main/java/.../<lang>/tree, and worked recipe examples live in src/test/java/.../<lang>. The easiest way to get started is to read the existing tests for your target language and adapt one.

Using Terraform/HCL as the concrete example: the HCL LST is defined under rewrite-hcl/src/main/java/org/openrewrite/hcl/tree, and for testing you can use RewriteTest with HclParser.builder() as the parser — see rewrite-hcl/src/test/java for reference test cases. The equivalent locations for the other languages:

LanguageLST treeTest examples
HCL / Terraformrewrite-hcl/.../hcl/treerewrite-hcl/src/test/java
YAML (Kubernetes, Helm, CI workflows)rewrite-yaml/.../yaml/treerewrite-yaml/src/test/java
Dockerrewrite-docker/.../docker/treerewrite-docker/src/test/java
Protobufrewrite-protobuf/.../protobuf/treerewrite-protobuf/src/test/java

For workflow-aware recipes that build on top of YAML (GitHub Actions, GitLab CI), see rewrite-github-actions and rewrite-gitlab for examples of recipes that match on workflow-specific structure rather than raw YAML.

For published recipe libraries you can depend on today: