---
title: "hashicorp/agent-skills"
description: "A collection of Agent skills and Claude Code plugins for HashiCorp products."
source: https://github.com/hashicorp/agent-skills
ref: main
license: MPL-2.0
licenseName: "Mozilla Public License 2.0"
canonical: https://skillsdocs.com/hashicorp/agent-skills
base: https://github.com/hashicorp/agent-skills/blob/main/
provenance: authored
chapters: 20
inlined: 20
withheld: 0
words: 11196
updated: 2026-09-21T01:04:08Z
generator: "Skills Docs"
---

> **hashicorp/agent-skills** — every Agent Skill in this repository, inlined verbatim.
>
> Canonical HTML: https://skillsdocs.com/hashicorp/agent-skills
> Per-skill Markdown: https://skillsdocs.com/hashicorp/agent-skills/<skill>.md
> Machine manifest: https://skillsdocs.com/hashicorp/agent-skills/.well-known/agent-skills/index.json
> JSON: https://skillsdocs.com/api/v1/books/hashicorp/agent-skills
> Install: `npx skills add hashicorp/agent-skills`
> Upstream: https://github.com/hashicorp/agent-skills @ `main`
> Licence: MPL-2.0
>
> Content is mirrored from GitHub and © its authors, served unmodified. Takedown: https://github.com/DreambaseAI/skillsdocs/issues/new?labels=takedown&title=Takedown+request

# hashicorp/agent-skills

A collection of Agent skills and Claude Code plugins for HashiCorp products.

- **Skills:** 20
- **Inlined:** 20 (licence detected)
- **Words:** 11,196
- **Reading time:** 53 min
- **Stars:** 873

## Table of contents

