Skip to main content
Every infrastructure stack needs to behave differently across environments — dev runs a small, cheap database with no backups; staging runs a medium one with some redundancy; prod runs a large, multi-AZ instance with prevent_destroy and a final snapshot. The naive approach is to duplicate the entire stack per environment. The Terraform approach is a maze of terraform.workspace conditionals and variable files. ubx’s answer is the extend block: a targeted, named override that changes specific attributes for a specific environment, leaving everything else inherited from the base. You write your resource once in main.iac, then write a small envs/prod/overrides.iac that overrides only what changes. The compiler merges them at apply time. Your base stays DRY, your environment differences are explicit, and there’s no branching or duplication.

What you’ll learn

  • How extend blocks override specific attributes of an existing resource
  • How environment override files are structured and loaded with --env
  • What the compiler merges and what it inherits

Why this matters

extend is targeted and named — it overrides a specific resource by type and label, not the whole file. If a main.iac has ten resources and only one needs a prod override, you write one extend block. The other nine are inherited unchanged.

The source code

# Base resource definition — applies in all environments.
unit "aws_rds_instance" "db" {
  identifier          = "myapp-${input.env}"
  allocated_storage   = 20
  engine              = "postgres"
  engine_version      = "15"
  instance_class      = "db.t3.micro"
  username            = "admin"
  password            = "placeholder"
  skip_final_snapshot = true

  tags = {
    Name = "myapp-${input.env}-db"
    Env  = input.env
  }
}

How it works

1

The merger loads the base and the override

When you run ubx plan --env prod, the compiler’s merger stage loads main.iac (the base) and envs/prod/overrides.iac (the override). It processes all .iac files in the envs/<env>/ directory alongside the base files.
2

extend merges attribute-by-attribute

The extend "unit" "aws_rds_instance" "db" block targets the base resource by type and label. Each attribute in the extend block overrides the corresponding attribute in the base. Attributes present in the base but absent from the extend are inherited unchanged — engine, username, tags, and identifier are untouched in prod.
3

The merged result is compiled normally

After merging, the compiler sees a single unified unit "aws_rds_instance" "db" block with the merged attributes. The type checker, IR pass, and emitter run on this merged view — there’s no env-specific branching in the generated code.

What ubx generates

// Generated by ubx — do not edit
// ubx plan --env prod
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const env = config.get("env") || "dev";

// Merged: base + envs/prod/overrides.iac
const db = new aws.rds.Instance("db", {
    identifier: `myapp-${env}`,
    allocatedStorage: 500,        // overridden
    engine: "postgres",           // inherited
    engineVersion: "15",          // inherited
    instanceClass: "db.r6g.xlarge", // overridden
    username: "admin",            // inherited
    password: "placeholder",      // inherited
    skipFinalSnapshot: false,     // overridden
    multiAz: true,                // overridden
    tags: { Name: `myapp-${env}-db`, Env: env }, // inherited
}, { retainOnDelete: true }); // lifecycle.prevent_destroy = true

export const dbEndpoint = db.endpoint;

Common mistakes

extend targets by exact type and label — extend "unit" "aws_rds_instance" "db" only matches a unit "aws_rds_instance" "db" block. If you rename the base resource’s label from "db" to "primary", the extend silently stops applying. ubx does not warn about unmatched extend blocks by default.
You can have multiple extend blocks in one override file — one per resource that needs environment-specific changes. Most resources don’t need overrides; only the ones that differ should appear in the env override file, keeping it small and readable.

Run it

ubx validate                    # base only
ubx validate --env prod         # base + prod overrides
ubx plan --env staging          # requires AWS credentials
ubx apply --env prod            # deploys prod config

What you learned

extend overrides specific attributes of a named resource without touching anything else
Environment override files live in envs/<env>/ and are loaded with --env
The compiler merges base + override before type-checking — the generated code contains the merged result

Next steps

Secret function

Pull sensitive values at apply time from any secrets backend

extend block reference

Full extend block syntax and merge rules