GitHub AI Engineering Academy · Part 5 of 16
GitHub Copilot with Terraform: AI-Assisted Infrastructure as Code
Academy curriculum (16 lessons)
Terraform is how a huge amount of real infrastructure gets built — networks, virtual machines, Kubernetes clusters, load balancers, databases, DNS records, object storage, IAM policies, whole cloud accounts, monitoring, and the secret references that tie them together. GitHub Copilot lives right next to that HCL, turning a comment or a chat prompt into a working first draft. But Terraform is not ordinary code: an apply can create and destroy real infrastructure, so every line Copilot writes is a draft to read, validate, scan, plan, and approve — never a decision to run unread.
This is Part 5 of the GitHub AI Engineering Academy. Part 4, GitHub Copilot with VS Code, showed Copilot across the whole DevOps surface inside the editor; this lesson goes deep on one of the most consequential surfaces — Infrastructure as Code. The principle from Part 2, GitHub Copilot for DevOps Engineers, holds harder here than anywhere: AI assists creation; Terraform tooling validates reality; humans approve. Copilot helps with the repetitive, mechanical parts of HCL — but the mandatory review pipeline is what keeps that speed safe:
Requirement
|
Copilot (draft HCL)
|
HCL
|
terraform fmt
|
terraform validate
|
lint / security scan
|
terraform plan
|
Human Review <-- read every change
|
Apply
The two review-shaped steps — the human reading the plan and approving the apply — are the difference between AI making you faster and AI deleting a database.
What You’ll Learn
- What Terraform is, briefly, for engineers who already know IaC — providers, resources, data sources, variables, outputs, state, modules, plans, and applies.
- Where Copilot genuinely helps with Terraform, and the judgment calls it cannot make for you.
- Building a demo repository — a realistic
terraform-ai-demo/layout with modules, environments, and a CI workflow. - Generating configuration — versions, providers, resources, variables, and outputs, framed as draft-then-validate.
- Providers, variables, outputs, modules, and expressions — each generated with Copilot, then hardened.
- Formatting, validation, linting, and security scanning — the deterministic tools that are the actual source of truth.
- Reading
terraform plan— create, update, destroy, and replace — with Copilot as an added review layer, not the authority. - Destructive commands and state — what can hurt you, and the guardrails around it.
- Troubleshooting, documentation, CI/CD, and OIDC — plus 25 reusable prompts and a hands-on lab.
What Is Terraform?
Terraform is a declarative Infrastructure as Code tool: you describe the desired state of your infrastructure in HCL (HashiCorp Configuration Language), and Terraform figures out the create, update, and delete operations needed to reach it. This lesson assumes you already work with IaC, so the tour is brief — enough to fix the vocabulary Copilot will generate against.
- Providers are plugins that talk to a platform’s API (a cloud, a Kubernetes cluster, a DNS host, a SaaS). You configure and version-pin them.
- Resources are the things you manage — a network, a bucket, a database instance, an IAM role.
- Data sources read existing infrastructure you do not manage, so you can reference it.
- Variables parameterize a configuration; outputs expose values (IDs, endpoints, IPs) to callers and to other modules.
- State is Terraform’s record of what it manages and the mapping to real resources. It is authoritative and sensitive.
- Modules are reusable, parameterized groups of resources with defined inputs and outputs.
- Plan and apply:
terraform plancomputes and shows the diff between desired and current state;terraform applyexecutes it.
If any of that is unfamiliar, the Terraform guides and the broader Infrastructure as Code guides cover the fundamentals, and the OpenTofu guides cover the open-source fork that shares the same language and workflow. This lesson is not a beginner course — it is about pairing Copilot with those fundamentals responsibly.
Why Pair GitHub Copilot with Terraform?
HCL is verbose and mechanical, which is exactly where an AI pair programmer earns its keep — and exactly where a plausible-but-wrong suggestion can do real damage. Be explicit about both sides.
What Copilot assists well:
- Repetitive HCL — boilerplate resource blocks, tags, and near-identical configurations across environments.
- Unfamiliar resources — a first draft of a provider or resource you rarely touch, so you know what arguments exist.
- Module scaffolding — extracting a
main.tf/variables.tf/outputs.tfstructure from repeated resources. - Variables and outputs — adding types, descriptions, defaults, validation, and sensitivity flags.
- Dynamic blocks and expressions —
for_each,for,merge,lookup,try, locals, and conditionals. - Documentation and validation workflows — READMEs, variable tables, and CI pipelines that lint, scan, and plan.
- Troubleshooting — explaining an error and proposing a diagnosis you then confirm.
What Copilot cannot replace:
- Provider documentation — exact argument names, behavior, and constraints live in the provider docs and registry.
- Architecture judgment — what to build, how to segment networks, where trust boundaries go.
- Cost analysis — Copilot does not know your bill; a “small” instance choice can be expensive at scale.
- Security review — whether a policy is least-privilege, whether storage is exposed, whether logging exists.
- State strategy — backend choice, locking, workspaces, and how you handle drift.
- Production approval — the human decision, through a change process, to let an apply run.
🤖 AI Infrastructure Tip — Frame every interaction the same way: Copilot proposes, you read and understand, a deterministic Terraform tool validates, and a human approves. The moment you skip the middle two steps, you have handed real infrastructure to a system that has never been on call for it.
Create a Terraform Demo Repository
A realistic Terraform repository separates reusable modules from the environments that consume them, keeps CI alongside the code, and never commits state or secrets. Have Copilot help you scaffold this structure, then review every file:
terraform-ai-demo/
.github/
workflows/
terraform.yml CI: fmt/validate/lint/scan/plan
modules/
network/
main.tf network resources
variables.tf module inputs
outputs.tf module outputs
environments/
dev/
main.tf calls modules for dev
terraform.tfvars dev values (gitignored)
production/
main.tf calls modules for prod
main.tf root composition
providers.tf provider config + auth
variables.tf root input variables
outputs.tf root outputs
versions.tf terraform + provider pins
terraform.tfvars.example non-secret sample values
.gitignore excludes state + .terraform
README.md usage and layout
What each piece is for:
versions.tfpins the Terraform core version and provider versions so builds are reproducible.providers.tfconfigures providers and their authentication — via environment or OIDC, never hardcoded keys.variables.tf/outputs.tfdefine the root configuration’s inputs and exposed values.main.tfcomposes the root: it wires variables into module calls.modules/network/is a reusable module with its ownmain.tf,variables.tf, andoutputs.tf.environments/devandenvironments/productionare separate compositions so a change to dev cannot accidentally alter prod.terraform.tfvars.exampledocuments the variables with non-secret sample values; real values or secrets are never committed..gitignoremust exclude*.tfstate,*.tfstate.*,.terraform/, and any*.tfvarscontaining secrets.
⚠️ Warning — This demo uses no real credentials, no real cloud account, and no committed state. Keep it that way. A Terraform repo is a high-value target: if state or provider keys leak, an attacker can read or reshape your infrastructure. Put state in a remote backend, keep secrets in env vars or a secret manager, and confirm your
.gitignorebefore the first commit.
Generating Your First Terraform Configuration with Copilot
The workflow is cloud-neutral: whatever platform you target, you ask Copilot for the version pins, provider configuration, resources, typed variables, and outputs — then validate. Only the provider and resource names change between clouds. For concrete code, this lesson uses one provider (AWS) so the examples are complete and fmt-clean; the AWS guides go deeper on the resources themselves.
Start with the version and provider scaffolding. In versions.tf, describe what you want as a comment or ask in Copilot Chat, and refine the draft:
# versions.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Then give Copilot a precise prompt for the first real resource — precision is what makes the draft reviewable:
“Create a small virtual network with a configurable CIDR block. Use typed input variables with descriptions and sensible defaults, and expose the network ID and CIDR as outputs.”
A reasonable draft across variables.tf, main.tf, and outputs.tf:
# variables.tf
variable "vpc_cidr" {
description = "CIDR block for the demo VPC."
type = string
default = "10.0.0.0/16"
}
variable "vpc_name" {
description = "Name tag for the demo VPC."
type = string
default = "terraform-ai-demo"
}
# main.tf
resource "aws_vpc" "demo" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = var.vpc_name
}
}
# outputs.tf
output "vpc_id" {
description = "ID of the demo VPC."
value = aws_vpc.demo.id
}
output "vpc_cidr" {
description = "CIDR block of the demo VPC."
value = aws_vpc.demo.cidr_block
}
Read the draft before trusting it. The variable blocks are typed and described; the resource references the variable rather than hardcoding the CIDR; the outputs expose the values a caller will need. That is a solid mechanical first draft — but Copilot does not know your account: which CIDR ranges are already in use, what tagging your policy mandates, or whether DNS hostnames are appropriate here. Those are review decisions, not generation decisions.
Copilot for Terraform Providers
Providers are where a configuration meets a real platform, so getting them right matters. Copilot helps you configure a provider, pin versions, set up aliases for multiple configurations (for example two regions), and wire up authentication. A typical providers.tf:
# providers.tf
provider "aws" {
region = var.aws_region
}
# A second, aliased configuration for another region.
provider "aws" {
alias = "dr"
region = var.dr_region
}
Notice there are no credentials in that block — and that is deliberate. Authentication comes from the environment: shared config, environment variables, an instance role, or (in CI) OIDC-based short-lived credentials. Copilot will sometimes suggest inlining keys “for convenience.” Refuse it.
⚠️ Warning — Never hardcode cloud access keys, API tokens, passwords, or private keys in HCL, and never ask Copilot to. Credentials in a
.tffile end up in Git history, in state, and in every clone of the repo. Authenticate through environment variables, cloud workload identity / OIDC, or a secret manager. If Copilot inlines a secret, delete it and replace it with a reference — before you commit, not after.
Pin provider versions in versions.tf (as above) rather than floating them, so an upstream release cannot silently change behavior between plans. Ask Copilot to explain a version constraint like ~> 5.0 if you are unsure what it allows — that is a good read-only use.
Copilot for Terraform Variables
Copilot is strong at hardening variables: adding descriptions, explicit types, sensible defaults, nullable settings, sensitive flags, and validation blocks that reject bad input early. A useful prompt against an existing, under-specified variables file:
“Review these variables and add types, descriptions, and validation without changing their behavior.”
The single most valuable addition is validation, because it turns a class of misconfigurations into an immediate, readable error instead of a failed apply. Use this shape for an environment selector:
variable "environment" {
description = "Deployment environment."
type = string
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "Environment must be dev, stage, or prod."
}
}
For a value that must never be logged, mark it sensitive:
variable "db_password" {
description = "Database admin password (injected, not committed)."
type = string
sensitive = true
}
sensitive = true keeps the value out of plan and apply output. It does not encrypt it or keep it out of state — the value still lives in state in plaintext, which is one more reason to protect state. Ask Copilot to add validation and sensitivity, then read each block: a validation condition that is subtly wrong is worse than none because it looks like a safeguard.
✅ Best Practice — Ask Copilot to add types, descriptions, and validation “without changing behavior,” then confirm with
terraform validateand aterraform planthat shows no unexpected diff. Constraints you can read and test are guardrails; constraints you skimmed are decoration.
Copilot for Terraform Outputs
Outputs are how a module or root configuration exposes the values other things depend on: network IDs, resource IDs, IP addresses, endpoints, cluster names, database connection details. Copilot is good at proposing a useful set once it has seen your resources. Ask “add outputs for the values a consumer of this module would need,” and expect something like:
output "vpc_id" {
description = "ID of the VPC."
value = aws_vpc.demo.id
}
output "private_subnet_ids" {
description = "IDs of the private subnets."
value = [for s in aws_subnet.private : s.id]
}
output "db_endpoint" {
description = "Database connection endpoint."
value = aws_db_instance.main.address
sensitive = true
}
Two review points. First, expose only what callers actually need — outputs are an interface, and a sprawling one is hard to change later. Second, mark anything sensitive as sensitive = true so it does not print in CI logs or plan output. Copilot often forgets the sensitivity flag on endpoints and connection strings; add it.
Copilot for Terraform Modules
Modules are where Copilot’s mechanical strength pays off most, and also where judgment matters most. The common path is refactoring: you have repeated, near-identical resources — say the same network wiring copied into dev and production — and you want a single reusable modules/network/ instead. This is exactly the kind of structured, behavior-preserving change Copilot handles well.
Give it a precise refactor prompt:
“Refactor this repeated network configuration into a reusable module under
modules/network/, preserving behavior. Expose the CIDR and subnet layout as input variables and the VPC ID and subnet IDs as outputs.”
A resulting module might look like this. modules/network/variables.tf:
variable "vpc_cidr" {
description = "CIDR block for the VPC."
type = string
}
variable "azs" {
description = "Availability zones for subnets."
type = list(string)
}
variable "name" {
description = "Name prefix for tagging."
type = string
}
modules/network/main.tf:
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
Name = var.name
}
}
resource "aws_subnet" "private" {
for_each = toset(var.azs)
vpc_id = aws_vpc.this.id
availability_zone = each.value
cidr_block = cidrsubnet(var.vpc_cidr, 4, index(var.azs, each.value))
tags = {
Name = "${var.name}-${each.value}"
}
}
modules/network/outputs.tf:
output "vpc_id" {
description = "ID of the VPC."
value = aws_vpc.this.id
}
output "subnet_ids" {
description = "IDs of the created subnets."
value = [for s in aws_subnet.private : s.id]
}
Then the environment simply calls it:
# environments/dev/main.tf
module "network" {
source = "../../modules/network"
name = "demo-dev"
vpc_cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
}
Review the module as an interface. Are the inputs the right knobs — no more, no less? Are the outputs everything a caller needs and nothing sensitive left unflagged? Is the boundary sensible, or has Copilot over-abstracted a two-resource pattern into a module that adds indirection without reuse? Pin module versions when you consume shared modules from a registry, and keep boundaries aligned with how the infrastructure actually changes.
✅ Best Practice — After any module refactor, run
terraform validateand aterraform planand confirm it reports no changes (or only the changes you intended). A behavior-preserving refactor that produces a surprisedestroy/createin the plan is not behavior-preserving — read the plan before you believe the prompt.
Copilot and Terraform Expressions
HCL’s expression language is where a lot of real logic lives, and Copilot is fluent in it: for_each and count for repetition, for expressions to transform collections, maps/lists/sets, locals for computed values, conditionals, and functions like merge, lookup, and try. A very common, high-value pattern is consolidating tags in a locals block:
locals {
common_tags = {
Project = "terraform-ai-demo"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_vpc" "demo" {
cidr_block = var.vpc_cidr
tags = merge(local.common_tags, {
Name = var.vpc_name
})
}
merge combines the shared common_tags with a per-resource Name, so every resource carries consistent tagging without copy-paste. Ask Copilot to “apply local.common_tags to every taggable resource using merge” and it will thread it through — then confirm it did not miss a resource.
Extend expressions carefully. for_each over a map is usually safer than count over a list, because changing a list’s order with count can force Terraform to destroy and recreate resources it thinks moved. Use try and lookup to make configurations resilient to optional inputs, but read what they fall back to. When Copilot writes a dense one-liner, ask it to /explain the expression before you accept it — a for expression you cannot read is a bug you cannot review.
Formatting, Validation, and Linting
This is where the “deterministic tools are the source of truth” principle becomes concrete. Three distinct tools do three distinct jobs, and none of them is Copilot.
Formatting is deterministic — automate it, do not ask AI to judge it:
terraform fmt -recursive
fmt rewrites .tf files to canonical style across the tree. It is mechanical and reproducible, so it belongs in a pre-commit hook and in CI as terraform fmt -check, not in a conversation with Copilot.
Validation checks that the configuration is syntactically valid and internally consistent, and — once providers are installed — that resource arguments match the provider schema:
terraform init
terraform validate
Understand precisely what this proves and does not. terraform validate catches undeclared variables, references to undeclared resources, wrong argument names, and type mismatches. It does not prove the infrastructure is correct, safe, or affordable — only that the HCL is coherent. AI-generated code is often syntactically plausible and still wrong in ways validate cannot see; provider-aware validation catches more, and only a real plan against real state is authoritative for what will change.
Linting enforces style and provider-specific best practices, and is a separate concern from both formatting and security:
tflint
TFLint flags deprecated syntax, invalid instance types, unpinned providers, and provider-specific pitfalls. Copilot is genuinely useful at interpreting a lint error — paste it and ask what it means and how to fix it — but it does not replace running the linter. The linter is the authority; Copilot is the explainer.
Terraform Security Scanning
Security scanning is a different job again — it looks for insecure infrastructure, not style. The tooling here has changed, so use current tools.
❗ Important —
tfsec, once the standard standalone Terraform scanner, has been merged into Trivy — the last standalone tfsec release was in 2025 and it no longer gets new rules. Do not adopt tfsec as your current scanner. Use Trivy or Checkov instead.
Scan the configuration with a maintained tool:
# Trivy scans Terraform (and Dockerfiles, K8s, Helm).
trivy config .
# Or Checkov, actively maintained by Palo Alto/Prisma.
checkov -d .
trivy config is a drop-in successor to tfsec and understands the same class of checks; checkov is an actively maintained alternative. Either one flags the misconfigurations that AI-generated HCL commonly ships: public object storage, open firewall/security-group rules (0.0.0.0/0), unencrypted volumes, over-broad IAM (wildcard actions or resources), missing logging or audit trails, and publicly reachable databases. Copilot is good at explaining why a finding is a risk and drafting a fix — but the scanner, not Copilot, decides whether the configuration is safe.
The full deterministic chain around a Copilot draft:
Copilot Generates
|
terraform validate
|
tflint
|
security scan (Trivy / Checkov)
|
terraform plan
|
Human Review
Copilot with terraform plan
terraform plan is the most important safety tool in the whole workflow, because it shows exactly what an apply would do to real infrastructure before it does it:
terraform plan
The plan uses four symbols you must be able to read at a glance:
+ create— a new resource will be created.~ update— an existing resource will be modified in place.- destroy— a resource will be destroyed. Read every one of these.-/+ replace— a resource will be destroyed and recreated (often because an immutable argument changed). This is destructive even though it “looks like” an update.
Copilot helps here as a review aid. Paste a long plan and ask:
“Explain this plan in plain language and highlight anything being destroyed or replaced.”
That summary is genuinely useful on a plan with dozens of resources — it surfaces the - destroy and -/+ replace lines you might skim past. But be clear about the hierarchy: Copilot’s interpretation is an added review layer, not the truth. The plan output itself is authoritative. If Copilot’s summary and the raw plan disagree, the raw plan wins — always read it, especially every deletion and replacement, before you apply.
🔍 Troubleshooting — If a plan shows an unexpected
-/+ replace, do not apply it and do not accept Copilot’s first explanation blindly. Ask which argument forced replacement, check the resource’s documentation for which arguments are immutable, and confirm against the actual diff. A surprise replacement of a stateful resource — a database, a volume — is exactly the mistake this step exists to catch.
Destructive Terraform Commands
Some commands can destroy infrastructure or corrupt state. Know them, and put a process around them.
⚠️ Warning — These commands can delete real resources or damage state:
terraform destroy— tears down everything the configuration manages.terraform apply— executes a plan, including anydestroy/replaceit contains.terraform state rm— removes a resource from state (Terraform forgets it manages it; the real resource is orphaned).terraform import— binds an existing resource into state; a wrong address can be very confusing to unwind.terraform taint— marks a resource for recreation.taintis legacy — preferterraform apply -replace=ADDR.-target=...and manual state edits — surgical operations that bypass the normal graph and are easy to get wrong. Never run these against production casually, and never on Copilot’s say-so alone.
The safe path for any destructive operation:
Understand the command
|
Backup / Protect State
|
terraform plan
|
Peer Review
|
Approval
|
Execute
Copilot can explain what a destructive command does and what it would affect — use it for that. It must never be the thing that decides to run one.
Copilot for Terraform Troubleshooting
Terraform errors are often terse. Copilot is a fast first responder: paste the error, ask for the likely cause, then confirm with the CLI. The loop is always error → explanation → diagnostic command → fix → validate. Common cases:
- Unsupported argument — an argument that is not valid for that resource (often a hallucinated or deprecated field). Ask Copilot which arguments the resource actually supports, then confirm against the provider docs and re-run
terraform validate. - Undeclared variable — a
var.xwith novariable "x"block. Add the declaration;validateconfirms. - Reference to undeclared resource — a typo in a resource address or a missing resource. Fix the reference;
validate. - Provider authentication failure — credentials missing or expired. Check env vars / OIDC / profile; Copilot can explain the auth chain, but you fix the environment, not the HCL.
- Dependency cycle — two resources reference each other. Ask Copilot to identify the cycle; break it with a
depends_onchange or by restructuring;validate. - State lock — a previous run did not release the lock. Understand why before forcing anything; use
terraform force-unlockonly when you are sure no other run is active. - Provider version conflict — constraints that cannot be satisfied. Reconcile the pins in
versions.tf; re-runterraform init. - Invalid
for_each—for_eachgiven a value with unknown keys at plan time, or a list where a set/map is required. Ask Copilot to reshape the input;validateandplan. - Type mismatch — a string where a list is expected, or similar. Fix the type;
validate.
In each case Copilot forms the hypothesis and you prove it with terraform validate, terraform plan, or the relevant terraform state command. The Bash and Python automation guides are handy when a fix turns into a small script.
Terraform State Concepts
State is Terraform’s memory — the mapping between your configuration and the real resources it manages. Understanding it is not optional, because AI can help you write HCL but cannot manage your state strategy for you.
- Purpose — state records what Terraform manages so
plancan compute an accurate diff. - Local vs remote — local state (a file on disk) is fine for a solo demo; teams use remote state (a backend) so state is shared, versioned, and lockable.
- Locking — remote backends lock state during operations so two applies cannot corrupt it.
- Sensitive contents — state can contain secrets in plaintext: passwords, keys, connection strings, private IPs.
- Drift — when reality diverges from state (someone changed a resource by hand);
planreveals it. - Backups — keep versioned backups so a bad state operation is recoverable.
⚠️ Warning — Do NOT paste full production state into Copilot or any public/unapproved AI system. State frequently contains secrets, and once pasted you cannot un-share them. If you need help with a state operation, describe the situation and share only sanitized, non-sensitive snippets. Keep state in a remote backend with locking, encryption, and backups — and restrict who can read it.
Copilot for Terraform Documentation
Documentation is a real time-saver with Copilot, as long as you treat it as a draft. Ask it to generate a module README, describe a module’s purpose, build a variable table (name, type, default, description), document outputs, summarize the architecture, or write a usage example. For a module, a prompt like “write a README for modules/network/ with a variables table, an outputs table, and a usage example” produces a solid first pass.
The hazard is drift: generated docs describe what the code looks like it does, and nobody re-runs the prompt when the code changes. So the rule is simple.
⚠️ Warning — Verify generated documentation against the real configuration, and re-check it after every infrastructure change. AI docs go stale silently; a variable table that lists a default the code no longer uses is worse than no table, because someone will trust it. Docs are a draft you own, not output you publish unread. And remember: generated docs never replace the authoritative provider and Terraform registry documentation for exact argument behavior.
GitHub Copilot + Terraform + GitHub Actions
CI is where the review pipeline becomes enforced rather than aspirational. Ask Copilot to draft .github/workflows/terraform.yml, then harden it against the verified building blocks. A pull-request workflow that formats, validates, lints, scans, and plans — but never auto-applies:
name: terraform
on:
pull_request:
permissions:
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
- name: Format check
run: terraform fmt -check -recursive
- name: Init
run: terraform init -backend=false
- name: Validate
run: terraform validate
- name: Lint
run: |
# Install and run TFLint, or use a
# pinned TFLint action.
tflint --recursive
- name: Security scan
run: |
# Trivy or Checkov as the IaC scanner.
trivy config .
- name: Plan
run: terraform plan -no-color
What to verify in any Copilot-generated Terraform workflow:
- Pinned, real actions —
actions/checkout@v4andhashicorp/setup-terraform@v3are current, verified building blocks. Reject any unversioned or inventeduses:, and confirm major versions against each action’s own repository. - Least-privilege
permissions:— start atcontents: readand add only what a step needs. Copilot often omits this — add it. - Plan, do not apply — this workflow runs
terraform planfor human review. It does not apply. Do not let arbitrary pull requests apply to production; keep apply behind a protected environment with required reviewers. - A scan that actually fails — a scan step that always passes is worse than none. Confirm it fails the job on serious findings.
The CI/CD guides cover pipeline patterns more broadly, and Part 2, GitHub Copilot for DevOps Engineers, covers Actions fundamentals.
Terraform OIDC and GitHub Actions
The single biggest security improvement you can make to Terraform CI is to stop using long-lived cloud credentials. Instead of storing a static access key as a secret, use OIDC (OpenID Connect) so the workflow requests a short-lived, workload-identity token from your cloud at run time. That requires giving the job permission to mint the token:
permissions:
id-token: write
contents: read
With id-token: write, the workflow can exchange GitHub’s OIDC token for temporary cloud credentials scoped to a specific role — no static keys in secrets, no long-lived credentials to leak, and access that expires on its own. The cloud-side trust configuration (which repo, which branch, which role) is set up in your cloud provider and is beyond this lesson; the point here is the shape. This is a foundation later lessons build on — keep it in mind as the default for any Terraform pipeline that touches a real cloud. The security hardening guides go deeper on identity and least privilege.
✅ Best Practice — Prefer OIDC / workload identity over stored cloud keys in CI. Short-lived, scoped credentials that expire beat a static key that lives in your secrets store forever. If you must use a static key temporarily, scope it tightly and rotate it — and never let Copilot inline it into HCL.
25 GitHub Copilot Prompts for Terraform Engineers
Reusable starting prompts. Each produces a draft to understand, validate, scan, plan, and review before it applies.
Scaffolding and generation
- “Create
versions.tfpinning Terraform core and the AWS provider with reasonable constraints.” - “Configure the provider in
providers.tfusing environment-based auth — no hardcoded credentials.” - “Create a small VPC with a configurable CIDR, typed variables with descriptions, and outputs for the ID and CIDR.”
- “Add two private subnets across availability zones using
for_each.” - “Generate a
terraform.tfvars.examplewith non-secret sample values and comments.”
Variables and outputs
- “Review these variables and add types, descriptions, and validation without changing behavior.”
- “Add a
validationblock that restrictsenvironmentto dev, stage, or prod.” - “Mark the password and connection-string variables as
sensitive.” - “Add outputs for the values a consumer of this module would need, flagging sensitive ones.”
- “Explain what this variable’s
typeconstraint allows and rejects.”
Modules and expressions
- “Refactor this repeated network configuration into a reusable module, preserving behavior.”
- “Design the input and output interface for a
modules/network/module.” - “Consolidate tags into a
localsblock and apply them withmergeacross all resources.” - “Rewrite this
count-based repetition asfor_eachover a map, and explain the safety difference.” - “Explain this
forexpression and what collection it produces.”
Validation, linting, and security
- “Explain this
terraform validateerror and how to fix it.” - “Interpret this TFLint warning and propose a compliant change.”
- “Explain this Trivy/Checkov finding, why it is a risk, and draft a remediation.”
- “Review this configuration for public storage, open firewall rules, and over-broad IAM.”
- “Add encryption and access logging to this storage resource.”
Plan, troubleshooting, and CI
- “Explain this
terraform planand highlight anything being destroyed or replaced.” - “This plan shows an unexpected
-/+ replace— which argument forces it and why?” - “Diagnose this provider authentication failure and list the diagnostic commands to run.”
- “Draft a PR workflow that runs fmt-check, init, validate, TFLint, a security scan, and plan — with least-privilege permissions and no auto-apply.”
- “Write a README for this module with a variables table, an outputs table, and a usage example.”
Lab: Build and Validate AI-Assisted Terraform Infrastructure
Put it together by building the demo network with Copilot as your assistant and the Terraform CLI as your source of truth. The theme throughout: AI assists creation; Terraform tooling validates reality; humans approve.
- Create the repository — scaffold the
terraform-ai-demo/structure above with Copilot, then confirm.gitignoreexcludes state,.terraform/, and any secret.tfvars. - Initialize — run
terraform initand read what providers it installs against yourversions.tfpins. - Scaffold with Copilot — ask for
versions.tf,providers.tf, and a small VPC with a configurable CIDR. Read every line before saving. - Add variables — have Copilot add types, descriptions, and a
validationblock forenvironment; confirm the constraints are correct. - Add outputs — expose the VPC ID and CIDR; mark anything sensitive as
sensitive = true. - Format — run
terraform fmt -recursiveand confirm the tree is clean. - Validate — run
terraform validateand fix anything it flags; remember this proves coherence, not correctness. - Lint — run
tflintand use Copilot to interpret, not resolve, any warnings. - Security scan — run
trivy config .(orcheckov -d .) and remediate real findings; ask Copilot to explain each one. - Plan — run
terraform planand read every+,~,-, and-/+line yourself. - Summarize the plan with Copilot — ask it to explain the plan and highlight destroys and replaces, and compare its summary against the raw output. The raw plan wins.
- Manually review — decide, as the human in the loop, whether this is safe to apply, and in which workspace.
- Document — have Copilot draft the README and variable/output tables, then verify every entry against the real configuration.
- Commit — commit each piece only after you have understood and validated it.
- Open a pull request — push and let the
terraform.ymlworkflow run fmt-check, validate, lint, scan, and plan; review the plan in the PR before anyone approves. Do not auto-apply.
By the end you will have used inline completions, Copilot Chat, and a full review pipeline — and practiced the habit that makes AI-assisted IaC safe: generate fast, then let deterministic tooling and a human decide what actually applies.
🛠️ DevOps Tip — Add a repo-level custom instructions file so Copilot follows your Terraform conventions by default: pinned provider versions, tagged resources, validated variables, no hardcoded credentials, least-privilege IAM. Verify the current custom-instructions setup in the GitHub/VS Code docs, since the mechanism evolves.
What’s Next
You now have Copilot working across the full Terraform surface — providers, variables, outputs, modules, expressions, plans, troubleshooting, state, documentation, and CI with OIDC — always framed the same way: AI assists creation; Terraform tooling validates reality; humans approve. The next lesson, Part 6: GitHub Copilot with Docker, applies the same discipline to containers. The two fit together directly: Terraform provisions the infrastructure; Docker packages the applications that run on it. Networks, clusters, and databases come from IaC; the images that run on them come from Dockerfiles — and both need review, scanning, and human approval before production.
If you arrived here out of order, work back through Part 4, GitHub Copilot with VS Code, and Part 2, GitHub Copilot for DevOps Engineers, and return to the GitHub AI Engineering Academy home for the full path. To go deeper on the tools here, the Terraform guides, OpenTofu guides, Infrastructure as Code guides, AWS guides, and the Ubuntu AI Infrastructure series are all good next steps.
Recommended GitHub Books
GitHub Copilot Unleashed
A deeper dive into AI-assisted development with GitHub Copilot — prompting, workflows, and getting more from the tool.
- Copilot
- AI-assisted development
- Productivity
Learning GitHub Actions
A guide to automating build, test, and deploy with GitHub Actions — workflows, jobs, runners, and secrets.
- GitHub Actions
- CI/CD
- Automation
Affiliate Disclosure: Some links on this page are affiliate links. If you purchase through one of these links, DevOps AI Toolkit may earn a commission at no additional cost to you. See our affiliate disclosure.
Frequently asked questions
Can GitHub Copilot write Terraform?
Yes. Copilot drafts HCL well because the language is verbose and pattern-heavy: provider blocks, typed variables, resources that reference each other, outputs, and validation blocks. It works as inline completions and Copilot Chat inside VS Code and other editors. But writing HCL and provisioning correct, safe infrastructure are different things. Everything Copilot produces is a first draft you must read, then validate with terraform tooling — fmt, validate, a linter, a security scanner, and a plan — before it applies. Copilot proposes; terraform validates reality; a human approves.
Is AI-generated Terraform safe?
Not automatically. AI-generated Terraform can create or destroy real infrastructure, and plausible-looking HCL regularly ships insecure defaults: public storage, open firewall rules, unencrypted volumes, over-broad IAM, or missing logging. terraform validate proves syntax and internal consistency, not that the design is correct, secure, or affordable. Treat AI-generated IaC exactly like any pull request: engineer review, fmt, validate, lint, security scan (Trivy or Checkov), a full terraform plan read line by line, and human approval before apply — in a non-production workspace first.
Can Copilot create Terraform modules?
Yes. Copilot is good at scaffolding a module's main.tf, variables.tf, and outputs.tf, and at refactoring repeated resources into a reusable module while preserving behavior. It handles the mechanical work — extracting inputs, wiring outputs, threading variables through call sites. What it cannot decide for you is the module boundary: what belongs inside, what stays configurable, and when a module is over-abstracted. Review the generated interface, run terraform validate and plan, and confirm the refactor produces no unexpected changes before you trust it.
Can Copilot explain Terraform plans?
Yes, and it is one of the strongest uses. Paste a terraform plan and ask Copilot to summarize it and highlight any resources marked for destroy or replace. It reads the plan symbols — plus create, tilde update, minus destroy, and the replace marker — and explains them in plain language, which is a genuine help on a large plan. But the explanation is an added review layer, not the source of truth. The plan itself is authoritative; read the real output before applying, especially any deletion or replacement.
Can Copilot troubleshoot Terraform errors?
Yes. Paste the error — an unsupported argument, an undeclared variable, a reference to an undeclared resource, a provider authentication failure, a dependency cycle, a state lock, a provider version conflict, an invalid for_each, or a type mismatch — and ask Copilot to explain the likely cause and suggest a fix. It narrows the search quickly. Then confirm with deterministic tooling: run terraform validate, terraform plan, or the relevant terraform state command, apply the fix, and re-validate. Copilot forms the hypothesis; the terraform CLI proves it.
Can GitHub Copilot replace Terraform documentation?
No. Copilot can draft READMEs, variable and output tables, module descriptions, and architecture summaries, which saves real time. But generated docs describe what the code looks like it does, and they drift the moment the code changes and no one re-runs the prompt. They never replace the authoritative provider and Terraform registry documentation for exact argument names, behavior, and constraints. Treat AI docs as a draft, verify every claim against the real configuration and the provider docs, and re-check them after every infrastructure change.
Should production Terraform be generated by AI?
AI can help draft the Terraform that eventually runs in production, but no configuration should reach production because AI wrote it. The gate is human: an engineer who reads the HCL, runs fmt, validate, lint, and a security scanner, reviews a full plan against real state, and approves the apply through a change process with required reviewers. AI-generated IaC can create and destroy real infrastructure, so the review is not optional. Use Copilot to move faster through the draft; keep a human accountable for what actually applies.
Can Terraform be validated automatically in GitHub Actions?
Yes, and it is a core practice. A pull-request workflow using hashicorp/setup-terraform@v3 and actions/checkout@v4 can run terraform fmt -check, terraform init, terraform validate, TFLint, and a security scan (Trivy or Checkov), then terraform plan and post the result for human review. Use a least-privilege permissions block and cloud authentication via OIDC rather than long-lived keys. What you must NOT do is auto-apply to production from arbitrary pull requests — plan in CI, but keep apply behind human approval and protected environments.
Can Copilot help write Terraform tests?
Yes. Copilot can draft tests using Terraform's native testing framework (terraform test with .tftest.hcl files), plus policy checks and validation blocks that assert on inputs. It is useful for scaffolding assertions and test fixtures. As with generated code, the drafts are a starting point: read them, confirm they actually assert the behavior you care about — not just that the configuration parses — and run them with the terraform CLI. A test that always passes is worse than no test.
Should Terraform state be shared with Copilot?
No. Terraform state can contain sensitive values in plaintext — passwords, keys, connection strings, private IPs — so you should not paste full production state into Copilot or any AI system, and you should not commit it. Keep state in a remote backend with locking, encryption, and backups, and restrict access to it. If you need Copilot's help with a state operation, describe the situation and share only sanitized, non-sensitive snippets. The state file is one of the most sensitive artifacts in an IaC repository; treat it accordingly.
← Back to GitHub AI Engineering Academy