DEV Community

Cover image for Managing Terraform Modules with Nx Monorepo
Antoine Caron
Antoine Caron

Posted on • Originally published at blog.slashgear.dev

Managing Terraform Modules with Nx Monorepo

Introduction

Managing infrastructure as code at scale is challenging. When your organization grows beyond a handful of Terraform modules, you quickly encounter familiar problems: code duplication, inconsistent testing practices, unclear dependencies between modules, and the eternal question of whether to use a monorepo or split everything into separate repositories.

If you've worked with Terraform in a team environment, you've probably faced these pain points:

  • Running terraform fmt and terraform validate across dozens of modules manually
  • Uncertainty about which modules are affected by a change
  • Difficulty enforcing consistent standards across all modules
  • Time wasted running tests on unaffected modules in CI/CD
  • The challenge of managing dependencies between related modules

What if you could leverage the same powerful tooling that frontend teams use to manage their monorepos? Enter Nx - a smart build system that can transform how you manage Terraform modules.

Want to see this in action? I've created a complete demo repository showcasing all the concepts covered in this article, with seven Scaleway Terraform modules fully configured with Nx.

The Challenge of Managing Terraform Modules at Scale

The Multi-Repo Dilemma

Many organizations start with separate Git repositories for each Terraform module. This approach seems clean at first - each module has its own versioning, CI/CD pipeline, and release cycle. However, as your infrastructure grows, this strategy reveals its weaknesses:

  • Versioning Hell: When Module A depends on Module B, you need to carefully manage version pinning. Update Module B? Now you need to update Module A's version constraint, test it, and release a new version.
  • Cross-Module Changes: A breaking change that affects multiple modules requires coordinating PRs across multiple repositories, each with its own review and merge timeline.
  • Inconsistent Tooling: Each repository might have different CI configurations, different testing approaches, or even different versions of tools like tflint or terraform-docs.

The Monorepo Challenges

Moving to a monorepo solves some problems but creates others:

  • Build Times: Without smart tooling, CI runs tests on ALL modules even when you only changed one.
  • Dependency Tracking: How do you know which modules depend on each other? Manual documentation? Good luck keeping that up to date.
  • Task Orchestration: Running terraform fmt across 50 modules means writing bash scripts that find all the directories, loop through them, and aggregate results.
  • Developer Experience: Developers need clear visibility into what's affected by their changes and confidence that they're running the right tests.

The traditional approach to Terraform module management leaves teams choosing between the coordination overhead of multi-repo or the inefficiency of a naive monorepo.

Why Nx for Terraform Modules?

Nx is a smart build system originally designed for JavaScript/TypeScript monorepos, but it's fundamentally language-agnostic. At its core, Nx provides exactly what we need for managing Terraform modules:

1. Intelligent Task Orchestration

Nx understands your project graph - which modules depend on which. When you run a task, Nx:

  • Only executes tasks on affected projects based on your git diff
  • Runs tasks in the correct order based on dependencies
  • Executes independent tasks in parallel to maximize speed

For Terraform, this means running terraform validate only on the modules that changed, and their dependents - not the entire monorepo.

2. Dependency Graph Visualization

Run nx graph and see a visual representation of how your modules relate to each other. This is invaluable for understanding impact and planning refactors.

Example of Dependency Graph made with NX for this usecase

3. Code Generation

Nx generators let you scaffold new modules with consistent structure. Want a new Terraform module with the right directory structure, a project.json, tests, and documentation? One command.

4. Consistent Tooling Across Projects

Define tasks once, inherit them across all modules. Every module gets fmt, validate, lint, and test targets without copying configuration files everywhere.

The beauty of Nx is that it doesn't change how you write Terraform - it enhances how you manage Terraform modules at scale.

Setting Up an Nx Workspace for Terraform

Let's build a practical Nx workspace for managing Terraform modules. I'll walk you through the setup step by step.

Creating the Workspace

First, create a new Nx workspace:

