Provision AWS with Terraform
Use Terraform to build, secure, and manage a starter AWS environment.
Introduction
30 Second Summary
Rebuilding the same online environment can mean retracing dozens of settings from memory. One missed choice can leave the second version less secure than the first.
In this project, you will use Terraform to provision a secure starter environment on AWS from your Windows computer. You will then use plans to expose configuration drift before restoring the environment to its declared state.
What You'll Build
Picture running a single Terraform configuration and seeing the terminal print three network IDs before the same project grows into a secure seven-resource AWS lab.
By the end of this project, you'll have:
- A live network foundation whose outputs show the IDs for your VPC, private subnet, and isolated security group.
- A secure connected stack that adds an isolated EC2 instance, private S3 bucket, and credential-free IAM identity without public access.
- A drift detection demo where Terraform spots a tag removed outside its configuration. A repair returns the lab to a no-change plan.
- Secret Mission: Change a replacement-sensitive security group setting. Predict its dependency impact from the plan before reverting it without applying.
Are there any prerequisites?
You need a Windows computer with internet access plus an AWS account covered by an active Free plan or sufficient credits. Visual Studio Code and the AWS CLI should already be installed, with the CLI using an identity allowed to manage the lab's AWS resources.
Before We Start
Before any cloud resources are created, this step locks in the scope of your disposable AWS lab built through Terraform: a VPC, private subnet, isolated security group, EC2 instance, S3 bucket, public-access block, and IAM user. You are also committing to stop or destroy billable resources when the lab ends.
Prepare Your Windows Terraform Workspace
Your disposable lab creates resources in Amazon Web Services (AWS) that can consume credits. Before Terraform touches your account, you need a clear billing boundary.
Your Windows terminal also needs Terraform CLI 1.17.0. The AWS CLI must identify the disposable account you intend to use.
In this step, get ready to:
- Confirm an eligible AWS Free plan or sufficient credits.
- Make Terraform CLI 1.17.0 available through your Windows PATH.
- Verify Terraform plus your active AWS caller from a new terminal.
Confirm your AWS billing boundary
Billing deserves a deliberate check before you provision anything. This pause protects you from starting the lab without eligible credits.
Stop Before Provisioning Without Credits
An eligible AWS Free plan can cover this lab while its credits remain available. A paid account can incur EC2, EBS, S3 storage, or request charges.
The billing screen gives you the boundary for this session. Do not continue to a later terraform apply if your account lacks eligible credits.
- Open your web browser.
- Navigate to the AWS Billing and Cost Management console.
- Sign in with the disposable AWS learning account.
- Review the Free plan expiration shown for the account.
- Review the available credit balance shown for the account.
- Continue only if the account has an eligible Free plan or sufficient credits.
Your billing boundary is now clear. You know whether this account can support the lab before any AWS resources exist.
Install Terraform CLI 1.17.0
Terraform runs as a Windows executable named terraform.exe. Adding its folder to PATH lets every new terminal find the executable.
- Press the Windows key to open search.
- Type PowerShell into search.
- Select Windows PowerShell from the results.
- Check the installed Terraform version by running:
terraform version
What Does This Check Show?
The command prints the Terraform binary version available through your current PATH. Your result determines whether you can continue or need to install the pinned release.
Your result maps to one of the three states below.
✔️ I see version 1.17.0
Terraform CLI 1.17.0 is already available through your Windows PATH. You can continue to the identity check below.
ⓧ I see an older version
Your terminal found Terraform, but this project requires version 1.17.0 for consistent plans.
- Open the official Terraform installation page for Windows in your browser.
- Download the Windows AMD64 archive for version 1.17.0.
You should see the Terraform archive in your Windows Downloads folder.
- Open your Downloads folder in File Explorer.
- Right-click the downloaded Terraform archive.
- Select Extract All.
- Enter C:\Terraform as the destination folder.
- Complete the extraction.
You should now see terraform.exe directly inside C:\Terraform.
The Windows environment settings are a little fiddly to locate. The next actions add the executable folder to your user PATH without changing the system-wide value.
- Press the Windows key to open search.
- Type Control Panel into search.
- Select Control Panel from the results.
- Select System.
- Select Advanced system settings.
- Select Environment Variables.
You should see separate areas for user variables plus system variables.
- Select Path under your user variables.
- Select Edit.
You should see the folders Windows currently searches for executables.
- Select New.
- Enter C:\Terraform.
- Select OK in the path editor.
- Select OK in the environment variables window.
Your user PATH now includes C:\Terraform.
- Close every open PowerShell window.
- Press the Windows key to open search.
- Type PowerShell into search.
- Select Windows PowerShell from the results.
- Confirm the pinned Terraform version by running:
terraform version
What Should You See?
The new terminal should report Terraform v1.17.0. This confirms Windows found the executable through your updated PATH.
ⓧ Command not found
Terraform is not installed or its folder is missing from your Windows PATH. Installing the pinned binary resolves both requirements.
- Open the official Terraform installation page for Windows in your browser.
- Download the Windows AMD64 archive for version 1.17.0.
You should see the Terraform archive in your Windows Downloads folder.
- Open your Downloads folder in File Explorer.
- Right-click the downloaded Terraform archive.
- Select Extract All.
- Enter C:\Terraform as the destination folder.
- Complete the extraction.
You should now see terraform.exe directly inside C:\Terraform.
Finding the Windows environment settings can feel hidden. The next actions add the executable folder to your user PATH.
- Press the Windows key to open search.
- Type Control Panel into search.
- Select Control Panel from the results.
- Select System.
- Select Advanced system settings.
- Select Environment Variables.
You should see separate areas for user variables plus system variables.
- Select Path under your user variables.
- Select Edit.
You should see the folders Windows currently searches for executables.
- Select New.
- Enter C:\Terraform.
- Select OK in the path editor.
- Select OK in the environment variables window.
Your user PATH now includes C:\Terraform.
- Close every open PowerShell window.
- Press the Windows key to open search.
- Type PowerShell into search.
- Select Windows PowerShell from the results.
- Confirm the installation by running:
terraform version
What Should You See?
The new terminal should report Terraform v1.17.0. Windows can now find Terraform through your updated PATH.
What Is PATH?
Windows searches each folder in PATH when you enter a command. Adding C:\Terraform lets you run Terraform from any folder.
Terraform Still Not Found?
- Confirm terraform.exe sits directly inside C:\Terraform.
- Confirm your user Path contains C:\Terraform.
- Close every terminal window before opening a fresh one.
Help me troubleshoot why Windows cannot find Terraform after installation.
Verify Terraform and your AWS identity
Terraform uses the AWS identity already available to the AWS CLI. The caller identity check tells you which account receives the lab resources.
- Close the terminal used for the installation check.
- Press the Windows key to open search.
- Type PowerShell into search.
- Select Windows PowerShell from the results.
Before you run the checks, do you expect both tools to point at the versions and account you chose?
- Verify Terraform plus the active AWS caller by running:
terraform version
aws sts get-caller-identity
What Do These Commands Confirm?
- The first command prints the Terraform binary version available in this terminal.
- The second command returns the active caller's UserId.
- The response includes the AWS Account value.
- The response includes the caller Arn.
You should see Terraform v1.17.0 first. The AWS response should identify the disposable learning account you intend to use.
That completes the safety check. Your terminal now has the pinned Terraform version plus a known AWS identity.
What Remains Untested?
The caller identity check confirms which AWS identity is active. Creation plus deletion permissions remain untested until Terraform plans and applies the lab.
Seeing the Wrong Version or Account?
- Reopen PowerShell after changing your user PATH.
- Use your existing AWS CLI authentication workflow to select the disposable learning account.
- Rerun both checks before moving forward.
Help me diagnose my Terraform version or AWS caller identity.
Your Windows toolchain now has a verified billing boundary, a pinned Terraform executable, plus the intended AWS caller. Next, you will turn that workspace into a working network foundation.
Build and Apply the Network Foundation
Your billing boundary is confirmed. Your AWS CLI session points at the intended disposable account.
A small working network gives you an early result before compute or storage expands the scope. Terraform references also reveal how infrastructure as code creates a dependency graph from relationships between resources.
In this step, get ready to:
- Create a Terraform root module with pinned versions.
- Declare a private network with three connected resources.
- Apply the configuration to AWS.
Create the root module
A root module is the group of Terraform files in the folder where you run Terraform. These first files pin the required versions plus define the deployment Region.
- Switch back to the PowerShell terminal from the previous step.
- Create the project directories by running these commands:
New-Item -ItemType "Directory" -Path "C:\Projects" -Force
New-Item -ItemType "Directory" -Path "C:\Projects\terraform-aws-starter" -Force
What do these commands do?
- The first command creates the parent directory at `C:\Projects`.
- The second command creates `terraform-aws-starter` inside that directory.
- The force option safely returns an existing directory without replacing its contents.
- Confirm the terminal output includes `terraform-aws-starter` under `C:\Projects`.
Folder creation failed?
Check whether your Windows account can create directories under C:\Projects.
Share the PowerShell output without credentials if you need help. Help me troubleshoot the folder creation problem.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Click File in the top menu.
- Click Open Folder....
- Select C:\Projects\terraform-aws-starter in the folder dialog.
- Click Select Folder.
- Confirm that you trust the folder if Visual Studio Code displays a Workspace Trust prompt.
The Explorer sidebar should show terraform-aws-starter as the open workspace. This confirms every file you create next lands in the correct directory.
- Click New File... at the top of the Explorer sidebar.
- Enter terraform.tf as the file name.
- Pin the Terraform CLI plus AWS provider versions by pasting this code:
terraform {
required_version = "= 1.17.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.70.0"
}
}
}
What does this configuration do?
- The required version keeps this project on Terraform `1.17.0`.
- The provider source selects the HashiCorp AWS provider.
- The provider version keeps its behavior on `6.70.0`.
- Save terraform.tf with Ctrl+S.
- Confirm terraform.tf appears in the Explorer sidebar without an unsaved-change dot.
File missing from Explorer?
Confirm the Explorer heading shows terraform-aws-starter. A different heading means another workspace is open.
If the file remains missing, help me check the Visual Studio Code workspace.
- Click New File... in the Explorer sidebar.
- Enter variables.tf as the file name.
- Define the AWS Region input by pasting this code:
variable "aws_region" {
description = "AWS Region for the disposable lab."
type = string
default = "us-east-1"
}
What does this variable do?
The aws_region variable keeps the deployment AWS Region in one reusable value. Its default places this lab in us-east-1.
- Save variables.tf with Ctrl+S.
- Confirm variables.tf appears beside terraform.tf in the Explorer sidebar.
Region variable highlighted?
Check that the default is us-east-1 with matching quotation marks. Confirm that every opening brace has a closing brace.
If the block still looks invalid, help me compare the Region variable.
- Click New File... in the Explorer sidebar.
- Enter provider.tf as the file name.
- Configure the AWS provider plus shared project tags by pasting this code:
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = "Lab"
ManagedBy = "Terraform"
Project = "nextwork-terraform-lab"
}
}
}
What does this provider configuration do?
- The Region reference reads the value from `aws_region`.
- The default tags label supported resources as lab infrastructure.
- The shared tags identify the project in AWS.
- Save provider.tf with Ctrl+S.
- Confirm the Explorer sidebar now lists all three root module files.
Provider block highlighted?
Confirm region = var.aws_region sits inside the provider block. Check that the default_tags block has matching braces.
If the structure remains invalid, help me find the HCL syntax problem.
Declare the private network
A VPC provides the network boundary for this lab. A private subnet reserves a smaller address range without assigning public IP addresses.
An isolated security group supplies explicit empty traffic rules. Terraform connects these resources through references instead of manual ordering.
Why use raw AWS resources?
Raw provider resources keep the VPC dependencies visible in your code. A community module would hide much of that relationship behind module inputs.
This approach lets you watch Terraform infer the creation order from resource references.
- Click New File... in the Explorer sidebar.
- Enter network.tf as the file name.
- Declare the lab VPC by pasting this code:
resource "aws_vpc" "lab" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "nw-terraform-lab-vpc"
}
}
What does the VPC block do?
- The CIDR block reserves the `10.0.0.0/16` address range for the lab.
- The DNS settings enable AWS-provided name resolution inside the VPC.
- The Name tag identifies the VPC in AWS.
- Save network.tf with Ctrl+S.
- Confirm aws_vpc.lab appears as the first resource block in network.tf.
VPC block looks incomplete?
Confirm the CIDR value is 10.0.0.0/16. Check that the tags map remains inside the VPC resource.
If a brace is misplaced, help me compare the VPC block.
- Place your cursor after the closing brace of aws_vpc.lab.
- Add the private subnet by pasting this code:
resource "aws_subnet" "private" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = false
tags = {
Name = "nw-terraform-lab-private-subnet"
}
}
What does the subnet block do?
- The VPC reference makes the subnet depend on `aws_vpc.lab`.
- The `10.0.1.0/24` range sits inside the VPC address range.
- The public IP setting prevents automatic public IP assignment.
- Save network.tf with Ctrl+S.
- Confirm aws_subnet.private appears below aws_vpc.lab.
Subnet reference highlighted?
Check that the reference reads aws_vpc.lab.id without quotation marks. Confirm the subnet CIDR ends with /24.
If the reference remains invalid, help me trace the subnet dependency.
- Place your cursor after the closing brace of aws_subnet.private.
- Add the isolated security group by pasting this code:
resource "aws_security_group" "isolated" {
name = "nw-terraform-lab-isolated"
description = "No ingress or egress for the disposable Terraform lab"
vpc_id = aws_vpc.lab.id
ingress = []
egress = []
tags = {
Name = "nw-terraform-lab-isolated"
}
}
What does the security group block do?
- The VPC reference places the security group inside the lab network.
- The empty ingress list permits no managed inbound traffic.
- The empty egress list removes the default outbound rule from this managed security group.
- Save network.tf with Ctrl+S.
- Confirm network.tf now contains three resource blocks.
Traffic rules not empty?
Confirm both traffic attributes use empty lists. Check that neither attribute contains a nested rule block.
If the block differs, help me compare the isolated security group.
Outputs expose the generated resource IDs after an apply. They save you from searching the AWS console for each network object.
- Click New File... in the Explorer sidebar.
- Enter network_outputs.tf as the file name.
- Expose all three network IDs by pasting this code:
output "vpc_id" {
description = "ID of the lab VPC."
value = aws_vpc.lab.id
}
output "private_subnet_id" {
description = "ID of the private lab subnet."
value = aws_subnet.private.id
}
output "isolated_security_group_id" {
description = "ID of the isolated security group."
value = aws_security_group.isolated.id
}
What do these outputs expose?
- The `vpc_id` output reads the ID assigned to `aws_vpc.lab`.
- The `private_subnet_id` output reads the ID assigned to `aws_subnet.private`.
- The `isolated_security_group_id` output reads the ID assigned to `aws_security_group.isolated`.
- Save network_outputs.tf with Ctrl+S.
- Confirm network_outputs.tf contains all three output blocks.
Output reference highlighted?
Compare each output reference with its resource name in network.tf. Terraform references are case-sensitive.
If an output remains invalid, help me trace the resource reference.
✔️ Awesome, I've got everything!
Your five Terraform files are ready. Save every file before initializing the workspace.
ⓧ I'd like to double check the full code
Compare each file with the complete network foundation below.
terraform.tf.
terraform {
required_version = "= 1.17.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.70.0"
}
}
}
variables.tf.
variable "aws_region" {
description = "AWS Region for the disposable lab."
type = string
default = "us-east-1"
}
provider.tf.
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = "Lab"
ManagedBy = "Terraform"
Project = "nextwork-terraform-lab"
}
}
}
network.tf.
resource "aws_vpc" "lab" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "nw-terraform-lab-vpc"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = false
tags = {
Name = "nw-terraform-lab-private-subnet"
}
}
resource "aws_security_group" "isolated" {
name = "nw-terraform-lab-isolated"
description = "No ingress or egress for the disposable Terraform lab"
vpc_id = aws_vpc.lab.id
ingress = []
egress = []
tags = {
Name = "nw-terraform-lab-isolated"
}
}
network_outputs.tf.
output "vpc_id" {
description = "ID of the lab VPC."
value = aws_vpc.lab.id
}
output "private_subnet_id" {
description = "ID of the private lab subnet."
value = aws_subnet.private.id
}
output "isolated_security_group_id" {
description = "ID of the isolated security group."
value = aws_security_group.isolated.id
}
Initialize and apply the network
Initialization installs the pinned provider for this workspace. Validation checks the configuration before Terraform proposes any AWS changes.
- Click View in the Visual Studio Code menu.
- Click Terminal.
- Confirm the terminal path ends with C:\Projects\terraform-aws-starter.
- Initialize the Terraform workspace by running:
terraform init
What does initialization do?
Terraform reads the required provider configuration. It installs AWS provider 6.70.0 for this workspace.
Initialization also records the provider selection in a dependency lock file.
You should see confirmation that Terraform initialized the directory. The Explorer sidebar should also contain a dependency lock file.
Initialization failed?
Confirm the terminal path ends with C:\Projects\terraform-aws-starter. Check that terraform.tf contains provider version 6.70.0.
If the provider download still fails, help me troubleshoot Terraform initialization.
- Check the configuration for syntax plus internal consistency by running:
terraform validate
What does validation check?
Terraform checks whether the files form a valid configuration. It also checks whether each reference points to a declared object.
You should see confirmation that the configuration is valid.
Validation found a problem?
Use the reported file plus line number to locate the affected block. Common causes here include a missing brace or a misspelled resource reference.
Share the output without credentials if you need help. Help me fix the Terraform validation error.
Before you run the plan, how many resources do you think Terraform will propose creating?
- Preview the proposed AWS changes by running:
terraform plan
What does the plan prove?
Terraform compares the configuration with local state. The plan proposes actions without carrying them out.
Resource references also reveal the dependency order. The subnet plus security group depend on the VPC ID.
You should see three additions with no changes or deletions. This also proves that the active AWS identity can complete the provider checks required for planning.
Plan does not show three additions?
Confirm network.tf contains the VPC plus subnet plus security group. An AWS permissions message means the active caller cannot complete a required provider operation.
If the plan remains unexpected, help me interpret the Terraform plan.
This next command creates AWS resources. Your earlier credit check keeps the lab inside the billing boundary you selected.
Before you apply, which resource do you expect Terraform to create before the subnet plus security group?
- Start the network deployment by running:
terraform apply
What happens during apply?
Terraform creates a fresh plan. It pauses before executing the proposed operations.
The resource references place the VPC before its dependent subnet plus security group.
- Review the apply plan for three additions.
- Enter yes at the approval prompt.
You should see vpc_id, private_subnet_id, plus isolated_security_group_id in the Outputs section. Each output contains an AWS-generated ID.
That is your first live Terraform result. The VPC now contains a private subnet plus an isolated security group.
Apply failed or outputs are missing?
Check the first AWS operation reported as failed. A permissions message means the active caller needs access to the affected VPC resource type.
If resources were created without outputs, confirm network_outputs.tf is saved inside C:\Projects\terraform-aws-starter.
If the issue remains, help me troubleshoot the network apply.
Your network foundation is live plus recorded in local Terraform state. Next, you will connect isolated compute, protected storage, plus a credential-free identity to it.
Add Compute, Storage, and Identity
Your network foundation is live. Its three resources prove that Terraform can follow references across AWS.
A network alone cannot demonstrate a connected AWS stack. This step adds an isolated Amazon EC2 instance. It also adds protected Amazon S3 storage plus a credential-free IAM user.
In this step, get ready to:
- Create a private S3 bucket with bucket-level public access controls.
- Connect an isolated EC2 instance to the existing subnet plus security group.
- Create a credential-free IAM user before applying the complete seven-resource configuration.
Protect object storage
The S3 bucket needs a generated name because bucket names must be unique. A separate public access block keeps all four bucket-level protections enabled.
- In Visual Studio Code, select the Explorer view in the Activity Bar.
- Select New File... in Explorer.
- Enter storage.tf as the file name.
- Press Enter to create the file.
You will see storage.tf open in a new editor tab.
- Define the private bucket by pasting this code into storage.tf:
resource "aws_s3_bucket" "lab" {
bucket_prefix = "nw-terraform-lab-"
force_destroy = true
tags = {
Name = "nw-terraform-lab-bucket"
}
}
What does this bucket configuration do?
- The bucket_prefix value asks AWS to generate a unique name beginning with nw-terraform-lab-.
- The force_destroy setting allows Terraform to remove stored objects during the later cleanup.
- The Name tag identifies the bucket as part of this disposable lab.
- Save storage.tf by pressing Ctrl+S.
- Check the new bucket declaration by running this command in the terminal from earlier:
terraform validate
What does this validation prove?
The command checks the complete root module for valid syntax plus consistent references. A successful result confirms that Terraform can load the new bucket declaration.
You should see Terraform confirm that the configuration is valid.
- Add the bucket-level public access controls below the bucket resource in storage.tf by pasting:
resource "aws_s3_bucket_public_access_block" "lab" {
bucket = aws_s3_bucket.lab.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
How is public access blocked?
- The bucket reference connects these controls to aws_s3_bucket.lab.
- The four boolean settings block public ACLs plus public bucket policies.
- The resource reference gives Terraform the dependency needed to create the bucket first.
- Save storage.tf by pressing Ctrl+S.
- Preview the storage additions by running:
terraform plan
What does this plan check?
Terraform refreshes the three existing network resources before comparing the configuration with local state. The command only proposes actions.
The plan should contain two additions for the bucket plus its public access block. Existing network resources should remain unchanged.
Storage plan not showing two additions?
Check that both resource blocks are inside storage.tf. Confirm that the second block references aws_s3_bucket.lab.id.
Ask for help with the exact plan you received: help me diagnose why my Terraform storage plan does not show two additions
Connect isolated compute and identity
The EC2 instance uses references to join the private subnet plus isolated security group. Its storage is encrypted. Its metadata service requires session tokens.
- Return to the Explorer view in Visual Studio Code.
- Select New File....
- Enter compute.tf as the file name.
- Press Enter to create the file.
You will see compute.tf open in the editor.
- Define the isolated EC2 instance by pasting this code into compute.tf:
resource "aws_instance" "lab" {
ami = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
instance_type = "t3.micro"
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.isolated.id]
associate_public_ip_address = false
credit_specification {
cpu_credits = "standard"
}
metadata_options {
http_tokens = "required"
}
root_block_device {
delete_on_termination = true
encrypted = true
volume_size = 8
volume_type = "gp3"
}
tags = {
Name = "nw-terraform-lab-ec2"
}
}
How is this instance isolated?
- The subnet_id reference places the instance in aws_subnet.private.
- The vpc_security_group_ids reference attaches aws_security_group.isolated.
- The associate_public_ip_address setting prevents assignment of a public IP address.
- The root block device uses an encrypted 8 GiB gp3 volume.
- The metadata configuration requires IMDSv2 session tokens.
- Save compute.tf by pressing Ctrl+S.
- Validate the connected compute configuration by running:
terraform validate
What does this validation check?
Terraform resolves the instance references against the network resources from the previous step. A successful result confirms that the resource addresses match the root module.
You should see Terraform confirm that the configuration is valid.
Compute configuration not valid?
Compare the subnet reference with aws_subnet.private.id. Compare the security group reference with aws_security_group.isolated.id.
Ask for help with the validation result: help me fix my Terraform compute validation error
The lab identity demonstrates that an IAM user can exist without long-lived credentials. This configuration creates no console password. It creates no access key.
- Return to the Explorer view.
- Select New File....
- Enter identity.tf as the file name.
- Press Enter to create the file.
You will see identity.tf open in the editor.
- Define the credential-free IAM user by pasting:
resource "aws_iam_user" "lab" {
name = "nw-terraform-lab-user"
tags = {
Name = "nw-terraform-lab-user"
}
}
What does this identity resource create?
The resource creates the IAM user named nw-terraform-lab-user. No password resource appears in the configuration.
No access key resource appears either. The user remains credential-free.
- Save identity.tf by pressing Ctrl+S.
- Validate the IAM user declaration by running:
terraform validate
What does this validation confirm?
Terraform checks the IAM user together with every network plus service resource. A successful result confirms that the complete configuration remains internally consistent.
You should see Terraform confirm that the configuration is valid.
Outputs make generated identifiers available after the apply. You will expose the instance ID plus generated bucket name plus IAM user name.
- Return to the Explorer view.
- Select New File....
- Enter service_outputs.tf as the file name.
- Press Enter to create the file.
You will see service_outputs.tf open in the editor.
- Expose the EC2 instance ID by pasting:
output "instance_id" {
description = "ID of the isolated EC2 instance."
value = aws_instance.lab.id
}
What does this output expose?
The instance_id output reads the generated ID from aws_instance.lab. Terraform displays the value after a successful apply.
- Save service_outputs.tf by pressing Ctrl+S.
- Validate the first service output by running:
terraform validate
What does this output check prove?
A successful result confirms that aws_instance.lab.id is available to the output block.
- Add the generated bucket name below the instance_id output by pasting:
output "bucket_name" {
description = "Generated name of the private S3 bucket."
value = aws_s3_bucket.lab.id
}
Why output the bucket ID?
For this S3 resource, the exported ID is the generated bucket name. The bucket_name output lets you read that name without searching the AWS console.
- Save service_outputs.tf by pressing Ctrl+S.
- Validate the bucket output by running:
terraform validate
What does this validation add?
Terraform now checks the output reference to aws_s3_bucket.lab.id as part of the complete root module.
- Add the IAM user name below the bucket_name output by pasting:
output "iam_user_name" {
description = "Name of the credential-free IAM user."
value = aws_iam_user.lab.name
}
What does this final output expose?
The iam_user_name output reads the name from aws_iam_user.lab. The output confirms the identity resource after the apply.
- Save service_outputs.tf by pressing Ctrl+S.
- Preview the complete service expansion by running:
terraform plan
What should this complete plan contain?
The plan should propose four additions. Those additions are the EC2 instance plus S3 bucket plus S3 Public Access Block plus IAM user.
The plan should propose no replacement. It should propose no deletion.
Plan shows an unexpected change?
Stop before applying. Compare every changed address with the expected four service additions.
Ask for help interpreting the plan: help me identify unexpected changes in my Terraform plan
Apply the complete AWS stack
The complete plan is your safety boundary. The apply should proceed only when it contains four additions with no unexpected replacement or deletion.
✔️ Awesome, I've got everything!
Great. Confirm that all nine .tf files are saved before you apply the plan.
ⓧ I'd like to double check the full code
Compare every file below with your saved root module.
terraform {
required_version = "= 1.17.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.70.0"
}
}
}
What should match in terraform.tf?
This file pins Terraform to 1.17.0. It pins the AWS provider to 6.70.0.
variable "aws_region" {
description = "AWS Region for the disposable lab."
type = string
default = "us-east-1"
}
What should match in variables.tf?
This variable keeps the disposable lab in us-east-1 unless an explicit override is supplied.
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = "Lab"
ManagedBy = "Terraform"
Project = "nextwork-terraform-lab"
}
}
}
What should match in provider.tf?
The provider reads the Region from var.aws_region. Its default tags identify supported resources as part of the lab.
resource "aws_vpc" "lab" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "nw-terraform-lab-vpc"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = false
tags = {
Name = "nw-terraform-lab-private-subnet"
}
}
resource "aws_security_group" "isolated" {
name = "nw-terraform-lab-isolated"
description = "No ingress or egress for the disposable Terraform lab"
vpc_id = aws_vpc.lab.id
ingress = []
egress = []
tags = {
Name = "nw-terraform-lab-isolated"
}
}
What should match in network.tf?
The existing network file still declares the VPC plus private subnet plus isolated security group. No edits are needed here.
output "vpc_id" {
description = "ID of the lab VPC."
value = aws_vpc.lab.id
}
output "private_subnet_id" {
description = "ID of the private lab subnet."
value = aws_subnet.private.id
}
output "isolated_security_group_id" {
description = "ID of the isolated security group."
value = aws_security_group.isolated.id
}
What should match in network_outputs.tf?
The existing outputs continue to expose the three network identifiers.
resource "aws_s3_bucket" "lab" {
bucket_prefix = "nw-terraform-lab-"
force_destroy = true
tags = {
Name = "nw-terraform-lab-bucket"
}
}
resource "aws_s3_bucket_public_access_block" "lab" {
bucket = aws_s3_bucket.lab.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
What should match in storage.tf?
This file contains the uniquely named bucket plus all four bucket-level public access settings.
resource "aws_instance" "lab" {
ami = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
instance_type = "t3.micro"
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.isolated.id]
associate_public_ip_address = false
credit_specification {
cpu_credits = "standard"
}
metadata_options {
http_tokens = "required"
}
root_block_device {
delete_on_termination = true
encrypted = true
volume_size = 8
volume_type = "gp3"
}
tags = {
Name = "nw-terraform-lab-ec2"
}
}
What should match in compute.tf?
This file contains the isolated EC2 instance. Check its network references plus storage settings carefully.
resource "aws_iam_user" "lab" {
name = "nw-terraform-lab-user"
tags = {
Name = "nw-terraform-lab-user"
}
}
What should match in identity.tf?
This file declares only the IAM user. It contains no password resource plus no access key resource.
output "instance_id" {
description = "ID of the isolated EC2 instance."
value = aws_instance.lab.id
}
output "bucket_name" {
description = "Generated name of the private S3 bucket."
value = aws_s3_bucket.lab.id
}
output "iam_user_name" {
description = "Name of the credential-free IAM user."
value = aws_iam_user.lab.name
}
What should match in service_outputs.tf?
This file exposes the instance ID plus generated bucket name plus IAM user name.
Applying creates resources that can consume AWS credits. Your earlier billing check established the boundary for this disposable lab.
Before you run the apply, do you expect Terraform to create four resources or modify the three network resources?
- Start the reviewed apply by running:
terraform apply
What does this command do?
Terraform refreshes the current AWS objects before preparing a fresh plan. It pauses for approval before executing the proposed operations.
- Review the fresh plan for four additions.
- Confirm that the plan contains no unexpected replacement or deletion.
Creating the EC2 instance can take a few minutes. A quiet terminal during that wait is expected.
- Enter yes to approve the reviewed plan.
Terraform creates the four service resources. You will see values for instance_id plus bucket_name plus iam_user_name in the Outputs section.
Apply failed before completion?
Read the resource address beside the failure. An authorization failure means the active AWS identity lacks permission for that resource type.
Ask for help without sharing credentials: help me interpret my Terraform apply failure
Before you inspect state, how many managed resource addresses should Terraform list?
- List every managed resource address by running:
terraform state list
What should I see?
Terraform should list exactly seven managed resource addresses:
- The address aws_iam_user.lab.
- The address aws_instance.lab.
- The address aws_s3_bucket.lab.
- The address aws_s3_bucket_public_access_block.lab.
- The address aws_security_group.isolated.
- The address aws_subnet.private.
- The address aws_vpc.lab.
That is the complete stack working. Terraform now manages connected networking plus isolated compute plus protected storage plus a credential-free identity.
Your seven-resource AWS environment is live under one declarative configuration. Next, you will create a controlled manual change so Terraform can expose configuration drift.
Create and Detect Configuration Drift
Your seven-resource AWS environment now matches the Terraform configuration. A manual cloud change can break that agreement without touching a single .tf file.
This step creates configuration drift by removing the EC2 Name tag through the AWS CLI. You will then ask Terraform to expose the mismatch.
In this step, get ready to:
- Remove only the EC2 Name tag outside Terraform.
- Compare the live instance with the declared configuration in a Terraform plan.
Remove the EC2 Name tag outside Terraform
Terraform still declares the instance tag in compute.tf. Removing the live tag through AWS creates a controlled mismatch between the configuration and the deployed instance.
- Switch back to the terminal at C:\Projects\terraform-aws-starter from earlier.
- Find the instance_id value in the previous apply outputs.
- Record the instance ID here: i-your-instance-id.
- Replace i-your-instance-id in the command below with i-your-instance-id.
- Remove only the instance's Name tag by running the completed command:
aws ec2 delete-tags --resources i-your-instance-id --tags Key=Name --region us-east-1
What does this command do?
- The --resources i-your-instance-id argument targets your isolated EC2 instance.
- The --tags Key=Name argument removes the tag key regardless of its current value.
- The --region us-east-1 argument sends the request to the Region that contains your lab.
Once AWS accepts the request, the live EC2 instance is missing only its Name tag. The Terraform files still declare that tag.
Tag Deletion Rejected?
- Confirm that you replaced i-your-instance-id with the complete value from the instance_id output.
- Confirm that the terminal still uses the verified AWS CLI identity from earlier.
- Confirm that the instance exists in us-east-1.
Help me troubleshoot the EC2 tag deletion command.
Detect the drift in a Terraform plan
A Terraform plan refreshes remote objects before comparing them with the configuration and prior state. This comparison reveals the manual tag deletion without changing any AWS resources.
- Leave every .tf file unchanged.
Before you check, which plan symbol do you expect Terraform to use for this mismatch?
- Compare the live EC2 instance with its declared configuration by running:
terraform plan
How Does the Plan Expose Drift?
- The planning command refreshes Terraform's view of the live EC2 instance.
- The ~ symbol marks an in-place update.
- The aws_instance.lab address identifies the resource whose live tags differ from the configuration.
You will see aws_instance.lab marked ~ update in-place. This is the intended evidence that Terraform detected the manual change.
Under the instance tags, the plan proposes restoring Name = "nw-terraform-lab-ec2".
- Stop here without applying the proposed update.
You caught the exact mismatch before Terraform changed anything. Next, you will restore the declared tag and prove that repeated planning settles into a no-change state.
Repair Drift and Prove Idempotency
The missing EC2 Name tag gave Terraform a real example of configuration drift. Your declared configuration still contains the intended value.
Infrastructure as code becomes useful when the declaration can restore reality. A second plan proves idempotency when it finds no remaining changes.
In this step, get ready to:
- Apply the one in-place update to restore the EC2 Name tag.
- Run a fresh plan to prove idempotency.
- List all seven managed addresses from local Terraform state.
Restore the declared EC2 tag
Applying a live change in AWS can feel risky. The reviewed plan limits this repair to one in-place tag update.
- Confirm the terminal from earlier is still at C:\Projects\terraform-aws-starter.
Before you run the repair, do you expect Terraform to replace the instance or update it in place?
- Prepare the drift repair by running this command:
terraform apply
How Does the Repair Work?
This command creates a fresh plan. It pauses for approval before Terraform sends the approved tag update to AWS.
Only the action you approve reaches the live environment.
- Review the preview until it shows only the one in-place update for aws_instance.lab.
The preview should mark aws_instance.lab as ~ update in-place. It should propose restoring Name = "nw-terraform-lab-ec2".
Plan Shows More Than One Change?
An extra file edit or an unsaved reversal can introduce unexpected actions.
- Decline the approval prompt.
- Confirm the only proposed change restores the missing Name tag on aws_instance.lab.
If AWS rejects the operation before approval, help me check the active identity and update permissions.
- Enter yes at the approval prompt.
Terraform reports that one resource changed. Your EC2 instance now carries the declared Name tag again.
That closes the repair loop. The declared configuration has restored the live resource.
Prove idempotency across the full state
Idempotency means repeating the same operation reaches the same stable result. With reality matching the declaration again, a new plan should have no work to perform.
Before you check, do you expect Terraform to propose another update or settle into a no-change result?
- Test the repaired configuration by running this command:
terraform plan
What Does This Plan Prove?
This command refreshes the remote objects. It compares their current settings with the configuration and local state.
You should see no actions proposed. Terraform reports that no changes are needed.
Still Seeing a Proposed Change?
A proposed action means the live environment or a configuration file still differs from the declared result.
- Confirm every .tf file is saved.
- Compare the changed resource address with the plan from the previous step.
If the remaining difference is unclear, help me interpret the unexpected Terraform plan.
Before you inspect the state, how many managed addresses do you expect Terraform to list?
- List every managed resource address by running this command:
terraform state list
What Should I See?
This command reads the resource addresses from local state. You should see exactly these seven addresses:
- aws_vpc.lab.
- aws_subnet.private.
- aws_security_group.isolated.
- aws_instance.lab.
- aws_s3_bucket.lab.
- aws_s3_bucket_public_access_block.lab.
- aws_iam_user.lab.
State List Has a Different Count?
A missing address means the matching resource is absent from this local state. An extra address means Terraform still tracks another resource.
- Compare each listed address with the seven expected addresses above.
- Confirm the terminal is still at C:\Projects\terraform-aws-starter.
If the count still differs, help me diagnose the Terraform state list safely.
You have closed Terraform's full reconciliation loop. The configuration repaired reality and now produces a stable plan.
The repair changed AWS. Every Terraform file remains unchanged.
✔️ My files match
Your files already match the declared configuration. No edits are needed.
ⓧ I'd like to double check the full code
- Compare each file in C:\Projects\terraform-aws-starter with the references below.
resource "aws_instance" "lab" {
ami = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
instance_type = "t3.micro"
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.isolated.id]
associate_public_ip_address = false
credit_specification {
cpu_credits = "standard"
}
metadata_options {
http_tokens = "required"
}
root_block_device {
delete_on_termination = true
encrypted = true
volume_size = 8
volume_type = "gp3"
}
tags = {
Name = "nw-terraform-lab-ec2"
}
}
What Does compute.tf Define?
This file declares the isolated EC2 instance. Its settings prevent a public IP address and require metadata session tokens.
The encrypted root volume is deleted with the instance. The Name tag is the value Terraform just restored.
resource "aws_iam_user" "lab" {
name = "nw-terraform-lab-user"
tags = {
Name = "nw-terraform-lab-user"
}
}
What Does identity.tf Define?
This file declares the lab user and its tag. It creates no console password or access key.
resource "aws_vpc" "lab" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "nw-terraform-lab-vpc"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = false
tags = {
Name = "nw-terraform-lab-private-subnet"
}
}
resource "aws_security_group" "isolated" {
name = "nw-terraform-lab-isolated"
description = "No ingress or egress for the disposable Terraform lab"
vpc_id = aws_vpc.lab.id
ingress = []
egress = []
tags = {
Name = "nw-terraform-lab-isolated"
}
}
What Does network.tf Define?
This file declares the VPC and private subnet. Resource references connect the subnet to the VPC.
The isolated security group has empty inbound and outbound rule sets.
output "vpc_id" {
description = "ID of the lab VPC."
value = aws_vpc.lab.id
}
output "private_subnet_id" {
description = "ID of the private lab subnet."
value = aws_subnet.private.id
}
output "isolated_security_group_id" {
description = "ID of the isolated security group."
value = aws_security_group.isolated.id
}
What Does network_outputs.tf Expose?
These outputs expose the generated network IDs after an apply. They let you identify the three network resources without searching the AWS console.
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = "Lab"
ManagedBy = "Terraform"
Project = "nextwork-terraform-lab"
}
}
}
What Does provider.tf Configure?
This file configures the AWS provider with the selected Region. It also applies the shared lab tags to supported resources.
output "instance_id" {
description = "ID of the isolated EC2 instance."
value = aws_instance.lab.id
}
output "bucket_name" {
description = "Generated name of the private S3 bucket."
value = aws_s3_bucket.lab.id
}
output "iam_user_name" {
description = "Name of the credential-free IAM user."
value = aws_iam_user.lab.name
}
What Does service_outputs.tf Expose?
These outputs expose the instance ID and generated bucket name. They also expose the credential-free user name.
resource "aws_s3_bucket" "lab" {
bucket_prefix = "nw-terraform-lab-"
force_destroy = true
tags = {
Name = "nw-terraform-lab-bucket"
}
}
resource "aws_s3_bucket_public_access_block" "lab" {
bucket = aws_s3_bucket.lab.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
What Does storage.tf Define?
This file declares the uniquely named bucket. Its public-access configuration enables all four bucket-level protections.
The destroy setting allows Terraform to remove stored objects when the lab is deleted.
terraform {
required_version = "= 1.17.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.70.0"
}
}
}
What Does terraform.tf Pin?
This file requires Terraform 1.17.0. It also pins the AWS provider to 6.70.0 for repeatable behavior.
variable "aws_region" {
description = "AWS Region for the disposable lab."
type = string
default = "us-east-1"
}
What Does variables.tf Control?
This input sets the AWS Region for the disposable lab. Its default keeps every resource in us-east-1.
Secret mission
Predict the Blast Radius of a Replacement
Temporarily rename the isolated security group to trigger a replacement-sensitive plan. Trace the effects across its dependents without applying anything. Restore the original name to recover a no-change plan.
Clean Up Your Resources
Clean Up Your Resources
This AWS lab can consume credits or create charges while its cloud resources remain deployed. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Your EC2 instance can consume credits while running.
Its EBS root volume can continue using storage after the instance stops.
A Linux t3.micro in US East (Northern Virginia) is listed at $0.0104 per hour. S3 storage can also consume credits or create charges.
- Choose Pause or Delete if you have finished the lab or your eligible credits are running low.
Resources you used:
- One VPC.
- One private subnet.
- One isolated security group.
- One t3.micro EC2 instance with an encrypted EBS root volume.
- One private S3 bucket.
- One S3 Public Access Block configuration for the private bucket.
- One credential-free IAM user.
- One local project folder at C:\Projects\terraform-aws-starter containing local Terraform state.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the AWS lab.
- Keep the seven managed AWS resources only while eligible credits remain available.
- Monitor your available credits while the EC2 instance remains running.
- Retain C:\Projects\terraform-aws-starter so the configuration and local state remain available.
- Leave the Terraform CLI installed for future infrastructure projects.
Pause - I'll come back to this later
Stop the running EC2 instance to reduce compute usage. Keep the other resources available for when you return.
- Stop the EC2 instance tagged nw-terraform-lab-ec2 through your existing AWS management workflow.
- Keep C:\Projects\terraform-aws-starter untouched so Terraform retains its local state.
The EBS volume remains after the instance stops. S3 storage can also continue using credits or creating charges.
Terraform can detect the out-of-band instance state when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. Terraform gives you a preview before it changes AWS.
- Switch back to the terminal at C:\Projects\terraform-aws-starter from earlier.
- Preview the seven planned deletions by running this command:
terraform plan -destroy
What does this preview do?
This command compares the managed infrastructure with a complete destruction plan. It does not change any AWS resource.
The preview should show seven deletions. It should show no creates or updates.
Preview does not show seven deletions?
- Check that network.tf still uses nw-terraform-lab-isolated as the security-group name.
- Confirm the terminal is still at C:\Projects\terraform-aws-starter.
- Help me reconcile this destruction preview.
Destroying the lab permanently removes the managed AWS resources. Terraform asks for approval before it acts.
- Start the destruction process by running this command:
terraform destroy
What does this command remove?
Terraform deprovisions all seven objects managed by this configuration. Removing the EC2 instance also removes its attached root volume.
The bucket's force_destroy setting allows Terraform to remove stored objects during destruction.
- Enter yes when Terraform asks you to approve the destruction.
You should see each managed resource removed. The terminal finishes with a successful destruction summary.
Destruction did not finish?
- Keep the local project folder until every AWS resource has been removed.
- Resolve the resource-specific issue shown in the terminal before repeating the destruction process.
- Help me finish this cleanup.
- Verify that local state contains no resource addresses by running this command:
terraform state list
What does this final check prove?
This command lists every resource address still recorded in local state. Empty output confirms that Terraform no longer manages any AWS object from this lab.
That is the cloud cleanup complete. Your verified state now contains no managed resource addresses.
Deleting the local folder is the final irreversible cleanup. It removes the configuration files and local state from your computer.
- Close the Visual Studio Code window using the X in the top-right corner.
- Press the Windows key to open search.
- Search for File Explorer.
- Open File Explorer from the search result.
- Navigate to C:\Projects.
- Select the terraform-aws-starter folder.
- Press Shift+Delete to permanently remove the selected folder.
- Confirm that terraform-aws-starter no longer appears in C:\Projects.
Nice Work!
Nice Work!
You did it! You built a secure seven-resource environment on AWS from a pinned Terraform configuration.
You've learned how to:
- Provisioned a connected AWS stack through Terraform references. Used outputs to read generated IDs. Used local state to account for exactly seven managed addresses.
- Kept the EC2 instance isolated from public access. Protected the S3 bucket with all four Public Access Block controls. Created the IAM user with no password or access key.
- Created configuration drift by deleting the EC2 Name tag outside Terraform. Detected the mismatch in a Terraform plan. Restored the missing tag through Terraform. Proved idempotency with a no-change plan.
- Completed the Secret Mission by predicting the blast radius of a replacement-sensitive security-group edit. Reverted the edit before AWS received any change.
Ready to quiz yourself?