Skip to main content
Pulumi logo Pulumi logo
  1. Docs
  2. Infrastructure as Code
  3. Languages & SDKs
  4. HCL
  5. Reference

Pulumi HCL reference

    Pulumi programs can be defined in many languages, and the Pulumi HCL dialect offers an additional language for authoring Pulumi programs using Terraform’s HCL syntax. You get familiar HCL blocks, expressions, and functions while using Pulumi’s state management, secrets, and deployment engine.

    A Pulumi HCL program consists of one or more .tf files in a directory whose Pulumi.yaml specifies runtime: hcl:

    name: my-project
    runtime: hcl
    

    HCL files declare infrastructure using the top-level blocks listed below. The full set of HCL expressions and Terraform built-in functions is supported, along with Pulumi-specific asset and archive functions. See Terraform compatibility for the small number of differences.

    Top-level blocks

    BlockPurpose
    variableDeclare input variables
    resourceManage cloud resources
    dataRead external data via provider invocations
    providerConfigure provider instances
    outputExport values from the stack
    localsDefine reusable intermediate values
    moduleInvoke local or remote modules as components
    callInvoke methods on resources
    movedRename resources without recreation
    removedDestroy resources dropped from the program
    importImport existing cloud resources
    checkNon-blocking assertions about infrastructure
    terraformVersion constraints and component declarations

    In many locations within these blocks, values are HCL expressions that reference variables, locals, resources, data sources, or modules. See Expressions for the supported forms.

    Variables

    variable blocks declare input values for the program. Each block has one label, the variable name, which is referenced in expressions as var.<name>.

    variable "region" {
      type        = string
      default     = "us-west-2"
      description = "AWS region to deploy into"
    }
    
    variable "instance_count" {
      type    = number
      default = 1
    }
    
    variable "name" {
      type = string
    
      validation {
        condition     = length(var.name) > 0
        error_message = "Name must not be empty."
      }
    }
    
    AttributeTypeRequiredDescription
    typetype expressionNoType constraint (for example, string, number, bool, list(string), map(number), object({...})).
    defaultexpressionNoDefault value when the variable is not configured.
    descriptionstringNoHuman-readable description.
    sensitiveboolNoWhen true, the value becomes a Pulumi secret.
    ephemeralboolNoEphemeral value; see Ephemeral values.
    nullableboolNoWhen false, rejects null values. Defaults to true.
    validationblockNoOne or more validation rules (see below).

    Each validation block has the following attributes:

    AttributeTypeRequiredDescription
    conditionexpressionYesExpression that must evaluate to true.
    error_messageexpressionYesError message shown when the condition is false.

    A variable declared without a default is required.

    Setting variable values

    Variables are set from the following sources, in priority order:

    1. Stack config: pulumi config set <project>:<varName> <value> (highest priority), which takes the place of Terraform’s -var.
    2. Variable-value files in the program directory, applied in this order, with later files winning: terraform.tfvars, terraform.tfvars.json, then every *.auto.tfvars and *.auto.tfvars.json in lexical order by file name.
    3. Environment variables: TF_VAR_<name>=<value>. A variable set to the empty string is set, and still outranks the default.
    4. Default values in variable blocks (lowest priority).

    A variable-value file assigns values to variable names, and its values are literals — they may not refer to anything else in the program:

    # terraform.tfvars
    instance_type = "t3.micro"
    tags          = { Name = "web-server" }
    

    Only the root module loads these files. A file shipped inside a module is never read, and a value for a name the root module does not declare reaches nothing and is reported as a warning.

    Reference variables in expressions as var.<name>:

    resource "aws_instance" "web" {
      instance_type = var.instance_type
    }
    

    Resources

    resource blocks declare managed infrastructure. The first label is the resource type (Terraform-style, for example aws_instance); the second label is the logical name; the body contains the resource’s input properties.

    resource "aws_instance" "web" {
      ami           = "ami-12345678"
      instance_type = "t3.micro"
    
      tags = {
        Name = "web-server"
      }
    }
    

    Meta-arguments

    ArgumentTypeDescription
    countnumberCreate multiple instances indexed by count.index.
    for_eachmap or setCreate instances keyed by each.key with each.value.
    depends_onlistExplicit dependencies on other resources.
    providerreferenceSpecific provider configuration to use.
    providerslistExplicit provider configurations (modules and components only).

    Lifecycle block

    resource "aws_instance" "web" {
      # ...
    
      lifecycle {
        create_before_destroy = true
        prevent_destroy       = true
        ignore_changes        = [tags]
        replace_triggered_by  = [aws_instance.other]
      }
    }
    
    AttributeTypeDescription
    create_before_destroyboolWhen true, creates the replacement before destroying the old resource.
    prevent_destroyboolRefuse plans that would destroy this resource.
    ignore_changeslistProperty paths to exclude from diff detection.
    replace_triggered_bylistReplace this resource when any referenced resource or attribute changes.

    Timeouts block

    resource "aws_instance" "web" {
      # ...
    
      timeouts {
        create = "60m"
        update = "30m"
        delete = "2h"
      }
    }
    
    AttributeTypeDescription
    createstringTimeout for create operations.
    readstringTimeout for read operations.
    updatestringTimeout for update operations.
    deletestringTimeout for delete operations.

    Preconditions and postconditions

    Preconditions and postconditions are nested inside the lifecycle block:

    resource "aws_instance" "web" {
      # ...
    
      lifecycle {
        precondition {
          condition     = var.instance_type != ""
          error_message = "Instance type must be specified."
        }
    
        postcondition {
          condition     = self.public_ip != ""
          error_message = "Instance must have a public IP."
        }
      }
    }
    

    Provisioners

    Provisioners run commands during a resource’s lifecycle. They map to the Pulumi Command provider.

    resource "aws_instance" "web" {
      ami           = "ami-12345678"
      instance_type = "t3.micro"
    
      provisioner "local-exec" {
        command = "echo ${self.public_ip} >> hosts.txt"
      }
    
      provisioner "remote-exec" {
        inline = [
          "sudo apt-get update",
          "sudo apt-get install -y nginx",
        ]
      }
    
      provisioner "file" {
        source      = "config.txt"
        destination = "/tmp/config.txt"
      }
    
      connection {
        type        = "ssh"
        user        = "ubuntu"
        private_key = file("~/.ssh/id_rsa")
        host        = self.public_ip
      }
    }
    
    Provisioner typePulumi equivalent
    local-execcommand:local:Command
    remote-execcommand:remote:Command
    filecommand:remote:CopyToRemote

    Provisioner blocks accept the following options:

    AttributeValuesDescription
    when"create", "destroy"When the provisioner runs.
    on_failure"continue", "fail"Behavior on failure.

    connection blocks support SSH only; WinRM is not supported. The self reference inside a provisioner refers to the current resource.

    Dynamic blocks

    Dynamic blocks generate repeated nested blocks from a collection:

    resource "aws_security_group" "web" {
      name = "web-sg"
    
      dynamic "ingress" {
        for_each = var.ingress_rules
        content {
          from_port   = ingress.value.from_port
          to_port     = ingress.value.to_port
          protocol    = ingress.value.protocol
          cidr_blocks = ingress.value.cidr_blocks
        }
      }
    }
    

    The label on the dynamic block becomes the iterator variable name. Inside the content block, <label>.value refers to the current element and <label>.key refers to its key or index.

    Referencing resources

    Resources are referenced by <type>.<name>:

    output "instance_id" {
      value = aws_instance.web.id
    }
    

    When using count, instances are indexed: aws_instance.web[0].id, or aws_instance.web[*].id for the splat form. When using for_each, instances are keyed: aws_instance.web["key"].id.

    Resource options

    Pulumi-specific resource options live in a nested pulumi block. Keeping them in a nested block ensures they never collide with a resource’s own provider-specific attributes, which may themselves be named, for example, version or parent.

    resource "aws_instance" "web" {
      # ...
    
      pulumi {
        name                      = "web-primary"
        parent                    = module.my_component
        additional_secret_outputs = ["password"]
        protect                   = true
        retain_on_delete          = true
        deleted_with              = aws_vpc.main
        replace_on_changes        = ["ami"]
        replace_with              = [aws_instance.replacement]
        hide_diffs                = ["user_data"]
        import_id                 = "i-1234567890abcdef0"
        aliases                   = ["old-name"]
        version                   = "6.0.0"
        plugin_download_url       = "https://example.com/plugins"
      }
    }
    

    The pulumi block accepts exactly the following attributes:

    AttributeTypeDescription
    namestringOverride the Pulumi logical name.
    parentreferenceParent resource for component hierarchy.
    additional_secret_outputslist(string)Output properties to encrypt in state.
    protectboolMark the resource protected in state; deleting it requires unprotecting first.
    retain_on_deleteboolKeep the cloud resource when removed from the program.
    deleted_withreferenceCascade deletion when the referenced resource is deleted.
    replace_withlistResources whose replacement triggers replacement of this one.
    hide_diffslist(string)Property paths whose diffs should not be displayed.
    replace_on_changeslist(string)Property paths that force replacement when changed.
    import_idstringCloud resource ID to import.
    aliaseslistAlternative names for this resource (used during renames).
    env_var_mappingsexpressionEnvironment variable remappings for the provider.
    versionstringProvider plugin version.
    plugin_download_urlstringURL to download the provider plugin from.

    Terraform’s lifecycle arguments cover the remaining Pulumi options: create_before_destroy lowers onto deleteBeforeReplace (inverted), ignore_changes onto ignoreChanges, replace_triggered_by onto replacementTrigger, and timeouts onto customTimeouts. prevent_destroy is enforced natively rather than mapped to an option; see Feature mappings.

    Data sources accept a narrower pulumi block — only the options the invoke path honors: parent, version, and plugin_download_url. Module blocks accept name and protect; see Modules.

    Data sources

    data blocks read information from providers via invocations. They use the same type-naming convention as resources, and results are referenced as data.<type>.<name>.<attribute>.

    data "aws_ami" "ubuntu" {
      most_recent = true
      owners      = ["099720109477"]
    
      filter {
        name   = "name"
        values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
      }
    }
    
    output "ami_id" {
      value = data.aws_ami.ubuntu.id
    }
    

    Data sources support the same meta-arguments as resources: count, for_each, depends_on, and provider.

    Providers

    Providers supply the implementation for resources and data sources.

    Required providers

    Providers resolve the same way as OpenTofu. By default they are looked up in the OpenTofu registry, so an unqualified aws resolves to registry.opentofu.org/hashicorp/aws and is bridged into Pulumi automatically. Declaring required_providers is only necessary to pin a source or a version. Requirements go inside the terraform block:

    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = ">= 6.0"
        }
        random = {
          source  = "pulumi/random"
          version = "4.16.0"
        }
      }
    }
    

    A version-only shorthand is also supported. Assigning a string instead of an object sets the version constraint and assumes the source is the same as the name:

    terraform {
      required_providers {
        mycloud = "~> 1.0"
      }
    }
    

    Sources prefixed with pulumi/ consume a native Pulumi provider; any other source is bridged from its Terraform provider using pulumi-terraform-provider. Pulumi providers require an exact semver version rather than a version constraint. After changing the set of providers, run pulumi install.

    Provider configuration

    Configure providers with provider blocks:

    provider "aws" {
      region = "us-west-2"
    }
    

    Pulumi-specific provider options go in a nested pulumi block so they cannot collide with the provider’s own configuration attributes:

    provider "aws" {
      region = "us-west-2"
    
      pulumi {
        version             = "6.0.0"
        plugin_download_url = "https://example.com/plugins"
        env_var_mappings    = { AWS_REGION = "region" }
      }
    }
    
    OptionDescription
    versionProvider plugin version.
    plugin_download_urlURL to download the provider plugin from.
    env_var_mappingsEnvironment variable remappings for the provider.
    additional_secret_outputsProvider output properties to encrypt in state.

    Multiple provider configurations

    Use alias to create multiple configurations of the same provider, then select one on a resource with the provider meta-argument or on a module with providers:

    provider "aws" {
      region = "us-west-2"
    }
    
    provider "aws" {
      alias  = "east"
      region = "us-east-1"
    }
    
    resource "aws_instance" "web" {
      provider = aws.east
      # ...
    }
    

    A provider block also accepts for_each, which declares one configuration per element of a collection.

    Outputs

    output blocks export values from the stack for access via pulumi stack output.

    output "instance_ip" {
      value       = aws_instance.web.public_ip
      description = "Public IP of the web server"
    }
    
    output "db_password" {
      value     = random_password.db.result
      sensitive = true
    }
    
    output "vpc_id" {
      value      = aws_vpc.main.id
      depends_on = [aws_internet_gateway.gw]
    
      precondition {
        condition     = aws_vpc.main.id != ""
        error_message = "VPC must have an ID."
      }
    }
    
    AttributeTypeRequiredDescription
    valueexpressionYesThe value to export.
    descriptionstringNoHuman-readable description.
    sensitiveboolNoWhen true, the output becomes a Pulumi secret.
    ephemeralboolNoEphemeral value; see Ephemeral values.
    depends_onlistNoExplicit dependencies.
    preconditionblockNoValidation checks before export.

    Locals

    locals blocks define reusable intermediate values. Multiple locals blocks are allowed in a program; reference locals as local.<name>.

    locals {
      common_tags = {
        Environment = "dev"
        Project     = "my-project"
      }
    
      name_prefix = "myapp-${var.environment}"
    
      user_data = <<-EOF
        #!/bin/bash
        echo "Hello, World!" > index.html
        nohup python3 -m http.server 80 &
      EOF
    }
    
    resource "aws_instance" "web" {
      tags      = local.common_tags
      user_data = local.user_data
    }
    

    Modules

    module blocks invoke reusable configurations as Pulumi component resources. Module outputs are referenced as module.<name>.<output_name>.

    module "vpc" {
      source     = "./modules/vpc"
      cidr_block = "10.0.0.0/16"
    }
    
    output "vpc_id" {
      value = module.vpc.vpc_id
    }
    

    Module sources

    Source typeExample
    Local path./modules/vpc
    Gitgit::https://github.com/org/repo.git?ref=v1.0.0
    Git with subdirectorygit::https://github.com/org/repo.git//modules/vpc?ref=v1.0.0
    GitHub shorthandgithub.com/org/repo
    Bitbucket shorthandbitbucket.org/org/repo
    Terraform Registryterraform-aws-modules/vpc/aws
    HTTP archivehttps://example.com/module.zip

    Remote modules are cached in ~/.pulumi/modules/.

    Module meta-arguments

    ArgumentTypeDescription
    sourcestringModule source (required).
    versionstringVersion constraint (for registry modules).
    countnumberCreate multiple module instances.
    for_eachmap or setCreate keyed module instances.
    depends_onlistExplicit dependencies.
    providersmapProvider configuration mappings for the module.

    Like resources, a module block accepts a nested pulumi block. Its name attribute overrides the Pulumi logical name of each module instance, evaluated per instance with count.index or each.key in scope. The overridden name also prefixes the derived names of everything inside the instance, and is visible to the module source as pulumi.module.name. Its protect attribute marks each instance’s component resource protected in state.

    module "vpc" {
      source = "./modules/vpc"
    
      pulumi {
        name = "vpc-primary"
      }
    }
    

    Call blocks

    call blocks invoke methods on existing resources. This is a Pulumi-specific extension with no Terraform equivalent, used to call resource methods.

    resource "call_custom" "my_resource" {
      value = "hello"
    }
    
    call "my_resource" "provider_value" {
    }
    
    output "result" {
      value = call.my_resource.provider_value.result
    }
    

    The first label is the logical name of a declared resource. The second label is the method name. The body contains arguments to the method. Results are referenced as call.<resource_name>.<method_name>.<attribute>.

    Moved, removed, and import blocks

    Moved blocks

    moved blocks rename resources without recreating them. They map to Pulumi’s aliases resource option.

    moved {
      from = aws_instance.old_name
      to   = aws_instance.new_name
    }
    
    AttributeTypeRequiredDescription
    fromreferenceYesOriginal resource address.
    toreferenceYesNew resource address.

    Removed blocks

    removed blocks declare that the resource or module at from has been deleted from the configuration and its remote objects should be destroyed. The address carries no instance keys — a removed block applies to every instance.

    removed {
      from = aws_instance.example
    
      lifecycle {
        destroy = true
      }
    }
    
    AttributeTypeRequiredDescription
    fromreferenceYesAddress of the removed resource or module.
    lifecycleblockYesMust set destroy = true.
    provisionerblockNoDestroy-time provisioners to run as the orphaned instances are deleted. Only when = "destroy" provisioners are allowed.

    Only destroy = true is supported. A resource absent from the program is destroyed by the Pulumi engine on its own, while destroy = false — Terraform’s “forget” behavior — has no engine mapping and is reported as an error.

    Import blocks

    import blocks import existing cloud resources into Pulumi state.

    import {
      to       = aws_instance.web
      id       = "i-1234567890abcdef0"
      provider = aws.east
    }
    
    AttributeTypeRequiredDescription
    toreferenceYesTarget resource address.
    idstringYesCloud resource ID to import.
    providerreferenceNoProvider configuration to use.
    for_eachmap or setNoImport one resource per element of a collection.

    To bring in a whole existing deployment at once, use pulumi import --from hcl with a Terraform or OpenTofu state file instead.

    Check blocks

    check blocks make non-blocking assertions about your infrastructure. Unlike a resource precondition or postcondition, a failed assert reports a warning and the operation continues rather than aborting.

    check "health" {
      data "http" "status" {
        url = "https://example.com"
      }
    
      assert {
        condition     = data.http.status.status_code == 200
        error_message = "${data.http.status.url} returned an unhealthy status."
      }
    }
    

    Each check block has a name (its label) and one or more assert blocks:

    AttributeTypeRequiredDescription
    conditionexpressionYesExpression that must evaluate to true.
    error_messageexpressionYesWarning shown when the condition is false.

    A check may also declare a single nested data source — a scoped data source — read fresh on every operation and visible only to that check’s assertions.

    Override files

    A file named override.tf, or any file whose name ends in _override.tf, is an override file. Its blocks amend blocks of the same address declared in the directory’s other files instead of declaring new ones.

    # main.tf
    resource "aws_instance" "web" {
      ami           = "ami-12345678"
      instance_type = "t3.micro"
    }
    
    # dev_override.tf
    resource "aws_instance" "web" {
      instance_type = "t3.large"
    }
    

    locals and terraform blocks — neither of which is addressable by a label — merge after the primary blocks are parsed. moved, import, and removed blocks may appear only in normal files; overriding them is an error.

    Escaping meta-arguments

    The special block type _ forces arguments to be interpreted as ordinary configuration rather than as meta-arguments. Use it when a provider defines an attribute whose name collides with a meta-argument such as count, for_each, provider, or depends_on. Each resource, data, module, provider, and provisioner block may contain at most one escaping block.

    resource "example_thing" "a" {
      _ {
        count = 3 # the provider's own "count" attribute, not the meta-argument
      }
    }
    

    Expressions

    Pulumi HCL supports the full HCL expression language.

    Literals

    "hello"           # string
    42                # number
    3.14              # number
    true              # bool
    null              # null
    ["a", "b", "c"]   # list
    {key = "value"}   # map
    

    String interpolation

    "Hello, ${var.name}!"
    "prefix-${local.env}-suffix"
    

    Heredocs

    <<-EOF
      multi-line
      string content
    EOF
    

    References

    ReferenceDescription
    var.<name>Input variable.
    local.<name>Local value.
    <type>.<name>Resource attribute.
    <type>.<name>[<index>]Counted resource instance.
    <type>.<name>["<key>"]For-each resource instance.
    data.<type>.<name>Data source attribute.
    module.<name>Module output.
    call.<res>.<method>Call block result.
    selfCurrent resource (in provisioners).
    count.indexCurrent count iteration index.
    each.keyCurrent for_each key.
    each.valueCurrent for_each value.
    path.modulePath to the current module.
    path.rootPath to the root module.
    path.cwdCurrent working directory.
    pulumi.stackCurrent stack name.
    pulumi.projectCurrent project name.
    pulumi.organizationCurrent organization name.
    pulumi.module.namePulumi logical name of the enclosing module instance (null at the root).

    Operators

    CategoryOperators
    Arithmetic+, -, *, /, %
    Comparison==, !=, <, <=, >, >=
    Logical&&, ||, !

    Conditional expression

    condition ? true_value : false_value
    

    For expressions

    # List comprehension
    [for name in var.names : upper(name)]
    
    # With index
    [for i, name in var.names : "${i}-${name}"]
    
    # Map comprehension
    {for k, v in var.tags : k => upper(v)}
    
    # With filter
    [for name in var.names : name if name != ""]
    

    Splat expressions

    # Equivalent to [for r in aws_instance.web : r.id]
    aws_instance.web[*].id
    

    Property access

    resource.name.property
    resource.name["key"]
    resource.name[0]
    

    try and can

    try(var.optional.nested.value, "default")
    can(var.optional.nested.value)  # returns true or false
    

    Built-in functions

    Pulumi HCL supports nearly all Terraform built-in functions, grouped by category below.

    CategoryFunctions
    Numericabs, ceil, floor, log, max, min, pow, signum, parseint
    Stringchomp, endswith, format, formatlist, indent, join, lower, regex, regexall, replace, split, startswith, strcontains, strrev, substr, title, trim, trimprefix, trimsuffix, trimspace, upper
    Collectionalltrue, anytrue, chunklist, coalesce, coalescelist, compact, concat, contains, distinct, element, entries, flatten, index, keys, length, list, lookup, map, matchkeys, merge, one, range, reverse, setintersection, setproduct, setsubtract, setunion, slice, sort, sum, transpose, values, zipmap
    Encodingbase64decode, base64encode, base64gzip, base64gunzip, csvdecode, jsondecode, jsonencode, textdecodebase64, textencodebase64, urlencode, urldecode, yamldecode, yamlencode
    Filesystemabspath, basename, dirname, file, filebase64, fileexists, fileset, pathexpand, templatefile, templatestring
    Date and timeformatdate, plantimestamp, timeadd, timecmp, timestamp
    Hash and cryptobase64sha256, base64sha512, bcrypt, filebase64sha256, filebase64sha512, filemd5, filesha1, filesha256, filesha512, md5, rsadecrypt, sha1, sha256, sha512, uuid, uuidv5
    IP networkcidrcontains, cidrhost, cidrnetmask, cidrsubnet, cidrsubnets
    Type conversioncan, ephemeralasnull, issensitive, nonsensitive, recover, sensitive, tobool, tolist, tomap, tonumber, toset, tostring, try, type

    Pulumi has no plan/apply split, so plantimestamp resolves to the current time exactly as timestamp does.

    The provider::terraform:: namespace provides decode_tfvars, encode_expr, and encode_tfvars.

    Pulumi-specific functions

    These functions have no Terraform equivalent:

    FunctionDescription
    fileAsset(path)Create a Pulumi FileAsset from a local file path.
    stringAsset(text)Create a Pulumi StringAsset from a string value.
    remoteAsset(uri)Create a Pulumi RemoteAsset from a URL.
    fileArchive(path)Create a Pulumi FileArchive from a local path.
    remoteArchive(uri)Create a Pulumi RemoteArchive from a URL.
    assetArchive(map)Create a Pulumi AssetArchive from a map of assets or archives.
    pulumiResourceName(resource)Get the logical name from a resource’s URN.
    pulumiResourceType(resource)Get the type token from a resource’s URN.
    entries(map)Convert a map or object to a list of {key, value} objects.
    recover(value, recovery)Return value, or evaluate recovery if value fails to evaluate.

    Functions not supported

    terraform.applying is Terraform-internal — Pulumi has no plan/apply split for it to report on — and has no equivalent.

    Stack references

    Access outputs from other Pulumi stacks using the pulumi_stack_reference resource:

    resource "pulumi_stack_reference" "network" {
      name = "myorg/networking/prod"
    }
    
    output "vpc_id" {
      value = pulumi_stack_reference.network.outputs["vpc_id"]
    }
    

    Terraform built-in resources

    Terraform’s two built-in types are supported:

    • terraform_data — a managed resource with no cloud counterpart, used to store values and to attach provisioners or triggers_replace to. It lowers onto the Pulumi engine’s built-in stash resource.
    • terraform_remote_state — a data source that reads another Terraform or OpenTofu state file, served by the external terraform provider.

    Terraform block

    The top-level terraform block holds required providers and version constraints, exactly as it does in Terraform and OpenTofu. In Pulumi HCL it additionally holds component declarations, a Pulumi-specific extension.

    Version constraints

    Pulumi HCL uses required_version_range to declare a supported Pulumi version range. Terraform’s required_version is accepted but ignored with a warning.

    terraform {
      required_version_range = ">= 3.0.0"
    }
    

    Multi-language components

    The component block, optionally with a package block, declares an HCL module as a reusable Pulumi component consumable from any Pulumi language. See the Pulumi HCL component reference for details.

    terraform {
      component {
        name   = "VpcNetwork"
        module = "index"
      }
      package {
        name    = "my-networking"
        version = "1.0.0"
      }
    }
    

    Terraform compatibility

    Pulumi HCL aims to run valid Terraform configurations without changes — the same .tf files, the same terraform block, and the same provider sources. This section covers the behavioral differences and the few unsupported features. If you find a case where tofu works and pulumi does not, please open an issue.

    Behavioral differences

    Sensitive values. Variables and outputs marked sensitive = true become Pulumi secrets, encrypted at rest in state.

    Property names. HCL uses snake_case. The plugin automatically converts to Pulumi’s camelCase for the engine. Map keys are not translated.

    Ephemeral values. See Ephemeral values below.

    Ephemeral values

    Terraform’s ephemeral values exist to keep a value out of state and plan files entirely. Pulumi’s state model already encrypts secrets at rest, so Pulumi HCL interprets ephemeral = true through that lens instead of reproducing Terraform’s persistence rules:

    • Persisted as secrets. A value from an ephemeral variable or output is stored in state, but encrypted, exactly like sensitive = true. It is masked in CLI output and in the Pulumi Console.
    • Diffs are hidden. An ephemeral value is free to differ on every run, so the resource property it flows into is registered with its diff hidden (the hideDiffs resource option), down to the exact property path — an ephemeral value inside one block of a repeated block hides only that block’s attribute.
    • Not sensitive. Ephemerality and sensitivity are tracked separately: issensitive returns false for a purely ephemeral value, matching Terraform.
    • ephemeralasnull is supported and behaves as in Terraform: it replaces the ephemeral parts of a value with typed nulls, preserving sensitivity marks, making the result usable anywhere.

    Differences from Terraform to be aware of:

    • Terraform rejects an ephemeral value used in a non-ephemeral context, such as a regular resource argument or a non-ephemeral output. Pulumi HCL accepts it and persists the value encrypted; there is no flow validation.
    • Terraform re-prompts for a required ephemeral variable on every run. Pulumi HCL reads it from stack configuration like any other variable, so set it once with pulumi config set --secret.
    • A root-level ephemeral output becomes a secret stack output rather than being omitted.

    Feature mappings

    Terraform featurePulumi equivalentNotes
    prevent_destroy(native)Enforced by Pulumi HCL itself, re-evaluated each run; the guard lifts when the argument is removed from the program.
    ignore_changesignoreChangesSame behavior.
    create_before_destroydeleteBeforeReplaceInverted; defaults to Terraform’s delete-first order.
    replace_triggered_byreplacementTriggerReplaces when a referenced value changes.
    moved blocksaliasesRenames without recreation.
    import blocksImport resource optionImports existing resources.
    timeoutscustomTimeoutsSame duration format.
    ModulesComponent resourcesAll source types supported.
    ProvisionersCommand providerlocal-exec, remote-exec, file.

    prevent_destroy is separate from Pulumi’s protect option, which is set explicitly in the pulumi block and recorded in state.

    State migration

    Pulumi cannot read a Terraform state file as its own, but you do not have to import resources one at a time. Point the converter at an existing Terraform or OpenTofu state file to import everything it describes in bulk:

    pulumi import --from hcl terraform.tfstate
    

    This reads the state file, maps each Terraform resource type to its Pulumi token, and imports the resources into your stack.

    Unsupported features

    • backend, required_version, provider_meta, and experiments — accepted inside the terraform block but ignored with a warning. Pulumi manages state independently and tracks its own version constraints via required_version_range, and language experiments have no Pulumi HCL equivalent.
    • cloud — not accepted inside the terraform block at all. It is absent from the block’s schema, so a cloud block is a parse error rather than a warning. Remove it; Pulumi’s own backend configuration lives outside the program.
    • WinRM connectionsconnection blocks support type = "ssh" only.
    • List<Object> empty versus null — HCL block syntax cannot distinguish an empty List<Object> from a null one, a known incompatibility with some Pulumi programs.
    • Resource-wide destroy ordering of late-created instances — Terraform rebuilds destroy-time dependencies from configuration, so every instance of a count or for_each resource waits for a consumer’s delete even when the consumer referenced only one instance (depends_on = [a["x"]]). Pulumi records each resource’s dependencies once, when it is created, and cannot depend on an instance that registers later. A sibling instance created after the consumer is therefore not held back by it during destroy and may be deleted first.
    • ignore_changes on terraform_data’s triggers_replace — honored only when it is present from the resource’s creation. Adding ignore_changes = [triggers_replace] in the same update that changes triggers_replace, on a resource first created without it, still forces one replacement: triggers_replace is carried as a replacement trigger rather than a stored input, so it cannot be reconciled against the prior state the way an ignored input is.

    CLI equivalents

    TerraformPulumi
    terraform planpulumi preview
    terraform applypulumi up
    terraform destroypulumi destroy
    terraform statepulumi state
    terraform importpulumi import
    WorkspacesStacks

      The infrastructure as code platform for any cloud.