npx create-nx-workspace@latest terraform-modules --preset=npm
cd terraform-modules
Enter fullscreen mode Exit fullscreen mode

Choose the "npm" preset - we're not building a JavaScript application, we just want Nx's task orchestration capabilities.

Workspace Structure

Here's a recommended structure for organizing Terraform modules:

terraform-modules/
├── nx.json
├── package.json
├── modules/
│   ├── scw-vpc/
│   │   ├── project.json
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── README.md
│   ├── scw-k8s/
│   │   ├── project.json
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── README.md
│   └── scw-database/
│       ├── project.json
│       └── ...
└── tools/
    └── scripts/
Enter fullscreen mode Exit fullscreen mode

Each Terraform module lives in modules/ and has its own project.json file where we define Nx tasks.

Configuring Nx for Terraform Projects

The project.json file is where Nx magic happens. Here's an example for the scw-vpc module:

{
  "name": "scw-vpc",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "projectType": "library",
  "sourceRoot": "modules/scw-vpc",
  "targets": {
    "fmt": {
      "executor": "nx:run-commands",
      "options": {
        "command": "terraform fmt -check",
        "cwd": "modules/scw-vpc"
      }
    },
    "fmt-fix": {
      "executor": "nx:run-commands",
      "options": {
        "command": "terraform fmt",
        "cwd": "modules/scw-vpc"
      }
    },
    "validate": {
      "executor": "nx:run-commands",
      "options": {
        "commands": ["terraform init -backend=false", "terraform validate"],
        "cwd": "modules/scw-vpc",
        "parallel": false
      }
    },
    "lint": {
      "executor": "nx:run-commands",
      "options": {
        "command": "tflint",
        "cwd": "modules/scw-vpc"
      }
    },
    "docs": {
      "executor": "nx:run-commands",
      "options": {
        "command": "terraform-docs markdown . > README.md",
        "cwd": "modules/scw-vpc"
      }
    }
  },
  "tags": ["type:terraform", "cloud:scaleway", "layer:network"]
}
Enter fullscreen mode Exit fullscreen mode

The nx:run-commands executor lets us run any shell command - perfect for Terraform CLI commands.

Now you can run:

nx fmt scw-vpc          # Check formatting
nx validate scw-vpc     # Validate configuration
nx lint scw-vpc         # Run tflint
nx run-many -t validate # Run validate on all modules
Enter fullscreen mode Exit fullscreen mode

Managing Dependencies Between Terraform Modules

One of Nx's most powerful features is its understanding of dependencies. But Terraform modules don't use import statements like JavaScript - so how does Nx know about dependencies?

Implicit Dependencies

Nx can infer dependencies by analyzing your Terraform code. When scw-k8s module references the scw-vpc module with a relative path:

module "vpc" {
  source = "../scw-vpc"
  # ...
}
Enter fullscreen mode Exit fullscreen mode

Nx can detect this relationship. Configure implicit dependencies in your root nx.json:

