{"id":2129,"date":"2026-02-21T00:40:45","date_gmt":"2026-02-20T11:40:45","guid":{"rendered":"https:\/\/www.ronella.xyz\/?p=2129"},"modified":"2026-02-21T00:40:45","modified_gmt":"2026-02-20T11:40:45","slug":"terraform-block-types","status":"publish","type":"post","link":"https:\/\/www.ronella.xyz\/?p=2129","title":{"rendered":"Terraform Block Types"},"content":{"rendered":"<p>Terraform configurations are built out of <em>blocks<\/em>. Understanding block types is critical because they define how you declare infrastructure, wire modules together, and control Terraform\u2019s behavior.<\/p>\n<hr \/>\n<h2>1. The Anatomy of a Block<\/h2>\n<p>Every Terraform block has the same basic shape:<\/p>\n<pre><code class=\"language-terraform\">TYPE &quot;label1&quot; &quot;label2&quot; {\n  argument_name = expression\n\n  nested_block_type {\n    # ...\n  }\n}<\/code><\/pre>\n<p>Key parts:<\/p>\n<ul>\n<li><strong>Type<\/strong>: The keyword at the start (<code>resource<\/code>, <code>provider<\/code>, <code>variable<\/code>, etc.). This tells Terraform what kind of thing you are defining.<\/li>\n<li>Labels: Extra identifiers whose meaning depends on the block type.\n<ul>\n<li>Example: <code>resource &quot;aws_instance&quot; &quot;web&quot;<\/code><\/li>\n<li>Type: <code>resource<\/code><\/li>\n<li>Labels: <code>&quot;aws_instance&quot;<\/code> (resource type), <code>&quot;web&quot;<\/code> (local name)<\/li>\n<\/ul>\n<\/li>\n<li>Body: The <code>{ ... }<\/code> section, which can contain:\n<ul>\n<li>Arguments: <code>name = expression<\/code><\/li>\n<li>Nested blocks: <code>block_type { ... }<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><strong>Rationale:<\/strong> The consistent shape makes the language predictable. Block type + labels define <em>what<\/em> the block is; the body defines <em>how<\/em> it behaves or is configured.<\/p>\n<hr \/>\n<h2>2. Core Top-Level Block Types<\/h2>\n<p>These blocks usually appear at the top level of your <code>.tf<\/code> files and together they define a module: its inputs, logic, and outputs.<\/p>\n<h2>2.1 <code>terraform<\/code> block<\/h2>\n<p>Configures Terraform itself:<\/p>\n<ul>\n<li>Required providers and their versions.<\/li>\n<li>Required Terraform version.<\/li>\n<li>Backend configuration (usually via a nested <code>backend<\/code> block in <code>terraform<\/code>).<\/li>\n<\/ul>\n<p>Example:<\/p>\n<pre><code class=\"language-terraform\">terraform {\n  required_version = &quot;&gt;= 1.6.0&quot;\n\n  required_providers {\n    aws = {\n      source  = &quot;hashicorp\/aws&quot;\n      version = &quot;~&gt; 5.0&quot;\n    }\n  }\n}<\/code><\/pre>\n<p><strong>Rationale:<\/strong> Keeps tooling constraints explicit and version-pinned, so behavior is deterministic across environments and team members.<\/p>\n<hr \/>\n<h2>2.2 <code>provider<\/code> block<\/h2>\n<p>Configures how Terraform talks to an external API (AWS, Azure, GCP, Kubernetes, etc.):<\/p>\n<pre><code class=\"language-terraform\">provider &quot;aws&quot; {\n  region = var.aws_region\n}<\/code><\/pre>\n<p>Typical aspects:<\/p>\n<ul>\n<li>Credentials and regions.<\/li>\n<li>Aliases for multiple configurations (e.g., <code>provider &quot;aws&quot; { alias = &quot;eu&quot; ... }<\/code>).<\/li>\n<\/ul>\n<p><strong>Rationale:<\/strong> Providers are the \u201cdrivers\u201d Terraform uses to translate configuration into real infrastructure; separating them lets you re-use the same module with different provider settings.<\/p>\n<hr \/>\n<h2>2.3 <code>resource<\/code> block<\/h2>\n<p>Declares infrastructure objects Terraform will create and manage.<\/p>\n<pre><code class=\"language-terraform\">resource &quot;aws_s3_bucket&quot; &quot;this&quot; {\n  bucket = &quot;${local.name}-bucket&quot;\n}<\/code><\/pre>\n<p>Structure:<\/p>\n<ul>\n<li>Type label: the provider-specific resource type (<code>&quot;aws_s3_bucket&quot;<\/code>).<\/li>\n<li>Name label: a local identifier (<code>&quot;this&quot;<\/code>, <code>&quot;web&quot;<\/code>, <code>&quot;db&quot;<\/code>, etc.).<\/li>\n<li>Body: arguments and nested blocks that define the resource\u2019s configuration.<\/li>\n<\/ul>\n<p><strong>Rationale:<\/strong> The <code>resource<\/code> block is the heart of Terraform; it expresses desired state. Every apply tries to reconcile actual infrastructure with what these blocks declare.<\/p>\n<hr \/>\n<h2>2.4 <code>data<\/code> block<\/h2>\n<p>Reads information about <strong>existing<\/strong> objects without creating anything.<\/p>\n<pre><code class=\"language-terraform\">data &quot;aws_ami&quot; &quot;latest_amazon_linux&quot; {\n  most_recent = true\n\n  filter {\n    name   = &quot;name&quot;\n    values = [&quot;amazon-linux-2-*&quot;]\n  }\n\n  owners = [&quot;amazon&quot;]\n}<\/code><\/pre>\n<p>You reference it as <code>data.aws_ami.latest_amazon_linux.id<\/code>.<\/p>\n<p><strong>Rationale:<\/strong> Data sources decouple \u201clookup\u201d from \u201ccreation\u201d. You avoid hardcoding IDs\/ARNs and can dynamically discover things like AMIs, VPC IDs, or roles.<\/p>\n<hr \/>\n<h2>2.5 <code>variable<\/code> block<\/h2>\n<p>Defines inputs to a module:<\/p>\n<pre><code class=\"language-terraform\">variable &quot;aws_region&quot; {\n  type        = string\n  description = &quot;AWS region to deploy into&quot;\n  default     = &quot;us-west-2&quot;\n}<\/code><\/pre>\n<p>Key fields:<\/p>\n<ul>\n<li><code>type<\/code>: basic or complex types (string, number, list, map, object, etc.).<\/li>\n<li><code>default<\/code>: makes a variable optional.<\/li>\n<li><code>description<\/code>: documentation for humans.<\/li>\n<\/ul>\n<p><strong>Rationale:<\/strong> Explicit inputs make modules reusable, testable, and self-documenting. They are your module\u2019s API.<\/p>\n<hr \/>\n<h2>2.6 <code>output<\/code> block<\/h2>\n<p>Exposes values from a module:<\/p>\n<pre><code class=\"language-terraform\">output &quot;bucket_name&quot; {\n  value       = aws_s3_bucket.this.bucket\n  description = &quot;Name of the S3 bucket created by this module.&quot;\n}<\/code><\/pre>\n<p><strong>Rationale:<\/strong> Outputs are your module\u2019s return values, allowing composition: root modules can print values, and child modules can feed outputs into other modules or systems (e.g., CI\/CD).<\/p>\n<hr \/>\n<h2>2.7 <code>locals<\/code> block<\/h2>\n<p>Defines computed values for use within a module:<\/p>\n<pre><code class=\"language-terraform\">locals {\n  name_prefix = &quot;demo&quot;\n  bucket_name = &quot;${local.name_prefix}-bucket&quot;\n}<\/code><\/pre>\n<p>Notes:<\/p>\n<ul>\n<li>You can have multiple <code>locals<\/code> blocks; Terraform merges them.<\/li>\n<li>Access them via <code>local.&lt;name&gt;<\/code>.<\/li>\n<\/ul>\n<p><strong>Rationale:<\/strong> Locals centralize derived values and remove duplication. That keeps your configuration DRY and easier to refactor.<\/p>\n<hr \/>\n<h2>3. Nested Blocks vs Arguments<\/h2>\n<p>Within a block body you use two constructs:<\/p>\n<ul>\n<li>\n<p><strong>Arguments<\/strong>: <code>key = expression<\/code><br \/>\nExample: <code>bucket = &quot;demo-bucket&quot;<\/code>.<\/p>\n<\/li>\n<li>\n<p><strong>Nested blocks<\/strong>: <code>block_type { ... }<\/code><br \/>\nExample:<\/p>\n<pre><code class=\"language-terraform\">resource \"aws_instance\" \"web\" {\nami           = data.aws_ami.latest_amazon_linux.id\ninstance_type = \"t3.micro\"\n\nnetwork_interface {\n  device_index = 0\n  network_interface_id = aws_network_interface.web.id\n}\n}<\/code><\/pre>\n<\/li>\n<\/ul>\n<p>Why have both?<\/p>\n<ul>\n<li>Arguments are single values; they are the usual \u201csettings\u201d.<\/li>\n<li>Nested blocks model structured, often repeatable configuration sections (e.g., <code>ingress<\/code> rules in security groups, <code>network_interface<\/code>, <code>lifecycle<\/code>, <code>tag<\/code> blocks in some providers).<\/li>\n<\/ul>\n<p><strong>Rationale:<\/strong> Using nested blocks for structured\/repeated sections keeps complex resources readable and makes it clear which values logically belong together.<\/p>\n<hr \/>\n<h2>4. Meta-Arguments and Lifecycle Blocks<\/h2>\n<p>Some names inside a resource are <strong>meta-arguments<\/strong> understood by Terraform itself rather than by the provider:<\/p>\n<p>Common meta-arguments:<\/p>\n<ul>\n<li><code>depends_on<\/code>: Add explicit dependencies when Terraform\u2019s graph inference isn\u2019t enough.<\/li>\n<li><code>count<\/code>: Create multiple instances of a resource using integer indexing.<\/li>\n<li><code>for_each<\/code>: Create multiple instances keyed by a map or set.<\/li>\n<li><code>provider<\/code>: Pin a resource to a specific provider configuration (e.g., <code>aws.eu<\/code>).<\/li>\n<li><code>lifecycle<\/code>: Special nested block that controls create\/update\/destroy behavior.<\/li>\n<\/ul>\n<p>Example <code>lifecycle<\/code>:<\/p>\n<pre><code class=\"language-terraform\">resource &quot;aws_s3_bucket&quot; &quot;this&quot; {\n  bucket = &quot;${local.name}-bucket&quot;\n\n  lifecycle {\n    prevent_destroy       = true\n    ignore_changes        = [tags]\n    create_before_destroy = true\n  }\n}<\/code><\/pre>\n<p><strong>Rationale:<\/strong> Meta-arguments give you control over resource <em>orchestration<\/em> rather than definition. They let you express cardinality, ordering, and safety rules without resorting to hacks or external tooling.<\/p>\n<hr \/>\n<h2>5. Putting It All Together<\/h2>\n<p>Below is a small but coherent configuration that demonstrates the main block types and how they interact. You can drop this into an empty directory as <code>main.tf<\/code>.<\/p>\n<pre><code class=\"language-terraform\">terraform {\n  required_version = &quot;&gt;= 1.6.0&quot;\n\n  required_providers {\n    aws = {\n      source  = &quot;hashicorp\/aws&quot;\n      version = &quot;~&gt; 5.0&quot;\n    }\n  }\n}\n\nvariable &quot;aws_region&quot; {\n  type        = string\n  description = &quot;AWS region to deploy into (used by LocalStack as well)&quot;\n  default     = &quot;us-east-1&quot;\n}\n\nlocals {\n  project = &quot;block-types-localstack-demo&quot;\n  bucket  = &quot;${local.project}-bucket&quot;\n}\n\nprovider &quot;aws&quot; {\n  region  = var.aws_region\n\n  # Dummy credentials \u2013 LocalStack doesn\u2019t actually validate them.\n  access_key = &quot;test&quot;\n  secret_key = &quot;test&quot;\n\n  # Talk to LocalStack instead of AWS.\n  s3_use_path_style           = true\n  skip_credentials_validation = true\n  skip_metadata_api_check     = true\n  skip_requesting_account_id  = true\n\n  endpoints {\n    s3 = &quot;http:\/\/localhost:4566&quot;\n    sts = &quot;http:\/\/localhost:4566&quot;\n  }\n}\n\n# This data source will return the LocalStack \u201ctest\u201d account (000000000000).\ndata &quot;aws_caller_identity&quot; &quot;current&quot; {}\n\nresource &quot;aws_s3_bucket&quot; &quot;this&quot; {\n  bucket = local.bucket\n\n  tags = {\n    Project = local.project\n    Owner   = data.aws_caller_identity.current.account_id\n  }\n\n  lifecycle {\n    prevent_destroy = true\n  }\n}\n\noutput &quot;bucket_name&quot; {\n  value       = aws_s3_bucket.this.bucket\n  description = &quot;The name of the created S3 bucket.&quot;\n}\n\noutput &quot;account_id&quot; {\n  value       = data.aws_caller_identity.current.account_id\n  description = &quot;AWS (LocalStack) account ID used for this deployment.&quot;\n}<\/code><\/pre>\n<h2>What this example shows<\/h2>\n<ul>\n<li><code>terraform<\/code> block: pins Terraform and the AWS provider.<\/li>\n<li><code>variable<\/code>: input for region.<\/li>\n<li><code>locals<\/code>: internal naming logic.<\/li>\n<li><code>provider<\/code>: AWS configuration.<\/li>\n<li><code>data<\/code>: a data source reading your current AWS identity.<\/li>\n<li><code>resource<\/code>: S3 bucket, including nested <code>lifecycle<\/code> and tags.<\/li>\n<li><code>output<\/code>: exposes bucket name and account ID.<\/li>\n<\/ul>\n<h2>How to Run and Validate with LocalStack<\/h2>\n<ol>\n<li>\n<p><strong>Start LocalStack<\/strong> (for example, via Docker):<\/p>\n<pre><code class=\"language-bash\">docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack\/localstack<\/code><\/pre>\n<p>This exposes the LocalStack APIs on <code>http:\/\/localhost:4566<\/code> as expected by the provider config.<\/p>\n<\/li>\n<li>\n<p><strong>Initialize Terraform<\/strong>:<\/p>\n<pre><code class=\"language-bash\">terraform init<\/code><\/pre>\n<\/li>\n<li>\n<p><strong>Format and validate<\/strong>:<\/p>\n<pre><code class=\"language-bash\">terraform fmt -check\nterraform validate<\/code><\/pre>\n<\/li>\n<li>\n<p><strong>Plan and apply against LocalStack<\/strong>:<\/p>\n<pre><code class=\"language-bash\">terraform plan\nterraform apply<\/code><\/pre>\n<p>Confirm with <code>yes<\/code> when prompted. Terraform will create the S3 bucket in LocalStack rather than AWS; the dummy credentials and endpoint mapping make this safe for local experimentation.<\/p>\n<\/li>\n<li>\n<p><strong>Check outputs<\/strong>:<\/p>\n<pre><code>terraform output\nterraform output bucket_name\nterraform output account_id<\/code><\/pre>\n<\/li>\n<li>\n<p>Configure profile<\/p>\n<pre><code class=\"language-bash\">aws configure --profile localstack<\/code><\/pre>\n<\/li>\n<li>\n<p><strong>Verify in LocalStack<\/strong> (using AWS CLI configured to point to LocalStack):<\/p>\n<pre><code class=\"language-bash\">aws --endpoint-url http:\/\/localhost:4566 s3 ls --profile localstack<\/code><\/pre>\n<p>You should see the bucket named in <code>bucket_name<\/code>. LocalStack typically uses <code>test<\/code> credentials and a default account ID of <code>000000000000<\/code>.<\/p>\n<\/li>\n<li>\n<p><strong>Destroy<\/strong> (noting <code>prevent_destroy<\/code>)<\/p>\n<p>Because of <code>prevent_destroy = true<\/code>, <code>terraform destroy<\/code> will refuse to delete the bucket. That\u2019s intentional, to illustrate the <code>lifecycle<\/code> block. Remove <code>prevent_destroy<\/code>, run <code>terraform apply<\/code> again, then:<\/p>\n<pre><code class=\"language-bash\">terraform destroy<\/code><\/pre>\n<\/li>\n<\/ol>\n<hr \/>\n<h2>6. A Quick Comparison Table<\/h2>\n<p>To solidify the concepts, here is a concise comparison of key block types:<\/p>\n<table>\n<thead>\n<tr>\n<th>Block type<\/th>\n<th>Purpose<\/th>\n<th>Typical labels<\/th>\n<th>Commonly uses nested blocks<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>terraform<\/code><\/td>\n<td>Configure Terraform itself<\/td>\n<td>None<\/td>\n<td><code>required_providers<\/code>, <code>backend<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>provider<\/code><\/td>\n<td>Configure connection to an API<\/td>\n<td>Provider name (e.g., <code>&quot;aws&quot;<\/code>)<\/td>\n<td>Occasionally provider-specific blocks<\/td>\n<\/tr>\n<tr>\n<td><code>resource<\/code><\/td>\n<td>Declare managed infrastructure<\/td>\n<td>Resource type, local name<\/td>\n<td><code>lifecycle<\/code>, <code>provisioner<\/code>, provider-specific<\/td>\n<\/tr>\n<tr>\n<td><code>data<\/code><\/td>\n<td>Read existing infrastructure<\/td>\n<td>Data source type, local name<\/td>\n<td>Provider-specific nested blocks<\/td>\n<\/tr>\n<tr>\n<td><code>variable<\/code><\/td>\n<td>Define module inputs<\/td>\n<td>Variable name<\/td>\n<td>None (just arguments)<\/td>\n<\/tr>\n<tr>\n<td><code>output<\/code><\/td>\n<td>Expose module outputs<\/td>\n<td>Output name<\/td>\n<td>None (just arguments)<\/td>\n<\/tr>\n<tr>\n<td><code>locals<\/code><\/td>\n<td>Define internal computed values<\/td>\n<td>None<\/td>\n<td>None (just arguments)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n","protected":false},"excerpt":{"rendered":"<p>Terraform configurations are built out of blocks. Understanding block types is critical because they define how you declare infrastructure, wire modules together, and control Terraform\u2019s behavior. 1. The Anatomy of a Block Every Terraform block has the same basic shape: TYPE &quot;label1&quot; &quot;label2&quot; { argument_name = expression nested_block_type { # &#8230; } } Key parts: [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[94],"tags":[],"_links":{"self":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2129"}],"collection":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2129"}],"version-history":[{"count":1,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2129\/revisions"}],"predecessor-version":[{"id":2130,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2129\/revisions\/2130"}],"wp:attachment":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2129"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2129"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2129"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}