1. [aws-ami-builder](https://skillsdocs.com/hashicorp/agent-skills/aws-ami-builder.md) — Build Amazon Machine Images (AMIs) with Packer using the amazon-ebs builder. Use when creating custom AMIs for EC2 instances.
2. [azure-image-builder](https://skillsdocs.com/hashicorp/agent-skills/azure-image-builder.md) — Build Azure managed images and Azure Compute Gallery images with Packer. Use when creating custom images for Azure VMs.
3. [push-to-registry](https://skillsdocs.com/hashicorp/agent-skills/push-to-registry.md) — Push Packer build metadata to HCP Packer registry for tracking and managing image lifecycle. Use when integrating Packer builds with HCP Packer for version con…
4. [windows-builder](https://skillsdocs.com/hashicorp/agent-skills/windows-builder.md) — Build Windows images with Packer using WinRM communicator and PowerShell provisioners. Use when creating Windows AMIs, Azure images, or VMware templates.
5. [azure-verified-modules](https://skillsdocs.com/hashicorp/agent-skills/azure-verified-modules.md) — Azure Verified Modules (AVM) requirements and best practices for developing certified Azure Terraform modules. Use when creating or reviewing Azure modules tha…
6. [new-terraform-provider](https://skillsdocs.com/hashicorp/agent-skills/new-terraform-provider.md) — Use this when scaffolding a new Terraform provider with the Plugin Framework: workspace layout, go module setup, provider server main.go, and a provider.go wit…
7. [provider-actions](https://skillsdocs.com/hashicorp/agent-skills/provider-actions.md) — Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after creat…
8. [provider-configuration](https://skillsdocs.com/hashicorp/agent-skills/provider-configuration.md) — Implement Terraform provider configuration and authentication with the Plugin Framework: provider schema for credentials (Optional + Sensitive attributes), env…
9. [provider-docs](https://skillsdocs.com/hashicorp/agent-skills/provider-docs.md) — Create, update, and review Terraform provider documentation for Terraform Registry using HashiCorp-recommended patterns, tfplugindocs templates, and schema des…
10. [provider-ephemeral-resources](https://skillsdocs.com/hashicorp/agent-skills/provider-ephemeral-resources.md) — Implement Terraform provider ephemeral resources with the Plugin Framework: the Open/Renew/Close lifecycle, ephemeral schema design, registration via Ephemeral…
11. [provider-framework-migration](https://skillsdocs.com/hashicorp/agent-skills/provider-framework-migration.md) — Migrate Terraform provider resources and data sources from Plugin SDKv2 to the Plugin Framework: muxing both plugins in one provider (terraform-plugin-mux, tf5…
12. [provider-resources](https://skillsdocs.com/hashicorp/agent-skills/provider-resources.md) — Implement Terraform Provider resources and data sources using the Plugin Framework: CRUD operations, schema design, plan modifiers and validators, not-found ha…
13. [provider-test-patterns](https://skillsdocs.com/hashicorp/agent-skills/provider-test-patterns.md) — Terraform provider acceptance test patterns using terraform-plugin-testing with the Plugin Framework. Covers test structure, TestCase/TestStep fields, ConfigSt…
14. [refactor-module](https://skillsdocs.com/hashicorp/agent-skills/refactor-module.md) — Transform monolithic Terraform configurations into reusable, maintainable modules following HashiCorp's module design principles and community best practices.
15. [run-acceptance-tests](https://skillsdocs.com/hashicorp/agent-skills/run-acceptance-tests.md) — Guide for running acceptance tests for a Terraform provider. Use this when asked to run an acceptance test or to run a test with the prefix `TestAcc`, when a t…
16. [terraform-policy](https://skillsdocs.com/hashicorp/agent-skills/terraform-policy.md) — Write, test, or convert Terraform Policy files (.policy.hcl, .policytest.hcl, Sentinel→tfpolicy). Triggers: policy.hcl, policytest, convert sentinel, tfpolicy,…
17. [terraform-search-import](https://skillsdocs.com/hashicorp/agent-skills/terraform-search-import.md) — Discover existing cloud resources using Terraform Search queries and bulk import them into Terraform management. Use when bringing unmanaged infrastructure und…
18. [terraform-stacks](https://skillsdocs.com/hashicorp/agent-skills/terraform-stacks.md) — Comprehensive guide for working with HashiCorp Terraform Stacks. Use when creating, modifying, or validating Terraform Stack configurations (.tfcomponent.hcl,…
19. [terraform-style-guide](https://skillsdocs.com/hashicorp/agent-skills/terraform-style-guide.md) — Generate Terraform HCL code following HashiCorp's official style conventions and best practices. Use when writing, reviewing, or generating Terraform configura…
20. [terraform-test](https://skillsdocs.com/hashicorp/agent-skills/terraform-test.md) — Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating inf…


## Front matter

_The repository README, verbatim except that relative links are resolved against https://github.com/hashicorp/agent-skills/blob/main/._

# HashiCorp Agent Skills

HashiCorp Agent Skills for Terraform and Packer.

| Product | Skills | Product bundle |
| --- | ---: | --- |
| [Terraform](https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/README.md) | 16 | `terraform` |
| [Packer](https://github.com/hashicorp/agent-skills/blob/main/plugins/packer/README.md) | 4 | `packer` |

See [SKILLS.md](https://github.com/hashicorp/agent-skills/blob/main/SKILLS.md) for the complete catalog and lifecycle status of each
Skill.

> **Legal note:** Your use of a third-party MCP client or LLM is subject solely
> to that provider's terms. IBM is not responsible for the performance of those
> third-party tools and may be unable to support issues caused by them.

## Table of Contents

- [Recent Updates](#recent-updates)
- [Install an Individual Skill](#install-an-individual-skill)
- [Install a Product Plugin Bundle](#install-a-product-plugin-bundle)
- [Repository Structure](#repository-structure)
- [Governance and Support](#governance-and-support)
- [License](#license)

## Recent Updates

This repository now organizes its 20 Skills under two product plugin roots:
`plugins/terraform/skills/` and `plugins/packer/skills/`.

### Migration from Legacy Plugin IDs and Paths

The product plugin bundles replace these legacy plugin IDs:
`terraform-code-generation`, `terraform-module-generation`,
`terraform-provider-development`, `terraform-policy-code`, `packer-builders`,
and `packer-hcp`.

Replace any legacy plugin installation with `terraform@hashicorp` or
`packer@hashicorp`. Replace individual paths under `terraform/<category>/skills`
or `packer/<category>/skills` with
`plugins/<product>/skills/<skill-name>`.

## Install an Individual Skill

Install Agent Skills in GitHub Copilot, Claude Code, Opencode, Cursor, IBM Bob,
and more.

List the repository's Skills:

```bash
npx skills add hashicorp/agent-skills
```

Install one Skill from its path:

```bash
npx skills add hashicorp/agent-skills/plugins/terraform/skills/terraform-style-guide
npx skills add hashicorp/agent-skills/plugins/packer/skills/aws-ami-builder
```

Every supported path appears in [SKILLS.md](https://github.com/hashicorp/agent-skills/blob/main/SKILLS.md).

## Install a Product Plugin Bundle

### Claude Code

```bash
claude plugin marketplace add hashicorp/agent-skills
claude plugin install terraform@hashicorp
claude plugin install packer@hashicorp
```

### Codex

Add this repository's `.agents/plugins/marketplace.json` as a repository
marketplace, then install the `terraform` or `packer` plugin in Codex. Both
marketplaces expose the same product bundles and Skill directories.

## Repository Structure

```text
agent-skills/
├── .agents/plugins/marketplace.json
├── .claude-plugin/marketplace.json
├── plugins/
│   ├── terraform/
│   │   ├── .claude-plugin/plugin.json
│   │   ├── .codex-plugin/plugin.json
│   │   └── skills/
│   └── packer/
│       ├── .claude-plugin/plugin.json
│       ├── .codex-plugin/plugin.json
│       └── skills/
└── SKILLS.md
```

## Governance and Support

- [CONTRIBUTING.md](https://github.com/hashicorp/agent-skills/blob/main/CONTRIBUTING.md) for contribution guidance. Contributions
  are restricted to HashiCorp-internal contributors until further notice.
- [SECURITY.md](https://github.com/hashicorp/agent-skills/blob/main/SECURITY.md) for instructions on reporting security or data
  sensitivity issues related to this repository's Agent Skills.
- [SUPPORT.md](https://github.com/hashicorp/agent-skills/blob/main/SUPPORT.md) defines repository support boundaries.
- [SUPPORTED_MODELS.md](https://github.com/hashicorp/agent-skills/blob/main/SUPPORTED_MODELS.md) defines the current Anthropic and
  OpenAI supported model matrix.
- `CODEOWNERS` for canonical Skill ownership and review-routing source.

## License

MPL-2.0

---

## Part: Packer

---

<!-- chapter:begin slug=aws-ami-builder position=1 -->

## 1. aws-ami-builder

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/packer/skills/aws-ami-builder/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/packer/skills/aws-ami-builder/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/aws-ami-builder.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: aws-ami-builder
description: Build Amazon Machine Images (AMIs) with Packer using the amazon-ebs builder. Use when creating custom AMIs for EC2 instances.
metadata:
  lifecycle-status: active
---

# AWS AMI Builder

Build Amazon Machine Images (AMIs) using Packer's `amazon-ebs` builder.

**Reference:** [Amazon EBS Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/amazon/latest/components/builder/ebs)

> **Note:** Building AMIs incurs AWS costs (EC2 instances, EBS storage, data transfer). Builds typically take 10-30 minutes depending on provisioning complexity.

## Basic AMI Template

```hcl
packer {
  required_plugins {
    amazon = {
      source  = "github.com/hashicorp/amazon"
      version = "~> 1.3"
    }
  }
}

variable "region" {
  type    = string
  default = "us-west-2"
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

source "amazon-ebs" "ubuntu" {
  region        = var.region
  instance_type = "t3.micro"

  source_ami_filter {
    filters = {
      name                = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"] # Canonical
  }

  ssh_username = "ubuntu"
  ami_name     = "my-app-${local.timestamp}"

  tags = {
    Name      = "my-app"
    BuildDate = local.timestamp
  }
}

build {
  sources = ["source.amazon-ebs.ubuntu"]

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get upgrade -y",
    ]
  }
}
```

## Common Source AMI Filters

### Ubuntu 22.04 LTS
```hcl
source_ami_filter {
  filters = {
    name                = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
    root-device-type    = "ebs"
    virtualization-type = "hvm"
  }
  most_recent = true
  owners      = ["099720109477"] # Canonical
}
```

### Amazon Linux 2023
```hcl
source_ami_filter {
  filters = {
    name                = "al2023-ami-*-x86_64"
    root-device-type    = "ebs"
    virtualization-type = "hvm"
  }
  most_recent = true
  owners      = ["amazon"]
}
```

## Multi-Region AMI

```hcl
source "amazon-ebs" "ubuntu" {
  region        = "us-west-2"
  instance_type = "t3.micro"

  source_ami_filter {
    filters = {
      name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
    }
    most_recent = true
    owners      = ["099720109477"]
  }

  ssh_username = "ubuntu"
  ami_name     = "my-app-${local.timestamp}"

  # Copy to additional regions
  ami_regions = ["us-east-1", "us-east-2", "eu-west-1"]
}
```

## Authentication

Packer uses AWS credential resolution:

1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
2. AWS credentials file: `~/.aws/credentials`
3. IAM instance profile (when running on EC2)

```bash
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-west-2"

packer build .
```

## Build Commands

```bash
# Initialize plugins
packer init .

# Validate template
packer validate .

# Build AMI
packer build .

# Build with variables
packer build -var "region=us-east-1" .
```

## Common Issues

**SSH Timeout**
- Ensure security group allows SSH (port 22)
- Verify subnet has internet access

**AMI Already Exists**
- AMI names must be unique
- Use timestamp in name: `my-app-${local.timestamp}`

**Volume Size Too Small**
- Check source AMI's volume size
- Set `launch_block_device_mappings.volume_size` accordingly

## References

- [Amazon EBS Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/amazon/latest/components/builder/ebs)
- [AWS AMI Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html)

<!-- chapter:end slug=aws-ami-builder -->

---

<!-- chapter:begin slug=azure-image-builder position=2 -->

## 2. azure-image-builder

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/packer/skills/azure-image-builder/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/packer/skills/azure-image-builder/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/azure-image-builder.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: azure-image-builder
description: Build Azure managed images and Azure Compute Gallery images with Packer. Use when creating custom images for Azure VMs.
metadata:
  lifecycle-status: active
---

# Azure Image Builder

Build Azure managed images and Azure Compute Gallery images using Packer's `azure-arm` builder.

**Reference:** [Azure ARM Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/azure/latest/components/builder/arm)

> **Note:** Building Azure images incurs costs (compute, storage, data transfer). Builds typically take 15-45 minutes depending on provisioning and OS.

## Basic Managed Image

```hcl
packer {
  required_plugins {
    azure = {
      source  = "github.com/hashicorp/azure"
      version = "~> 2.0"
    }
  }
}

variable "client_id" {
  type      = string
  sensitive = true
}

variable "client_secret" {
  type      = string
  sensitive = true
}

variable "subscription_id" {
  type = string
}

variable "tenant_id" {
  type = string
}

variable "resource_group" {
  type    = string
  default = "packer-images-rg"
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

source "azure-arm" "ubuntu" {
  client_id       = var.client_id
  client_secret   = var.client_secret
  subscription_id = var.subscription_id
  tenant_id       = var.tenant_id

  managed_image_resource_group_name = var.resource_group
  managed_image_name                = "my-app-${local.timestamp}"

  os_type         = "Linux"
  image_publisher = "Canonical"
  image_offer     = "0001-com-ubuntu-server-jammy"
  image_sku       = "22_04-lts-gen2"

  location = "East US"
  vm_size  = "Standard_B2s"

  azure_tags = {
    Name      = "my-app"
    BuildDate = local.timestamp
  }
}

build {
  sources = ["source.azure-arm.ubuntu"]

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get upgrade -y",
    ]
  }
}
```

## Azure Compute Gallery

```hcl
source "azure-arm" "ubuntu" {
  client_id       = var.client_id
  client_secret   = var.client_secret
  subscription_id = var.subscription_id
  tenant_id       = var.tenant_id

  os_type         = "Linux"
  image_publisher = "Canonical"
  image_offer     = "0001-com-ubuntu-server-jammy"
  image_sku       = "22_04-lts-gen2"

  location = "East US"
  vm_size  = "Standard_B2s"

  shared_image_gallery_destination {
    resource_group       = "gallery-rg"
    gallery_name         = "myImageGallery"
    image_name           = "ubuntu-webapp"
    image_version        = "1.0.${formatdate("YYYYMMDD", timestamp())}"
    replication_regions  = ["East US", "West US 2"]
    storage_account_type = "Standard_LRS"
  }
}
```

## Authentication

### Service Principal
```bash
# Create service principal
az ad sp create-for-rbac \
  --name "packer-sp" \
  --role Contributor \
  --scopes /subscriptions/<subscription-id>

# Set environment variables
export ARM_CLIENT_ID="<client-id>"
export ARM_CLIENT_SECRET="<client-secret>"
export ARM_SUBSCRIPTION_ID="<subscription-id>"
export ARM_TENANT_ID="<tenant-id>"
```

### Managed Identity
```hcl
source "azure-arm" "ubuntu" {
  use_azure_cli_auth = true
  subscription_id    = var.subscription_id
  # ... rest of configuration
}
```

## Build Commands

```bash
# Set authentication
export ARM_CLIENT_ID="your-client-id"
export ARM_CLIENT_SECRET="your-client-secret"
export ARM_SUBSCRIPTION_ID="your-subscription-id"
export ARM_TENANT_ID="your-tenant-id"

# Initialize plugins
packer init .

# Validate template
packer validate .

# Build image
packer build .
```

## Common Issues

**Authentication Failed**
- Verify service principal credentials
- Ensure Contributor role on resource group
- Check subscription and tenant IDs

**Compute Gallery Version Exists**
- Image versions are immutable
- Use unique version numbers with date/build number
- Cannot overwrite existing versions

**Timeout During Provisioning**
- Check network connectivity from build VM
- Verify NSG rules allow required traffic
- Increase timeout if needed

## References

- [Azure ARM Builder](https://developer.hashicorp.com/packer/integrations/hashicorp/azure/latest/components/builder/arm)
- [Azure Compute Gallery](https://learn.microsoft.com/en-us/azure/virtual-machines/azure-compute-gallery)

<!-- chapter:end slug=azure-image-builder -->

---

<!-- chapter:begin slug=push-to-registry position=3 -->

## 3. push-to-registry

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/packer/skills/push-to-registry/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/packer/skills/push-to-registry/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/push-to-registry.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: push-to-registry
description: Push Packer build metadata to HCP Packer registry for tracking and managing image lifecycle. Use when integrating Packer builds with HCP Packer for version control and governance.
metadata:
  lifecycle-status: active
---

# Push to HCP Packer Registry

Configure Packer templates to push build metadata to HCP Packer registry.

**Reference:** [HCP Packer Registry](https://developer.hashicorp.com/hcp/docs/packer)

> **Note:** HCP Packer is free for basic use. Builds push metadata only (not actual images), adding minimal overhead (<1 minute).

## Basic Registry Configuration

```hcl
packer {
  required_version = ">= 1.7.7"
}

variable "image_name" {
  type    = string
  default = "web-server"
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

source "amazon-ebs" "ubuntu" {
  region        = "us-west-2"
  instance_type = "t3.micro"

  source_ami_filter {
    filters = {
      name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
    }
    most_recent = true
    owners      = ["099720109477"]
  }

  ssh_username = "ubuntu"
  ami_name     = "${var.image_name}-${local.timestamp}"
}

build {
  sources = ["source.amazon-ebs.ubuntu"]

  hcp_packer_registry {
    bucket_name = var.image_name
    description = "Ubuntu 22.04 base image for web servers"

    bucket_labels = {
      "os"   = "ubuntu"
      "team" = "platform"
    }

    build_labels = {
      "build-time" = local.timestamp
    }
  }

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get upgrade -y",
    ]
  }
}
```

## Authentication

Set environment variables before building:

```bash
export HCP_CLIENT_ID="your-service-principal-client-id"
export HCP_CLIENT_SECRET="your-service-principal-secret"
export HCP_ORGANIZATION_ID="your-org-id"
export HCP_PROJECT_ID="your-project-id"

packer build .
```

### Create HCP Service Principal

1. Navigate to HCP → Access Control (IAM)
2. Create Service Principal
3. Grant "Contributor" role on project
4. Generate client secret
5. Save client ID and secret

## Registry Configuration Options

### bucket_name (required)
The image identifier. Must stay consistent across builds!

```hcl
bucket_name = "web-server"  # Keep this constant
```

### bucket_labels (optional)
Metadata at bucket level. Updates with each build.

```hcl
bucket_labels = {
  "os"        = "ubuntu"
  "team"      = "platform"
  "component" = "web"
}
```

### build_labels (optional)
Metadata for each iteration. Immutable after build completes.

```hcl
build_labels = {
  "build-time" = local.timestamp
  "git-commit" = var.git_commit
}
```

## CI/CD Integration

### GitHub Actions

```yaml
name: Build and Push to HCP Packer

on:
  push:
    branches: [main]

env:
  HCP_CLIENT_ID: ${{ secrets.HCP_CLIENT_ID }}
  HCP_CLIENT_SECRET: ${{ secrets.HCP_CLIENT_SECRET }}
  HCP_ORGANIZATION_ID: ${{ secrets.HCP_ORGANIZATION_ID }}
  HCP_PROJECT_ID: ${{ secrets.HCP_PROJECT_ID }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-packer@main

      - name: Build and push
        run: |
          packer init .
          packer build \
            -var "git_commit=${{ github.sha }}" \
            .
```

## Querying in Terraform

```hcl
data "hcp_packer_artifact" "ubuntu" {
  bucket_name  = "web-server"
  channel_name = "production"
  platform     = "aws"
  region       = "us-west-2"
}

resource "aws_instance" "web" {
  ami           = data.hcp_packer_artifact.ubuntu.external_identifier
  instance_type = "t3.micro"

  tags = {
    PackerBucket = data.hcp_packer_artifact.ubuntu.bucket_name
  }
}
```

## Common Issues

**Authentication Failed**
- Verify HCP_CLIENT_ID and HCP_CLIENT_SECRET
- Ensure service principal has Contributor role
- Check organization and project IDs

**Bucket Name Mismatch**
- Keep `bucket_name` consistent across builds
- Don't include timestamps in bucket_name
- Creates new bucket if name changes

**Build Fails**
- Packer fails immediately if can't push metadata
- Prevents drift between artifacts and registry
- Check network connectivity to HCP API

## Best Practices

- **Consistent bucket names** - Never change for same image type
- **Meaningful labels** - Use for versions, teams, compliance
- **CI/CD automation** - Automate builds and registry pushes
- **Immutable build labels** - Put changing data (git SHA, date) in build_labels

## References

- [HCP Packer Documentation](https://developer.hashicorp.com/hcp/docs/packer)
- [hcp_packer_registry Block](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/blocks/build/hcp_packer_registry)
- [HCP Terraform Provider](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/data-sources/packer_artifact)

<!-- chapter:end slug=push-to-registry -->

---

<!-- chapter:begin slug=windows-builder position=4 -->

## 4. windows-builder

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/packer/skills/windows-builder/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/packer/skills/windows-builder/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/windows-builder.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: windows-builder
description: Build Windows images with Packer using WinRM communicator and PowerShell provisioners. Use when creating Windows AMIs, Azure images, or VMware templates.
metadata:
  lifecycle-status: active
---

# Windows Builder

Platform-agnostic patterns for building Windows images with Packer.

**Reference:** [WinRM Communicator](https://developer.hashicorp.com/packer/docs/communicators/winrm)

> **Note:** Windows builds incur significant costs and time. Expect 45-120 minutes per build due to Windows Updates. Failed builds may leave resources running - always verify cleanup.

## WinRM Communicator Setup

Windows requires WinRM for Packer communication.

### AWS Example

```hcl
source "amazon-ebs" "windows" {
  region        = "us-west-2"
  instance_type = "t3.medium"

  source_ami_filter {
    filters = {
      name = "Windows_Server-2022-English-Full-Base-*"
    }
    most_recent = true
    owners      = ["amazon"]
  }

  ami_name = "windows-server-2022-${local.timestamp}"

  communicator   = "winrm"
  winrm_username = "Administrator"
  winrm_use_ssl  = true
  winrm_insecure = true
  winrm_timeout  = "15m"

  user_data_file = "scripts/setup-winrm.ps1"
}
```

### WinRM Setup Script (scripts/setup-winrm.ps1)

```powershell
<powershell>
# Configure WinRM
winrm quickconfig -q
winrm set winrm/config '@{MaxTimeoutms="1800000"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'

# Configure firewall
netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow

# Restart WinRM
net stop winrm
net start winrm
</powershell>
```

### Azure Example

```hcl
source "azure-arm" "windows" {
  client_id       = var.client_id
  client_secret   = var.client_secret
  subscription_id = var.subscription_id
  tenant_id       = var.tenant_id

  managed_image_resource_group_name = "images-rg"
  managed_image_name                = "windows-${local.timestamp}"

  os_type         = "Windows"
  image_publisher = "MicrosoftWindowsServer"
  image_offer     = "WindowsServer"
  image_sku       = "2022-datacenter-g2"

  location = "East US"
  vm_size  = "Standard_D2s_v3"

  # Azure auto-configures WinRM
  communicator   = "winrm"
  winrm_use_ssl  = true
  winrm_insecure = true
  winrm_timeout  = "15m"
  winrm_username = "packer"
}
```

## PowerShell Provisioners

### Install Software

```hcl
build {
  sources = ["source.amazon-ebs.windows"]

  # Install Chocolatey
  provisioner "powershell" {
    inline = [
      "Set-ExecutionPolicy Bypass -Scope Process -Force",
      "iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
    ]
  }

  # Install applications
  provisioner "powershell" {
    inline = [
      "choco install -y googlechrome",
      "choco install -y 7zip",
    ]
  }

  # Install IIS
  provisioner "powershell" {
    inline = [
      "Install-WindowsFeature -Name Web-Server -IncludeManagementTools"
    ]
  }
}
```

### Windows Updates

```hcl
provisioner "powershell" {
  inline = [
    "Install-PackageProvider -Name NuGet -Force",
    "Install-Module -Name PSWindowsUpdate -Force",
    "Import-Module PSWindowsUpdate",
    "Get-WindowsUpdate -Install -AcceptAll -AutoReboot",
  ]
  timeout = "2h"
}

# Wait for reboots
provisioner "windows-restart" {
  restart_timeout = "30m"
}
```

## Cleanup

```hcl
provisioner "powershell" {
  inline = [
    "# Clear temp files",
    "Remove-Item -Path 'C:\\Windows\\Temp\\*' -Recurse -Force -ErrorAction SilentlyContinue",
    "# Clear Windows Update cache",
    "Stop-Service -Name wuauserv -Force",
    "Remove-Item -Path 'C:\\Windows\\SoftwareDistribution\\*' -Recurse -Force -ErrorAction SilentlyContinue",
    "Start-Service -Name wuauserv",
  ]
}
```

## Common Issues

**WinRM Timeout**
- Increase `winrm_timeout` to 15m or more
- Verify security group allows ports 5985/5986
- Check user data script completed successfully

**PowerShell Execution Policy**
```hcl
provisioner "powershell" {
  inline = [
    "Set-ExecutionPolicy Bypass -Scope Process -Force",
    "# Your commands here",
  ]
}
```

**Long Build Times**
- Windows Updates can take 1-2 hours
- Use pre-patched base images when available
- Set provisioner `timeout = "2h"`

## References

- [WinRM Communicator](https://developer.hashicorp.com/packer/docs/communicators/winrm)
- [PowerShell Provisioner](https://developer.hashicorp.com/packer/docs/provisioners/powershell)

<!-- chapter:end slug=windows-builder -->

---

## Part: Terraform

---

<!-- chapter:begin slug=azure-verified-modules position=5 -->

## 5. azure-verified-modules

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/azure-verified-modules/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/azure-verified-modules/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/azure-verified-modules.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: azure-verified-modules
description: Azure Verified Modules (AVM) requirements and best practices for developing certified Azure Terraform modules. Use when creating or reviewing Azure modules that need AVM certification.
metadata:
  lifecycle-status: active
---

# Azure Verified Modules (AVM) Requirements

This guide covers the mandatory requirements for Azure Verified Modules certification. These requirements ensure consistency, quality, and maintainability across Azure Terraform modules.

**References:**
- [Azure Verified Modules](https://azure.github.io/Azure-Verified-Modules/)
- [AVM Module Specifications](https://azure.github.io/Azure-Verified-Modules/specs/module-specs/)

## Table of Contents

- [Module Cross-Referencing](#module-cross-referencing)
- [Azure Provider Requirements](#azure-provider-requirements)
- [Code Style Standards](#code-style-standards)
- [Variable Requirements](#variable-requirements)
- [Output Requirements](#output-requirements)
- [Local Values Standards](#local-values-standards)
- [Terraform Configuration Requirements](#terraform-configuration-requirements)
- [Testing Requirements](#testing-requirements)
- [Documentation Requirements](#documentation-requirements)
- [Breaking Changes & Feature Management](#breaking-changes--feature-management)
- [Contribution Standards](#contribution-standards)
- [Compliance Checklist](#compliance-checklist)

---

## Module Cross-Referencing

**Severity:** MUST | **Requirement:** TFFR1

When building Resource or Pattern modules, module owners **MAY** cross-reference other modules. However:

- Modules **MUST** be referenced using HashiCorp Terraform registry reference to a pinned version
  - Example: `source = "Azure/xxx/azurerm"` with `version = "1.2.3"`
- Modules **MUST NOT** use git references (e.g., `git::https://xxx.yyy/xxx.git` or `github.com/xxx/yyy`)
- Modules **MUST NOT** contain references to non-AVM modules

---

## Azure Provider Requirements

**Severity:** MUST | **Requirement:** TFFR3

Authors **MUST** only use the following Azure providers:

| Provider | Min Version | Max Version |
|----------|-------------|-------------|
| azapi    | >= 2.0      | < 3.0       |
| azurerm  | >= 4.0      | < 5.0       |

**Requirements:**

- Authors **MAY** select either Azurerm, Azapi, or both providers
- **MUST** use `required_providers` block to enforce provider versions
- **SHOULD** use pessimistic version constraint operator (`~>`)

**Example:**

```hcl
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.0"
    }
  }
}
```

---

## Code Style Standards

### Lower snake_casing

**Severity:** MUST | **Requirement:** TFNFR4

**MUST** use lower snake_casing for:

- Locals
- Variables
- Outputs
- Resources (symbolic names)
- Modules (symbolic names)

Example: `snake_casing_example`

### Resource & Data Source Ordering

**Severity:** SHOULD | **Requirement:** TFNFR6

- Resources that are depended on **SHOULD** come first
- Resources with dependencies **SHOULD** be defined close to each other

### Count & for_each Usage

**Severity:** MUST | **Requirement:** TFNFR7

- Use `count` for conditional resource creation
- **MUST** use `map(xxx)` or `set(xxx)` as resource's `for_each` collection
- The map's key or set's element **MUST** be static literals

**Example:**

```hcl
resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_map  # map(string)
  name                 = "${each.value}-pair"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.0.1.0/24"]
}
```

### Resource & Data Block Internal Ordering

**Severity:** SHOULD | **Requirement:** TFNFR8

**Order within resource/data blocks:**

1. **Meta-arguments (top)**:
   - `provider`
   - `count`
   - `for_each`

2. **Arguments/blocks (middle, alphabetical)**:
   - Required arguments
   - Optional arguments
   - Required nested blocks
   - Optional nested blocks

3. **Meta-arguments (bottom)**:
   - `depends_on`
   - `lifecycle` (with sub-order: `create_before_destroy`, `ignore_changes`, `prevent_destroy`)

Separate sections with blank lines.

### Module Block Ordering

**Severity:** SHOULD | **Requirement:** TFNFR9

**Order within module blocks:**

1. **Top meta-arguments**:
   - `source`
   - `version`
   - `count`
   - `for_each`

2. **Arguments (alphabetical)**:
   - Required arguments
   - Optional arguments

3. **Bottom meta-arguments**:
   - `depends_on`
   - `providers`

### Lifecycle ignore_changes Syntax

**Severity:** MUST | **Requirement:** TFNFR10

The `ignore_changes` attribute **MUST NOT** be enclosed in double quotes.

**Good:**

```hcl
lifecycle {
  ignore_changes = [tags]
}
```

**Bad:**

```hcl
lifecycle {
  ignore_changes = ["tags"]
}
```

### Null Comparison for Conditional Creation

**Severity:** SHOULD | **Requirement:** TFNFR11

For parameters requiring conditional resource creation, wrap with `object` type to avoid "known after apply" issues during plan stage.

**Recommended:**

```hcl
variable "security_group" {
  type = object({
    id = string
  })
  default = null
}
```

### Dynamic Blocks for Optional Nested Objects

**Severity:** MUST | **Requirement:** TFNFR12

Nested blocks under conditions **MUST** use this pattern:

```hcl
dynamic "identity" {
  for_each = <condition> ? [<some_item>] : []

  content {
    # block content
  }
}
```

### Default Values with coalesce/try

**Severity:** SHOULD | **Requirement:** TFNFR13

**Good:**

```hcl
coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
```

**Bad:**

```hcl
var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name
```

### Provider Declarations in Modules

**Severity:** MUST | **Requirement:** TFNFR27

- `provider` **MUST NOT** be declared in modules (except for `configuration_aliases`)
- `provider` blocks in modules **MUST** only use `alias`
- Provider configurations **SHOULD** be passed in by module users

---

## Variable Requirements

### Not Allowed Variables

**Severity:** MUST | **Requirement:** TFNFR14

Module owners **MUST NOT** add variables like `enabled` or `module_depends_on` to control entire module operation. Boolean feature toggles for specific resources are acceptable.

### Variable Definition Order

**Severity:** SHOULD | **Requirement:** TFNFR15

Variables **SHOULD** follow this order:

1. All required fields (alphabetical)
2. All optional fields (alphabetical)

### Variable Naming Rules

**Severity:** SHOULD | **Requirement:** TFNFR16

- Follow [HashiCorp's naming rules](https://www.terraform.io/docs/extend/best-practices/naming.html)
- Feature switches **SHOULD** use positive statements: `xxx_enabled` instead of `xxx_disabled`

### Variables with Descriptions

**Severity:** SHOULD | **Requirement:** TFNFR17

- `description` **SHOULD** precisely describe the parameter's purpose and expected data type
- Target audience is module users, not developers
- For `object` types, use HEREDOC format

### Variables with Types

**Severity:** MUST | **Requirement:** TFNFR18

- `type` **MUST** be defined for every variable
- `type` **SHOULD** be as precise as possible
- `any` **MAY** only be used with adequate reasons
- Use `bool` instead of `string`/`number` for true/false values
- Use concrete `object` instead of `map(any)`

### Sensitive Data Variables

**Severity:** SHOULD | **Requirement:** TFNFR19

If a variable's type is `object` and contains sensitive fields, the entire variable **SHOULD** be `sensitive = true`, or extract sensitive fields into separate variables.

### Non-Nullable Defaults for Collections

**Severity:** SHOULD | **Requirement:** TFNFR20

Nullable **SHOULD** be set to `false` for collection values (sets, maps, lists) when using them in loops. For scalar values, null may have semantic meaning.

### Discourage Nullability by Default

**Severity:** MUST | **Requirement:** TFNFR21

`nullable = true` **MUST** be avoided unless there's a specific semantic need for null values.

### Avoid sensitive = false

**Severity:** MUST | **Requirement:** TFNFR22

`sensitive = false` **MUST** be avoided (this is the default).

### Sensitive Default Value Conditions

**Severity:** MUST | **Requirement:** TFNFR23

A default value **MUST NOT** be set for sensitive inputs (e.g., default passwords).

### Handling Deprecated Variables

**Severity:** MUST | **Requirement:** TFNFR24

- Move deprecated variables to `deprecated_variables.tf`
- Annotate with `DEPRECATED` at the beginning of description
- Declare the replacement's name
- Clean up during major version releases

---

## Output Requirements

### Additional Terraform Outputs

**Severity:** SHOULD | **Requirement:** TFFR2

Authors **SHOULD NOT** output entire resource objects as these may contain sensitive data and the schema can change with API or provider versions.

**Best Practices:**

- Output *computed* attributes of resources as discrete outputs (anti-corruption layer pattern)
- **SHOULD NOT** output values that are already inputs (except `name`)
- Use `sensitive = true` for sensitive attributes
- For resources deployed with `for_each`, output computed attributes in a map structure

**Examples:**

```hcl
# Single resource computed attribute
output "foo" {
  description = "MyResource foo attribute"
  value       = azurerm_resource_myresource.foo
}

# for_each resources
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azurerm_resource_mychildresource : key => value.foo
  }
}

# Sensitive output
output "bar" {
  description = "MyResource bar attribute"
  value       = azurerm_resource_myresource.bar
  sensitive   = true
}
```

### Sensitive Data Outputs

**Severity:** MUST | **Requirement:** TFNFR29

Outputs containing confidential data **MUST** be declared with `sensitive = true`.

### Handling Deprecated Outputs

**Severity:** MUST | **Requirement:** TFNFR30

- Move deprecated outputs to `deprecated_outputs.tf`
- Define new outputs in `outputs.tf`
- Clean up during major version releases

---

## Local Values Standards

### locals.tf Organization

**Severity:** MAY | **Requirement:** TFNFR31

- `locals.tf` **SHOULD** only contain `locals` blocks
- **MAY** declare `locals` blocks next to resources for advanced scenarios

### Alphabetical Local Arrangement

**Severity:** MUST | **Requirement:** TFNFR32

Expressions in `locals` blocks **MUST** be arranged alphabetically.

### Precise Local Types

**Severity:** SHOULD | **Requirement:** TFNFR33

Use precise types (e.g., `number` for age, not `string`).

---

## Terraform Configuration Requirements

### Terraform Version Requirements

**Severity:** MUST | **Requirement:** TFNFR25

**`terraform.tf` requirements:**

- **MUST** contain only one `terraform` block
- First line **MUST** define `required_version`
- **MUST** include minimum version constraint
- **MUST** include maximum major version constraint
- **SHOULD** use `~> #.#` or `>= #.#.#, < #.#.#` format

**Example:**

```hcl
terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}
```

### Providers in required_providers

**Severity:** MUST | **Requirement:** TFNFR26

- `terraform` block **MUST** contain `required_providers` block
- Each provider **MUST** specify `source` and `version`
- Providers **SHOULD** be sorted alphabetically
- Only include directly required providers
- `source` **MUST** be in format `namespace/name`
- `version` **MUST** include minimum and maximum major version constraints
- **SHOULD** use `~> #.#` or `>= #.#.#, < #.#.#` format

---

## Testing Requirements

### Test Tooling

**Severity:** MUST | **Requirement:** TFNFR5

**Required testing tools for AVM:**

- Terraform (`terraform validate/fmt/test`)
- terrafmt
- Checkov
- tflint (with azurerm ruleset)
- Go (optional for custom tests)

### Test Provider Configuration

**Severity:** SHOULD | **Requirement:** TFNFR36

For robust testing, `prevent_deletion_if_contains_resources` **SHOULD** be explicitly set to `false` in test provider configurations.

---

## Documentation Requirements

### Module Documentation Generation

**Severity:** MUST | **Requirement:** TFNFR2

- Documentation **MUST** be automatically generated via [Terraform Docs](https://github.com/terraform-docs/terraform-docs)
- A `.terraform-docs.yml` file **MUST** be present in the module root

---

## Breaking Changes & Feature Management

### Using Feature Toggles

**Severity:** MUST | **Requirement:** TFNFR34

New resources added in minor/patch versions **MUST** have a toggle variable to avoid creation by default:

```hcl
variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azurerm_route_table" "this" {
  count = var.create_route_table ? 1 : 0
  # ...
}
```

### Reviewing Potential Breaking Changes

**Severity:** MUST | **Requirement:** TFNFR35

**Breaking changes requiring caution:**

**Resource blocks:**

1. Adding new resource without conditional creation
2. Adding arguments with non-default values
3. Adding nested blocks without `dynamic`
4. Renaming resources without `moved` blocks
5. Changing `count` to `for_each` or vice versa

**Variable/Output blocks:**

1. Deleting/renaming variables
2. Changing variable `type`
3. Changing variable `default` values
4. Changing `nullable` to false
5. Changing `sensitive` from false to true
6. Adding variables without `default`
7. Deleting outputs
8. Changing output `value`
9. Changing output `sensitive` value

---

## Contribution Standards

### GitHub Repository Branch Protection

**Severity:** MUST | **Requirement:** TFNFR3

Module owners **MUST** set branch protection policies on the default branch (typically `main`):

1. Require Pull Request before merging
2. Require approval of most recent reviewable push
3. Dismiss stale PR approvals when new commits are pushed
4. Require linear history
5. Prevent force pushes
6. Not allow deletions
7. Require CODEOWNERS review
8. No bypassing settings allowed
9. Enforce for administrators

---

## Compliance Checklist

Use this checklist when developing or reviewing Azure Verified Modules:

### Module Structure
- [ ] Module cross-references use registry sources with pinned versions
- [ ] Azure providers (azurerm/azapi) versions meet AVM requirements
- [ ] `.terraform-docs.yml` present in module root
- [ ] CODEOWNERS file present

### Code Style
- [ ] All names use lower snake_casing
- [ ] Resources ordered with dependencies first
- [ ] `for_each` uses `map()` or `set()` with static keys
- [ ] Resource/data/module blocks follow proper internal ordering
- [ ] `ignore_changes` not quoted
- [ ] Dynamic blocks used for conditional nested objects
- [ ] `coalesce()` or `try()` used for default values

### Variables
- [ ] No `enabled` or `module_depends_on` variables
- [ ] Variables ordered: required (alphabetical) then optional (alphabetical)
- [ ] All variables have precise types (avoid `any`)
- [ ] All variables have descriptions
- [ ] Collections have `nullable = false`
- [ ] No `sensitive = false` declarations
- [ ] No default values for sensitive inputs
- [ ] Deprecated variables moved to `deprecated_variables.tf`

### Outputs
- [ ] Outputs use anti-corruption layer pattern (discrete attributes)
- [ ] Sensitive outputs marked `sensitive = true`
- [ ] Deprecated outputs moved to `deprecated_outputs.tf`

### Terraform Configuration
- [ ] `terraform.tf` has version constraints (`~>` format)
- [ ] `required_providers` block present with all providers
- [ ] No `provider` declarations in module (except aliases)
- [ ] Locals arranged alphabetically

### Testing & Quality
- [ ] Required testing tools configured
- [ ] New resources have feature toggles
- [ ] Breaking changes reviewed and documented

---

## Summary Statistics

- **Functional Requirements:** 3
- **Non-Functional Requirements:** 34
- **Total Requirements:** 37

### By Severity
- **MUST:** 21 requirements
- **SHOULD:** 14 requirements
- **MAY:** 2 requirements

---

*Based on: Azure Verified Modules - Terraform Requirements*

<!-- chapter:end slug=azure-verified-modules -->

---

<!-- chapter:begin slug=new-terraform-provider position=6 -->

## 6. new-terraform-provider

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/new-terraform-provider/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/new-terraform-provider/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/new-terraform-provider.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (2), referenced from this skill's directory:
  - `assets/main.go` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/new-terraform-provider/assets/main.go
  - `assets/provider.go` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/new-terraform-provider/assets/provider.go

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: new-terraform-provider
description: >-
  Use this when scaffolding a new Terraform provider with the Plugin
  Framework: workspace layout, go module setup, provider server main.go,
  and a provider.go with schema and Configure. Also use when a user wants
  to start building a provider for a new API or asks how to begin a
  terraform-provider-* project.
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

To scaffold a new Terraform provider with Plugin Framework:

1. If I am already in a Terraform provider workspace, then confirm that I want
   to create a new workspace. If I do not want to create a new workspace, then
   skip all remaining steps.
1. Create a new workspace root directory. The root directory name should be
   prefixed with "terraform-provider-". Perform all subsequent steps in this
   new workspace.
1. Initialize a new Go module.
1. Run `go get -u github.com/hashicorp/terraform-plugin-framework@latest`.
1. Write a main.go file that follows [the example](assets/main.go).
1. Write an `internal/provider/provider.go` file that follows
   [the example](assets/provider.go). Rename the `demo` provider, the
   `DEMO_*` environment variables, and the authentication attributes to
   match the target API.
1. Remove TODO comments from `main.go` and `provider.go`.
1. Run `go mod tidy`
1. Run `go build -o /dev/null`
1. Run `go test ./...`
The scaffold resolves credentials as explicit config with environment
variable fallback. To grow that into a full credential provider chain
(shared credentials files, profiles, platform identity, configure-time
validation), use the `provider-configuration` skill (if available). To add
the first resource or data source, use the `provider-resources` skill (if
available).

<!-- chapter:end slug=new-terraform-provider -->

---

<!-- chapter:begin slug=provider-actions position=7 -->

## 7. provider-actions

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-actions/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-actions/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-actions.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-actions
description: Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after create, update, destroy).
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Terraform Provider Actions Implementation Guide

## Overview

Terraform Actions enable imperative operations during the Terraform lifecycle. Actions are experimental features that allow performing provider operations at specific lifecycle events (before/after create, update, destroy).

**References:**
- [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework)
- [Terraform Plugin Framework Actions](https://developer.hashicorp.com/terraform/plugin/framework/actions)

## First Action Setup

When adding the first action to a provider that has never had one, several one-time scaffolding steps are required:

1. **Implement `ProviderWithActions`** — add an `Actions()` method to the provider that returns `[]func() action.Action`.
2. **Set `ActionData` in `Configure`** — the provider's `Configure` method must set `resp.ActionData = v` alongside the existing `ResourceData`, `DataSourceData`, and `EphemeralResourceData` assignments.
3. **Create `ActionWithConfigure` base type** — if the provider uses embedded base types (e.g. `ResourceWithConfigure`), create an equivalent `ActionWithConfigure` type implementing `action.ConfigureRequest` / `action.ConfigureResponse`.
4. **Action-schema helper variants** — if the provider injects common schema attributes (e.g. `namespace`) via helper functions, action-schema variants are needed since `action/schema` types differ from `resource/schema` types.

## File Structure

Most providers keep actions alongside resources in the provider package:

```
internal/provider/
├── <action_name>_action.go       # Action implementation
└── <action_name>_action_test.go  # Action tests
```

(Large multi-service providers use `internal/service/<service>/` packages
instead — follow the target repository's layout.)

Documentation lives with the other generated docs:
```
docs/actions/
└── <action_name>.md              # User-facing documentation
```

(Some older, large providers hand-write
`website/docs/actions/<name>.html.markdown` instead — match the repo.)

## Action Schema Definition

Actions use the Terraform Plugin Framework with a standard schema pattern:

```go
func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            // Required configuration parameters
            "resource_id": schema.StringAttribute{
                Required:    true,
                Description: "ID of the resource to operate on",
            },
            // Optional parameters with defaults
            "timeout": schema.Int64Attribute{
                Optional:    true,
                Description: "Operation timeout in seconds",
                Default:     int64default.StaticInt64(1800),
                Computed:    true,
            },
        },
    }
}
```

### Common Schema Issues

**Pay special attention to the schema definition** - common issues after a first draft:

1. **Type Mismatches**
   - Model structs use `types.String`/`types.Int64` and schemas use
     `types.StringType` from
     `github.com/hashicorp/terraform-plugin-framework/types` — don't mix in
     types from other packages
   - Some large providers layer their own custom type package on top (e.g.
     terraform-provider-aws's internal `fwtypes`); inside such a repo,
     follow its convention consistently instead of the plain types

2. **List/Map Element Types**
   ```go
   // WRONG - missing ElementType
   "items": schema.ListAttribute{
       Optional: true,
   }

   // CORRECT
   "items": schema.ListAttribute{
       Optional:    true,
       ElementType: types.StringType,
   }
   ```

3. **Computed vs Optional**
   - Attributes with defaults must be both `Optional: true` and `Computed: true`
   - Don't mark action inputs as `Computed` unless they have defaults

4. **Validator Imports**
   ```go
   // Ensure proper imports
   "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
   "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
   ```

5. **Region/Provider Attribute** (multi-region providers, e.g. AWS)
   - Use the provider's shared region handling when it has one
   - Don't manually re-define provider-level configuration in an action schema

6. **Nested Attributes**
   - Use appropriate nested object types for complex structures
   - Ensure nested types are properly defined

### Schema Validation Checklist

Before submitting, verify:
- [ ] All attributes have descriptions
- [ ] List/Map attributes have ElementType defined
- [ ] Validators are imported and applied correctly
- [ ] Model struct uses correct framework types
- [ ] Optional attributes with defaults are marked Computed
- [ ] Code compiles without type errors
- [ ] Run `go build` to catch type mismatches

## Action Invoke Method

The Invoke method contains the action logic:

```go
func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
    var data actionModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    // a.client was stored by Configure (from req.ProviderData), the same
    // pattern resources use.
    resp.SendProgress(action.InvokeProgressEvent{Message: "Starting operation..."})

    // Implement action logic with error handling
    // Use context for timeout management
    // Poll for completion if async operation

    resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
}
```

## Key Implementation Requirements

### 1. Progress Reporting

- Use `resp.SendProgress(action.InvokeProgressEvent{...})` for real-time updates
- Provide meaningful progress messages during long operations
- Update progress at key milestones
- Include elapsed time for long operations

### 2. Timeout Management

- Always include configurable timeout parameter (default: 1800s)
- Use `context.WithTimeout()` for API calls
- Handle timeout errors gracefully
- Validate timeout ranges (typically 60-7200 seconds)

### 3. Error Handling

- Add diagnostics with `resp.Diagnostics.AddError()`
- Provide clear error messages with context
- Include API error details when relevant
- Map provider error types to user-friendly messages
- Document all possible error cases

Example error handling:
```go
// Handle specific errors
var notFound *types.ResourceNotFoundException
if errors.As(err, &notFound) {
    resp.Diagnostics.AddError(
        "Resource Not Found",
        fmt.Sprintf("Resource %s was not found", resourceID),
    )
    return
}

// Generic error handling
resp.Diagnostics.AddError(
    "Operation Failed",
    fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err),
)
```

### 4. Provider SDK Integration

- Use the API client stored at Configure time (`a.client`), shared with
  resources and data sources
- Handle pagination for list operations
- Implement retry logic for transient failures
- Use appropriate error types

### 5. Parameter Validation

- Use framework validators for input validation
- Validate resource existence before operations
- Check for conflicting parameters
- Validate against provider naming requirements

### 6. Polling and Waiting

For operations that require waiting for completion, poll on a ticker under
a context deadline, reporting progress as you go. (Alternatively use
`retry.StateChangeConf` from
`github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry`, the same waiter
primitive resources use.)

```go
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

// Poll fast, report slow: progress events cross the plugin protocol, so
// throttle them instead of emitting one per poll.
start := time.Now()
var lastProgress time.Time
for {
    res, err := findResource(ctx, a.client, id)
    if err != nil {
        resp.Diagnostics.AddError("Error polling operation", fmt.Sprintf("checking status of %s: %s", id, err))
        return
    }
    switch res.Status {
    case "AVAILABLE", "COMPLETED":
        resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
        return
    case "CREATING", "PENDING":
        if time.Since(lastProgress) >= 30*time.Second {
            lastProgress = time.Now()
            resp.SendProgress(action.InvokeProgressEvent{
                Message: fmt.Sprintf("Status: %s, Elapsed: %v", res.Status, time.Since(start).Round(time.Second)),
            })
        }
    default:
        resp.Diagnostics.AddError("Operation Failed", fmt.Sprintf("%s entered unexpected status %q", id, res.Status))
        return
    }

    select {
    case <-ctx.Done():
        resp.Diagnostics.AddError("Operation Timed Out", fmt.Sprintf("%s did not complete within %v", id, timeout))
        return
    case <-ticker.C:
    }
}
```

## Common Action Patterns

### Batch Operations
- Process items in configurable batches
- Report progress per batch
- Handle partial failures gracefully
- Support prefix/filter parameters

### Command Execution
- Submit command and get operation ID
- Poll for completion status
- Retrieve and report output
- Handle timeout during polling
- Validate resources exist before execution

### Service Invocation
- Invoke service with parameters
- Wait for completion (if synchronous)
- Return output/results
- Handle service-specific errors

### Resource State Changes
- Validate current state
- Apply state change
- Poll for target state
- Handle transitional states

### Async Job Submission
- Submit job with configuration
- Get job ID
- Optionally wait for completion
- Report job status

## Action Triggers

Actions are invoked via `action_trigger` lifecycle blocks in Terraform configurations. A standalone `action` block without a corresponding trigger is declared but never executed.

### HCL Syntax

Action parameters must be wrapped in a `config {}` block. Trigger references use the `action.` prefix, and `actions` is a list. Events are bare identifiers, not quoted strings.

```hcl
action "provider_service_action" "name" {
  config {
    parameter = value
  }
}

resource "terraform_data" "trigger" {
  lifecycle {
    action_trigger {
      events  = [after_create]
      actions = [action.provider_service_action.name]
    }
  }
}
```

### Available Trigger Events

**Supported events (as of Terraform 1.14):**
- `before_create` - Before resource creation
- `after_create` - After resource creation
- `before_update` - Before resource update
- `after_update` - After resource update

**Not supported (as of Terraform 1.14; check current release notes):**
- `before_destroy` - Not available (will cause validation error)
- `after_destroy` - Not available (will cause validation error)

## Testing Actions

### Acceptance Tests

- Test action invocation with valid parameters
- Test timeout scenarios
- Test error conditions
- Verify provider state changes
- Test progress reporting
- Test with custom parameters
- Test trigger-based invocation

### Test Pattern

```go
func TestAccExampleAction_basic(t *testing.T) {
    resource.ParallelTest(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        TerraformVersionChecks: []tfversion.TerraformVersionCheck{
            tfversion.SkipBelow(tfversion.Version1_14_0),
        },
        Steps: []resource.TestStep{
            {
                Config: testAccActionConfig_basic(),
                ConfigStateChecks: []statecheck.StateCheck{
                    // assert the observable effect of the action on the
                    // triggering resource
                },
            },
        },
    })
}
```

### Test Cleanup with Sweep Functions

Actions invoked in tests can leave real resources behind; register sweepers
(list → filter test-prefixed names → delete) so leaked resources are
cleanable. Sweepers are not action-specific — use the
`provider-test-patterns` skill (if available) for the sweep function
pattern, registration, `TestMain`, and dependency ordering.

### Using `terraform_data` as a No-Op Trigger

`terraform_data` can serve as a no-op trigger resource for action tests that don't need real infrastructure. This is valuable for error-case and validation tests:

```hcl
resource "terraform_data" "trigger" {
  lifecycle {
    action_trigger {
      events  = [after_create]
      actions = [action.provider_service_action.test]
    }
  }
}

action "provider_service_action" "test" {
  config {
    param = "invalid-value"
  }
}
```

### Using `PostApplyFunc` to Verify Side Effects

Actions don't produce state that can be checked with `resource.TestCheckResourceAttr`. Use `PostApplyFunc` on `resource.TestStep` to query the API after apply and confirm the action produced the expected side effect:

```go
Steps: []resource.TestStep{
    {
        Config: testConfig,
        PostApplyFunc: func() {
            // query the API to verify the action's side effect occurred
        },
    },
},
```

### Testing Best Practices

**Service-Specific Prerequisites**
- Always check for service-specific prerequisites that must be met before actions can succeed
- Document prerequisites in action documentation and test configurations

**Error Pattern Matching**
- Terraform wraps action errors with additional context
- Use flexible regex patterns: `regexp.MustCompile(\`(?s)Error Title.*key phrase\`)`

**Test Patterns Not Applicable to Actions**
1. Actions trigger on lifecycle events, not config reapplication
2. Before/After Destroy Tests: Not supported as of Terraform 1.14

### Running Tests

Compile-check first, then run the focused acceptance test:
```bash
go test -c -o /dev/null ./internal/provider
TF_ACC=1 go test ./internal/provider -run TestAccExampleAction_ -timeout 60m
```

Use the `run-acceptance-tests` skill (if available) for environment variable
setup, debugging failing tests, and sweeper runs.

## Documentation Standards

Generate action documentation with `tfplugindocs` where the provider uses
it (use the `provider-docs` skill, if available, for that workflow). Each
action documentation page must include:

1. **Front Matter** (hand-written legacy layouts only)
   ```yaml
   ---
   subcategory: "Service Name"
   layout: "provider"
   page_title: "Provider: provider_service_action"
   description: |-
     Brief description of what the action does.
   ---
   ```

2. **Header with Warnings**
   - Beta/Alpha notice about experimental status
   - Warning about potential unintended consequences
   - Link to provider documentation

3. **Example Usage**
   - Basic usage example
   - Advanced usage with all options
   - Trigger-based example with `terraform_data`
   - Real-world use case examples

4. **Argument Reference**
   - List all required and optional arguments
   - Include descriptions and defaults
   - Note any validation rules

5. **Documentation Linting** (optional tooling)
   - If the repo uses `terrafmt`, run `terrafmt fmt` before submission and
     verify with `terrafmt diff`

## Changelog Entry Format (provider-specific convention)

Some providers (e.g. terraform-provider-aws) track release notes with
[go-changelog](https://github.com/hashicorp/go-changelog): one file per PR
in a `.changelog/` directory. Check the target repo's CONTRIBUTING guide;
skip this if the repo doesn't use it.

```
.changelog/<pr_number>.txt
```

Content format:
```release-note:new-action
action/provider_service_action: Brief description of the action
```

## Pre-Submission Checklist

Before submitting your action implementation:

- [ ] Code compiles: `go build -o /dev/null .`
- [ ] Tests compile: `go test -c -o /dev/null ./internal/provider`
- [ ] Code formatted: `gofmt` (or the repo's `make fmt`)
- [ ] Documentation generated or formatted per the repo's convention
- [ ] Changelog entry created (if the repo uses one)
- [ ] Schema uses correct types
- [ ] All List/Map attributes have ElementType
- [ ] Progress updates implemented for long operations
- [ ] Error messages include context and resource identifiers
- [ ] Documentation includes multiple examples
- [ ] Documentation includes prerequisites and warnings

## References

- [Terraform Plugin Framework Documentation](https://developer.hashicorp.com/terraform/plugin/framework)
- [Terraform Provider Development](https://developer.hashicorp.com/terraform/plugin)
- [terraform-plugin-framework GitHub](https://github.com/hashicorp/terraform-plugin-framework)
- [terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing)
- [Writing a Terraform Action (blog)](https://danielmschmidt.de/posts/2025-09-26-writing-a-terraform-action/)
- Reference implementations: `terraform-provider-tfe` (`action_query_run.go`, `action_query_run_test.go`), `terraform-provider-vault` (`action_rotate_root.go`)

<!-- chapter:end slug=provider-actions -->

---

<!-- chapter:begin slug=provider-configuration position=8 -->

## 8. provider-configuration

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-configuration/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-configuration/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-configuration.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (2), referenced from this skill's directory:
  - `references/case-studies.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-configuration/references/case-studies.md
  - `references/credential-chain.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-configuration/references/credential-chain.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-configuration
description: >-
  Implement Terraform provider configuration and authentication with the
  Plugin Framework: provider schema for credentials (Optional + Sensitive
  attributes), environment variable fallbacks, credential provider chains
  (static config, then environment variables, shared credentials file, and
  platform identity), unknown-value guards in Configure(), secret redaction,
  configure-time credential validation, and diagnostics that name every
  source tried. Use when implementing or reviewing a provider's Configure
  method or provider schema, adding authentication options (API keys,
  tokens, profiles, credentials files, assume-role), deciding how a provider
  should resolve credentials, debugging "no valid credential sources" or
  missing-credentials errors, or unit testing credential resolution.
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Terraform Provider Configuration and Authentication

How a provider accepts connection settings and resolves credentials. Poor
authentication UX is the first thing every user of a provider hits; a
well-designed credential provider chain is what separates a production-grade
provider from a demo. The examples use a fictional `examplecloud` provider
and the [Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework/providers).

**References** (load when needed):
- `references/credential-chain.md` — complete, compilable credential chain
  implementation (providers, chain, file profiles, Configure wiring, tests)
- `references/case-studies.md` — how the AWS provider (`aws-sdk-go-base`)
  and smaller providers structure real credential chains

---

## Provider Schema for Authentication

Every authentication attribute must be `Optional`, never `Required` — a
`Required` attribute forces users to put credentials in configuration and
makes environment-variable and credentials-file resolution impossible. Mark
secrets `Sensitive` so Terraform redacts them in plan output, and state the
environment-variable fallback in each description so `tfplugindocs` publishes
the resolution rules.

```go
func (p *examplecloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "endpoint": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API endpoint. May also be set via the `EXAMPLECLOUD_ENDPOINT` environment variable.",
            },
            "api_key": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API key. May also be set via the `EXAMPLECLOUD_API_KEY` environment variable, or in a shared credentials file.",
            },
            "api_secret": schema.StringAttribute{
                Optional:            true,
                Sensitive:           true,
                MarkdownDescription: "API secret. May also be set via the `EXAMPLECLOUD_API_SECRET` environment variable, or in a shared credentials file.",
            },
            "profile": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Named profile in the shared credentials file. May also be set via the `EXAMPLECLOUD_PROFILE` environment variable. Defaults to `default`.",
            },
            "skip_credentials_validation": schema.BoolAttribute{
                Optional:            true,
                MarkdownDescription: "Skip the identity check normally performed during provider configuration.",
            },
        },
    }
}
```

Never add a `Default` to a credential attribute, and never hardcode a
credential anywhere in the provider. Defaults belong in the resolution logic
(where environment variables and files can override them), not in the schema.

## The Credential Provider Chain

Resolve credentials by consulting an ordered list of sources and taking the
first one that produces a **complete** set. This is the pattern the AWS
provider uses via [`aws-sdk-go-base`](https://github.com/hashicorp/aws-sdk-go-base),
and it generalizes to any provider. The canonical precedence, highest first:

1. **Static configuration** — values set directly in the `provider` block.
   Explicit always wins.
2. **Environment variables** — `EXAMPLECLOUD_API_KEY`, etc. The CI-friendly
   path.
3. **Shared credentials file** — named profiles in
   `~/.examplecloud/credentials`, for humans with multiple accounts.
4. **Platform identity** — instance metadata, workload identity, or OIDC
   token exchange, where the platform offers it. Credentials nobody has to
   store.

Two rules make the chain predictable:

- **Resolve secrets as a set, not field-by-field.** If the environment
  supplies an API key but no secret, that source offers nothing — fall
  through to the next source for *both* values. Mixing an env-var key with a
  file-profile secret produces authentication failures that are nearly
  impossible for users to debug.
- **Resolve non-secret connection settings field-by-field.** `endpoint`,
  `profile`, or `insecure` can each independently follow
  config > env > file > default, because a mismatch there is visible and
  harmless.

The core abstraction is a single-method interface with a sentinel error that
distinguishes "this source has nothing to offer" (fall through) from "this
source is misconfigured" (surface it):

```go
// ErrNoCredentials signals a source had nothing to offer. The chain falls
// through to the next source. Any other error means the source was
// configured but unusable (e.g. malformed credentials file) and is
// preserved so the final diagnostics can surface it.
var ErrNoCredentials = errors.New("no credentials found")

type Credentials struct {
    APIKey    string
    APISecret string
    Source    string // which provider supplied them, for logging
}

func (c Credentials) Complete() bool {
    return c.APIKey != "" && c.APISecret != ""
}

type Provider interface {
    Retrieve(ctx context.Context) (Credentials, error)
    Name() string
}
```

A `Chain` (itself a `Provider`, so chains compose) walks the providers in
order and returns the first complete set of credentials. Every skipped
source is recorded into an aggregate `ChainError` whose `Error()` lists each
source with the reason it was skipped, and whose `Is` method makes
`errors.Is(err, ErrNoCredentials)` true only when every source fell through
cleanly — so `Configure` can tell "nothing supplied" from "something
supplied but broken" with one check. The full implementation — the chain
loop, the static, environment, and file providers, and the
`NewDefaultChain` constructor that owns the canonical order — lives in
`references/credential-chain.md`.

## Wiring the Chain into Configure

`Configure` runs once per Terraform operation, before any resource CRUD.
The shape:

```go
func (p *examplecloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config examplecloudProviderModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    if resp.Diagnostics.HasError() {
        return
    }

    // 1. Guard against unknown values (e.g. api_key = some_resource.output).
    if config.APIKey.IsUnknown() {
        resp.Diagnostics.AddAttributeError(
            path.Root("api_key"),
            "Unknown API Key",
            "The provider cannot connect because api_key depends on a value known only after apply. "+
                "Set a static value, or use the EXAMPLECLOUD_API_KEY environment variable.",
        )
    }
    // ... repeat for each auth attribute, then:
    if resp.Diagnostics.HasError() {
        return
    }

    // 2. Resolve credentials through the chain.
    chain := credentials.NewDefaultChain(
        config.APIKey.ValueString(),
        config.APISecret.ValueString(),
        credentials.Options{Profile: config.Profile.ValueString()},
    )
    creds, err := chain.Retrieve(ctx)
    if err != nil {
        if errors.Is(err, credentials.ErrNoCredentials) {
            resp.Diagnostics.AddError(
                "No Valid Credential Sources Found",
                "No examplecloud credentials were found. Sources tried, in order:\n\n"+err.Error()+
                    "\n\nSet api_key and api_secret in the provider block, export "+
                    "EXAMPLECLOUD_API_KEY and EXAMPLECLOUD_API_SECRET, or add a profile to "+
                    "~/.examplecloud/credentials. See https://example.com/docs/auth.",
            )
        } else {
            resp.Diagnostics.AddError("Failed to Resolve Credentials", err.Error())
        }
        return
    }
    tflog.Debug(ctx, "resolved credentials", map[string]any{"source": creds.Source})

    // 3. Build the client once; share it with every resource and data source.
    client := examplecloud.NewClient(endpoint, creds.APIKey, creds.APISecret)
    resp.DataSourceData = client
    resp.ResourceData = client
}
```

Why each step matters:

- **Unknown-value guards.** During planning, an attribute wired to another
  resource's output is *unknown*, not null. Without the guard the provider
  silently treats it as empty, falls through the chain, and authenticates as
  the wrong identity — or fails with a misleading "missing credentials"
  error. Name the environment-variable workaround in the guard message.
- **The sentinel check picks the right message.** "You gave me nothing"
  (actionable list of options) is a different failure from "you gave me
  something broken" (show the parse error). Collapsing them into one message
  is how providers end up with users pasting secrets into config to debug.
- **Log the source, never the secret.** Knowing *which* source won is the
  single most useful debugging fact and costs nothing to log.

## Diagnostics That Unblock Users

An authentication error message is the provider's most-read documentation.
Every credential failure diagnostic should name:

- **Every source tried, in order, with why it was skipped** — the
  `ChainError` provides this. `aws-sdk-go-base` does the same with its
  `NoValidCredentialSourcesError`.
- **The exact environment variable names** and the credentials file path and
  profile that were consulted — not "set the appropriate environment
  variables".
- **A documentation URL** for the provider's authentication guide.

Use warnings (not errors) for conditions that are suspicious but not fatal,
naming what took precedence: a `profile` set while environment credentials
are also present (which wins?), or a credentials file with group/world-read
permissions (suggest `chmod 0600`).

## Secret Hygiene

- Give the `Credentials` type `String()` and `GoString()` methods that
  redact secret fields, so a stray `%v`, `%+v`, or error wrap can never leak
  a secret into logs or diagnostics.
- Never include credential *values* in diagnostics, log lines, or wrapped
  errors — log the source name and non-secret identifiers only.
- Warn when a credentials file is readable by other users
  (`info.Mode().Perm()&0o077 != 0`); skip this check on Windows, where POSIX
  permission bits are not meaningful.

## Configure-Time Validation

Resolve the chain eagerly in `Configure` — never lazily on first resource
use — so a credentials problem fails one time, at plan, with a good message,
instead of failing in the middle of an apply. If the API has a cheap
identity endpoint (the equivalent of AWS `sts:GetCallerIdentity` or a
`/whoami`), call it after resolving credentials so *invalid* (not just
missing) credentials also fail at configure time. Gate it behind a
`skip_credentials_validation` attribute for air-gapped or stubbed
environments.

## Unit Testing the Chain

The chain is pure logic — test it with unit tests (`Test` prefix, no
`TF_ACC`), not acceptance tests. Make the environment injectable (a
`getenv func(string) string` field defaulting to `os.Getenv`, or use
`t.Setenv`) and point the file provider at `t.TempDir()` fixtures. The
tests that matter:

- **Per-source**: each provider returns its credentials when set and
  `ErrNoCredentials` when incomplete (a key with no secret is incomplete).
- **Precedence**: static beats env; env beats file; chain falls through to
  the file when nothing above supplies a complete set.
- **Failure aggregation**: with all sources empty,
  `errors.Is(err, ErrNoCredentials)` is true and the message names every
  source.
- **Hard errors**: a malformed credentials file or an *explicitly requested*
  profile that does not exist surfaces a descriptive error rather than
  silently falling through (a merely defaulted profile falls through).
- **Redaction**: `fmt.Sprintf("%v")` and `%+v` of a `Credentials` value
  never contain the secret.

Full test examples are in `references/credential-chain.md`.

## Checklist

- [ ] All auth attributes `Optional`; secrets marked `Sensitive: true`
- [ ] Attribute descriptions name their environment-variable fallbacks
- [ ] Unknown-value guards on every auth attribute in `Configure`
- [ ] Chain precedence: static config > env vars > credentials file > platform identity
- [ ] Secrets resolved as a complete set; non-secret settings field-by-field
- [ ] Sentinel `ErrNoCredentials` distinguishes fall-through from hard failure
- [ ] Missing-credentials diagnostic lists every source tried + docs URL
- [ ] `Credentials` type redacts secrets in `String()`/`GoString()`
- [ ] Credentials-file permission warning (non-Windows)
- [ ] Eager resolution in `Configure`; optional identity check with `skip_credentials_validation`
- [ ] Unit tests cover per-source behavior, precedence, aggregation, redaction
- [ ] No credential value ever logged or embedded in an error

## Related Skills

Use the `new-terraform-provider` skill (if available) to scaffold the
provider this configuration lives in, and the `provider-resources` skill for
consuming the configured client from resources and data sources.

<!-- chapter:end slug=provider-configuration -->

---

<!-- chapter:begin slug=provider-docs position=9 -->

## 9. provider-docs

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-docs/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-docs/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-docs.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (2), referenced from this skill's directory:
  - `agents/openai.yaml` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-docs/agents/openai.yaml
  - `references/hashicorp-provider-docs.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-docs/references/hashicorp-provider-docs.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-docs
description: Create, update, and review Terraform provider documentation for Terraform Registry using HashiCorp-recommended patterns, tfplugindocs templates, and schema descriptions. Use when adding or changing provider configuration, resources, data sources, ephemeral resources, list resources, functions, actions, or guides; when validating generated docs; and when troubleshooting missing or incorrect Registry documentation.
metadata:
  lifecycle-status: active
---

# Terraform Provider Docs

## Follow This Workflow

1. Confirm scope and documentation targets.
- Map code changes to the exact doc targets: provider index, resources, data sources, ephemeral resources, list resources, functions, actions, or guides.
- Decide whether content should come from schema descriptions, templates, or both.

2. Write schema descriptions first.
- Add precise user-facing descriptions to schema fields so generated docs stay aligned with behavior.
- Keep wording specific to argument purpose, constraints, defaults, and computed behavior.

3. Add or update template files in `docs/`.
- Create only files that map to implemented provider objects.
- Use HashiCorp-recommended template paths:
  - `docs/index.md.tmpl`
  - `docs/data-sources/<name>.md.tmpl`
  - `docs/resources/<name>.md.tmpl`
  - `docs/ephemeral-resources/<name>.md.tmpl`
  - `docs/list-resources/<name>.md.tmpl`
  - `docs/functions/<name>.md.tmpl`
  - `docs/actions/<name>.md.tmpl` (tfplugindocs generates action docs with Terraform v1.14.0+)
  - `docs/guides/<name>.md.tmpl`
- Keep templates focused on overview and examples; rely on generated sections for field-by-field details.
- Keep HCL examples in the `examples/` directory — one example per file, pulled into templates with `tffile` — rather than inlined in templates (see Example File Conventions in `references/hashicorp-provider-docs.md`). Examples must not contain `terraform`, `provider`, or `output` blocks.
- For action pages, follow the structure in `references/hashicorp-provider-docs.md` (Action Pages section): examples must show both the `action` block and the `action_trigger` lifecycle wiring, and actions get no attribute/output section.

4. Generate documentation with `tfplugindocs`.
- Prefer repository defaults when configured:
```bash
go generate ./...
```
- Otherwise run the generator directly:
```bash
go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-name <provider_name>
```
- Re-run generation after every schema or template edit.

5. Validate the generated markdown.
- Verify files in `docs/` match the current provider implementation.
- Verify examples are valid HCL and reflect current argument/attribute names.
- Verify required/optional/computed semantics in docs match schema behavior.

6. Apply Registry publication rules before release.
- Use semantic version tags prefixed with `v` (for example `v1.2.3`).
- Create release tags from the default branch.
- Keep `terraform-registry-manifest.json` in the repository root.
- Expect docs to be versioned in Registry and switchable with the version selector.

7. Preview or troubleshoot publication when needed.
- Use the HashiCorp preview process to inspect rendered docs before release when accuracy risk is high.
- If docs are missing in Registry, check tag format, tag source branch, manifest file presence, and provider publication status.

## Enforce Quality Bar

- Keep documentation behaviorally accurate; never describe unsupported arguments or attributes.
- Keep examples minimal, realistic, and runnable.
- Keep terminology and naming consistent across provider, resources, and data sources.
- Avoid duplicating generated argument/attribute blocks in manual templates.
- Keep doc changes tied to the same PR as schema/API changes whenever possible.

## Load References On Demand

- Read `references/hashicorp-provider-docs.md` for source-backed rules and official links.
- Load only the sections needed for the current change to keep context lean.

<!-- chapter:end slug=provider-docs -->

---

<!-- chapter:begin slug=provider-ephemeral-resources position=10 -->

## 10. provider-ephemeral-resources

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-ephemeral-resources/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-ephemeral-resources/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-ephemeral-resources.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-ephemeral-resources
description: >-
  Implement Terraform provider ephemeral resources with the Plugin
  Framework: the Open/Renew/Close lifecycle, ephemeral schema design,
  registration via EphemeralResources, renewal for expiring credentials,
  and how ephemeral values flow into write-only attributes and provider
  configuration. Use when adding an ephemeral resource, exposing
  secrets/tokens/certificates that must never persist in state or plan,
  deciding between an ephemeral resource and a data source, or wiring
  short-lived credentials from one provider into another.
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Terraform Provider Ephemeral Resources

Ephemeral resources (Terraform 1.10+) produce values that are **never
persisted to state or plan**. They exist for exactly one job: handing
secrets — tokens, generated passwords, short-lived certificates, decrypted
values — to the parts of a configuration that need them, without writing
them to disk. Any data source that returns a sensitive value is a candidate
to be (or to also exist as) an ephemeral resource.

Official docs: [Ephemeral Resources](https://developer.hashicorp.com/terraform/plugin/framework/ephemeral-resources).

## When to Use One

| Situation | Use |
|---|---|
| Read-only lookup of non-sensitive data | Data source |
| Value is sensitive and only needed at apply time (DB password for a provider block, token for a write-only attribute) | Ephemeral resource |
| Sensitive value that downstream *managed resources* must store (e.g. as an attribute) | Regular resource/data source — but pair with write-only attributes where possible |
| Credential that expires mid-operation (STS-style tokens, short-TTL leases) | Ephemeral resource with `Renew` |

Ephemeral results can be used in provider configuration, write-only
attributes, provisioner configuration, and other ephemeral contexts — but
not in regular attributes, because those persist to state.

## Lifecycle

Terraform calls up to three methods per operation:

- **`Open`** (required) — fetch or create the value; runs during plan
  and/or apply whenever the result is needed. There is no state to refresh
  and nothing to import.
- **`Renew`** (optional) — called when the wall clock passes the
  `RenewAt` returned by `Open`/`Renew`, for values that expire while
  Terraform is still running. Renew cannot return a new result — it can
  only extend/refresh what `Open` produced (e.g. re-lease the same
  credential); if the value itself changes on renewal, the API is not
  renewable in this sense and `Open` must return a longer-lived value.
- **`Close`** (optional) — called when Terraform is done with the value;
  revoke leases or delete temporary credentials here.

`Open` can pass bytes forward via `resp.Private`; `Renew` and `Close`
receive them — use this for lease IDs needed to renew/revoke.

## Implementation

```go
var (
    _ ephemeral.EphemeralResource              = &tokenEphemeralResource{}
    _ ephemeral.EphemeralResourceWithConfigure = &tokenEphemeralResource{}
    _ ephemeral.EphemeralResourceWithRenew     = &tokenEphemeralResource{}
    _ ephemeral.EphemeralResourceWithClose     = &tokenEphemeralResource{}
)

func NewTokenEphemeralResource() ephemeral.EphemeralResource {
    return &tokenEphemeralResource{}
}

type tokenEphemeralResource struct {
    client *examplecloud.Client
}

type tokenEphemeralResourceModel struct {
    RoleName types.String `tfsdk:"role_name"`
    Token    types.String `tfsdk:"token"`
    LeaseID  types.String `tfsdk:"lease_id"`
}

func (r *tokenEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
    resp.TypeName = req.ProviderTypeName + "_token"
}

func (r *tokenEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "role_name": schema.StringAttribute{
                Required:            true,
                MarkdownDescription: "Role to obtain a token for.",
            },
            "token": schema.StringAttribute{
                Computed:            true,
                Sensitive:           true,
                MarkdownDescription: "The issued token. Never persisted to state.",
            },
            "lease_id": schema.StringAttribute{
                Computed:            true,
                MarkdownDescription: "Identifier of the token lease.",
            },
        },
    }
}

func (r *tokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
    var data tokenEphemeralResourceModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    lease, err := r.client.IssueToken(ctx, data.RoleName.ValueString())
    if err != nil {
        resp.Diagnostics.AddError(
            "Error opening Token",
            fmt.Sprintf("issuing token for role (%s): %s", data.RoleName.ValueString(), err),
        )
        return
    }

    data.Token = types.StringValue(lease.Token)
    data.LeaseID = types.StringValue(lease.ID)

    resp.RenewAt = lease.ExpiresAt.Add(-2 * time.Minute) // renew with margin
    resp.Private.SetKey(ctx, "lease_id", []byte(lease.ID))
    resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}

func (r *tokenEphemeralResource) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {
    leaseID, diags := req.Private.GetKey(ctx, "lease_id")
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }

    lease, err := r.client.RenewLease(ctx, string(leaseID))
    if err != nil {
        resp.Diagnostics.AddError("Error renewing Token", err.Error())
        return
    }
    resp.RenewAt = lease.ExpiresAt.Add(-2 * time.Minute)
}

func (r *tokenEphemeralResource) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {
    leaseID, diags := req.Private.GetKey(ctx, "lease_id")
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }

    if err := r.client.RevokeLease(ctx, string(leaseID)); err != nil {
        resp.Diagnostics.AddError("Error closing Token", err.Error())
    }
}
```

`Configure` follows the same ProviderData-cast pattern as resources (the
`provider-resources` skill, if available, shows it); the client comes from
`resp.EphemeralResourceData` set in the provider's `Configure`.

## Registration

The provider opts in via `provider.ProviderWithEphemeralResources`:

```go
var _ provider.ProviderWithEphemeralResources = &examplecloudProvider{}

func (p *examplecloudProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
    return []func() ephemeral.EphemeralResource{
        NewTokenEphemeralResource(),
    }
}
```

Set `resp.EphemeralResourceData = client` in the provider's `Configure`
alongside `ResourceData`/`DataSourceData`.

## Design Rules

- **Never log the value, never put it in a diagnostic.** The whole point is
  non-persistence; an error message containing the token defeats it.
- Mark the secret attribute `Sensitive: true` anyway — it guards rendering
  in the ephemeral value's own lifecycle output.
- No plan modifiers, no import, no `id` convention — there is no state for
  any of them to act on.
- Schema inputs follow the same rules as data source arguments; expose the
  API's identifiers (`role_name`), not invented ones.
- Set `RenewAt` with a safety margin before the real expiry; Terraform
  renews lazily, not on a precise timer.
- If the upstream value cannot be revoked, skip `Close` rather than
  implementing a no-op that suggests revocation happens.

## Testing

Ephemeral results never reach state, so tests assert them indirectly — the
standard pattern echoes the ephemeral value through the `echoprovider` into
a regular resource the test can inspect. Minimum coverage: a basic
open-and-use test and per-attribute tests alongside required fields. Use
the `provider-test-patterns` skill (if available) — its ephemeral testing
reference covers the echoprovider setup, version gating
(`tfversion.SkipBelow(tfversion.Version1_10_0)`), and multi-step patterns.

## Documentation

Registry docs live at `docs/ephemeral-resources/<name>.md`, generated by
`tfplugindocs` like every other page type. Use the `provider-docs` skill
(if available) for the workflow; document the renewal/revocation behavior
explicitly — users need to know whether closing their Terraform run revokes
the credential.

## Checklist

- [ ] Value genuinely must not persist (otherwise a data source is simpler)
- [ ] `Open` implemented; `Renew`/`Close` only where the API supports them
- [ ] Secret attributes `Sensitive: true`; value never logged or in diagnostics
- [ ] Lease/handle passed via `Private`, not via the result
- [ ] `RenewAt` set with margin for expiring credentials
- [ ] Registered in `EphemeralResources()`; `EphemeralResourceData` set in provider Configure
- [ ] Echo-provider acceptance tests, version-gated to Terraform >= 1.10
- [ ] Docs page explains lifetime, renewal, and revocation behavior

<!-- chapter:end slug=provider-ephemeral-resources -->

---

<!-- chapter:begin slug=provider-framework-migration position=11 -->

## 11. provider-framework-migration

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-framework-migration/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-framework-migration/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-framework-migration.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (1), referenced from this skill's directory:
  - `references/schema-mapping.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-framework-migration/references/schema-mapping.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-framework-migration
description: >-
  Migrate Terraform provider resources and data sources from Plugin SDKv2 to
  the Plugin Framework: muxing both plugins in one provider
  (terraform-plugin-mux, tf5to6server), per-resource migration workflow,
  SDKv2-to-Framework schema mapping (ForceNew, ValidateFunc,
  DiffSuppressFunc, Default, Timeouts, blocks), null-vs-zero-value
  behavioral traps, and state-compatibility verification. Use when
  converting or translating SDKv2 resources to the Framework, setting up a
  muxed provider server, deciding whether a resource should be migrated at
  all, or debugging plan diffs and state errors that appeared after a
  migration.
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Migrating from Plugin SDKv2 to the Plugin Framework

The Plugin Framework is required for net-new resources and data sources;
SDKv2 is maintenance-only. Migration is **per-resource and incremental**: a
muxed provider serves SDKv2 and Framework implementations side by side, so
you never need a big-bang rewrite. This skill covers the mux setup, the
per-resource workflow, and the behavioral traps that turn a mechanical
translation into a silent breaking change.

**Reference** (load when needed):
- `references/schema-mapping.md` — the full SDKv2 → Framework translation
  table with code pairs

Official guide: [Framework migration](https://developer.hashicorp.com/terraform/plugin/framework/migrating).

## Decide Whether to Migrate at All

Migration has real risk and little user-visible payoff, so triage first:

- **Do not migrate complex or heavily-used resources** without a driving
  need (a Framework-only feature, a bug that SDKv2 cannot fix). The two
  SDKs differ behaviorally — most importantly around null versus zero
  values — and those differences surface as breaking changes for existing
  users. This is the standing policy in large providers like
  terraform-provider-aws.
- **Simple resources migrate safely**: flat schemas, no `DiffSuppressFunc`,
  no `CustomizeDiff`, no `StateFunc`, no complex nested blocks.
- New capabilities never require migrating old code — mux and write the new
  resource in the Framework alongside the old ones.

To tell what mode a provider is in, check `go.mod`: `terraform-plugin-mux`
present means it already serves both; only `terraform-plugin-sdk/v2` means
SDKv2-only (mux setup is your first step); only
`terraform-plugin-framework` means the migration is done.

## Step 1: Mux the Provider

Combine both plugin servers in `main.go`. Serving protocol version 6
requires upgrading the SDKv2 server with `tf5to6server` (protocol 6 needs
Terraform CLI >= 1.0; if you must support 0.12+, mux at protocol 5 with
`tf6to5server`/`tf5muxserver` instead — but the Framework provider then
cannot use protocol-6-only features like nested attributes):

```go
package main

import (
    "context"
    "flag"
    "log"

    "github.com/hashicorp/terraform-plugin-framework/providerserver"
    "github.com/hashicorp/terraform-plugin-go/tfprotov6"
    "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server"
    "github.com/hashicorp/terraform-plugin-mux/tf5to6server"
    "github.com/hashicorp/terraform-plugin-mux/tf6muxserver"

    "example.org/terraform-provider-examplecloud/internal/provider"
    sdkprovider "example.org/terraform-provider-examplecloud/internal/sdkprovider"
)

func main() {
    var debug bool
    flag.BoolVar(&debug, "debug", false, "run with support for debuggers")
    flag.Parse()

    ctx := context.Background()

    upgradedSDKServer, err := tf5to6server.UpgradeServer(
        ctx,
        sdkprovider.Provider().GRPCProvider,
    )
    if err != nil {
        log.Fatal(err)
    }

    providers := []func() tfprotov6.ProviderServer{
        providerserver.NewProtocol6(provider.New(version)()),
        func() tfprotov6.ProviderServer { return upgradedSDKServer },
    }

    muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...)
    if err != nil {
        log.Fatal(err)
    }

    var serveOpts []tf6server.ServeOpt
    if debug {
        serveOpts = append(serveOpts, tf6server.WithManagedDebug())
    }

    err = tf6server.Serve("registry.terraform.io/example/examplecloud",
        muxServer.ProviderServer, serveOpts...)
    if err != nil {
        log.Fatal(err)
    }
}
```

Mux requirements that bite in practice:

- **Provider schemas must match exactly** across both plugins — same
  provider-level attributes, same types, same descriptions. Keep one source
  of truth for the provider configuration and mirror it.
- **Each resource and data source may exist in only one** of the two
  plugins. Migration's final step is deleting the SDKv2 registration.
- If publishing to the Registry with protocol 6, set
  `"metadata": {"protocol_versions": ["6.0"]}` in
  `terraform-registry-manifest.json`.

## Step 2: Baseline Before You Touch Anything

The migrated resource must be indistinguishable to users. Prove it with
tests that exist *before* the migration:

1. Ensure the resource has passing acceptance coverage: `_basic` with an
   import step (`ImportStateVerify: true`), `_disappears`, and per-attribute
   update tests. If coverage is missing, write it against the SDKv2
   implementation first — these tests are the migration's acceptance
   criteria and must pass **unchanged** afterward.
2. Note behaviors tests don't capture: attribute defaults, what happens
   when optional attributes are omitted (null vs `""`/`0`/`false` is about
   to matter), and any `DiffSuppressFunc`/`StateFunc` normalization.

## Step 3: Port the Resource

Translate schema and CRUD using the mapping table in
`references/schema-mapping.md`. The rules that prevent breaking changes:

- **Blocks stay blocks.** An SDKv2 `Elem: &schema.Resource{...}` written as
  `block { ... }` syntax in user configs must become a Framework **Block**
  (`schema.ListNestedBlock`/`SetNestedBlock`) — converting it to a nested
  *attribute* changes the HCL syntax users must write, which is a breaking
  change. Nested attributes are for new schema only.
- **Null is not zero.** SDKv2 `d.Get("name")` returned `""` for unset;
  the Framework model gives you `types.String` that distinguishes null,
  unknown, and `""`. Everywhere the old code checked `== ""` or relied on
  `GetOk`, decide explicitly what null means, and make sure you send the
  API the same thing SDKv2 sent (usually: omit the field when null).
- **Keep the `id` attribute.** Net-new Framework resources may omit a
  redundant `id`, but a *migrated* resource must keep its exact schema —
  removing or renaming attributes breaks existing state and configs.
- **State must round-trip.** The Framework reads the state SDKv2 wrote. If
  every attribute keeps its name and type, no state upgrade is needed. If
  the old schema stored a value the new types package normalizes
  differently, you need a `StateUpgrader` — treat that as a signal the
  resource may be in the do-not-migrate bucket.

## Step 4: Move the Registration

Register the resource in the Framework provider's `Resources()` and delete
it from the SDKv2 provider's `ResourcesMap` in the same commit — mux errors
on duplicates.

## Step 5: Verify

1. The pre-existing acceptance tests pass **without modification** —
   especially `ImportStateVerify`, which diffs imported state against
   stored state and catches most null-vs-zero regressions.
2. Add a state-compatibility step: apply a config with the last released
   (SDKv2) provider version, then plan with the migrated build — the plan
   must be empty. In `terraform-plugin-testing` this is a two-step test
   using `ExternalProviders` for the old version, then
   `ProtoV6ProviderFactories` with `ConfigPlanChecks` asserting an empty
   plan. The `provider-test-patterns` skill (if available) documents the
   pattern.
3. `terraform plan` against a real pre-migration state file shows no diff.

## Checklist

- [ ] Resource is simple enough to migrate (no complex diff customization), or there's a driving need
- [ ] Mux serves both plugins; provider-level schemas identical in both
- [ ] Acceptance tests existed before migration and pass unchanged after
- [ ] Blocks remained blocks; attribute names and types unchanged; `id` kept
- [ ] Null/omitted semantics preserved (API receives what SDKv2 sent)
- [ ] SDKv2 registration removed in the same change
- [ ] Empty-plan verified against state written by the previous release
- [ ] Changelog entry added, if the repo tracks release notes

## Related Skills

Use the `provider-resources` skill (if available) for Framework CRUD,
finder, and waiter patterns in the ported code, and `provider-test-patterns`
for the regression and version-upgrade test patterns.

<!-- chapter:end slug=provider-framework-migration -->

---

<!-- chapter:begin slug=provider-resources position=12 -->

## 12. provider-resources

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-resources/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-resources/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-resources.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (2), referenced from this skill's directory:
  - `references/design-principles.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-resources/references/design-principles.md
  - `references/retries-and-waiters.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-resources/references/retries-and-waiters.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-resources
description: >-
  Implement Terraform Provider resources and data sources using the Plugin
  Framework: CRUD operations, schema design, plan modifiers and validators,
  not-found handling, waiters for eventually consistent APIs, import support,
  resource design principles, and required acceptance test coverage. Use when
  adding or changing a resource or data source, deciding whether an API
  concept should be a resource, wiring a resource to the provider's
  configured client, handling drift or resource-not-found, or reviewing a
  resource implementation before submission.
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Terraform Provider Resources Implementation Guide

## Overview

This guide covers developing Terraform Provider resources and data sources.
Resources represent infrastructure objects that Terraform manages through
Create, Read, Update, and Delete (CRUD) operations.

**Use the [Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework)
for all net-new resources and data sources.** Plugin SDKv2 is for maintaining
resources that already exist on it; do not write new code against it. A
provider can serve both during migration by muxing
([terraform-plugin-mux](https://developer.hashicorp.com/terraform/plugin/mux)),
so adopting the Framework never requires a big-bang rewrite. To tell which
mode an existing provider is in, check `go.mod`: `terraform-plugin-mux`
present means it serves both SDKv2 and Framework code; only
`terraform-plugin-sdk/v2` means SDKv2-only; only
`terraform-plugin-framework` means Framework-only. Be cautious about
*migrating* existing SDKv2 resources: the Framework distinguishes null from
zero values, so naive migrations change behavior for existing users (use the
`provider-framework-migration` skill, if available).

**References** (load when needed):
- `references/design-principles.md` — what should (and should not) become a
  resource; data source semantics; relationship and async-task modeling
- `references/retries-and-waiters.md` — eventual consistency, retry
  patterns, and status/wait function structure

## File Structure

Most providers keep every resource in a single package:

```
internal/provider/
├── provider.go                  # Provider schema + Configure
├── widget_resource.go           # Resource implementation
├── widget_resource_test.go      # Acceptance tests
├── widget_data_source.go        # Data source (if applicable)
└── widget_data_source_test.go
```

Large multi-service providers (e.g. terraform-provider-aws) split into
`internal/service/<service>/` packages instead, with an idiomatic file
taxonomy worth adopting once a package grows: `consts.go`, `find.go`
(finders), `status.go` (status functions), `wait.go` (waiters), `sweep.go`
(test sweepers), `exports_test.go`.

Documentation lives in `docs/` and is generated with `tfplugindocs`:

```
docs/
├── resources/<name>.md          # generated; optional <name>.md.tmpl template
└── data-sources/<name>.md
```

(Hand-written `website/docs/r/*.html.markdown` trees exist in some older,
large providers — follow the target repo's convention when editing one.)

## Resource Structure

A Framework resource is a struct holding the API client, with interface
assertions making the implemented behaviors explicit:

```go
var (
    _ resource.Resource                = &widgetResource{}
    _ resource.ResourceWithConfigure   = &widgetResource{}
    _ resource.ResourceWithImportState = &widgetResource{}
)

func NewWidgetResource() resource.Resource {
    return &widgetResource{}
}

type widgetResource struct {
    client *examplecloud.Client
}

func (r *widgetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
    resp.TypeName = req.ProviderTypeName + "_widget"
}

// Configure receives the client the provider built in its own Configure.
func (r *widgetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    if req.ProviderData == nil {
        return // provider not yet configured (e.g. validation phase)
    }
    client, ok := req.ProviderData.(*examplecloud.Client)
    if !ok {
        resp.Diagnostics.AddError(
            "Unexpected Resource Configure Type",
            fmt.Sprintf("Expected *examplecloud.Client, got: %T.", req.ProviderData),
        )
        return
    }
    r.client = client
}

func (r *widgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "name": schema.StringAttribute{
                Required: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.RequiresReplace(),
                },
                Validators: []validator.String{
                    stringvalidator.LengthBetween(1, 255),
                },
            },
            "id": schema.StringAttribute{
                Computed: true,
                PlanModifiers: []planmodifier.String{
                    stringplanmodifier.UseStateForUnknown(),
                },
            },
        },
    }
}
```

How the provider's `Configure` produces that client — schema, credential
resolution, validation — is covered by the `provider-configuration` skill
(if available).

**On `id`:** SDKv2 required a magic `id` attribute; the Framework does not.
If the API has its own identifier, expose it under its real meaning and do
not add a second, redundant `id`. Only keep `id` when it *is* the API's
identifier (as above).

## CRUD Operations

### Create

```go
func (r *widgetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var data widgetResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    input := &examplecloud.CreateWidgetInput{
        Name: data.Name.ValueStringPointer(),
    }

    output, err := r.client.CreateWidget(ctx, input)
    if err != nil {
        resp.Diagnostics.AddError(
            "Error creating Widget",
            fmt.Sprintf("creating Widget (%s): %s", data.Name.ValueString(), err),
        )
        return
    }

    data.ID = types.StringPointerValue(output.ID)

    // For eventually consistent APIs, wait for the resource to be usable
    // before returning — see references/retries-and-waiters.md.

    resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
```

### Read

Read must handle out-of-band deletion by removing the resource from state so
the next plan recreates it, rather than erroring forever:

```go
func (r *widgetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var data widgetResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    output, err := findWidgetByID(ctx, r.client, data.ID.ValueString())
    if isNotFound(err) {
        tflog.Warn(ctx, "Widget not found, removing from state", map[string]any{"id": data.ID.ValueString()})
        resp.State.RemoveResource(ctx)
        return
    }
    if err != nil {
        resp.Diagnostics.AddError(
            "Error reading Widget",
            fmt.Sprintf("reading Widget (%s): %s", data.ID.ValueString(), err),
        )
        return
    }

    data.Name = types.StringPointerValue(output.Name)

    resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
```

### Update

Only call the API for attributes that actually changed; compare plan against
state:

```go
func (r *widgetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan, state widgetResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    if resp.Diagnostics.HasError() {
        return
    }

    if !plan.Description.Equal(state.Description) {
        input := &examplecloud.UpdateWidgetInput{
            ID:          plan.ID.ValueStringPointer(),
            Description: plan.Description.ValueStringPointer(),
        }
        if _, err := r.client.UpdateWidget(ctx, input); err != nil {
            resp.Diagnostics.AddError(
                "Error updating Widget",
                fmt.Sprintf("updating Widget (%s): %s", plan.ID.ValueString(), err),
            )
            return
        }
    }

    resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
```

### Delete

Treat "already gone" as success — the desired end state is reached:

```go
func (r *widgetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    var data widgetResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    _, err := r.client.DeleteWidget(ctx, &examplecloud.DeleteWidgetInput{
        ID: data.ID.ValueStringPointer(),
    })
    if isNotFound(err) {
        return
    }
    if err != nil {
        resp.Diagnostics.AddError(
            "Error deleting Widget",
            fmt.Sprintf("deleting Widget (%s): %s", data.ID.ValueString(), err),
        )
        return
    }
}
```

### Import

With `ResourceWithImportState` asserted, passthrough of the identifier is
one line:

```go
func (r *widgetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
    resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
```

For multi-part identifiers, parse a delimited import ID (commonly
comma-separated) and set each attribute explicitly.

## Resource Design Principles

Before implementing, check the shape of the thing being modeled (full
treatment in `references/design-principles.md`):

- A resource is the *smallest* useful building block; if the API offers
  CRUD for it, it likely deserves its own resource.
- A resource should talk to **one** API/service only — cross-service
  resources break permissions, auditing, and endpoint configuration.
- Data sources are read-only and side-effect free. A *singular* data source
  errors on zero or multiple matches; a *plural* data source (plural noun
  name) returns zero-or-more as a collection and errors on neither.
- Attached policies/rules, long-running task invocations, and versioned
  artifacts usually deserve their *own* resources rather than attributes on
  the parent.
- Start/stop or enable/disable state belongs as an attribute *in* the
  resource, not as a separate resource.

## Schema Design

### Attribute Types

| Terraform Type | Framework Type | Use Case |
|----------------|----------------|----------|
| `string` | `schema.StringAttribute` | Names, identifiers |
| `number` | `schema.Int64Attribute`, `schema.Float64Attribute` | Counts, sizes |
| `bool` | `schema.BoolAttribute` | Feature flags |
| `list` | `schema.ListAttribute` | Ordered collections |
| `set` | `schema.SetAttribute` | Unordered unique items |
| `map` | `schema.MapAttribute` | Key-value pairs |
| `object` | `schema.SingleNestedAttribute` | Complex nested config |

Give every attribute a `MarkdownDescription` — `tfplugindocs` publishes it,
and it is the primary user-facing documentation.

### Plan Modifiers

```go
// Force replacement when value changes
stringplanmodifier.RequiresReplace()

// Keep a known value during plan instead of (known after apply)
stringplanmodifier.UseStateForUnknown()
```

### Validators

```go
stringvalidator.LengthBetween(1, 255)
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-]+$`), "must be lowercase alphanumeric with hyphens")
stringvalidator.OneOf("small", "medium", "large")
int64validator.Between(1, 100)
listvalidator.SizeAtLeast(1)
```

### Sensitive Attributes

```go
"password": schema.StringAttribute{
    Required:  true,
    Sensitive: true,
},
```

## State Management

### Finders

Centralize "get one thing or a typed not-found" in a finder so Read, Delete,
waiters, and tests all share identical not-found semantics:

```go
func findWidgetByID(ctx context.Context, client *examplecloud.Client, id string) (*examplecloud.Widget, error) {
    output, err := client.GetWidget(ctx, &examplecloud.GetWidgetInput{ID: &id})
    if err != nil {
        var apiErr *examplecloud.NotFoundError
        if errors.As(err, &apiErr) {
            return nil, &retry.NotFoundError{LastError: err}
        }
        return nil, fmt.Errorf("getting Widget (%s): %w", id, err)
    }
    if output == nil || output.Widget == nil {
        return nil, &retry.NotFoundError{Message: "empty result"}
    }
    return output.Widget, nil
}

func isNotFound(err error) bool {
    var nfe *retry.NotFoundError
    return errors.As(err, &nfe)
}
```

### Waiting for Resource States

Many APIs return from Create/Delete before the resource is usable/gone. Use
`retry.StateChangeConf` (from
`github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry` — usable from
Framework providers), with a status function built on the finder and
timeouts in named constants:

```go
stateConf := &retry.StateChangeConf{
    Pending: []string{"CREATING", "PENDING"},
    Target:  []string{"ACTIVE"},
    Refresh: statusWidget(ctx, r.client, id), // one poll of the finder: (obj, status, err)
    Timeout: widgetCreatedTimeout,
}
outputRaw, err := stateConf.WaitForStateContext(ctx)
```

The full status/wait function pairs (create and delete waiters, failure-state
handling, post-create not-found retries, eventual-consistency patterns) are
in `references/retries-and-waiters.md` — read it whenever the API is
asynchronous or eventually consistent.

## Testing

Every resource ships with, at minimum:

- **`_basic`** — create with minimal config, assert attributes, then an
  import step (`ImportState: true`, `ImportStateVerify: true`)
- **`_disappears`** — delete the object out-of-band mid-test; the next plan
  must propose recreation, not error
- **Per-attribute tests** — exercise updates for each non-trivial argument

Naming grammar: tests `TestAcc{Resource}_{group?}_{description}`, helpers
`testAccCheck{Resource}Exists` / `testAccCheck{Resource}Destroy`, config
functions `testAcc{Resource}Config_{description}`. Keep configs
self-contained, randomize real resource names, and never hardcode
environment-specific values (account IDs, zones, versions).

```go
func TestAccWidget_basic(t *testing.T) {
    rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
    resourceName := "examplecloud_widget.test"

    resource.ParallelTest(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckWidgetDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccWidgetConfig_basic(rName),
                ConfigStateChecks: []statecheck.StateCheck{
                    statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(rName)),
                    statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("id"), knownvalue.NotNull()),
                },
            },
            {
                ResourceName:      resourceName,
                ImportState:       true,
                ImportStateVerify: true,
            },
        },
    })
}

func testAccWidgetConfig_basic(rName string) string {
    return fmt.Sprintf(`
resource "examplecloud_widget" "test" {
  name = %[1]q
}
`, rName)
}
```

Use the `provider-test-patterns` skill (if available) for the full testing
treatment: config helper style (`%[1]q` indexed verbs), statecheck/plancheck,
CompareValue, custom StateCheck implementations for exists/disappears
helpers, sweepers, and ephemeral resource testing. Use the
`run-acceptance-tests` skill for executing and debugging test runs.

## Error Handling

Match API errors by type, not message text, and wrap with context:

```go
var notFound *examplecloud.NotFoundError
if errors.As(err, &notFound) {
    // resource doesn't exist
}

// Wrapping inside helpers: preserve the cause with %w
return fmt.Errorf("creating Widget (%s): %w", name, err)
```

Diagnostics follow a consistent grammar — summary names the operation and
type, detail carries identifier and cause:

```go
resp.Diagnostics.AddError(
    "Error creating Widget",
    fmt.Sprintf("creating Widget (%s): %s", name, err),
)

resp.Diagnostics.AddAttributeError(
    path.Root("name"),
    "Invalid name",
    "Name must be lowercase alphanumeric",
)
```

## Documentation

Write attribute `MarkdownDescription`s first — they are the source of
truth. Then generate Registry documentation with `tfplugindocs`
(`go generate ./...` where wired up), adding `docs/**/*.md.tmpl` templates
only for prose and examples the generator cannot derive. Use the
`provider-docs` skill (if available) for the full documentation workflow and
Registry publication rules.

## Pre-Submission Checklist

- [ ] Plugin Framework used (no new SDKv2 code)
- [ ] Resource has all CRUD operations implemented
- [ ] Read removes missing resources from state; Delete tolerates already-deleted
- [ ] No redundant `id` attribute (real API identifier exposed instead)
- [ ] Import implemented and covered by an `ImportStateVerify` step
- [ ] `_basic`, `_disappears`, and per-attribute tests present
- [ ] Waiters used where the API is eventually consistent
- [ ] Error messages name the operation, type, and identifier
- [ ] Sensitive attributes marked; every attribute has a description
- [ ] Docs generated with `tfplugindocs`
- [ ] Changelog entry added, if the repo tracks release notes (check CONTRIBUTING)

## References

- [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework)
- [Resource Development](https://developer.hashicorp.com/terraform/plugin/framework/resources)
- [Data Source Development](https://developer.hashicorp.com/terraform/plugin/framework/data-sources)
- [HashiCorp Provider Design Principles](https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles)

<!-- chapter:end slug=provider-resources -->

---

<!-- chapter:begin slug=provider-test-patterns position=13 -->

## 13. provider-test-patterns

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/provider-test-patterns/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-test-patterns/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/provider-test-patterns.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (3), referenced from this skill's directory:
  - `references/checks.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-test-patterns/references/checks.md
  - `references/ephemeral.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-test-patterns/references/ephemeral.md
  - `references/sweepers.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/provider-test-patterns/references/sweepers.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: provider-test-patterns
description: >-
  Terraform provider acceptance test patterns using terraform-plugin-testing
  with the Plugin Framework. Covers test structure, TestCase/TestStep fields,
  ConfigStateChecks with custom statecheck.StateCheck implementations,
  plan checks, CompareValue for cross-step assertions, config helpers,
  import testing with ImportStateKind, sweepers, and scenario patterns
  (basic, update, disappears, validation, regression), and ephemeral resource
  testing with the echoprovider package. Use when writing, reviewing, or
  debugging provider acceptance tests, including questions about statecheck,
  plancheck, TestCheckFunc, CheckDestroy, ExpectError, import state
  verification, ephemeral resources, or how to structure test files.
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Provider Acceptance Test Patterns

Patterns for writing acceptance tests using
[terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing)
with the [Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework).

Source: [HashiCorp Testing Patterns](https://developer.hashicorp.com/terraform/plugin/testing/testing-patterns)

**References** (load when needed):
- `references/checks.md` — statecheck, plancheck, knownvalue types, tfjsonpath, comparers
- `references/sweepers.md` — sweeper setup, TestMain, dependencies
- `references/ephemeral.md` — ephemeral resource testing, echoprovider, multi-step patterns

---

## Test Lifecycle

The framework runs each TestStep through: **plan → apply → refresh → final
plan**. If the final plan shows a diff, the test fails (unless
`ExpectNonEmptyPlan` is set). After all steps, destroy runs followed by
`CheckDestroy`. This means every test automatically verifies that
configurations apply cleanly and produce no drift — no assertions needed for
that.

---

## Test Function Structure

```go
func TestAccExample_basic(t *testing.T) {
    var widget example.Widget
    rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
    resourceName := "example_widget.test"

    resource.ParallelTest(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckExampleDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccExampleConfig_basic(rName),
                ConfigStateChecks: []statecheck.StateCheck{
                    stateCheckExampleExists(resourceName, &widget),
                    statecheck.ExpectKnownValue(resourceName,
                        tfjsonpath.New("name"), knownvalue.StringExact(rName)),
                    statecheck.ExpectKnownValue(resourceName,
                        tfjsonpath.New("id"), knownvalue.NotNull()),
                },
            },
        },
    })
}
```

Use `resource.ParallelTest` by default. Use `resource.Test` only when tests
share state or cannot run concurrently.

---

## Provider Factory

```go
// provider_test.go — Plugin Framework with Protocol 6 (use Protocol5 variant if needed)
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
    "example": providerserver.NewProtocol6WithError(New("test")()),
}
```

---

## TestCase Fields

| Field | Purpose |
|-------|---------|
| `PreCheck` | `func()` — verify prerequisites (env vars, API access) |
| `ProtoV6ProviderFactories` | Plugin Framework provider factories |
| `CheckDestroy` | `TestCheckFunc` — verify resources destroyed after all steps |
| `Steps` | `[]TestStep` — sequential test operations |
| `TerraformVersionChecks` | `[]tfversion.TerraformVersionCheck` — gate by CLI version |

---

## TestStep Fields

### Config Mode

| Field | Purpose |
|-------|---------|
| `Config` | Inline HCL string to apply |
| `ConfigStateChecks` | `[]statecheck.StateCheck` — modern assertions (preferred) |
| `ConfigPlanChecks` | `resource.ConfigPlanChecks{PreApply: []plancheck.PlanCheck{...}}` |
| `ExpectError` | `*regexp.Regexp` — expect failure matching pattern |
| `ExpectNonEmptyPlan` | `bool` — expect non-empty plan after apply |
| `PlanOnly` | `bool` — plan without applying |
| `Destroy` | `bool` — run destroy step |
| `PreConfig` | `func()` — setup before step |

### Import Mode

| Field | Purpose |
|-------|---------|
| `ImportState` | `true` to enable import mode |
| `ImportStateVerify` | Verify imported state matches prior state |
| `ImportStateVerifyIgnore` | `[]string` — attributes to skip during verify |
| `ImportStateKind` | `resource.ImportBlockWithID` — import block generation |
| `ResourceName` | Resource address to import |
| `ImportStateId` | Override the ID used for import |

---

## Check Functions

### Modern: ConfigStateChecks (preferred)

Type-safe with aggregated error reporting. Compose built-in checks with custom
`statecheck.StateCheck` implementations. See `references/checks.md` for full
knownvalue types, tfjsonpath navigation, and comparers.

```go
ConfigStateChecks: []statecheck.StateCheck{
    stateCheckExampleExists(resourceName, &widget),
    statecheck.ExpectKnownValue(resourceName,
        tfjsonpath.New("name"), knownvalue.StringExact("my-widget")),
    statecheck.ExpectKnownValue(resourceName,
        tfjsonpath.New("enabled"), knownvalue.Bool(true)),
    statecheck.ExpectKnownValue(resourceName,
        tfjsonpath.New("id"), knownvalue.NotNull()),
    statecheck.ExpectSensitiveValue(resourceName,
        tfjsonpath.New("api_key")),
},
```

Do not mix `Check` (legacy) and `ConfigStateChecks` in the same step.

### Legacy: Check (for CheckDestroy and migration)

`CheckDestroy` on `TestCase` requires `TestCheckFunc`. The `Check` field on
`TestStep` also accepts `TestCheckFunc` but prefer `ConfigStateChecks` for new
tests.

```go
Check: resource.ComposeAggregateTestCheckFunc(
    resource.TestCheckResourceAttr(name, "key", "expected"),
    resource.TestCheckResourceAttrSet(name, "id"),
    resource.TestCheckNoResourceAttr(name, "removed"),
    resource.TestMatchResourceAttr(name, "url", regexp.MustCompile(`^https://`)),
    resource.TestCheckResourceAttrPair(res1, "ref_id", res2, "id"),
),
```

`ComposeAggregateTestCheckFunc` reports all errors; `ComposeTestCheckFunc`
fails fast on the first.

---

## Config Helpers

Use numbered format verbs — `%[1]q` for quoted strings, `%[1]s` for raw:

```go
func testAccExampleConfig_basic(rName string) string {
    return fmt.Sprintf(`
resource "example_widget" "test" {
  name = %[1]q
}
`, rName)
}

func testAccExampleConfig_full(rName, description string) string {
    return fmt.Sprintf(`
resource "example_widget" "test" {
  name        = %[1]q
  description = %[2]q
  enabled     = true
}
`, rName, description)
}
```

---

## Scenario Patterns

### Basic + Update (combine in one test — updates are supersets of basic)

```go
Steps: []resource.TestStep{
    {
        Config: testAccExampleConfig_basic(rName),
        ConfigStateChecks: []statecheck.StateCheck{
            stateCheckExampleExists(resourceName, &widget),
            statecheck.ExpectKnownValue(resourceName,
                tfjsonpath.New("name"), knownvalue.StringExact(rName)),
        },
    },
    {
        Config: testAccExampleConfig_full(rName, "updated"),
        ConfigStateChecks: []statecheck.StateCheck{
            stateCheckExampleExists(resourceName, &widget),
            statecheck.ExpectKnownValue(resourceName,
                tfjsonpath.New("description"), knownvalue.StringExact("updated")),
        },
    },
},
```

### Import

After a config step, verify import produces identical state. Use
`ImportStateKind` for import block generation:

```go
{
    ResourceName:      resourceName,
    ImportState:       true,
    ImportStateVerify: true,
    ImportStateKind:   resource.ImportBlockWithID,
},
```

### Disappears (resource deleted externally)

```go
{
    Config: testAccExampleConfig_basic(rName),
    ConfigStateChecks: []statecheck.StateCheck{
        stateCheckExampleExists(resourceName, &widget),
        stateCheckExampleDisappears(resourceName),
    },
    ExpectNonEmptyPlan: true,
},
```

### Validation (expect error)

```go
{
    Config:      testAccExampleConfig_invalidName(""),
    ExpectError: regexp.MustCompile(`name must not be empty`),
},
```

### Regression (two-commit workflow)

A proper bug fix uses at least two commits: first commit the regression test
(which fails, confirming the bug), then commit the fix (test passes). This
lets reviewers independently verify the test reproduces the issue by checking
out the first commit, then advancing to the fix.

Name and document regression tests to identify the issue they fix. Include a
link to the original bug report when possible.

```go
// TestAccExample_regressionGH1234 verifies fix for https://github.com/org/repo/issues/1234
func TestAccExample_regressionGH1234(t *testing.T) {
    rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
    resourceName := "example_widget.test"

    resource.ParallelTest(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckExampleDestroy,
        Steps: []resource.TestStep{
            {
                // Reproduce the issue: this config triggered the bug
                Config: testAccExampleConfig_regressionGH1234(rName),
                ConfigStateChecks: []statecheck.StateCheck{
                    stateCheckExampleExists(resourceName, nil),
                    statecheck.ExpectKnownValue(resourceName,
                        tfjsonpath.New("computed_field"), knownvalue.NotNull()),
                },
            },
        },
    })
}
```

---

## Helper Functions

### Custom StateCheck: Exists

Implement `statecheck.StateCheck` for API existence verification. Separate the
exists check into its own function for reuse across steps — the source
recommends this as a design principle:

```go
type exampleExistsCheck struct {
    resourceAddress string
    widget          *example.Widget
}

func (e exampleExistsCheck) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) {
    r, err := stateResourceAtAddress(req.State, e.resourceAddress)
    if err != nil {
        resp.Error = err
        return
    }

    id, ok := r.AttributeValues["id"].(string)
    if !ok {
        resp.Error = fmt.Errorf("no id found for %s", e.resourceAddress)
        return
    }

    conn := testAccAPIClient()
    widget, err := conn.GetWidget(id)
    if err != nil {
        resp.Error = fmt.Errorf("%s not found via API: %w", e.resourceAddress, err)
        return
    }

    if e.widget != nil {
        *e.widget = *widget
    }
}

func stateCheckExampleExists(name string, widget *example.Widget) statecheck.StateCheck {
    return exampleExistsCheck{resourceAddress: name, widget: widget}
}
```

### Custom StateCheck: Disappears

Delete a resource via API to simulate external deletion:

```go
type exampleDisappearsCheck struct {
    resourceAddress string
}

func (e exampleDisappearsCheck) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) {
    r, err := stateResourceAtAddress(req.State, e.resourceAddress)
    if err != nil {
        resp.Error = err
        return
    }

    id := r.AttributeValues["id"].(string)
    conn := testAccAPIClient()
    resp.Error = conn.DeleteWidget(id)
}

func stateCheckExampleDisappears(name string) statecheck.StateCheck {
    return exampleDisappearsCheck{resourceAddress: name}
}
```

### State Resource Lookup (shared utility)

```go
func stateResourceAtAddress(state *tfjson.State, address string) (*tfjson.StateResource, error) {
    if state == nil || state.Values == nil || state.Values.RootModule == nil {
        return nil, fmt.Errorf("no state available")
    }
    for _, r := range state.Values.RootModule.Resources {
        if r.Address == address {
            return r, nil
        }
    }
    return nil, fmt.Errorf("not found in state: %s", address)
}
```

### Destroy Check (TestCheckFunc — required by CheckDestroy)

```go
func testAccCheckExampleDestroy(s *terraform.State) error {
    conn := testAccAPIClient()
    for _, rs := range s.RootModule().Resources {
        if rs.Type != "example_widget" {
            continue
        }
        _, err := conn.GetWidget(rs.Primary.ID)
        if err == nil {
            return fmt.Errorf("widget %s still exists", rs.Primary.ID)
        }
        if !isNotFoundError(err) {
            return err
        }
    }
    return nil
}
```

### PreCheck

```go
func testAccPreCheck(t *testing.T) {
    t.Helper()
    if os.Getenv("EXAMPLE_API_KEY") == "" {
        t.Fatal("EXAMPLE_API_KEY must be set for acceptance tests")
    }
}
```

<!-- chapter:end slug=provider-test-patterns -->

---

<!-- chapter:begin slug=refactor-module position=14 -->

## 14. refactor-module

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/refactor-module/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/refactor-module/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/refactor-module.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: refactor-module
description: Transform monolithic Terraform configurations into reusable, maintainable modules following HashiCorp's module design principles and community best practices.
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Skill: Refactor Module

## Overview
This skill guides AI agents in transforming monolithic Terraform configurations into reusable, maintainable modules following HashiCorp's module design principles and community best practices.

## Capability Statement
The agent will analyze existing Terraform code and systematically refactor it into well-structured modules with:
- Clear interface contracts (variables and outputs)
- Proper encapsulation and abstraction
- Versioning and documentation
- Testing frameworks
- Migration path for existing state

## Prerequisites
- Existing Terraform configuration to refactor
- Understanding of resource dependencies
- Access to inspect current state via `terraform state list` / `terraform show -json` (for migration planning)
- Knowledge of module registry patterns

## Input Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `source_directory` | string | Yes | Path to existing Terraform configuration |
| `module_name` | string | Yes | Name for the new module |
| `abstraction_level` | string | No | "simple", "intermediate", "advanced" (default: intermediate) |
| `preserve_state` | boolean | Yes | Whether to maintain state compatibility |
| `target_registry` | string | No | Target module registry (local, private, public) |

## Execution Steps

### 1. Analysis Phase
```markdown
**Identify Refactoring Candidates**
- Group resources by logical function
- Identify repeated patterns
- Map resource dependencies
- Detect configuration coupling
- Analyze variable usage patterns

**Complexity Assessment**
- Count resource relationships
- Measure variable propagation depth
- Identify cross-resource references
- Evaluate state migration complexity
```

### 2. Module Design

#### Interface Design
```hcl
# Define clear input contract
variable "network_config" {
  description = "Network configuration parameters"
  type = object({
    cidr_block         = string
    availability_zones = list(string)
    enable_nat         = bool
  })

  validation {
    condition     = can(cidrhost(var.network_config.cidr_block, 0))
    error_message = "CIDR block must be valid IPv4 CIDR."
  }
}

# Define output contract
output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = { for k, v in aws_subnet.private : k => v.id }
}
```

#### Encapsulation Strategy
```markdown
**What to Include in Module:**
- Tightly coupled resources (VPC + subnets)
- Resources with shared lifecycle
- Configuration with clear boundaries

**What to Keep Separate:**
- Cross-cutting concerns (monitoring, tagging)
- Resources with different lifecycles
- Provider-specific configurations
```

### 3. Code Transformation

#### Before: Monolithic Configuration
```hcl
# main.tf (monolithic)
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "production-vpc"
    Environment = "prod"
  }
}

resource "aws_subnet" "public_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "public-subnet-1"
    Type = "public"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1b"

  tags = {
    Name = "public-subnet-2"
    Type = "public"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "production-igw"
  }
}

# ... more repetitive subnet and routing resources
```

#### After: Modular Structure
```hcl
# modules/vpc/main.tf
locals {
  subnet_count = length(var.availability_zones)
}

resource "aws_vpc" "main" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = var.enable_dns_hostnames
  enable_dns_support   = var.enable_dns_support

  tags = merge(
    var.tags,
    {
      Name = var.name
    }
  )
}

resource "aws_subnet" "public" {
  for_each = var.create_public_subnets ? toset(var.availability_zones) : []

  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.cidr_block, 8, index(var.availability_zones, each.value))
  availability_zone       = each.value
  map_public_ip_on_launch = true

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-public-${each.value}"
      Type = "public"
    }
  )
}

resource "aws_internet_gateway" "main" {
  count  = var.create_public_subnets ? 1 : 0
  vpc_id = aws_vpc.main.id

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-igw"
    }
  )
}

# modules/vpc/variables.tf
variable "name" {
  description = "Name prefix for all resources"
  type        = string
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
  type        = string

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "Must be a valid IPv4 CIDR block."
  }
}

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
}

variable "create_public_subnets" {
  description = "Whether to create public subnets"
  type        = bool
  default     = true
}

variable "enable_dns_hostnames" {
  description = "Enable DNS hostnames in the VPC"
  type        = bool
  default     = true
}

variable "enable_dns_support" {
  description = "Enable DNS support in the VPC"
  type        = bool
  default     = true
}

variable "tags" {
  description = "Tags to apply to all resources"
  type        = map(string)
  default     = {}
}

# modules/vpc/outputs.tf
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "vpc_cidr_block" {
  description = "CIDR block of the VPC"
  value       = aws_vpc.main.cidr_block
}

output "public_subnet_ids" {
  description = "Map of availability zones to public subnet IDs"
  value       = { for k, v in aws_subnet.public : k => v.id }
}

output "internet_gateway_id" {
  description = "ID of the internet gateway"
  value       = try(aws_internet_gateway.main[0].id, null)
}

# Root configuration using module
module "vpc" {
  source = "./modules/vpc"

  name               = "production"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  tags = {
    Environment = "production"
    ManagedBy   = "Terraform"
  }
}
```

### 4. State Migration

#### Inspecting Current State
Before writing `moved` blocks or `state mv` commands, inspect the current state to map
existing resource addresses. Prefer the documented, stable, and more token-efficient
commands over reading the raw state file:

```bash
# Enumerate current resource addresses (the inputs for `moved` / `state mv`)
terraform state list

# Inspect resolved attribute values when you need them
terraform show -json | jq '.values.root_module'
```

`terraform show -json` requires providers to be installed (`terraform init`), since it
renders values against provider schemas. **Fall back to the raw state
(`terraform state pull` / `terraform.tfstate`) only when** providers aren't available and
`init` can't run, you need only coarse info (addresses, outputs, `serial`/`lineage`), or you
must avoid executing Terraform. Avoid parsing the raw version-4 state format as a stable
interface. **Note:** state contains sensitive values in plaintext in every format — never
echo state contents into logs or output.

#### Generate Migration Plan
```hcl
# migration.tf
# Use moved blocks for state refactoring (Terraform 1.1+)

moved {
  from = aws_vpc.main
  to   = module.vpc.aws_vpc.main
}

moved {
  from = aws_subnet.public_1
  to   = module.vpc.aws_subnet.public["us-east-1a"]
}

moved {
  from = aws_subnet.public_2
  to   = module.vpc.aws_subnet.public["us-east-1b"]
}

moved {
  from = aws_internet_gateway.main
  to   = module.vpc.aws_internet_gateway.main[0]
}
```

#### Manual State Migration (Pre-1.1)
```bash
# Generate state migration commands
terraform state mv aws_vpc.main module.vpc.aws_vpc.main
terraform state mv aws_subnet.public_1 'module.vpc.aws_subnet.public["us-east-1a"]'
terraform state mv aws_subnet.public_2 'module.vpc.aws_subnet.public["us-east-1b"]'
terraform state mv aws_internet_gateway.main 'module.vpc.aws_internet_gateway.main[0]'
```

### 5. Module Documentation

```markdown
# VPC Module

## Overview
Creates a VPC with configurable public and private subnets across multiple availability zones.

## Features
- Multi-AZ subnet deployment
- Optional NAT gateway configuration
- VPC Flow Logs integration
- Customizable CIDR allocation

## Usage

\`\`\`hcl
module "vpc" {
  source = "./modules/vpc"

  name               = "my-vpc"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b"]

  create_public_subnets  = true
  create_private_subnets = true
  enable_nat_gateway     = true

  tags = {
    Environment = "production"
  }
}
\`\`\`

## Requirements

| Name | Version |
|------|---------|
| terraform | >= 1.5.0 |
| aws | ~> 5.0 |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| name | Name prefix for resources | `string` | n/a | yes |
| cidr_block | VPC CIDR block | `string` | n/a | yes |
| availability_zones | List of AZs | `list(string)` | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| vpc_id | VPC identifier |
| public_subnet_ids | Map of public subnet IDs |
| private_subnet_ids | Map of private subnet IDs |

## Examples

See [examples/](./examples/) directory for complete usage examples.
```

### 6. Testing

Use skill terraform-test

**Test File**: A `.tftest.hcl` or `.tftest.json` file containing test configuration and run blocks that validate your Terraform configuration.

**Test Block**: Optional configuration block that defines test-wide settings (available since Terraform 1.6.0).

**Run Block**: Defines a single test scenario with optional variables, provider configurations, and assertions. Each test file requires at least one run block.

**Assert Block**: Contains conditions that must evaluate to true for the test to pass. Failed assertions cause the test to fail.

**Mock Provider**: Simulates provider behavior without creating real infrastructure (available since Terraform 1.7.0).

**Test Modes**: Tests run in apply mode (default, creates real infrastructure) or plan mode (validates logic without creating resources).

#### File Structure

Terraform test files use the `.tftest.hcl` or `.tftest.json` extension and are typically organized in a `tests/` directory. Use clear naming conventions to distinguish between unit tests (plan mode) and integration tests (apply mode):

```
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
    ├── unit_test.tftest.hcl      # Unit test (plan mode)
    └── integration_test.tftest.hcl  # Integration test (apply mode - creates real resources)
```

## Refactoring Patterns

### Pattern 1: Resource Grouping
Extract related resources into cohesive modules:
- Networking (VPC, Subnets, Route Tables)
- Compute (ASG, Launch Templates, Load Balancers)
- Data (RDS, ElastiCache, S3)

### Pattern 2: Configuration Layering
```hcl
# Base module with defaults
module "vpc_base" {
  source = "./modules/vpc-base"
  # Minimal required inputs
}

# Environment-specific wrapper
module "vpc_prod" {
  source = "./modules/vpc-production"
  # Inherits from base, adds prod-specific config
}
```

### Pattern 3: Composition
```hcl
# Small, focused modules
module "vpc" {
  source = "./modules/vpc"
}

module "security_groups" {
  source = "./modules/security-groups"
  vpc_id = module.vpc.vpc_id
}

module "application" {
  source     = "./modules/application"
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnet_ids
  sg_ids     = module.security_groups.app_sg_ids
}
```

## Common Pitfalls

### 1. Over-Abstraction
```hcl
# ❌ Don't create overly generic modules
variable "resources" {
  type = map(map(any))  # Too flexible, hard to validate
}

# ✅ Do use specific, typed interfaces
variable "database_config" {
  type = object({
    engine         = string
    instance_class = string
  })
}
```

### 2. Tight Coupling
```hcl
# ❌ Don't couple modules through direct references
# module A
output "instance_id" { value = aws_instance.app.id }

# module B (in same config)
resource "aws_eip" "app" {
  instance = module.a.instance_id  # Tight coupling
}

# ✅ Do pass dependencies through root module
module "compute" {
  source = "./modules/compute"
}

resource "aws_eip" "app" {
  instance = module.compute.instance_id
}
```

### 3. State Migration Errors
Always test migration in non-production first:
```bash
# Create plan to verify no changes after migration
terraform plan -out=migration.tfplan

# Review carefully
terraform show migration.tfplan

# Apply only if plan shows no changes
terraform apply migration.tfplan
```

## Version Control Strategy

```hcl
# Use semantic versioning for modules
module "vpc" {
  source  = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.0"
  version = "~> 1.2"
}

# Pin to specific versions in production
# Use version ranges in development
```

## Success Criteria

- [ ] Module has single, well-defined responsibility
- [ ] All variables have descriptions and types
- [ ] Validation rules prevent invalid configurations
- [ ] Outputs provide sufficient information for consumers
- [ ] Documentation includes usage examples
- [ ] Tests verify module behavior
- [ ] State migration completed without resource recreation
- [ ] No plan differences after refactoring

## Related Skills
- [Terraform code generation](https://raw.githubusercontent.com/hashicorp/agent-skills/refs/heads/main/plugins/terraform/skills/terraform-style-guide/SKILL.md) - Style guide for the new Terraform Module
- [Azure Verified Modules](https://raw.githubusercontent.com/hashicorp/agent-skills/refs/heads/main/plugins/terraform/skills/azure-verified-modules/SKILL.md) - Recommended module specifications for Azure

## Resources
- [Terraform Module Development](https://developer.hashicorp.com/terraform/language/modules/develop)
- [Module Best Practices](https://developer.hashicorp.com/terraform/cloud-docs/registry/design)

## Revision History

| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2025-11-07 | Initial skill definition |

<!-- chapter:end slug=refactor-module -->

---

<!-- chapter:begin slug=run-acceptance-tests position=15 -->

## 15. run-acceptance-tests

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/run-acceptance-tests/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/run-acceptance-tests/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/run-acceptance-tests.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: run-acceptance-tests
description: Guide for running acceptance tests for a Terraform provider. Use this when asked to run an acceptance test or to run a test with the prefix `TestAcc`, when a test fails with missing environment variables, or when diagnosing a failing or suspiciously passing acceptance test.
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

An acceptance test is a Go test function with the prefix `TestAcc`.

Before running: acceptance tests create **real infrastructure** against the
provider's live API, which may incur cost. Confirm the configured
credentials point at a test account before proceeding.

To run a focused acceptance test named `TestAccFeatureHappyPath`:

1. Run `go test -run=TestAccFeatureHappyPath -timeout 60m` with the
   following environment variables:
   - `TF_ACC=1`

   Default to non-verbose test output. Always pass an explicit `-timeout`:
   `go test` kills any test run after 10 minutes by default, and acceptance
   tests routinely exceed that.
1. The acceptance tests may require additional environment variables for
   specific providers. To discover which ones:
   - Read the test's `PreCheck` / `testAccPreCheck` function and search the
     test files: `grep -rn "os.Getenv" --include="*_test.go"`.
   - Check the repository's README, CONTRIBUTING, or `.env.example` for
     documented test setup.
   - The provider's `Configure` method shows how credentials are resolved;
     use the `provider-configuration` skill (if available) to understand a
     credential provider chain.

   Set the variables for the single test invocation
   (`EXAMPLE_API_KEY=... TF_ACC=1 go test ...`) rather than exporting them
   into the shell profile, and never write secret values into files inside
   the repository.

To diagnose a failing acceptance test, use these options, in order. These
options are cumulative: each option includes all the options above it.

1. Run the test again. Use the `-count=1` option to ensure that `go test` does
   not use a cached result.
1. Offer verbose `go test` output. Use the `-v` option.
1. Offer debug-level logging. Enable debug-level logging with the environment
   variable `TF_LOG=debug`.
1. Offer to persist the acceptance test's Terraform workspace. Enable
   persistence with the environment variable `TF_ACC_WORKING_DIR_PERSIST=1`.

A passing acceptance test may be a false negative. To "flip" a passing
acceptance test named `TestAccFeatureHappyPath`:

1. Edit the value of one of the TestCheckFuncs in one of the TestSteps in the
   TestCase.
1. Run the acceptance test. Expect the test to fail.
1. If the test fails, then undo the edit and report a successful flip. Else,
   keep the edit and report an unsuccessful flip.

If a test run is interrupted, real resources may be left behind; run the
provider's sweepers if it registers them (see the `provider-test-patterns`
skill's sweeper reference, if available). For writing or restructuring
tests, use the `provider-test-patterns` skill.

<!-- chapter:end slug=run-acceptance-tests -->

---

<!-- chapter:begin slug=terraform-policy position=16 -->

## 16. terraform-policy

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/terraform-policy/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/terraform-policy.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (54), referenced from this skill's directory:
  - `.gitignore` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/.gitignore
  - `examples/conversion/cloudfront-associated-with-waf/cloudfront-associated-with-waf.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/cloudfront-associated-with-waf/cloudfront-associated-with-waf.policy.hcl
  - `examples/conversion/cloudfront-associated-with-waf/cloudfront-associated-with-waf.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/cloudfront-associated-with-waf/cloudfront-associated-with-waf.sentinel
  - `examples/conversion/cloudfront-associated-with-waf/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/cloudfront-associated-with-waf/README.md
  - `examples/conversion/cloudtrail-server-side-encryption-enabled/cloudtrail-server-side-encryption-enabled.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/cloudtrail-server-side-encryption-enabled/cloudtrail-server-side-encryption-enabled.policy.hcl
  - `examples/conversion/cloudtrail-server-side-encryption-enabled/cloudtrail-server-side-encryption-enabled.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/cloudtrail-server-side-encryption-enabled/cloudtrail-server-side-encryption-enabled.sentinel
  - `examples/conversion/cloudtrail-server-side-encryption-enabled/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/cloudtrail-server-side-encryption-enabled/README.md
  - `examples/conversion/dms-endpoint-should-be-ssl-configured/dms-endpoint-should-be-ssl-configured.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/dms-endpoint-should-be-ssl-configured/dms-endpoint-should-be-ssl-configured.policy.hcl
  - `examples/conversion/dms-endpoint-should-be-ssl-configured/dms-endpoint-should-be-ssl-configured.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/dms-endpoint-should-be-ssl-configured/dms-endpoint-should-be-ssl-configured.sentinel
  - `examples/conversion/dms-endpoint-should-be-ssl-configured/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/dms-endpoint-should-be-ssl-configured/README.md
  - `examples/conversion/dms-endpoints-should-use-ssl/dms-endpoints-should-use-ssl.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/dms-endpoints-should-use-ssl/dms-endpoints-should-use-ssl.policy.hcl
  - `examples/conversion/dms-endpoints-should-use-ssl/dms-endpoints-should-use-ssl.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/dms-endpoints-should-use-ssl/dms-endpoints-should-use-ssl.sentinel
  - `examples/conversion/dms-endpoints-should-use-ssl/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/dms-endpoints-should-use-ssl/README.md
  - `examples/conversion/ec2-network-acl-should-have-subnet-ids/ec2-network-acl-should-have-subnet-ids.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/ec2-network-acl-should-have-subnet-ids/ec2-network-acl-should-have-subnet-ids.policy.hcl
  - `examples/conversion/ec2-network-acl-should-have-subnet-ids/ec2-network-acl-should-have-subnet-ids.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/ec2-network-acl-should-have-subnet-ids/ec2-network-acl-should-have-subnet-ids.sentinel
  - `examples/conversion/ec2-network-acl-should-have-subnet-ids/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/ec2-network-acl-should-have-subnet-ids/README.md
  - `examples/conversion/ec2-vpc-default-security-group-no-traffic/ec2-vpc-default-security-group-no-traffic.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/ec2-vpc-default-security-group-no-traffic/ec2-vpc-default-security-group-no-traffic.policy.hcl
  - `examples/conversion/ec2-vpc-default-security-group-no-traffic/ec2-vpc-default-security-group-no-traffic.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/ec2-vpc-default-security-group-no-traffic/ec2-vpc-default-security-group-no-traffic.sentinel
  - `examples/conversion/ec2-vpc-default-security-group-no-traffic/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/ec2-vpc-default-security-group-no-traffic/README.md
  - `examples/conversion/efs-access-point-should-enforce-user-identity/efs-access-point-should-enforce-user-identity.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/efs-access-point-should-enforce-user-identity/efs-access-point-should-enforce-user-identity.policy.hcl
  - `examples/conversion/efs-access-point-should-enforce-user-identity/efs-access-point-should-enforce-user-identity.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/efs-access-point-should-enforce-user-identity/efs-access-point-should-enforce-user-identity.sentinel
  - `examples/conversion/efs-access-point-should-enforce-user-identity/README.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/efs-access-point-should-enforce-user-identity/README.md
  - `examples/conversion/elasticache-redis-replication-group-encryption-at-transit-enabled/elasticache-redis-replication-group-encryption-at-transit-enabled.policy.hcl` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/elasticache-redis-replication-group-encryption-at-transit-enabled/elasticache-redis-replication-group-encryption-at-transit-enabled.policy.hcl
  - `examples/conversion/elasticache-redis-replication-group-encryption-at-transit-enabled/elasticache-redis-replication-group-encryption-at-transit-enabled.sentinel` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-policy/examples/conversion/elasticache-redis-replication-group-encryption-at-transit-enabled/elasticache-redis-replication-group-encryption-at-transit-enabled.sentinel
  - …and 30 more, listed in https://skillsdocs.com/api/v1/books/hashicorp/agent-skills/skills/terraform-policy

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: terraform-policy
description: "Write, test, or convert Terraform Policy files (.policy.hcl, .policytest.hcl, Sentinel→tfpolicy). Triggers: policy.hcl, policytest, convert sentinel, tfpolicy, write a policy."
license: MPL-2.0
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.1.0"
---

# terraform-policy

**UTILITY SKILL** — INVOKES: [tfpolicy-author](references/tfpolicy-author.md) | [tfpolicy-test](references/tfpolicy-test.md)

## USE FOR:

- Writing a new `.policy.hcl` policy from a description or requirement
- Converting a `.sentinel` policy to Terraform Policy
- Writing or debugging a `.policytest.hcl` test file
- Migrating a Sentinel policy library to Terraform Policy

Before giving authoring or testing instructions, check the installed `tfpolicy` CLI version and tailor guidance accordingly:
- If the CLI is `0.1.x`, do **not** require `policy { required_providers { ... } }`; generate policies compatible with tfpolicy 0.1.x syntax and behavior.
- If the CLI is `0.2.0` or newer, include a top-level `policy { required_providers { ... } }` block when authoring `.policy.hcl`. It is mandatory for `tfpolicy validate`; version-range validation is best effort, and wildcard targets such as `resource_policy "*"` are not schema-validated.
- If the CLI version is unknown, ask the user to check it first or provide guidance that clearly distinguishes the `0.1.x` and `0.2.0+` paths.

## DO NOT USE FOR:

- Writing `.tftest.hcl` files for Terraform modules — use `terraform-test`
- General Terraform HCL authoring — use `terraform-style-guide`

## Routing

| Task | Sub-skill |
|------|-----------|
| Write or convert a `.policy.hcl` policy | [tfpolicy-author](references/tfpolicy-author.md) |
| Write or debug a `.policytest.hcl` test | [tfpolicy-test](references/tfpolicy-test.md) |

## Examples

- "Block EC2 instances without encryption" → [tfpolicy-author](references/tfpolicy-author.md)
- "Convert this Sentinel policy to tfpolicy" → [tfpolicy-author](references/tfpolicy-author.md)
- "Write a policytest for my EBS policy" → [tfpolicy-test](references/tfpolicy-test.md)

## Troubleshooting

- **Wrong skill triggered?** Load the sub-skill directly from the routing table above.

```bash
npx skills add hashicorp/agent-skills/terraform/terraform-policy/skills/tfpolicy-author
npx skills add hashicorp/agent-skills/terraform/terraform-policy/skills/tfpolicy-test
```

<!-- chapter:end slug=terraform-policy -->

---

<!-- chapter:begin slug=terraform-search-import position=17 -->

## 17. terraform-search-import

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/terraform-search-import/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-search-import/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/terraform-search-import.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (2), referenced from this skill's directory:
  - `references/MANUAL-IMPORT.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-search-import/references/MANUAL-IMPORT.md
  - `scripts/list_resources.sh` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-search-import/scripts/list_resources.sh

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: terraform-search-import
description: Discover existing cloud resources using Terraform Search queries and bulk import them into Terraform management. Use when bringing unmanaged infrastructure under Terraform control, auditing cloud resources, or migrating to IaC.
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.1.0"
  compatibility: Requires Terraform 1.14 or newer and providers with list resource support
---

# Terraform Search and Bulk Import

Discover existing cloud resources using declarative queries and generate configuration for bulk import into Terraform state.

**References:**
- [Terraform Search - list block](https://developer.hashicorp.com/terraform/language/block/tfquery/list)
- [Bulk Import](https://developer.hashicorp.com/terraform/language/import/bulk)

## When to Use

- Bringing unmanaged resources under Terraform control
- Auditing existing cloud infrastructure
- Migrating from manual provisioning to IaC
- Discovering resources across multiple regions/accounts

## IMPORTANT: Check Provider Support First

**BEFORE starting, you MUST verify the target resource type is supported:**

```bash
# Check what list resources are available
./scripts/list_resources.sh aws      # Specific provider
./scripts/list_resources.sh          # All configured providers
```

## Decision Tree

1. **Identify target resource type** (e.g., aws_s3_bucket, aws_instance)
2. **Check if supported**: Run `./scripts/list_resources.sh <provider>`
3. **Choose workflow**:
   - ** If supported**: Check for terraform version available.
   - ** If terraform version is above 1.14.0** Use Terraform Search workflow (below)
   - ** If not supported or terraform version is below 1.14.0 **: Use Manual Discovery workflow (see [references/MANUAL-IMPORT.md](references/MANUAL-IMPORT.md))

   **Note**: The list of supported resources is rapidly expanding. Always verify current support before using manual import.

## Prerequisites

Before writing queries, verify the provider supports list resources for your target resource type.

### Discover Available List Resources

Run the helper script to extract supported list resources from your provider:

```bash
# From a directory with provider configuration (runs terraform init if needed)
./scripts/list_resources.sh aws      # Specific provider
./scripts/list_resources.sh          # All configured providers
```

Or manually query the provider schema:

```bash
terraform providers schema -json | jq '.provider_schemas | to_entries | map({key: (.key | split("/")[-1]), value: (.value.list_resource_schemas // {} | keys)})'
```

Terraform Search requires an initialized working directory. Ensure you have a configuration with the required provider before running queries:

```hcl
# terraform.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}
```

Run `terraform init` to download the provider, then proceed with queries.

## Terraform Search Workflow (Supported Resources Only)

1. Create `.tfquery.hcl` files with `list` blocks defining search queries
2. Run `terraform query` to discover matching resources
3. Generate configuration with `-generate-config-out=<file>`
4. Review and refine generated `resource` and `import` blocks
5. Run `terraform plan` and `terraform apply` to import

## Query File Structure

Query files use `.tfquery.hcl` extension and support:
- `provider` blocks for authentication
- `list` blocks for resource discovery
- `variable` and `locals` blocks for parameterization

```hcl
# discovery.tfquery.hcl
provider "aws" {
  region = "us-west-2"
}

list "aws_instance" "all" {
  provider = aws
}
```

## List Block Syntax

```hcl
list "<list_type>" "<symbolic_name>" {
  provider = <provider_reference>  # Required

  # Optional: filter configuration (provider-specific)
  # The `config` block schema is provider-specific. Discover available options using `terraform providers schema -json | jq '.provider_schemas."registry.terraform.io/hashicorp/<provider>".list_resource_schemas."<resource_type>"'`

  config {
    filter {
      name   = "<filter_name>"
      values = ["<value1>", "<value2>"]
    }
    region = "<region>"  # AWS-specific
  }
  # Optional: limit results
  limit = 100
}
```

## Supported List Resources

Provider support for list resources varies by version. **Always check what's available for your specific provider version using the discovery script.**

## Query Examples

### Basic Discovery

```hcl
# Find all EC2 instances in configured region
list "aws_instance" "all" {
  provider = aws
}
```

### Filtered Discovery

```hcl
# Find instances by tag
list "aws_instance" "production" {
  provider = aws

  config {
    filter {
      name   = "tag:Environment"
      values = ["production"]
    }
  }
}

# Find instances by type
list "aws_instance" "large" {
  provider = aws

  config {
    filter {
      name   = "instance-type"
      values = ["t3.large", "t3.xlarge"]
    }
  }
}
```

### Multi-Region Discovery

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

locals {
  regions = ["us-west-2", "us-east-1", "eu-west-1"]
}

list "aws_instance" "all_regions" {
  for_each = toset(local.regions)
  provider = aws

  config {
    region = each.value
  }
}
```

### Parameterized Queries

```hcl
variable "target_environment" {
  type    = string
  default = "staging"
}

list "aws_instance" "by_env" {
  provider = aws

  config {
    filter {
      name   = "tag:Environment"
      values = [var.target_environment]
    }
  }
}
```

## Running Queries

```bash
# Execute queries and display results
terraform query

# Generate configuration file
terraform query -generate-config-out=imported.tf

# Pass variables
terraform query -var='target_environment=production'
```

## Query Output Format

```
list.aws_instance.all   account_id=123456789012,id=i-0abc123,region=us-west-2   web-server
```

Columns: `<query_address>   <identity_attributes>   <name_tag>`

## Generated Configuration

The `-generate-config-out` flag creates:

```hcl
# __generated__ by Terraform
resource "aws_instance" "all_0" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  # ... all attributes
}

import {
  to       = aws_instance.all_0
  provider = aws
  identity = {
    account_id = "123456789012"
    id         = "i-0abc123"
    region     = "us-west-2"
  }
}
```

## Post-Generation Cleanup

Generated configuration includes all attributes. Clean up by:

1. Remove computed/read-only attributes
2. Replace hardcoded values with variables
3. Add proper resource naming
4. Organize into appropriate files

```hcl
# Before: generated
resource "aws_instance" "all_0" {
  ami                    = "ami-0c55b159cbfafe1f0"
  instance_type          = "t2.micro"
  arn                    = "arn:aws:ec2:..."  # Remove - computed
  id                     = "i-0abc123"        # Remove - computed
  # ... many more attributes
}

# After: cleaned
resource "aws_instance" "web_server" {
  ami           = var.ami_id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id

  tags = {
    Name        = "web-server"
    Environment = var.environment
  }
}
```

## Import by Identity

Generated imports use identity-based import (Terraform 1.12+):

```hcl
import {
  to       = aws_instance.web
  provider = aws
  identity = {
    account_id = "123456789012"
    id         = "i-0abc123"
    region     = "us-west-2"
  }
}
```

## Verifying Imported State

After running `terraform apply` to import resources, verify what actually landed in state.
Prefer the documented, stable, and more token-efficient commands over reading the raw state
file:

```bash
# Confirm resources are now managed (also confirms addresses)
terraform state list

# Inspect resolved attribute values for imported resources
terraform show -json | jq '.values.root_module.resources[] | {address, type, name}'
```

`terraform show -json` requires providers to be installed (`terraform init`), since it
renders values against provider schemas. **Fall back to the raw state
(`terraform state pull` / `terraform.tfstate`) only when** providers aren't available and
`init` can't run, you need only coarse info (addresses, outputs, `serial`/`lineage`), or you
must avoid executing Terraform. Avoid parsing the raw version-4 state format as a stable
interface. **Note:** state contains sensitive values in plaintext in every format — never
echo state contents into logs or output.

## Best Practices

### Query Design
- Start broad, then add filters to narrow results
- Use `limit` to prevent overwhelming output
- Test queries before generating configuration

### Configuration Management
- Review all generated code before applying
- Remove unnecessary default values
- Use consistent naming conventions
- Add proper variable abstraction

## Troubleshooting

| Issue | Solution |
|-------|----------|
| "No list resources found" | Check provider version supports list resources |
| Query returns empty | Verify region and filter values |
| Generated config has errors | Remove computed attributes, fix deprecated arguments |
| Import fails | Ensure resource not already in state |

## Complete Example

```hcl
# main.tf - Initialize provider
terraform {
  required_version = ">= 1.14"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"  # Always use latest version
    }
  }
}

# discovery.tfquery.hcl - Define queries
provider "aws" {
  region = "us-west-2"
}

list "aws_instance" "team_instances" {
  provider = aws

  config {
    filter {
      name   = "tag:Owner"
      values = ["platform"]
    }
    filter {
      name   = "instance-state-name"
      values = ["running"]
    }
  }

  limit = 50
}
```

```bash
# Execute workflow
terraform init
terraform query
terraform query -generate-config-out=generated.tf
# Review and clean generated.tf
terraform plan
terraform apply
```

<!-- chapter:end slug=terraform-search-import -->

---

<!-- chapter:begin slug=terraform-stacks position=18 -->

## 18. terraform-stacks

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/terraform-stacks/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/terraform-stacks.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (6), referenced from this skill's directory:
  - `references/api-monitoring.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/references/api-monitoring.md
  - `references/component-blocks.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/references/component-blocks.md
  - `references/deployment-blocks.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/references/deployment-blocks.md
  - `references/examples.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/references/examples.md
  - `references/linked-stacks.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/references/linked-stacks.md
  - `references/troubleshooting.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-stacks/references/troubleshooting.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: terraform-stacks
description: Comprehensive guide for working with HashiCorp Terraform Stacks. Use when creating, modifying, or validating Terraform Stack configurations (.tfcomponent.hcl, .tfdeploy.hcl files), working with stack components and deployments from local modules, public registry, or private registry sources, managing multi-region or multi-environment infrastructure, or troubleshooting Terraform Stacks syntax and structure.
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Terraform Stacks

Terraform Stacks simplify infrastructure provisioning and management at scale by providing a configuration layer above traditional Terraform modules. Stacks enable declarative orchestration of multiple components across environments, regions, and cloud accounts.

## Core Concepts

**Stack**: A complete unit of infrastructure composed of components and deployments that can be managed together.

**Component**: An abstraction around a Terraform module that defines infrastructure pieces. Each component specifies a source module, inputs, and providers.

**Deployment**: An instance of all components in a stack with specific input values. Use deployments for different environments (dev/staging/prod), regions, or cloud accounts.

**Stack Language**: A separate HCL-based language (not regular Terraform HCL) with distinct blocks and file extensions.

## File Structure

Terraform Stacks use specific file extensions:

- **Component configuration**: `.tfcomponent.hcl`
- **Deployment configuration**: `.tfdeploy.hcl`
- **Provider lock file**: `.terraform.lock.hcl` (generated by CLI)

All configuration files must be at the root level of the Stack repository. HCP Terraform processes all files in dependency order.

### Recommended File Organization

```
my-stack/
├── .terraform-version               # The required Terraform version for this Stack
├── variables.tfcomponent.hcl        # Variable declarations
├── providers.tfcomponent.hcl        # Provider configurations
├── components.tfcomponent.hcl       # Component definitions
├── outputs.tfcomponent.hcl          # Stack outputs
├── deployments.tfdeploy.hcl         # Deployment definitions
├── .terraform.lock.hcl              # Provider lock file (generated)
└── modules/                         # Local modules (optional - only if using local modules)
    ├── s3/
    └── compute/
```

**Note**: The `modules/` directory is only required when using local module sources. Components can reference modules from:
- Local file paths: `./modules/vpc`
- Public registry: `terraform-aws-modules/vpc/aws`
- Private registry: `app.terraform.io/<org-name>/vpc/aws`
- Git: `git::https://github.com/org/repo.git//path?ref=v1.0.0`

HCP Terraform processes all `.tfcomponent.hcl` and `.tfdeploy.hcl` files in dependency order.

## Required Terraform version (.terraform-version)

Use Terraform v1.13.x or later to access the Stacks CLI plugin and to run
terraform stacks CLI commands. Begin by adding a .terraform-version file to
your Stack's root directory to specify the Terraform version required for your
Stack. For example, the following file specifies Terraform v1.14.5:

```
1.14.5
```

## Component Configuration (.tfcomponent.hcl)

### Variable Block

Declare input variables for the Stack configuration. Variables must define a `type` field and do not support the `validation` argument.

```hcl
variable "aws_region" {
  type        = string
  description = "AWS region for deployments"
  default     = "us-west-1"
}

variable "identity_token" {
  type        = string
  description = "OIDC identity token"
  ephemeral   = true  # Does not persist to state file
}

variable "instance_count" {
  type     = number
  nullable = false
}
```

**Important**: Use `ephemeral = true` for credentials and tokens (identity tokens, API keys, passwords) to prevent them from persisting in state files. Use `stable` for longer-lived values like license keys that need to persist across runs.

### Required Providers Block

```hcl
required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 6.0"
  }
  random = {
    source  = "hashicorp/random"
    version = "~> 3.5.0"
  }
}
```

### Provider Block

Provider blocks differ from traditional Terraform:

1. Support `for_each` meta-argument
2. Define aliases in the block header (not as an argument)
3. Accept configuration through a `config` block

**Single Provider Configuration:**

```hcl
provider "aws" "this" {
  config {
    region = var.aws_region
    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }
  }
}
```

**Multiple Provider Configurations with for_each:**

```hcl
provider "aws" "configurations" {
  for_each = var.regions

  config {
    region = each.value
    assume_role_with_web_identity {
      role_arn           = var.role_arn
      web_identity_token = var.identity_token
    }
  }
}
```

**Authentication Best Practice**: Use **workload identity** (OIDC) as the preferred authentication method for Stacks. This approach:
- Avoids long-lived static credentials
- Provides temporary, scoped credentials per deployment run
- Integrates with cloud provider IAM (AWS IAM Roles, Azure Managed Identities, GCP Service Accounts)
- Eliminates need for platform-managed environment variables

Configure workload identity using `identity_token` blocks and `assume_role_with_web_identity` in provider configuration. For detailed setup instructions for AWS, Azure, and GCP, see: https://developer.hashicorp.com/terraform/cloud-docs/dynamic-provider-credentials

### Component Block

Each Stack requires at least one component block. Add a component for each module to include in the Stack. Components reference modules from local paths, registries, or Git.

```hcl
component "vpc" {
  source  = "app.terraform.io/my-org/vpc/aws"  # Local, registry, or Git URL
  version = "2.1.0"          # For registry modules

  inputs = {
    cidr_block  = var.vpc_cidr
    name_prefix = var.name_prefix
  }

  providers = {
    aws = provider.aws.this
  }
}
```

See `references/component-blocks.md` for examples of dependencies, for_each, public registry modules, Git sources, and more.

**Key Points:**
- Reference outputs: `component.<name>.<output>` or `component.<name>[key].<output>` for for_each
- Dependencies inferred automatically from component references
- Aggregate with for expressions: `[for x in component.s3 : x.bucket_name]`
- For components with `for_each`, reference specific instances: `component.<name>[each.value].<output>`
- Provider references are normal values: `provider.<type>.<alias>` or `provider.<type>.<alias>[each.value]`

### Output Block

Outputs require a `type` argument and do not support `preconditions`:

```hcl
output "vpc_id" {
  type        = string
  description = "VPC ID"
  value       = component.vpc.vpc_id
}

output "endpoint_urls" {
  type      = map(string)
  value     = {
    for region, comp in component.api : region => comp.endpoint_url
  }
  sensitive = false
}
```

### Locals Block

Locals blocks work the same in both `.tfcomponent.hcl` and `.tfdeploy.hcl` files:

```hcl
locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform Stacks"
    Project     = var.project_name
  }

  region_config = {
    for region in var.regions : region => {
      name_suffix = "${var.environment}-${region}"
    }
  }
}
```

### Removed Block

Use to safely remove components from a Stack. HCP Terraform requires the component's providers to remove it.

```hcl
removed {
  from   = component.old_component
  source = "./modules/old-module"

  providers = {
    aws = provider.aws.this
  }
}
```

## Deployment Configuration (.tfdeploy.hcl)

### Identity Token Block

Generate JWT tokens for OIDC authentication with cloud providers:

```hcl
identity_token "aws" {
  audience = ["aws.workload.identity"]
}

identity_token "azure" {
  audience = ["api://AzureADTokenExchange"]
}
```

Reference tokens in deployments using `identity_token.<name>.jwt`

### Store Block

Access HCP Terraform variable sets within Stack deployments:

```hcl
store "varset" "aws_credentials" {
  id       = "varset-ABC123"  # Alternatively use: name = "varset_name"
  source   = "tfc-cloud-shared"
  category = "terraform"      # Alternatively use: category = "env" for environment variables
}

deployment "production" {
  inputs = {
    aws_access_key = store.varset.aws_credentials.AWS_ACCESS_KEY_ID
  }
}
```

Use to centralize credentials and share variables across Stacks. See `references/deployment-blocks.md` for details.

### Deployment Block

Define deployment instances (minimum 1, maximum 20 per Stack):

```hcl
deployment "production" {
  inputs = {
    aws_region     = "us-west-1"
    instance_count = 3
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}

# Create multiple deployments for different environments
deployment "development" {
  inputs = {
    aws_region     = "us-east-1"
    instance_count = 1
    name_suffix    = "dev"
    role_arn       = local.role_arn
    identity_token = identity_token.aws.jwt
  }
}
```

**To destroy a deployment**: Set `destroy = true`, upload configuration, approve destroy run, then remove the deployment block. See `references/deployment-blocks.md` for details.

### Deployment Group Block

Group deployments together for shared settings (HCP Terraform Premium tier feature). Free/standard tiers use default groups named `{deployment-name}_default`.

```hcl
deployment_group "canary" {
  auto_approve_checks = [deployment_auto_approve.safe_changes]
}

deployment "dev" {
  inputs = { /* ... */ }
  deployment_group = deployment_group.canary
}
```

Multiple deployments can reference the same group. See `references/deployment-blocks.md` for details.

### Deployment Auto-Approve Block

Define rules to automatically approve deployment plans (HCP Terraform Premium tier feature):

```hcl
deployment_auto_approve "safe_changes" {
  deployment_group = deployment_group.canary

  check {
    condition = context.plan.changes.remove == 0
    reason    = "Cannot auto-approve plans with resource deletions"
  }
}
```

**Available context variables**: `context.plan.applyable`, `context.plan.changes.add/change/remove/total`, `context.success`

**Note:** `orchestrate` blocks are deprecated. Use `deployment_group` and `deployment_auto_approve` instead.

See `references/deployment-blocks.md` for all context variables and patterns.

### Publish Output and Upstream Input Blocks

Link Stacks together by publishing outputs from one Stack and consuming them in another:

```hcl
# In network Stack - publish outputs
publish_output "vpc_id_network" {
  type  = string
  value = deployment.network.vpc_id
}

# In application Stack - consume outputs
upstream_input "network_stack" {
  type   = "stack"
  source = "app.terraform.io/my-org/my-project/networking-stack"
}

deployment "app" {
  inputs = {
    vpc_id = upstream_input.network_stack.vpc_id_network
  }
}
```

See `references/linked-stacks.md` for complete documentation and examples.

## Terraform Stacks CLI

**Note**: Terraform Stacks is Generally Available (GA) as of Terraform CLI v1.13+. Stacks now count toward Resources Under Management (RUM) for HCP Terraform billing.

### Initialize and Validate

```bash
terraform stacks init              # Download providers, modules, generate lock file
terraform stacks providers-lock    # Regenerate lock file (add platforms if needed)
terraform stacks validate          # Check syntax without uploading
```

### Deployment Workflow

**Important**: No `plan` or `apply` commands. Upload configuration triggers deployment runs automatically.

```bash
# 1. Upload configuration (triggers deployment runs)
terraform stacks configuration upload

# 2. Monitor deployments
terraform stacks deployment-run list                          # List runs (non-interactive)
terraform stacks deployment-group watch -deployment-group=... # Stream status updates

# 3. Approve deployments (if auto-approve not configured)
terraform stacks deployment-run approve-all-plans -deployment-run-id=...
terraform stacks deployment-group approve-all-plans -deployment-group=...
terraform stacks deployment-run cancel -deployment-run-id=...  # Cancel if needed
```

### Configuration Management

```bash
terraform stacks configuration list                    # List configuration versions
terraform stacks configuration fetch -configuration-id=...  # Download configuration
terraform stacks configuration watch                   # Monitor upload status
```

### Other Commands

```bash
terraform stacks create              # Create new Stack (interactive)
terraform stacks fmt                 # Format Stack files
terraform stacks list                # Show all Stacks
terraform stacks version             # Display version
terraform stacks deployment-group rerun -deployment-group=...  # Rerun deployment
```

## Monitoring Deployments with HCP Terraform API

For programmatic monitoring in automation, CI/CD, or non-interactive environments (like AI agents), use the HCP Terraform API instead of CLI watch commands. The API provides endpoints for:

- Configuration status and validation
- Deployment group summaries
- Deployment run status
- Deployment step details (plan/apply)
- Error diagnostics with file locations and code snippets
- Stack outputs via artifacts endpoint

**Key points:**
- CLI watch commands stream indefinitely and don't work in automation
- Use artifacts endpoint to retrieve Stack outputs: `GET /api/v2/stack-deployment-steps/{step-id}/artifacts?name=apply-description`
- Diagnostics endpoint requires `stack_deployment_step_id` query parameter
- Artifacts endpoint returns HTTP 307 redirect (use `curl -L`)

For complete API workflow, authentication, polling best practices, and example scripts, see `references/api-monitoring.md`.

## Common Patterns

**Component Dependencies**: Dependencies are automatically inferred when one component references another's output (e.g., `subnet_ids = component.vpc.private_subnet_ids`).

**Multi-Region Deployment**: Use `for_each` on providers and components to deploy across multiple regions. Each region gets its own provider configuration and component instances.

**Deferred Changes**: Stacks support deferred changes to handle dependencies where values are only known after apply. This enables complex multi-component deployments where some resources depend on runtime values from other components (cluster endpoints, generated passwords, etc.).

For complete examples including multi-region deployments, component dependencies, deferred changes patterns, and linked Stacks, see `references/examples.md`.

## Best Practices

1. **Component Granularity**: Create components for logical infrastructure units that share a lifecycle
2. **Module Compatibility**:
   - Modules used with Stacks cannot include provider blocks (configure providers in Stack configuration)
   - **Test public registry modules** before using in production Stacks - some modules may have compatibility issues
   - Consider using raw resources for critical infrastructure if module compatibility is uncertain
   - Example: Some terraform-aws-modules versions have been found to have compatibility issues with Stacks (e.g., ALB and ECS modules)
3. **State Isolation**: Each deployment has its own isolated state
4. **Input Variables**: Use variables for values that differ across deployments; use locals for shared values
5. **Provider Lock Files**: Always generate and commit `.terraform.lock.hcl` to version control
6. **Naming Conventions**: Use descriptive names for components and deployments
7. **Deployment Groups**: You can organize deployments into deployment groups. Deployment groups enable auto-approval rules, logical organization, and provide a foundation for scaling. Deployment groups are an HCP Terraform Premium tier feature
8. **Testing**: Test Stack configurations in dev/staging deployments before production

## Troubleshooting

**Circular Dependencies**: Refactor to break circular references or use intermediate components.

**Deployment Destruction**: Cannot destroy from UI. Set `destroy = true` in deployment block, upload configuration, and HCP Terraform creates a destroy run.

**Empty Diagnostics**: Add required `stack_deployment_step_id` query parameter to diagnostics API requests.

**Module Compatibility**: Test public registry modules before production use. Some modules may have compatibility issues with Stacks.

## References

For detailed documentation, see:
- `references/component-blocks.md` - Complete component block reference with all arguments and syntax
- `references/deployment-blocks.md` - Complete deployment block reference with all configuration options
- `references/linked-stacks.md` - Publish outputs and upstream inputs for linking Stacks together
- `references/examples.md` - Complete working examples for multi-region and component dependencies
- `references/api-monitoring.md` - Full API workflow for programmatic monitoring and automation
- `references/troubleshooting.md` - Detailed troubleshooting guide for common issues and solutions

<!-- chapter:end slug=terraform-stacks -->

---

<!-- chapter:begin slug=terraform-style-guide position=19 -->

## 19. terraform-style-guide

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/terraform-style-guide/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-style-guide/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/terraform-style-guide.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (1), referenced from this skill's directory:
  - `SECURITY.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-style-guide/SECURITY.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: terraform-style-guide
description: Generate Terraform HCL code following HashiCorp's official style conventions and best practices. Use when writing, reviewing, or generating Terraform configurations.
metadata:
  lifecycle-status: active
---

# Terraform Style Guide

Generate and maintain Terraform code following HashiCorp's official style conventions and best practices.

**Reference:** [HashiCorp Terraform Style Guide](https://developer.hashicorp.com/terraform/language/style)

## Code Generation Strategy

When generating Terraform code:

1. Start with provider configuration and version constraints
2. Create data sources before dependent resources
3. Build resources in dependency order
4. Add outputs for key resource attributes
5. Use variables for all configurable values

## File Organization

| File | Purpose |
|------|---------|
| `terraform.tf` | Terraform and provider version requirements |
| `providers.tf` | Provider configurations |
| `main.tf` | Primary resources and data sources |
| `variables.tf` | Input variable declarations (alphabetical) |
| `outputs.tf` | Output value declarations (alphabetical) |
| `locals.tf` | Local value declarations |

### Example Structure

```hcl
# terraform.tf
terraform {
  required_version = ">= 1.14"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

# variables.tf
variable "environment" {
  description = "Target deployment environment"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

# locals.tf
locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# main.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-vpc"
  })
}

# outputs.tf
output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}
```

## Code Formatting

### Indentation and Alignment

- Use **two spaces** per nesting level (no tabs)
- Align equals signs for consecutive arguments

```hcl
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  subnet_id     = "subnet-12345678"

  tags = {
    Name        = "web-server"
    Environment = "production"
  }
}
```

### Block Organization

Arguments precede blocks, with meta-arguments first:

```hcl
resource "aws_instance" "example" {
  # Meta-arguments
  count = 3

  # Arguments
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  # Blocks
  root_block_device {
    volume_size = 20
  }

  # Lifecycle last
  lifecycle {
    create_before_destroy = true
  }
}
```

## Naming Conventions

- Use **lowercase with underscores** for all names
- Use **descriptive nouns** excluding the resource type
- Be specific and meaningful
- Resource names must be singular, not plural
- Default to `main` for resources where a specific descriptive name is redundant or unavailable, provided only one instance exists

```hcl
# Bad
resource "aws_instance" "webAPI-aws-instance" {}
resource "aws_instance" "web_apis" {}
variable "name" {}

# Good
resource "aws_instance" "web_api" {}
resource "aws_vpc" "main" {}
variable "application_name" {}
```

## Variables

Every variable must include `type` and `description`:

```hcl
variable "instance_type" {
  description = "EC2 instance type for the web server"
  type        = string
  default     = "t2.micro"

  validation {
    condition     = contains(["t2.micro", "t2.small", "t2.medium"], var.instance_type)
    error_message = "Instance type must be t2.micro, t2.small, or t2.medium."
  }
}

variable "database_password" {
  description = "Password for the database admin user"
  type        = string
  sensitive   = true
}
```

## Outputs

Every output must include `description`:

```hcl
output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web.id
}

output "database_password" {
  description = "Database administrator password"
  value       = aws_db_instance.main.password
  sensitive   = true
}
```

## Dynamic Resource Creation

### Prefer for_each over count

```hcl
# Bad - count for multiple resources
resource "aws_instance" "web" {
  count = var.instance_count
  tags  = { Name = "web-${count.index}" }
}

# Good - for_each with named instances
variable "instance_names" {
  type    = set(string)
  default = ["web-1", "web-2", "web-3"]
}

resource "aws_instance" "web" {
  for_each = var.instance_names
  tags     = { Name = each.key }
}
```

### count for Conditional Creation

```hcl
resource "aws_cloudwatch_metric_alarm" "cpu" {
  count = var.enable_monitoring ? 1 : 0

  alarm_name = "high-cpu-usage"
  threshold  = 80
}
```

## Security Best Practices

Refer to SECURITY.md. It includes guidance on encrypting resources,
preventing sensitive data in state, and secure configurations.

## Version Pinning

```hcl
terraform {
  required_version = ">= 1.14"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}
```

Use the latest major version of each provider and the latest minor version of
Terraform, unless otherwise constrained by a dependency lock file or by other
modules used by the configuration.

**Version constraint operators:**
- `= 1.0.0` - Exact version
- `>= 1.0.0` - Greater than or equal
- `~> 1.0` - Allow rightmost component to increment
- `>= 1.0, < 2.0` - Version range

## Provider Configuration

```hcl
provider "aws" {
  region = "us-west-2"

  default_tags {
    tags = {
      ManagedBy = "Terraform"
      Project   = var.project_name
    }
  }
}

# Aliased provider for multi-region
provider "aws" {
  alias  = "east"
  region = "us-east-1"
}
```

## Version Control

**Never commit:**
- `terraform.tfstate`, `terraform.tfstate.backup`
- `.terraform/` directory
- `*.tfplan`
- `.tfvars` files with sensitive data

**Always commit:**
- All `.tf` configuration files
- `.terraform.lock.hcl` (dependency lock file)

## Validation Tools

Run before committing:

```bash
terraform fmt -recursive
terraform validate
```

Additional tools:
- `tflint` - Linting and best practices
- `checkov` / `tfsec` - Security scanning

## Code Review Checklist

- [ ] Code formatted with `terraform fmt`
- [ ] Configuration validated with `terraform validate`
- [ ] Files organized according to standard structure
- [ ] All variables have type and description
- [ ] All outputs have descriptions
- [ ] Resource names use descriptive nouns with underscores
- [ ] Version constraints pinned explicitly
- [ ] Sensitive values marked with `sensitive = true`
- [ ] No hardcoded credentials or secrets
- [ ] Security best practices applied

---

*Based on: [HashiCorp Terraform Style Guide](https://developer.hashicorp.com/terraform/language/style)*

<!-- chapter:end slug=terraform-style-guide -->

---

<!-- chapter:begin slug=terraform-test position=20 -->

## 20. terraform-test

- **Source:** https://github.com/hashicorp/agent-skills/blob/main/plugins/terraform/skills/terraform-test/SKILL.md
- **Raw:** https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-test/SKILL.md
- **Markdown:** https://skillsdocs.com/hashicorp/agent-skills/terraform-test.md
- **Licence:** MPL-2.0 — https://spdx.org/licenses/MPL-2.0.html

Bundled files (3), referenced from this skill's directory:
  - `references/CI_CD.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-test/references/CI_CD.md
  - `references/EXAMPLES.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-test/references/EXAMPLES.md
  - `references/MOCK_PROVIDERS.md` — https://raw.githubusercontent.com/hashicorp/agent-skills/main/plugins/terraform/skills/terraform-test/references/MOCK_PROVIDERS.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: terraform-test
description: Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating infrastructure behavior with assertions, mocking providers and data sources, testing module outputs and resource configurations, or troubleshooting Terraform test syntax and execution.
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.2"
---

# Terraform Test

Terraform's built-in testing framework validates that configuration updates don't introduce breaking changes. Tests run against temporary resources, protecting existing infrastructure and state files.

## Reference Files

- `references/MOCK_PROVIDERS.md` — Mock provider syntax, common defaults, when to use mocks (Terraform 1.7.0+ only — skip if the user's version is below 1.7)
- `references/CI_CD.md` — GitHub Actions and GitLab CI pipeline examples
- `references/EXAMPLES.md` — Complete example test suite (unit, integration, and mock tests for a VPC module)

Read the relevant reference file when the user asks about mocking, CI/CD integration, or wants a full example.

## Core Concepts

- **Test file** (`.tftest.hcl` / `.tftest.json`): Contains `run` blocks that validate your configuration
- **Run block**: A single test scenario with optional variables, providers, and assertions
- **Assert block**: Conditions that must be true for the test to pass
- **Mock provider**: Simulates provider behavior without real infrastructure (Terraform 1.7.0+)
- **Test modes**: `apply` (default, creates real resources) or `plan` (validates logic only)

## File Structure

```
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
    ├── defaults_unit_test.tftest.hcl         # plan mode — fast, no resources
    ├── validation_unit_test.tftest.hcl        # plan mode
    └── full_stack_integration_test.tftest.hcl # apply mode — creates real resources
```

Use `*_unit_test.tftest.hcl` for plan-mode tests and `*_integration_test.tftest.hcl` for apply-mode tests so they can be filtered separately in CI.

## Test File Structure

```hcl
# Optional: test-wide settings
test {
  parallel = true  # Enable parallel execution for all run blocks (default: false)
}

# Optional: file-level variables (highest precedence, override all other sources)
variables {
  aws_region    = "us-west-2"
  instance_type = "t2.micro"
}

# Optional: provider configuration
provider "aws" {
  region = var.aws_region
}

# Required: at least one run block
run "test_default_configuration" {
  command = plan

  assert {
    condition     = aws_instance.example.instance_type == "t2.micro"
    error_message = "Instance type should be t2.micro by default"
  }
}
```

## Run Block

```hcl
run "test_name" {
  command  = plan  # or apply (default)
  parallel = true  # optional, since v1.9.0

  # Override file-level variables
  variables {
    instance_type = "t3.large"
  }

  # Reference a specific module
  module {
    source  = "./modules/vpc"  # local or registry only (not git/http)
    version = "5.0.0"          # registry modules only
  }

  # Control state isolation
  state_key = "shared_state"  # since v1.9.0

  # Plan behavior
  plan_options {
    mode    = refresh-only  # or normal (default)
    refresh = true
    replace = [aws_instance.example]
    target  = [aws_instance.example]
  }

  # Assertions
  assert {
    condition     = aws_instance.example.id != ""
    error_message = "Instance should have a valid ID"
  }

  # Expected failures (test passes if these fail)
  expect_failures = [
    var.instance_count
  ]
}
```

## Common Test Patterns

### Validate outputs

```hcl
run "test_outputs" {
  command = plan

  assert {
    condition     = output.vpc_id != null
    error_message = "VPC ID output must be defined"
  }

  assert {
    condition     = can(regex("^vpc-", output.vpc_id))
    error_message = "VPC ID should start with 'vpc-'"
  }
}
```

### Conditional resources

```hcl
run "test_nat_gateway_disabled" {
  command = plan

  variables {
    create_nat_gateway = false
  }

  assert {
    condition     = length(aws_nat_gateway.main) == 0
    error_message = "NAT gateway should not be created when disabled"
  }
}
```

### Resource counts

```hcl
run "test_resource_count" {
  command = plan

  variables {
    instance_count = 3
  }

  assert {
    condition     = length(aws_instance.workers) == 3
    error_message = "Should create exactly 3 worker instances"
  }
}
```

### Tags

```hcl
run "test_resource_tags" {
  command = plan

  variables {
    common_tags = {
      Environment = "production"
      ManagedBy   = "Terraform"
    }
  }

  assert {
    condition     = aws_instance.example.tags["Environment"] == "production"
    error_message = "Environment tag should be set correctly"
  }

  assert {
    condition     = aws_instance.example.tags["ManagedBy"] == "Terraform"
    error_message = "ManagedBy tag should be set correctly"
  }
}
```

### Data sources

```hcl
run "test_data_source_lookup" {
  command = plan

  assert {
    condition     = data.aws_ami.ubuntu.id != ""
    error_message = "Should find a valid Ubuntu AMI"
  }

  assert {
    condition     = can(regex("^ami-", data.aws_ami.ubuntu.id))
    error_message = "AMI ID should be in correct format"
  }
}
```

### Validation rules

```hcl
run "test_invalid_environment" {
  command = plan

  variables {
    environment = "invalid"
  }

  expect_failures = [
    var.environment
  ]
}
```

### Sequential tests with dependencies

```hcl
run "setup_vpc" {
  command = apply

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC should be created"
  }
}

run "test_subnet_in_vpc" {
  command = plan

  variables {
    vpc_id = run.setup_vpc.vpc_id
  }

  assert {
    condition     = aws_subnet.example.vpc_id == run.setup_vpc.vpc_id
    error_message = "Subnet should be in the VPC from setup_vpc"
  }
}
```

### Plan options (refresh-only, targeted)

```hcl
run "test_refresh_only" {
  command = plan

  plan_options {
    mode = refresh-only
  }

  assert {
    condition     = aws_instance.example.tags["Environment"] == "production"
    error_message = "Tags should be refreshed correctly"
  }
}

run "test_specific_resource" {
  command = plan

  plan_options {
    target = [aws_instance.example]
  }

  assert {
    condition     = aws_instance.example.instance_type == "t2.micro"
    error_message = "Targeted resource should be planned"
  }
}
```

### Parallel modules

```hcl
run "test_networking_module" {
  command  = plan
  parallel = true

  module {
    source = "./modules/networking"
  }

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC should be created"
  }
}

run "test_compute_module" {
  command  = plan
  parallel = true

  module {
    source = "./modules/compute"
  }

  assert {
    condition     = output.instance_id != ""
    error_message = "Instance should be created"
  }
}
```

### State key sharing

```hcl
run "create_foundation" {
  command   = apply
  state_key = "foundation"

  assert {
    condition     = aws_vpc.main.id != ""
    error_message = "Foundation VPC should be created"
  }
}

run "create_application" {
  command   = apply
  state_key = "foundation"

  variables {
    vpc_id = run.create_foundation.vpc_id
  }

  assert {
    condition     = aws_instance.app.vpc_id == run.create_foundation.vpc_id
    error_message = "Application should use foundation VPC"
  }
}
```

### Cleanup ordering (S3 objects before bucket)

```hcl
run "create_bucket" {
  command = apply

  assert {
    condition     = aws_s3_bucket.example.id != ""
    error_message = "Bucket should be created"
  }
}

run "add_objects" {
  command = apply

  assert {
    condition     = length(aws_s3_object.files) > 0
    error_message = "Objects should be added"
  }
}

# Cleanup destroys in reverse: objects first, then bucket
```

### Multiple aliased providers

```hcl
provider "aws" {
  alias  = "primary"
  region = "us-west-2"
}

provider "aws" {
  alias  = "secondary"
  region = "us-east-1"
}

run "test_with_specific_provider" {
  command = plan

  providers = {
    aws = provider.aws.secondary
  }

  assert {
    condition     = aws_instance.example.availability_zone == "us-east-1a"
    error_message = "Instance should be in us-east-1 region"
  }
}
```

### Complex conditions

```hcl
assert {
  condition = alltrue([
    for subnet in aws_subnet.private :
    can(regex("^10\\.0\\.", subnet.cidr_block))
  ])
  error_message = "All private subnets should use 10.0.0.0/8 CIDR range"
}
```

## Cleanup

Resources are destroyed in **reverse run block order** after test completion. This matters for dependencies (e.g., S3 objects before bucket). Use `terraform test -no-cleanup` to skip cleanup for debugging.

## Running Tests

```bash
terraform test                                        # all tests
terraform test tests/defaults.tftest.hcl             # specific file
terraform test -filter=test_vpc_configuration        # by run block name
terraform test -test-directory=integration-tests     # custom directory
terraform test -verbose                              # detailed output
terraform test -no-cleanup                           # skip resource cleanup
```

## Best Practices

1. **Naming**: `*_unit_test.tftest.hcl` for plan mode, `*_integration_test.tftest.hcl` for apply mode
2. **Test naming**: Use descriptive run block names that explain the scenario being tested
3. **Default to plan**: Use `command = plan` unless you need to test real resource behavior
4. **Use mocks** for external dependencies — faster and no credentials needed (see `references/MOCK_PROVIDERS.md`)
5. **Error messages**: Make them specific enough to diagnose failures without running the test again
6. **Negative tests**: Use `expect_failures` to verify validation rules reject bad inputs
7. **Variable coverage**: Test different variable combinations to validate all code paths — test variables have the highest precedence and override all other sources
8. **Module sources**: Test files only support local paths and registry modules — not git or HTTP URLs
9. **Parallel execution**: Use `parallel = true` for independent tests with different state files
10. **Cleanup**: Integration tests destroy resources in reverse run block order automatically; use `-no-cleanup` for debugging
11. **CI/CD**: Run unit tests on every PR, integration tests on merge (see `references/CI_CD.md`)

## Troubleshooting

| Issue | Solution |
|-------|----------|
| Assertion failures | Use `-verbose` to see actual vs expected values |
| Missing credentials | Use mock providers for unit tests |
| Unsupported module source | Convert git/HTTP sources to local modules |
| Tests interfering | Use `state_key` or separate modules for isolation |
| Slow tests | Use `command = plan` and mocks; run integration tests separately |

## References

- [Terraform Testing Documentation](https://developer.hashicorp.com/terraform/language/tests)
- [Terraform Test Command](https://developer.hashicorp.com/terraform/cli/commands/test)

<!-- chapter:end slug=terraform-test -->