{
  "pluginsConfig": {
    "@nx/dependency-checks": {
      "checkVersionMismatch": false
    }
  },
  "targetDefaults": {
    "validate": {
      "dependsOn": ["^validate"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Explicit Dependencies

For more control, explicitly declare dependencies in your project.json:

{
  "name": "scw-k8s",
  "implicitDependencies": ["scw-vpc"],
  "targets": {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

This tells Nx that scw-k8s depends on scw-vpc. Now when you run:

nx validate scw-k8s
Enter fullscreen mode Exit fullscreen mode

Nx will first validate scw-vpc, then scw-k8s. And more importantly:

nx affected -t validate
Enter fullscreen mode Exit fullscreen mode

If you change scw-vpc, Nx knows that scw-k8s is affected and will run validation on both modules.

Visualizing the Graph

See your entire module ecosystem:

nx graph
Enter fullscreen mode Exit fullscreen mode

This opens an interactive browser view showing all your modules and their relationships. It's incredibly useful for:

  • Onboarding new team members
  • Planning large refactors
  • Understanding blast radius of changes
  • Identifying circular dependencies

Building a Task Pipeline

Nx really shines when you build task pipelines that automatically run in the correct order. Let's create a comprehensive set of targets for our Terraform modules.

Defining Target Dependencies

In your nx.json, use targetDefaults to create a pipeline:

{
  "targetDefaults": {
    "fmt": {
      "dependsOn": []
    },
    "validate": {
      "dependsOn": ["^validate", "fmt"]
    },
    "lint": {
      "dependsOn": ["validate"]
    },
    "security": {
      "dependsOn": ["lint"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The ^ prefix means "dependencies of dependencies". When you run nx security scw-k8s:

  1. Nx validates all modules that scw-k8s depends on
  2. Then formats scw-k8s
  3. Then validates scw-k8s
  4. Then lints scw-k8s
  5. Finally runs security scans on scw-k8s

All automatically, in parallel where possible.

Common Targets

Here's a complete project.json with all the targets you'll typically need:

{
  "name": "scw-vpc",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "projectType": "library",
  "sourceRoot": "modules/scw-vpc",
  "targets": {
    "fmt": {
      "executor": "nx:run-commands",
      "options": {
        "command": "terraform fmt -check",
        "cwd": "modules/scw-vpc"
      }
    },
    "fmt-fix": {
      "executor": "nx:run-commands",
      "options": {
        "command": "terraform fmt",
        "cwd": "modules/scw-vpc"
      }
    },
    "validate": {
      "executor": "nx:run-commands",
      "options": {
        "commands": ["terraform init -backend=false", "terraform validate"],
        "cwd": "modules/scw-vpc",
        "parallel": false
      }
    },
    "lint": {
      "executor": "nx:run-commands",
      "options": {
        "commands": ["tflint --init", "tflint"],
        "cwd": "modules/scw-vpc",
        "parallel": false
      }
    },
    "security": {
      "executor": "nx:run-commands",
      "options": {
        "command": "checkov -d .",
        "cwd": "modules/scw-vpc"
      }
    },
    "docs": {
      "executor": "nx:run-commands",
      "options": {
        "command": "terraform-docs markdown . > README.md",
        "cwd": "modules/scw-vpc"
      }
    }
  },
  "tags": ["type:terraform", "cloud:scaleway", "layer:network"]
}
Enter fullscreen mode Exit fullscreen mode

Running Tasks Across Multiple Projects

# Run fmt on all modules
nx run-many -t fmt

# Run affected validations based on git changes
nx affected -t validate,lint

# Run tasks in specific order
nx run-many -t fmt,validate,lint,security

# Run on modules with specific tags
nx run-many -t validate --projects=tag:cloud:scaleway
Enter fullscreen mode Exit fullscreen mode

Code Generation and Scaffolding

Manually creating project.json files for each new module gets tedious. Nx generators solve this by scaffolding consistent module structures with a single command.

Creating a Custom Generator

Create a generator in tools/generators/terraform-module/:

// tools/generators/terraform-module/index.js
const { formatFiles, generateFiles, Tree } = require("@nx/devkit");
const path = require("path");

async function terraformModuleGenerator(tree, schema) {
  const { name, cloud, layer } = schema;
  const projectRoot = `modules/${name}`;

  // Generate files from templates
  generateFiles(tree, path.join(__dirname, "files"), projectRoot, {
    name,
    cloud,
    layer,
    tmpl: "",
  });

  // Update workspace
  const projectConfiguration = {
    name,
    projectType: "library",
    sourceRoot: projectRoot,
    targets: {
      fmt: {
        /* ... */
      },
      validate: {
        /* ... */
      },
      lint: {
        /* ... */
      },
      test: {
        /* ... */
      },
    },
    tags: [`type:terraform`, `cloud:${cloud}`, `layer:${layer}`],
  };

  addProjectConfiguration(tree, name, projectConfiguration);
  await formatFiles(tree);
}

module.exports = terraformModuleGenerator;
module.exports.schema = {
  properties: {
    name: { type: "string" },
    cloud: { type: "string", enum: ["scaleway", "aws", "gcp", "azure"] },
    layer: {
      type: "string",
      enum: ["network", "compute", "storage", "security"],
    },
  },
  required: ["name", "cloud", "layer"],
};
Enter fullscreen mode Exit fullscreen mode

Template Files

Create templates in tools/generators/terraform-module/files/:

# tools/generators/terraform-module/files/main.tf__tmpl__
terraform {
  required_version = ">= 1.0"

  required_providers {
    <%= cloud %> = {
      source  = "hashicorp/<%= cloud %>"
      version = "~> 5.0"
    }
  }
}

# Add your resources here
Enter fullscreen mode Exit fullscreen mode
# tools/generators/terraform-module/files/variables.tf__tmpl__
variable "name" {
  description = "Name of the <%= name %> module"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode
# tools/generators/terraform-module/files/outputs.tf__tmpl__
output "id" {
  description = "ID of the created resource"
  value       = ""
}
Enter fullscreen mode Exit fullscreen mode

Using the Generator

Now create new modules consistently:

nx g @nx/workspace:workspace-generator terraform-module \
  --name=scw-function \
  --cloud=scaleway \
  --layer=compute

# Creates:
# modules/scw-function/
# ├── project.json
# ├── main.tf
# ├── variables.tf
# ├── outputs.tf
# └── README.md
Enter fullscreen mode Exit fullscreen mode

Every new module gets the same structure, targets, and tags. No copy-pasting required.

Testing Strategies

Testing infrastructure code is critical but often overlooked. With Nx, you can build a comprehensive testing pipeline that runs efficiently across all your modules.

Level 1: Static Analysis

terraform fmt & validate
Basic syntax and semantic validation:

nx run-many -t fmt,validate
Enter fullscreen mode Exit fullscreen mode

tflint
Catch common mistakes and enforce best practices:

{
  "lint": {
    "executor": "nx:run-commands",
    "options": {
      "commands": ["tflint --init", "tflint --format compact"],
      "cwd": "modules/scw-vpc",
      "parallel": false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

checkov
Security and compliance scanning:

nx run-many -t security  # Runs checkov on all modules
Enter fullscreen mode Exit fullscreen mode

Orchestrating Static Analysis with Nx

The magic happens when you combine Nx's dependency graph with your validation pipeline:

# Only validate modules affected by your changes
nx affected -t validate,lint --base=main

# Validate a module and all its dependencies
nx validate scw-k8s --with-deps

# Run security scans in parallel (independent modules)
nx run-many -t security --parallel=3

# Run full quality checks on affected modules
nx affected -t fmt,validate,lint,security --base=main
Enter fullscreen mode Exit fullscreen mode

This can reduce CI time from hours to minutes for large infrastructure repos.

CI/CD Integration

The real power of Nx shows up in CI/CD. Instead of running all checks on all modules, you only run what's needed.

GitHub Actions Example

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  affected:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 # Important for nx affected

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.6.0

      - name: Install dependencies
        run: npm ci

      - name: Install tflint
        run: |
          curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

      - name: Derive SHAs for affected command
        uses: nrwl/nx-set-shas@v3

      - name: Run affected format check
        run: npx nx affected -t fmt --base=$NX_BASE --head=$NX_HEAD

      - name: Run affected validate
        run: npx nx affected -t validate --base=$NX_BASE --head=$NX_HEAD

      - name: Run affected lint
        run: npx nx affected -t lint --base=$NX_BASE --head=$NX_HEAD --parallel=3

      - name: Run affected security scans
        run: npx nx affected -t security --base=$NX_BASE --head=$NX_HEAD
Enter fullscreen mode Exit fullscreen mode

What This Does

  1. Only checks affected modules: If you change scw-vpc, only scw-vpc and modules that depend on it run checks
  2. Parallel execution: Independent modules run in parallel (configured with --parallel=3)
  3. Fast feedback: Most PRs only touch 1-2 modules, so CI completes in seconds instead of minutes

GitLab CI Example

# .gitlab-ci.yml
stages:
  - validate
  - security

.nx-affected:
  image: node:18
  before_script:
    - npm ci
    - curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
  variables:
    NX_BASE: $CI_MERGE_REQUEST_DIFF_BASE_SHA
    NX_HEAD: $CI_COMMIT_SHA

affected:validate:
  extends: .nx-affected
  stage: validate
  script:
    - npx nx affected -t validate,lint --base=$NX_BASE --head=$NX_HEAD --parallel=3

affected:security:
  extends: .nx-affected
  stage: security
  script:
    - npx nx affected -t security --base=$NX_BASE --head=$NX_HEAD
  only:
    - merge_requests
Enter fullscreen mode Exit fullscreen mode

Results

In a real-world scenario with 30+ Terraform modules:

  • Before Nx: Every PR ran checks on all 30 modules (~15 min)
  • After Nx: Most PRs only check 1-3 affected modules (~2 min)
  • Savings: 87% reduction in CI time

Managing Releases with Nx

One of the most powerful yet underrated features of Nx is its ability to manage releases in a monorepo. For Terraform modules, proper release management is crucial for versioning, changelog generation, and coordinating changes across dependent modules.

Using Nx Release

Nx provides a built-in release system that handles versioning, changelog generation, and git tagging automatically.

Initial Setup

First, configure Nx Release in your nx.json:

{
  "release": {
    "projects": ["modules/*"],
    "projectsRelationship": "independent",
    "version": {
      "generatorOptions": {
        "packageRoot": "{projectRoot}",
        "currentVersionResolver": "git-tag"
      }
    },
    "changelog": {
      "projectChangelogs": {
        "createRelease": "github",
        "file": "{projectRoot}/CHANGELOG.md",
        "renderOptions": {
          "authors": true,
          "commitReferences": true
        }
      }
    },
    "git": {
      "commit": true,
      "tag": true,
      "commitMessage": "chore(release): publish {projectName} {version}"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating a Release

When you're ready to release modules that have changed:

# See which modules have changed since last release
nx release --dry-run

# Release only affected modules with automatic version bump
nx release --projects=tag:cloud:scaleway

# Release a specific module with a specific version
nx release scw-vpc --specifier=1.2.0
Enter fullscreen mode Exit fullscreen mode

Automated Semantic Versioning

Nx can automatically determine version bumps based on conventional commits:

# Patch version (fix: commits)
git commit -m "fix(scw-vpc): correct subnet CIDR calculation"

# Minor version (feat: commits)
git commit -m "feat(scw-k8s): add autoscaling support"

# Major version (BREAKING CHANGE: in commit body)
git commit -m "feat(scw-vpc)!: redesign network architecture

BREAKING CHANGE: VPC module now requires new subnet configuration"

# Run release
nx release --projects=tag:cloud:scaleway --dry-run
# Nx will suggest: scw-vpc: 1.2.3 → 2.0.0, scw-k8s: 1.5.0 → 1.6.0
Enter fullscreen mode Exit fullscreen mode

Changelog Generation

Nx automatically generates changelogs for each module:

# modules/scw-vpc/CHANGELOG.md

## 2.0.0 (2025-11-01)

### ⚠ BREAKING CHANGES

- **scw-vpc:** VPC module now requires new subnet configuration

### Features

- **scw-vpc:** redesign network architecture ([a1b2c3d](https://github.com/org/repo/commit/a1b2c3d))

### Authors

- Antoine Caron (@Slashgear)
Enter fullscreen mode Exit fullscreen mode

Release Workflow in CI

Automate releases with GitHub Actions:

# .github/workflows/release.yml
name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Install dependencies
        run: npm ci

      - name: Configure Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          npx nx release --skip-publish
Enter fullscreen mode Exit fullscreen mode

Tagging Strategy

Nx creates git tags for each module release:

# Tags created by Nx
scw-vpc@2.0.0
scw-k8s@1.6.0
scw-database@1.3.1
Enter fullscreen mode Exit fullscreen mode

External consumers can reference specific versions:

# Using git tags to reference specific module versions
module "vpc" {
  source = "git::https://github.com/org/terraform-modules.git//modules/scw-vpc?ref=scw-vpc@2.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Independent vs Fixed Versioning

Nx supports two release strategies:

Independent (recommended for Terraform modules):

  • Each module has its own version
  • Modules only bump when they change
  • "projectsRelationship": "independent"

Fixed (useful for tightly coupled modules):

  • All modules share the same version
  • All versions bump together even if unchanged
  • "projectsRelationship": "fixed"

Benefits

  • Automatic versioning: Based on conventional commits
  • Generated changelogs: No manual CHANGELOG.md updates
  • Git tagging: Automatic and consistent
  • Affected awareness: Only release modules that changed
  • GitHub releases: Automatic release creation with notes

This makes managing dozens of Terraform modules significantly less painful than manual versioning and tagging.

Real-World Use Case

Let's walk through a concrete example: a SaaS company managing their Scaleway infrastructure with Nx. This example is based on the nx-terraform-demo repository, which you can clone and explore yourself.

The Setup

They have the following modules:

  • scw-vpc - Private network configuration
  • scw-k8s - Kubernetes Kapsule cluster (depends on vpc)
  • scw-database - Managed PostgreSQL (depends on vpc)
  • scw-object-storage - S3-compatible object storage (independent)
  • scw-registry - Container registry (depends on vpc)
  • scw-loadbalancer - Load balancer (depends on vpc, k8s)
  • scw-monitoring - Observability stack (depends on k8s)

The Dependency Graph

scw-vpc
  ├── scw-k8s
  │   ├── scw-loadbalancer
  │   └── scw-monitoring
  ├── scw-database
  └── scw-registry

scw-object-storage (independent)
Enter fullscreen mode Exit fullscreen mode

Run nx graph to visualize this automatically.

Scenario: Updating the VPC Module

A developer needs to add a new subnet to scw-vpc. Here's what happens:

# Developer makes changes to scw-vpc
git checkout -b feat/add-subnet
# ... edit scw-vpc/main.tf ...

# Check what's affected
nx show projects --affected
# Output: scw-vpc, scw-k8s, scw-database, scw-registry, scw-loadbalancer, scw-monitoring

# Run validation only on affected modules
nx affected -t validate,lint
# Validates: vpc → k8s, database, registry → loadbalancer, monitoring

# Push and CI runs affected checks
# Only the 6 affected modules are validated, not all 7
Enter fullscreen mode Exit fullscreen mode

Result: Instead of running checks on all modules, only the affected ones run. The independent module (scw-object-storage) is skipped entirely.

Scenario: Adding New Feature to Monitoring

# Developer updates scw-monitoring
nx show projects --affected
# Output: scw-monitoring (only!)

# CI only validates this one module
nx affected -t validate,lint,security --base=main
Enter fullscreen mode Exit fullscreen mode

Result: All other 6 modules skipped, massive time savings.

The Impact

Before Nx:

  • Every PR: validate all 7 modules, run all checks (~20 min)
  • Developers waited, context-switched, lost productivity

After Nx:

  • Small changes: 1-2 modules affected (~3 min)
  • VPC changes: 6 modules affected (~8 min)
  • Clear visibility with nx graph on what breaks

Best Practices

After implementing Nx with Terraform modules across multiple teams, here are the lessons learned:

1. Use Meaningful Tags

Tags enable powerful filtering. Be strategic:

{
  "tags": [
    "type:terraform", // All terraform projects
    "cloud:scaleway", // Cloud provider
    "layer:network", // Infrastructure layer
    "team:platform", // Owning team
    "env:multi" // Supports multiple envs
  ]
}
Enter fullscreen mode Exit fullscreen mode

Then run targeted commands:

nx run-many -t validate --projects=tag:team:platform
nx run-many -t security --projects=tag:cloud:scaleway
Enter fullscreen mode Exit fullscreen mode

2. Keep Modules Small and Focused

Each module should do one thing well:

  • ✅ Good: scw-vpc, scw-k8s, scw-database
  • ❌ Bad: scw-full-infrastructure

Small modules = better reusability and easier testing.

3. Use Target Dependencies Wisely

Define a clear pipeline in nx.json:

{
  "targetDefaults": {
    "validate": {
      "dependsOn": ["^validate", "fmt"]
    },
    "test": {
      "dependsOn": ["^test", "validate", "lint"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This ensures tasks run in the right order automatically.

4. Document with terraform-docs

Automate documentation generation:

{
  "docs": {
    "executor": "nx:run-commands",
    "options": {
      "command": "terraform-docs markdown table --output-file README.md .",
      "cwd": "modules/scw-vpc"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Run nx run-many -t docs to update all module docs at once.

5. Implement Pre-commit Hooks

Use Husky to run quick checks before commits:

npx husky install
npx husky add .husky/pre-commit "npx nx affected -t fmt-fix,validate"
Enter fullscreen mode Exit fullscreen mode

Limitations and Considerations

Nx is powerful, but it's not a silver bullet. Here are honest tradeoffs to consider:

Learning Curve

Your team needs to learn:

  • Nx concepts (projects, targets, affected commands)
  • Node.js/npm basics (even though you're not writing JavaScript)
  • How to write project.json files

Mitigation: Start with 3-5 modules, prove the value, then expand.

Node.js Dependency

You now need Node.js in your infrastructure workflows. Some teams find this odd.

Counter-argument: Node.js is ubiquitous, lightweight, and your CI already has it. The benefits far outweigh this concern.

State Management Unchanged

Nx doesn't help with:

  • Terraform state files
  • State locking
  • Remote backends
  • Workspace management

You still need proper Terraform state hygiene. Nx is purely about workspace organization and task orchestration.

Not Ideal for Small Projects

If you have 1-3 Terraform modules that rarely change, Nx is overkill. The overhead isn't worth it.

Use Nx when:

  • 5+ modules
  • Frequent changes
  • Multiple team members
  • Complex dependencies

Module Source References

Modules in the monorepo use relative paths:

source = "../scw-vpc"
Enter fullscreen mode Exit fullscreen mode

External consumers need a different approach:

  • Publish to a module registry
  • Use git tags with specific refs
  • Or keep internal modules internal

When NOT to Use This Approach

  • Single module projects: Just use plain Terraform
  • Completely independent modules: If modules never interact, separate repos might be simpler
  • Team resistant to change: Cultural fit matters more than technical benefits

Be pragmatic. Nx solves specific problems - make sure you have those problems first.

Conclusion

Managing Terraform modules at scale doesn't have to be painful. Nx brings proven monorepo practices from the frontend world to infrastructure code, offering:

  • Intelligent task orchestration - Run only what's affected
  • Dependency awareness - Understand module relationships automatically
  • Faster CI/CD - 80%+ time savings in real-world scenarios
  • Better DX - Consistent tooling, code generation, and clear workflows

The initial setup requires investment - learning Nx concepts, creating project.json files, updating CI workflows. But for teams managing 5+ Terraform modules with frequent changes, the ROI is substantial.

Start small: pick 3-5 modules, prove the concept, measure the impact. If it works for you (and it likely will), expand to your full infrastructure codebase.

The future of infrastructure management is smart, graph-aware tooling. Nx brings us closer to that future today.

Resources

Demo Repository

  • nx-terraform-demo - Complete working example with 7 Scaleway Terraform modules managed with Nx

Nx Documentation

Terraform Tooling

Scaleway Provider

Further Reading


Have you tried managing Terraform with Nx? I'd love to hear about your experience. Find me on Bluesky or GitHub!

Top comments (0)