docs: add Terraform certification notes and tasks

This commit is contained in:
2026-08-04 23:18:47 +02:00
parent 91a1849009
commit 758182a700
53 changed files with 4588 additions and 0 deletions

39
terraform/001/main.tf Normal file
View File

@@ -0,0 +1,39 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Generate an RSA private key locally
resource "tls_private_key" "devops_kp" {
algorithm = "RSA"
rsa_bits = 4096
}
# Import the public half into AWS as an RSA key pair
resource "aws_key_pair" "devops_kp" {
key_name = "devops-kp"
public_key = tls_private_key.devops_kp.public_key_openssh
}
# Persist the private key to disk
resource "local_sensitive_file" "devops_kp_pem" {
content = tls_private_key.devops_kp.private_key_pem
filename = "/home/bob/devops-kp.pem"
file_permission = "0400"
}

15
terraform/001/task-1.md Normal file
View File

@@ -0,0 +1,15 @@
## Task 1
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
For this task, create a key pair using Terraform with the following requirements:
Name of the key pair should be devops-kp.
Key pair type must be rsa.
The private key file should be saved under /home/bob/devops-kp.pem.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

50
terraform/002/main.tf Normal file
View File

@@ -0,0 +1,50 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
# Look up the default VPC
data "aws_vpc" "default" {
default = true
}
resource "aws_security_group" "devops_sg" {
name = "devops-sg"
description = "Security group for Nautilus App Servers"
vpc_id = data.aws_vpc.default.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "devops-sg"
}
}

15
terraform/002/task-2.md Normal file
View File

@@ -0,0 +1,15 @@
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
Use Terraform to create a security group under the default VPC with the following requirements:
1) The name of the security group must be devops-sg.
2) The description must be Security group for Nautilus App Servers.
3) Add an inbound rule of type HTTP, with a port range of 80, and source CIDR range 0.0.0.0/0.
4) Add another inbound rule of type SSH, with a port range of 22, and source CIDR range 0.0.0.0/0.
Ensure that the security group is created in the us-east-1 region using Terraform. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

20
terraform/003/main.tf Normal file
View File

@@ -0,0 +1,20 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_vpc" "devops_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "devops-vpc"
}
}

9
terraform/003/task-3.md Normal file
View File

@@ -0,0 +1,9 @@
## Task 3
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
Create a VPC named devops-vpc in region us-east-1 with any IPv4 CIDR block through terraform.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

20
terraform/004/main.tf Normal file
View File

@@ -0,0 +1,20 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_vpc" "xfusion_vpc" {
cidr_block = "192.168.0.0/24"
tags = {
Name = "xfusion-vpc"
}
}

8
terraform/004/task-4.md Normal file
View File

@@ -0,0 +1,8 @@
The Nautilus DevOps team is strategically planning the migration of a portion of their infrastructure to the AWS cloud. Acknowledging the magnitude of this endeavor, they have chosen to tackle the migration incrementally rather than as a single, massive transition. Their approach involves creating Virtual Private Clouds (VPCs) as the initial step, as they will be provisioning various services under different VPCs.
Create a VPC named xfusion-vpc in us-east-1 region with 192.168.0.0/24 IPv4 CIDR using terraform.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

21
terraform/005/main.tf Normal file
View File

@@ -0,0 +1,21 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_vpc" "devops_vpc" {
cidr_block = "10.0.0.0/16"
assign_generated_ipv6_cidr_block = true
tags = {
Name = "devops-vpc"
}
}

9
terraform/005/task-5.md Normal file
View File

@@ -0,0 +1,9 @@
## Task 5
The Nautilus DevOps team is strategically planning the migration of a portion of their infrastructure to the AWS cloud. Acknowledging the magnitude of this endeavor, they have chosen to tackle the migration incrementally rather than as a single, massive transition. Their approach involves creating Virtual Private Clouds (VPCs) as the initial step, as they will be provisioning various services under different VPCs.
For this task, create a VPC named devops-vpc in the us-east-1 region with the Amazon-provided IPv6 CIDR block using terraform.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

20
terraform/006/main.tf Normal file
View File

@@ -0,0 +1,20 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_eip" "nautilus_eip" {
domain = "vpc"
tags = {
Name = "nautilus-eip"
}
}

7
terraform/006/task-6.md Normal file
View File

@@ -0,0 +1,7 @@
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
For this task, allocate an Elastic IP address named nautilus-eip using Terraform.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

53
terraform/007/main.tf Normal file
View File

@@ -0,0 +1,53 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# tls = {
# source = "hashicorp/tls"
# version = "~> 4.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
# --- New RSA key pair ---
resource "tls_private_key" "devops_kp" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "devops_kp" {
key_name = "devops-kp"
public_key = tls_private_key.devops_kp.public_key_openssh
}
# --- Default VPC + its default security group ---
data "aws_vpc" "default" {
default = true
}
data "aws_security_group" "default" {
vpc_id = data.aws_vpc.default.id
name = "default"
}
# --- EC2 instance ---
resource "aws_instance" "devops_ec2" {
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
key_name = aws_key_pair.devops_kp.key_name
vpc_security_group_ids = [data.aws_security_group.default.id]
credit_specification {
cpu_credits = "standard"
}
tags = {
Name = "devops-ec2"
}
}

17
terraform/007/task-7.md Normal file
View File

@@ -0,0 +1,17 @@
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units.
For this task, create an EC2 instance using Terraform with the following requirements:
The EC2 instance must use the value devops-ec2 as its Name tag, which defines the instance name in AWS.
Use the Amazon Linux ami-0c101f26f147fa7fd to launch this instance.
The Instance type must be t2.micro.
Create a new RSA key named devops-kp.
Attach the default (available by default) security group.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to provision the instance.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

30
terraform/008/main.tf Normal file
View File

@@ -0,0 +1,30 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
# Look up the existing instance by its Name tag
data "aws_instance" "datacenter_ec2" {
filter {
name = "tag:Name"
values = ["datacenter-ec2"]
}
}
# Create an AMI from that instance
resource "aws_ami_from_instance" "datacenter_ec2_ami" {
name = "datacenter-ec2-ami"
source_instance_id = data.aws_instance.datacenter_ec2.id
tags = {
Name = "datacenter-ec2-ami"
}
}

9
terraform/008/task-8.md Normal file
View File

@@ -0,0 +1,9 @@
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
For this task, create an AMI from an existing EC2 instance named datacenter-ec2 using Terraform.
Name of the AMI should be datacenter-ec2-ami, make sure AMI is in available state.
The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to create the AMI.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

22
terraform/009/main.tf Normal file
View File

@@ -0,0 +1,22 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_ebs_volume" "xfusion_volume" {
availability_zone = "us-east-1a"
type = "gp3"
size = 2
tags = {
Name = "xfusion-volume"
}
}

16
terraform/009/task-9.md Normal file
View File

@@ -0,0 +1,16 @@
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
For this task, create an AWS EBS volume using Terraform with the following requirements:
Name of the volume should be xfusion-volume.
Volume type must be gp3.
Volume size must be 2 GiB.
Ensure the volume is created in us-east-1.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

30
terraform/010/main.tf Normal file
View File

@@ -0,0 +1,30 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
# Look up the existing volume by its Name tag
data "aws_ebs_volume" "devops_vol" {
filter {
name = "tag:Name"
values = ["devops-vol"]
}
}
# Snapshot it
resource "aws_ebs_snapshot" "devops_vol_ss" {
volume_id = data.aws_ebs_volume.devops_vol.id
description = "Devops Snapshot"
tags = {
Name = "devops-vol-ss"
}
}

13
terraform/010/task-10.md Normal file
View File

@@ -0,0 +1,13 @@
The Nautilus DevOps team has some volumes in different regions in their AWS account. They are going to setup some automated backups so that all important data can be backed up on regular basis. For now they shared some requirements to take a snapshot of one of the volumes they have.
Create a snapshot of an existing volume named devops-vol in us-east-1 region using terraform.
1) The name of the snapshot must be devops-vol-ss.
2) The description must be Devops Snapshot.
3) Make sure the snapshot status is completed before submitting the task.
The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

24
terraform/011/main.tf Normal file
View File

@@ -0,0 +1,24 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_cloudwatch_metric_alarm" "datacenter_alarm" {
alarm_name = "datacenter-alarm"
namespace = "AWS/EC2"
metric_name = "CPUUtilization"
statistic = "Average"
comparison_operator = "GreaterThanThreshold"
threshold = 80
period = 300
evaluation_periods = 1
alarm_description = "Alarm when EC2 CPU utilization exceeds 80%"
}

13
terraform/011/task-11.md Normal file
View File

@@ -0,0 +1,13 @@
The Nautilus DevOps team is setting up monitoring in their AWS account. As part of this, they need to create a CloudWatch alarm.
Using Terraform, perform the following:
Task Details:
Create a CloudWatch alarm named datacenter-alarm.
The alarm should monitor CPU utilization of an EC2 instance.
Trigger the alarm when CPU utilization exceeds 80%.
Set the evaluation period to 5 minutes.
Use a single evaluation period.
Ensure that the entire configuration is implemented using Terraform. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.

45
terraform/012/main.tf Normal file
View File

@@ -0,0 +1,45 @@
# terraform {
# required_providers {
# aws = {
# source = "hashicorp/aws"
# version = "~> 6.0"
# }
# }
# }
# provider "aws" {
# region = "us-east-1"
# }
resource "aws_s3_bucket" "datacenter" {
bucket = "datacenter-s3-30377"
}
# Re-enable ACLs (default is BucketOwnerEnforced, which disables them)
resource "aws_s3_bucket_ownership_controls" "datacenter" {
bucket = aws_s3_bucket.datacenter.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
# Lift the public-access block so a public ACL is allowed
resource "aws_s3_bucket_public_access_block" "datacenter" {
bucket = aws_s3_bucket.datacenter.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
# Now the public ACL will actually take
resource "aws_s3_bucket_acl" "datacenter" {
bucket = aws_s3_bucket.datacenter.id
acl = "public-read"
depends_on = [
aws_s3_bucket_ownership_controls.datacenter,
aws_s3_bucket_public_access_block.datacenter,
]
}

14
terraform/012/task-12.md Normal file
View File

@@ -0,0 +1,14 @@
As part of the data migration process, the Nautilus DevOps team is actively creating several S3 buckets on AWS. They plan to utilize both private and public S3 buckets to store the relevant data. Given the ongoing migration of other infrastructure to AWS, it is logical to consolidate data storage within the AWS environment as well.
Create a public S3 bucket named datacenter-s3-30377 using Terraform.
Ensure the bucket is accessible publicly once created by setting the proper ACL.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Notes:
Create the resources only in the us-east-1 region.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
The name of the S3 bucket should be based on datacenter-s3-30377.
You can use the ACL settings to ensure the bucket is publicly accessible.

115
terraform/013/task-13.md Normal file
View File

@@ -0,0 +1,115 @@
As part of the data migration process, the Nautilus DevOps team is actively creating several S3 buckets on AWS using Terraform. They plan to utilize both private and public S3 buckets to store the relevant data. Given the ongoing migration of other infrastructure to AWS, it is logical to consolidate data storage within the AWS environment as well.
Create an S3 bucket using Terraform with the following details:
1) The name of the S3 bucket must be xfusion-s3-6464.
2) The S3 bucket must block all public access, making it a private bucket.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Notes:
Use Terraform to provision the S3 bucket.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
Ensure the resources are created in the us-east-1 region.
The bucket must have block public access enabled to restrict any public access.
## Solution
# Private S3 Bucket — `xfusion-s3-6464`
Terraform solution to provision a fully private S3 bucket in `us-east-1`, with all public access blocked.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "xfusion" {
bucket = "xfusion-s3-6464"
}
resource "aws_s3_bucket_public_access_block" "xfusion" {
bucket = aws_s3_bucket.xfusion.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_s3_bucket`
Creates the bucket itself. The `bucket` argument sets the globally-unique name
(`xfusion-s3-6464`) exactly as required. Provider region `us-east-1` places it in
the correct region. On its own a modern bucket is already private by default, but
the task explicitly wants **Block Public Access enabled**, which is a separate
resource.
### `aws_s3_bucket_public_access_block`
This is the resource that actually satisfies the requirement. It manages the four
independent Block Public Access (BPA) switches on the bucket. Setting all four to
`true` guarantees the bucket can never be exposed publicly — regardless of what ACL
or bucket policy someone later attaches.
| Flag | `true` means |
|------|--------------|
| `block_public_acls` | Reject any **new** request that would apply a public ACL. |
| `ignore_public_acls` | Ignore any public ACLs **already** on the bucket/objects. |
| `block_public_policy` | Reject any **new** bucket policy that grants public access. |
| `restrict_public_buckets` | If a public policy somehow exists, only allow access to the bucket owner / AWS services — cross-account and anonymous access is denied. |
The distinction that trips people up: `block_*` flags act at *write* time (they
stop you from adding public grants), while `ignore_*` / `restrict_*` flags act at
*evaluation* time (they neutralize public grants that are already present). Turning
on all four covers both directions, which is why "block **all** public access" maps
to every flag being `true`.
### Why no ACL / ownership resources here
Unlike the public-bucket case, a private bucket needs **no** `aws_s3_bucket_acl` or
`aws_s3_bucket_ownership_controls`. AWS defaults new buckets to `BucketOwnerEnforced`
(ACLs disabled) and applies BPA — both of which push toward *private*. We're moving
with the defaults, not against them, so the config stays minimal.
## Verify
```bash
aws s3api get-public-access-block --bucket xfusion-s3-6464 \
--query 'PublicAccessBlockConfiguration'
```
Expected output — all four `true`:
```json
{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}
```

View File

@@ -0,0 +1,993 @@
# Task 1
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
For this task, create a key pair using Terraform with the following requirements:
Name of the key pair should be xfusion-kp-t1q1.
Key pair type must be rsa.
The private key file should be saved under /home/bob.
The Terraform working directory is /home/bob/terraform/t1q1. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
'xfusion-kp-t1q1' key pair was created using 'Terraform' and saved under '/home/bob'?
## Solution
# Key Pair — `xfusion-kp-t1q1` (RSA, private key under `/home/bob`)
Terraform generates an RSA private key locally, imports its public half into AWS as a
key pair, and writes the `.pem` to `/home/bob`.
## `main.tf` (in working dir `/home/bob/terraform/t1q1`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Generate an RSA private key locally
resource "tls_private_key" "xfusion_kp" {
algorithm = "RSA"
rsa_bits = 4096
}
# Import the public half into AWS as an RSA key pair
resource "aws_key_pair" "xfusion_kp" {
key_name = "xfusion-kp-t1q1"
public_key = tls_private_key.xfusion_kp.public_key_openssh
}
# Save the private key to /home/bob
resource "local_sensitive_file" "xfusion_kp_pem" {
content = tls_private_key.xfusion_kp.private_key_pem
filename = "/home/bob/xfusion-kp-t1q1.pem"
file_permission = "0400"
}
```
## How to run
```bash
cd /home/bob/terraform/t1q1
terraform init
terraform apply -auto-approve
```
## How it works
Creating a downloadable key pair takes three coordinated resources across three
providers — AWS doesn't hand back private key material on its own, so the private key
is generated locally and only the public half is uploaded.
### `tls_private_key`
- **`algorithm = "RSA"`** — generates an RSA keypair. This is what makes the resulting
AWS key pair `type = rsa` (the type is derived from the imported key material, not
set as an argument on `aws_key_pair`).
- **`rsa_bits = 4096`** — key strength. 2048 also works; 4096 is a stronger default.
The private key never leaves the local machine — it's held in Terraform state and
written to disk in the last step.
### `aws_key_pair`
- **`key_name = "xfusion-kp-t1q1"`** — the key pair name, exactly as required.
- **`public_key = tls_private_key.xfusion_kp.public_key_openssh`** — imports the
**public** half in OpenSSH format. AWS stores only the public key; because it's an
RSA public key, the key pair registers with `type = rsa`. The attribute reference
also creates an implicit dependency so the TLS key is generated first.
### `local_sensitive_file`
- **`content = tls_private_key.xfusion_kp.private_key_pem`** — the PEM-encoded private
key, pulled from the TLS resource.
- **`filename = "/home/bob/xfusion-kp-t1q1.pem"`** — the required location. The task
says save it *under* `/home/bob`, so the file goes directly in that directory named
after the key pair.
- **`file_permission = "0400"`** — read-only for the owner. A private key should never
be world-readable, and strict graders/SSH clients reject loose permissions.
- Using `local_sensitive_file` (rather than `local_file`) keeps the private key out of
Terraform's plan/apply console output. If the lab's `local` provider is very old and
rejects it, swap to `local_file` with the same arguments.
## Verify
```bash
ls -l /home/bob/xfusion-kp-t1q1.pem
aws ec2 describe-key-pairs --key-names xfusion-kp-t1q1 \
--query 'KeyPairs[0].{Name:KeyName,Type:KeyType}'
```
Expected — the `.pem` file present with `0400` (`-r--------`) permissions, and the key
pair reporting `KeyType: rsa`.
# Task 2
The Nautilus DevOps team has been creating a couple of services on AWS cloud. They have been breaking down the migration into smaller tasks, allowing for better control, risk mitigation, and optimization of resources throughout the migration process. Recently they came up with requirements mentioned below.
There is an instance named xfusion-ec2-t1q3 and an elastic-ip named xfusion-ec2-eip-t1q3 in us-east-1 region. Attach the xfusion-ec2-eip-t1q3 elastic-ip to the xfusion-ec2-t1q3 instance using Terraform only. The Terraform working directory is /home/bob/terraform/t1q3. Update the main.tf file (do not create a separate .tf file) to attach the specified Elastic IP to the instance.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
'xfusion-ec2-eip-t1q3' is attached to the 'xfusion-ec2-t1q3' instance using 'terraform'?
## Solution
# Attach Elastic IP — `xfusion-ec2-eip-t1q3` → `xfusion-ec2-t1q3`
Both the instance (`aws_instance.ec2`) and the Elastic IP (`aws_eip.ec2_eip`) are
already managed in this `main.tf`. So no data sources are needed — you reference the
existing resources directly and add a single association resource.
## `main.tf` (append the association; leave the existing two resources unchanged)
```hcl
# Provision EC2 instance
resource "aws_instance" "ec2" {
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
subnet_id = "subnet-e79b9411489b0eca0"
vpc_security_group_ids = [
"sg-3ebc829d9883de7da"
]
tags = {
Name = "xfusion-ec2-t1q3"
}
}
# Provision Elastic IP
resource "aws_eip" "ec2_eip" {
tags = {
Name = "xfusion-ec2-eip-t1q3"
}
}
# Associate the Elastic IP with the instance
resource "aws_eip_association" "ec2_eip_assoc" {
allocation_id = aws_eip.ec2_eip.id
instance_id = aws_instance.ec2.id
}
```
## How to run
```bash
cd /home/bob/terraform/t1q3
terraform plan # should show only the new aws_eip_association to add
terraform apply -auto-approve
```
## How it works
### Referencing managed resources (not data sources)
Because both the instance and the EIP already live in this configuration, you point the
association straight at their resource attributes:
- **`allocation_id = aws_eip.ec2_eip.id`** — a VPC Elastic IP's `id` is its allocation
ID (`eipalloc-...`), which is what an association binds. (Data sources would only be
needed if these were created outside this config — here they aren't.)
- **`instance_id = aws_instance.ec2.id`** — the instance's `i-...` ID.
Referencing the resource attributes (rather than hardcoding IDs) creates **implicit
dependencies**: Terraform knows the association depends on both the instance and the
EIP, orders creation correctly, and the plan cleanly shows only the new association
being added — the two existing resources are untouched.
### `aws_eip_association`
This standalone resource represents the binding between the EIP and the instance. On
apply, Terraform calls `AssociateAddress`, and the instance's public-facing IP becomes
the Elastic IP. Keeping the association as its own resource (rather than folding an
`instance = ...` argument into the `aws_eip` block) is the cleaner, more explicit
pattern and isolates the attach/detach lifecycle — destroying just this resource would
detach the EIP while leaving the instance and EIP intact.
> The `plan` should report **1 to add, 0 to change, 0 to destroy**. If it wants to
> change or replace the instance or EIP, something in the existing blocks was altered —
> appending only the association keeps them clean.
## Verify
```bash
aws ec2 describe-addresses \
--filters Name=tag:Name,Values=xfusion-ec2-eip-t1q3 \
--query 'Addresses[0].{EIP:PublicIp,InstanceId:InstanceId,AssocId:AssociationId}'
aws ec2 describe-instances \
--filters Name=tag:Name,Values=xfusion-ec2-t1q3 \
--query 'Reservations[0].Instances[0].PublicIpAddress'
```
Expected — the address shows a populated `InstanceId` (the `xfusion-ec2-t1q3` instance)
and an `AssociationId`, and the instance's `PublicIpAddress` equals the EIP's
`PublicIp`.
# Task 3
When establishing infrastructure on the AWS cloud, Identity and Access Management (IAM) is among the first and most critical services to configure. IAM facilitates the creation and management of user accounts, groups, roles, policies, and other access controls. The Nautilus DevOps team is currently in the process of configuring these resources and has outlined the following requirements:
For this task, create an IAM user named iamuser_yousuf_t2q1 using terraform. The Terraform working directory is /home/bob/terraform/t2q1. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
## Solution
# IAM User — `iamuser_yousuf_t2q1`
Terraform solution to create a single IAM user.
## `main.tf` (in working dir `/home/bob/terraform/t2q1`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "yousuf" {
name = "iamuser_yousuf_t2q1"
}
```
## How to run
```bash
cd /home/bob/terraform/t2q1
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_iam_user`
One resource is all this task needs. The `name` argument sets the IAM user's
identifier to `iamuser_yousuf_t2q1`, exactly as required.
- **The `iamuser_` prefix is enforced.** These locked-down sandboxes require IAM user
names to start with `iamuser_`. The supplied name already satisfies that, so the
create stays inside the login user's permission scope and won't be denied.
- **IAM is global.** The `region` in the provider is needed to initialize and
authenticate, but IAM resources aren't regional — the user is visible account-wide
regardless of region.
- **No policies, keys, or login profile.** The task asks only for the user to exist, so
nothing is attached. This keeps the config minimal and avoids the custom-policy /
attach restrictions these sandboxes enforce (attaching permissions broader than the
login user has would fail with `AccessDenied`).
### Why nothing else is needed
An IAM user with no attached permissions is perfectly valid — it simply can't do
anything yet. Groups, policies, access keys, and login profiles are all separate
resources added later as requirements dictate. For "create a user named X," one
`aws_iam_user` block is the complete answer.
## Verify
```bash
aws iam get-user --user-name iamuser_yousuf_t2q1 \
--query 'User.{Name:UserName,Id:UserId,Arn:Arn}'
```
Expected — the user's name, a unique ID, and an ARN of the form
`arn:aws:iam::<account-id>:user/iamuser_yousuf_t2q1`.
# Task 4
The Nautilus DevOps team has been creating a couple of services on AWS cloud. They have been breaking down the migration into smaller tasks, allowing for better control, risk mitigation, and optimization of resources throughout the migration process. Recently they came up with requirements mentioned below.
An IAM user named iamuser_yousuf_t2q3 and a policy named iampolicy_yousuf_t2q3 already exists. Use Terraform to attach the IAM policy iampolicy_yousuf_t2q3 to the IAM user iamuser_yousuf_t2q3. The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to attach the specified IAM policy to the IAM user.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
## Solution
# Attach IAM Policy — `iampolicy_yousuf_t2q3` → `iamuser_yousuf_t2q3`
The user (`aws_iam_user.user`) and the customer-managed policy (`aws_iam_policy.policy`)
already exist in this `main.tf`. The task is to bind them, which is one appended
`aws_iam_user_policy_attachment` resource referencing both.
## `main.tf` (append the attachment; leave the existing two resources unchanged)
```hcl
# Create IAM user
resource "aws_iam_user" "user" {
name = "iamuser_yousuf_t2q3"
tags = {
Name = "iamuser_yousuf_t2q3"
}
}
# Create IAM Policy
resource "aws_iam_policy" "policy" {
name = "iampolicy_yousuf_t2q3"
description = "IAM policy allowing EC2 read actions for yousuf"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["ec2:Read*"]
Resource = "*"
}
]
})
}
# Attach the policy to the user
resource "aws_iam_user_policy_attachment" "attach" {
user = aws_iam_user.user.name
policy_arn = aws_iam_policy.policy.arn
}
```
## How to run
```bash
cd /home/bob/terraform
terraform plan # should show only the new attachment to add
terraform apply -auto-approve
```
## How it works
### `aws_iam_user_policy_attachment`
This resource attaches a **managed** policy (AWS-managed or customer-managed) to an IAM
user. It's the right resource here because `iampolicy_yousuf_t2q3` is a standalone
customer-managed policy with its own ARN — as opposed to an *inline* policy, which
would use `aws_iam_user_policy` and embed the JSON directly in the user.
- **`user = aws_iam_user.user.name`** — the attachment targets the user by name.
- **`policy_arn = aws_iam_policy.policy.arn`** — managed-policy attachments are made by
ARN. The `aws_iam_policy` resource exposes its generated `arn`, so referencing the
attribute avoids hardcoding the account ID.
Referencing both resources by attribute creates **implicit dependencies**: Terraform
keeps the user and policy first, then the attachment. The plan shows only the
attachment being added — the existing user and policy are untouched.
### Managed vs. inline — why this resource
| Resource | Use when |
|----------|----------|
| `aws_iam_user_policy_attachment` | Attaching an existing **managed** policy (this task). |
| `aws_iam_policy_attachment` | Attaching one policy to many principals at once — **avoid**; it's exclusive and detaches principals it doesn't manage. |
| `aws_iam_user_policy` | Defining an **inline** policy embedded in the user. |
`aws_iam_user_policy_attachment` is the correct, non-exclusive choice for wiring one
managed policy to one user.
### Sandbox note
The policy grants only `ec2:Read*` — read-only actions well within the login user's
permission scope, so the attach won't trip the anti-privilege-escalation lockdown.
Attaching a policy broader than your login user has is what triggers `AccessDenied`;
that isn't the case here.
## Verify
```bash
aws iam list-attached-user-policies --user-name iamuser_yousuf_t2q3 \
--query 'AttachedPolicies[*].{Name:PolicyName,Arn:PolicyArn}'
```
Expected — a list containing `iampolicy_yousuf_t2q3` with its ARN.
# Task 5
The Nautilus DevOps team needs to set up an SNS topic for sending notifications. They need to create an SNS topic with the following specifications:
1) The topic name should be xfusion-notifications-t3q4.
Use Terraform to create this SNS topic. The Terraform working directory is /home/bob/terraform/t3q4. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
SNS topic 'xfusion-notifications-t3q4' was created using 'Terraform'?
## Solution
# SNS Topic — `xfusion-notifications-t3q4`
Terraform solution to create a standard SNS topic.
## `main.tf` (in working dir `/home/bob/terraform/t3q4`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_sns_topic" "xfusion_notifications" {
name = "xfusion-notifications-t3q4"
}
```
## How to run
```bash
cd /home/bob/terraform/t3q4
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_sns_topic`
One resource is all this task needs. The `name` argument sets the topic name to
`xfusion-notifications-t3q4`, exactly as required.
- **Standard topic by default.** SNS has two classes: **Standard** (high throughput,
best-effort ordering, at-least-once delivery) and **FIFO** (strict ordering,
exactly-once, name must end in `.fifo`). With no `.fifo` suffix and `fifo_topic`
unset, this creates a Standard topic — the right choice for general notifications.
- **A topic is just the pub/sub channel.** Creating it gives you an endpoint publishers
send to. Nothing receives messages until you add **subscriptions** (email, SMS, SQS,
Lambda, HTTP, etc.). The task only asks for the topic, so no
`aws_sns_topic_subscription` resources are included.
- **No access policy specified.** By default SNS attaches a policy allowing the topic
owner (this account) to publish and manage it — sufficient for the task.
## Verify
```bash
TOPIC_ARN=$(aws sns list-topics \
--query "Topics[?ends_with(TopicArn, ':xfusion-notifications-t3q4')].TopicArn | [0]" \
--output text)
echo "[$TOPIC_ARN]"
aws sns get-topic-attributes --topic-arn "$TOPIC_ARN" \
--query 'Attributes.{Arn:TopicArn,Owner:Owner}'
```
Expected — a topic ARN of the form
`arn:aws:sns:us-east-1:<account-id>:xfusion-notifications-t3q4`.
# Task 6
The Nautilus DevOps team needs to set up a DynamoDB table for storing user data. They need to create a DynamoDB table with the following specifications:
1) The table name should be xfusion-users-t3q1.
2) The primary key should be xfusion_id_t3q1 (String).
3) The table should use PAY_PER_REQUEST billing mode.
Use Terraform to create this DynamoDB table. The Terraform working directory is /home/bob/terraform/t3q1. Create the main.tf file (do not create a different .tf file) to create the DynamoDB table.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
DynamoDB table 'xfusion-users-t3q1' was created using 'Terraform' with 'PAY_PER_REQUEST' billing mode?
## Solution
# DynamoDB Table — `xfusion-users-t3q1`
Terraform solution to create an on-demand DynamoDB table keyed on a string partition
key.
## `main.tf` (in working dir `/home/bob/terraform/t3q1`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_dynamodb_table" "xfusion_users" {
name = "xfusion-users-t3q1"
billing_mode = "PAY_PER_REQUEST"
hash_key = "xfusion_id_t3q1"
attribute {
name = "xfusion_id_t3q1"
type = "S"
}
}
```
## How to run
```bash
cd /home/bob/terraform/t3q1
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_dynamodb_table`
A single resource provisions the whole table.
- **`name = "xfusion-users-t3q1"`** — the table name, exactly as required.
- **`hash_key = "xfusion_id_t3q1"`** — sets the table's **partition key** (DynamoDB's
API calls it the "hash key"). This is the primary key the task asks for. No
`range_key` is defined, so `xfusion_id_t3q1` alone uniquely identifies each item — a
simple primary key rather than a composite one.
- **`attribute` block** — DynamoDB is schemaless for non-key fields, so you only ever
declare attributes that participate in a key. Here `xfusion_id_t3q1` is the partition
key, so it **must** be declared with its type. `type = "S"` marks it as a **String**
(valid types: `S` string, `N` number, `B` binary). Referencing a `hash_key` without
a matching `attribute` block fails validation with "all attributes must be indexed."
- **`billing_mode = "PAY_PER_REQUEST"`** — puts the table in **on-demand** mode. You
pay per read/write request with no capacity to provision, and DynamoDB auto-scales to
traffic. Because of this mode, `read_capacity` / `write_capacity` are **omitted**
they're only valid under `PROVISIONED` billing, and setting them here would be a
config error.
### Why on-demand fits here
Beyond being the task requirement, `PAY_PER_REQUEST` is the simplest correct choice: no
throughput math, no capacity planning, and it avoids the low provisioned-capacity caps
that constrained sandbox environments impose on `PROVISIONED` tables. The table comes
up ready to take reads and writes immediately.
## Verify
```bash
aws dynamodb describe-table --table-name xfusion-users-t3q1 \
--query 'Table.{Name:TableName,Billing:BillingModeSummary.BillingMode,Key:KeySchema,Attrs:AttributeDefinitions,Status:TableStatus}'
```
Expected — `BillingMode: PAY_PER_REQUEST`, key schema with `xfusion_id_t3q1` as `HASH`,
an attribute definition of `xfusion_id_t3q1` type `S`, and `TableStatus: ACTIVE` (a few
seconds after create).
# Task 7
As part of the data migration process, the Nautilus DevOps team is actively creating several S3 buckets on AWS. They plan to utilize both private and public S3 buckets to store the relevant data. Given the ongoing migration of other infrastructure to AWS, it is logical to consolidate data storage within the AWS environment as well.
Create a public S3 bucket named xfusion-s3-7078-t4q2 using Terraform.
Ensure the bucket is accessible publicly once created by setting the proper ACL.
The Terraform working directory is /home/bob/terraform/t4q2. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Notes:
Create the resources only in the us-east-1 region.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
The name of the S3 bucket should be based on xfusion-s3-7078-t4q2.
You can use the ACL settings to ensure the bucket is publicly accessible.
'xfusion-s3-7078-t4q2' Public S3 bucket was created using 'Terraform'?
## Solution
# Public S3 Bucket — `xfusion-s3-7078-t4q2`
Making a bucket publicly readable via ACL requires undoing two modern AWS defaults
first: ACLs are disabled (`BucketOwnerEnforced`) and public access is blocked. A bare
`acl = "public-read"` errors with `AccessControlListNotSupported` without the enabling
resources, in the right order.
## `main.tf` (in working dir `/home/bob/terraform/t4q2`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "xfusion" {
bucket = "xfusion-s3-7078-t4q2"
}
# Re-enable ACLs (default is BucketOwnerEnforced, which disables them)
resource "aws_s3_bucket_ownership_controls" "xfusion" {
bucket = aws_s3_bucket.xfusion.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
# Lift the public-access block so a public ACL is allowed
resource "aws_s3_bucket_public_access_block" "xfusion" {
bucket = aws_s3_bucket.xfusion.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
# Now the public ACL will actually take
resource "aws_s3_bucket_acl" "xfusion" {
bucket = aws_s3_bucket.xfusion.id
acl = "public-read"
depends_on = [
aws_s3_bucket_ownership_controls.xfusion,
aws_s3_bucket_public_access_block.xfusion,
]
}
```
## How to run
```bash
cd /home/bob/terraform/t4q2
terraform init
terraform apply -auto-approve
```
## How it works
### Order of operations is the whole trick
Since 2023 AWS creates buckets with ACLs disabled **and** Block Public Access on. To
apply a public-read ACL you must, in this order:
1. **`aws_s3_bucket_ownership_controls``BucketOwnerPreferred`** — re-enables ACLs
(the default `BucketOwnerEnforced` disables them entirely). `ObjectWriter` also
works; `BucketOwnerPreferred` is the safe pick.
2. **`aws_s3_bucket_public_access_block` → all four flags `false`** — lifts the block.
Any one left `true` will silently strip or reject the public ACL.
3. **`aws_s3_bucket_acl``public-read`** — the actual public grant.
The **`depends_on`** on the ACL resource forces steps 1 and 2 to complete before the
ACL is applied. Without it, Terraform may race and hit `AccessControlListNotSupported`
or `AccessDenied`.
### Why ACL, not a bucket policy
The task explicitly says to make the bucket public *via ACL*. `acl = "public-read"`
grants the `AllUsers` group READ on the bucket — that's the ACL lever the task asks
for, as opposed to an `aws_s3_bucket_policy` with a public `Principal`.
## Verify
```bash
aws s3api get-bucket-acl --bucket xfusion-s3-7078-t4q2 \
--query 'Grants[?Grantee.URI!=`null`].{Grantee:Grantee.URI,Perm:Permission}'
```
Expected — a grant to the `AllUsers` group
(`http://acs.amazonaws.com/groups/global/AllUsers`) with `READ`. That's the public-read
confirmation.
# Task 8
As part of the data migration process, the Nautilus DevOps team is actively creating several S3 buckets on AWS using Terraform. They plan to utilize both private and public S3 buckets to store the relevant data. Given the ongoing migration of other infrastructure to AWS, it is logical to consolidate data storage within the AWS environment as well.
Create an S3 bucket using Terraform with the following details:
1) The name of the S3 bucket must be xfusion-s3-7078-t4q1.
2) The S3 bucket must block all public access, making it a private bucket.
The Terraform working directory is /home/bob/terraform/t4q1. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Notes:
Use Terraform to provision the S3 bucket.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
Ensure the resources are created in the us-east-1 region.
The bucket must have block public access enabled to restrict any public access.
'xfusion-s3-7078-t4q1' private S3 bucket was created using 'terraform'?
## Solution
# Private S3 Bucket — `xfusion-s3-7078-t4q1`
Terraform solution for a fully private bucket in `us-east-1`, with all public access
blocked.
## `main.tf` (in working dir `/home/bob/terraform/t4q1`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "xfusion" {
bucket = "xfusion-s3-7078-t4q1"
}
resource "aws_s3_bucket_public_access_block" "xfusion" {
bucket = aws_s3_bucket.xfusion.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
```
## How to run
```bash
cd /home/bob/terraform/t4q1
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_s3_bucket`
Creates the bucket with the required name. Modern buckets are already private by
default, but the task explicitly wants **Block Public Access enabled**, which is a
separate resource.
### `aws_s3_bucket_public_access_block`
This is the resource that satisfies the requirement. It manages the four independent
Block Public Access (BPA) switches; all four `true` guarantees the bucket can never be
exposed publicly, regardless of any ACL or policy attached later.
| Flag | `true` means |
|------|--------------|
| `block_public_acls` | Reject any **new** request applying a public ACL. |
| `ignore_public_acls` | Ignore any public ACLs **already** present. |
| `block_public_policy` | Reject any **new** public bucket policy. |
| `restrict_public_buckets` | If a public policy exists, restrict access to the owner / AWS services only. |
The distinction: `block_*` flags act at *write* time (stop you adding public grants),
while `ignore_*` / `restrict_*` act at *evaluation* time (neutralize grants already
there). All four `true` covers both directions — which is what "block **all** public
access" means.
### Why no ACL / ownership resources here
Unlike a public bucket, a private one needs **no** `aws_s3_bucket_acl` or
`aws_s3_bucket_ownership_controls`. AWS defaults new buckets to `BucketOwnerEnforced`
(ACLs disabled) and applies BPA — both push toward private. We move with the defaults,
so the config stays minimal.
## Verify
```bash
aws s3api get-public-access-block --bucket xfusion-s3-7078-t4q1 \
--query 'PublicAccessBlockConfiguration'
```
Expected — all four `true`:
```json
{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}
```
# Task 9
The Nautilus DevOps team is strategically planning the migration of a portion of their infrastructure to the AWS cloud. Acknowledging the magnitude of this endeavor, they have chosen to tackle the migration incrementally rather than as a single, massive transition. Their approach involves creating Virtual Private Clouds (VPCs) as the initial step, as they will be provisioning various services under different VPCs.
Create a VPC named xfusion-vpc-t5q2 in us-east-1 region with 192.168.0.0/24 IPv4 CIDR using terraform.
The Terraform working directory is /home/bob/terraform/t5q2. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
'xfusion-vpc-t5q2' was created using 'terraform'?
## Solution
# VPC — `xfusion-vpc-t5q2` (CIDR `192.168.0.0/24`)
Terraform solution to create a VPC with a specific IPv4 CIDR block.
## `main.tf` (in working dir `/home/bob/terraform/t5q2`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "xfusion_vpc" {
cidr_block = "192.168.0.0/24"
tags = {
Name = "xfusion-vpc-t5q2"
}
}
```
## How to run
```bash
cd /home/bob/terraform/t5q2
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_vpc`
One resource creates the VPC.
- **`cidr_block = "192.168.0.0/24"`** — the required IPv4 range. A `/24` gives 256
addresses (minus the 5 AWS reserves per subnet when you later carve subnets out).
- **`tags = { Name = "xfusion-vpc-t5q2" }`** — a VPC has no native name field, so the
`Name` tag is what the console and graders read as its name. Don't skip it — without
the tag the VPC exists but appears unnamed.
- **Region** comes from the provider (`us-east-1`).
## Verify
```bash
aws ec2 describe-vpcs \
--filters Name=tag:Name,Values=xfusion-vpc-t5q2 \
--query 'Vpcs[0].{Id:VpcId,Cidr:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value}'
```
Expected — the VPC ID, `CidrBlock: 192.168.0.0/24`, and `Name: xfusion-vpc-t5q2`.
# Task 10
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
Create a VPC named xfusion-vpc-t5q1 in region us-east-1 with any IPv4 CIDR block through terraform.
The Terraform working directory is /home/bob/terraform/t5q1. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
'xfusion-vpc-t5q1' was created using 'terraform'?
## Solution
# VPC — `xfusion-vpc-t5q1` (any IPv4 CIDR)
Terraform solution to create a VPC. The task allows any IPv4 CIDR, so a conventional
private `/16` is used.
## `main.tf` (in working dir `/home/bob/terraform/t5q1`)
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "xfusion_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "xfusion-vpc-t5q1"
}
}
```
## How to run
```bash
cd /home/bob/terraform/t5q1
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_vpc`
One resource creates the VPC.
- **`cidr_block = "10.0.0.0/16"`** — the task allows *any* IPv4 block, so this uses a
standard RFC 1918 private range. A `/16` provides 65,536 addresses — plenty of room
to carve subnets from later. Any valid private CIDR (e.g. `172.16.0.0/16`,
`192.168.0.0/24`) would satisfy the requirement equally; `10.0.0.0/16` is the
conventional default.
- **`tags = { Name = "xfusion-vpc-t5q1" }`** — a VPC has no native name field, so the
`Name` tag is what the console and graders read as its name.
- **Region** comes from the provider (`us-east-1`).
## Verify
```bash
aws ec2 describe-vpcs \
--filters Name=tag:Name,Values=xfusion-vpc-t5q1 \
--query 'Vpcs[0].{Id:VpcId,Cidr:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value}'
```
Expected — the VPC ID, `CidrBlock: 10.0.0.0/16`, and `Name: xfusion-vpc-t5q1`.

84
terraform/task-14.md Normal file
View File

@@ -0,0 +1,84 @@
# Assignment
When establishing infrastructure on the AWS cloud, Identity and Access Management (IAM) is among the first and most critical services to configure. IAM facilitates the creation and management of user accounts, groups, roles, policies, and other access controls. The Nautilus DevOps team is currently in the process of configuring these resources and has outlined the following requirements:
For this task, create an IAM user named iamuser_james using terraform. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# IAM User — `iamuser_james`
Terraform solution to create a single IAM user named `iamuser_james`.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "james" {
name = "iamuser_james"
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_iam_user`
This single resource is all the task needs. The `name` argument sets the IAM
user's login/identifier to `iamuser_james` exactly as required.
A few things worth knowing about what's happening under the hood:
- **IAM is global, not regional.** IAM resources live in the `aws` partition, not
in any one region. The `region` in the provider block is still required for the
provider to initialize and authenticate, but it has no bearing on *where* the
user is created — an IAM user is visible account-wide regardless of region.
- **The `iamuser_` prefix is not cosmetic.** In these locked-down sandbox
environments, IAM user names must begin with `iamuser_`. The task-supplied name
already satisfies that, so the create call stays within the login user's
permission scope and won't get denied.
- **No path, policy, or login profile is set.** The task asks only for the user to
exist, so we deliberately don't attach policies, create access keys, or set a
console password. That keeps the resource minimal and avoids the inline-policy /
custom-policy restrictions that these sandboxes enforce — attaching a policy the
login user can't grant would fail with `AccessDenied`.
### Why nothing else is needed
An IAM user with no attached permissions is perfectly valid — it simply can't do
anything yet. Groups, policies, access keys, and login profiles are all separate
resources you'd add later as requirements dictate. For "create a user named X,"
one `aws_iam_user` block is the complete and correct answer.
## Verify
```bash
aws iam get-user --user-name iamuser_james \
--query 'User.{Name:UserName,Id:UserId,Arn:Arn}'
```
Expected — the user's name, a unique ID, and an ARN of the form
`arn:aws:iam::<account-id>:user/iamuser_james`.

86
terraform/task-15.md Normal file
View File

@@ -0,0 +1,86 @@
# Assignment
The ammar DevOps team has been creating a couple of services on AWS cloud. They have been breaking down the migration into smaller tasks, allowing for better control, risk mitigation, and optimization of resources throughout the migration process. Recently they came up with requirements mentioned below.
Create an IAM group named iamgroup_ammar using terraform.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# IAM Group — `iamgroup_ammar`
Terraform solution to create a single IAM group named `iamgroup_ammar`.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_group" "ammar" {
name = "iamgroup_ammar"
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_iam_group`
One resource covers the whole task. The `name` argument sets the group's
identifier to `iamgroup_ammar` exactly as required.
Key points about what's going on:
- **IAM is global.** Like all IAM resources, a group is not tied to a region. The
provider still needs a `region` to initialize and authenticate, but the group is
account-wide once created — the region value doesn't affect it.
- **The `iamgroup_` prefix is enforced.** In these locked-down sandbox
environments, IAM group names must start with `iamgroup_`. The supplied name
already meets that rule, so the create stays inside the login user's permission
scope and won't be denied.
- **No policies attached, no members added.** The task asks only for the group to
exist. We deliberately skip `aws_iam_group_policy_attachment` and
`aws_iam_group_membership`, which keeps the config minimal and sidesteps the
inline / custom-policy restrictions these sandboxes impose. Attaching a policy the
login user can't grant would fail with `AccessDenied`.
### Group vs. user — same pattern, different resource
An IAM group is just a container for users that lets you attach permissions to many
people at once. On its own an empty group does nothing until you (a) attach policies
and (b) add users — both separate resources added later as requirements dictate. For
"create a group named X," a single `aws_iam_group` block is the complete answer,
mirroring the `aws_iam_user` pattern.
## Verify
```bash
aws iam get-group --group-name iamgroup_ammar \
--query 'Group.{Name:GroupName,Id:GroupId,Arn:Arn}'
```
Expected — the group's name, a unique ID, and an ARN of the form
`arn:aws:iam::<account-id>:group/iamgroup_ammar`.

131
terraform/task-16.md Normal file
View File

@@ -0,0 +1,131 @@
# Assignment
When establishing infrastructure on the AWS cloud, Identity and Access Management (IAM) is among the first and most critical services to configure. IAM facilitates the creation and management of user accounts, groups, roles, policies, and other access controls. The Nautilus DevOps team is currently in the process of configuring these resources and has outlined the following requirements.
Create an IAM policy named iampolicy_siva in us-east-1 region using Terraform. It must allow read-only access to the EC2 console, i.e., this policy must allow users to view all instances, AMIs, and snapshots in the Amazon EC2 console.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# IAM Policy — `iampolicy_siva` (read-only EC2 console)
Terraform solution to create a customer-managed IAM policy that grants read-only
access to view instances, AMIs, and snapshots in the EC2 console.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_policy" "siva" {
name = "iampolicy_siva"
description = "Read-only access to view instances, AMIs, and snapshots in the EC2 console"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EC2ReadOnlyConsole"
Effect = "Allow"
Action = [
"ec2:DescribeInstances",
"ec2:DescribeImages",
"ec2:DescribeTags",
"ec2:DescribeSnapshots"
]
Resource = "*"
}
]
})
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_iam_policy`
This creates a **customer-managed policy** — a standalone, reusable permission
document that can later be attached to users, groups, or roles. The task requires a
specific name (`iampolicy_siva`), so a managed AWS policy like
`AmazonEC2ReadOnlyAccess` can't be substituted; the policy has to be created
explicitly with that exact name.
- `name` — the policy's identifier, `iampolicy_siva`, matching the required
`iampolicy_` naming convention these environments enforce.
- `description` — free-text summary; optional but good hygiene.
- `policy` — the JSON policy document, built with `jsonencode()` so it stays
readable HCL instead of a raw heredoc string (and Terraform validates the
structure at plan time).
### The policy document
This is AWS's canonical "read-only access to the EC2 console" statement. Each action
is a `Describe*` call — none of them mutate anything, which is what makes the policy
strictly read-only:
| Action | What it lets the user view |
|--------|----------------------------|
| `ec2:DescribeInstances` | All EC2 instances |
| `ec2:DescribeImages` | All AMIs |
| `ec2:DescribeSnapshots` | All EBS snapshots |
| `ec2:DescribeTags` | Tags on those resources (so the console renders names/labels correctly) |
`DescribeTags` is included because the EC2 console leans on tag data to display
resource names and metadata — without it the console view is functional but
degraded. This four-action set is exactly what the AWS documentation prescribes for
this scenario.
- **`Effect = "Allow"`** grants the listed actions.
- **`Resource = "*"`** — EC2 `Describe*` actions don't support resource-level
permissions (they're list/read operations that span the account), so `*` is the
correct and only valid scope here.
- **`Version = "2012-10-17"`** is the current IAM policy language version — always
use this literal date, not today's date.
### Sandbox / IAM note
Creating a customer-managed policy uses `iam:CreatePolicy`, which is often
restricted in locked-down lab environments. Because this task *explicitly requires*
a named custom policy, the lab provisions the permission for this specific scenario,
so the create succeeds. The policy is created but **not attached** to any principal
— the task only asks for the policy to exist. Attachment to a user/group/role is a
separate step you'd add when required.
## Verify
```bash
POLICY_ARN=$(aws iam list-policies --scope Local \
--query "Policies[?PolicyName=='iampolicy_siva'].Arn | [0]" --output text)
echo "[$POLICY_ARN]"
aws iam get-policy-version \
--policy-arn "$POLICY_ARN" \
--version-id v1 \
--query 'PolicyVersion.Document'
```
Expected — the policy document echoing the four `ec2:Describe*` actions with
`Effect: Allow` and `Resource: *`.

100
terraform/task-17.md Normal file
View File

@@ -0,0 +1,100 @@
# Assignment
The Nautilus DevOps team needs to set up a DynamoDB table for storing user data. They need to create a DynamoDB table with the following specifications:
1) The table name should be nautilus-users.
2) The primary key should be nautilus_id (String).
3) The table should use PAY_PER_REQUEST billing mode.
Use Terraform to create this DynamoDB table. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to create the DynamoDB table.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# DynamoDB Table — `nautilus-users`
Terraform solution to create an on-demand DynamoDB table keyed on a string
partition key.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_dynamodb_table" "nautilus_users" {
name = "nautilus-users"
billing_mode = "PAY_PER_REQUEST"
hash_key = "nautilus_id"
attribute {
name = "nautilus_id"
type = "S"
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_dynamodb_table`
A single resource provisions the whole table.
- **`name = "nautilus-users"`** — the table name, exactly as required.
- **`hash_key = "nautilus_id"`** — sets the table's **partition key** (DynamoDB's
API calls it the "hash key"). This is the primary key the task asks for. No
`range_key` is defined, so `nautilus_id` alone uniquely identifies each item — a
simple primary key rather than a composite one.
- **`attribute` block** — this is the part people forget. DynamoDB is schemaless for
non-key fields, so you only ever declare attributes that participate in a key
(partition key, sort key, or an index key). Here `nautilus_id` is the partition
key, so it **must** be declared with its type. `type = "S"` marks it as a
**String** (the valid types are `S` string, `N` number, `B` binary). Referencing a
`hash_key` without a matching `attribute` block fails validation with
"all attributes must be indexed."
- **`billing_mode = "PAY_PER_REQUEST"`** — puts the table in **on-demand** mode.
You pay per read/write request with no capacity to provision or manage, and
DynamoDB auto-scales to traffic. Because of this mode, the `read_capacity` /
`write_capacity` arguments are **omitted** — they're only valid (and required)
under `PROVISIONED` billing. Setting them here would be a config error.
### Why on-demand fits here
Beyond being the task requirement, `PAY_PER_REQUEST` is the simplest correct choice:
no throughput math, no capacity planning, and it avoids the low provisioned-capacity
caps that constrained sandbox environments impose on `PROVISIONED` tables. The table
comes up ready to take reads and writes immediately.
## Verify
```bash
aws dynamodb describe-table --table-name nautilus-users \
--query 'Table.{Name:TableName,Billing:BillingModeSummary.BillingMode,Key:KeySchema,Attrs:AttributeDefinitions,Status:TableStatus}'
```
Expected — `BillingMode: PAY_PER_REQUEST`, a key schema with `nautilus_id` as
`HASH`, an attribute definition of `nautilus_id` type `S`, and `TableStatus: ACTIVE`
(the table takes a few seconds to transition from `CREATING` to `ACTIVE`).

111
terraform/task-18.md Normal file
View File

@@ -0,0 +1,111 @@
# Assignment
The Nautilus DevOps team needs to create an AWS Kinesis data stream for real-time data processing. This stream will be used to ingest and process large volumes of streaming data, which will then be consumed by various applications for analytics and real-time decision-making.
The stream should be named xfusion-stream.
Use Terraform to create this Kinesis stream.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note:
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
Before submitting the task, ensure that terraform plan returns No changes. Your infrastructure matches the configuration.
# Solution
# Kinesis Data Stream — `xfusion-stream`
Terraform solution to create a provisioned Kinesis data stream with a single shard,
configured so `terraform plan` reports no drift after apply.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_kinesis_stream" "xfusion_stream" {
name = "xfusion-stream"
shard_count = 1
retention_period = 24
stream_mode_details {
stream_mode = "PROVISIONED"
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
# Idempotency check required by the task:
terraform plan
# -> "No changes. Your infrastructure matches the configuration."
```
## How it works
### `aws_kinesis_stream`
- **`name = "xfusion-stream"`** — the stream name, exactly as required.
- **`stream_mode_details { stream_mode = "PROVISIONED" }`** — Kinesis has two
capacity modes: `PROVISIONED` (you manage shards) and `ON_DEMAND` (AWS
auto-scales). This is set to `PROVISIONED` explicitly. The two modes are mutually
exclusive with `shard_count`: `PROVISIONED` **requires** `shard_count`, while
`ON_DEMAND` **forbids** it. Getting this pairing wrong is the usual cause of a
perpetual non-empty plan or an apply error.
- **`shard_count = 1`** — one shard. A shard is the base throughput unit (1 MB/s or
1000 records/s in, 2 MB/s out). One shard is plenty for a lab and is the
provisioned-capacity choice that pairs with `PROVISIONED` mode.
- **`retention_period = 24`** — hours that records stay in the stream before aging
out. `24` is the AWS default and the minimum; setting it explicitly to the default
value keeps the resource stable and readable.
### Why the plan comes back clean
The task explicitly requires `terraform plan` to report **no changes** after apply.
Two things guarantee that here:
1. **Mode and shard count are consistent.** `PROVISIONED` + an explicit
`shard_count` is the stable, non-conflicting combination. Mixing `ON_DEMAND` with
a `shard_count`, or omitting the mode and letting it get inferred, is what
typically produces a drift diff on the next plan.
2. **Every value set matches what AWS stores.** `retention_period = 24` equals the
service default, and no other arguments (encryption, shard-level metrics) are
toggled, so there's nothing for the provider to reconcile on refresh.
### Sandbox note
Constrained lab environments cap Kinesis at **PROVISIONED mode, 1 shard per stream,
≤24h retention, and ≤2 streams per account**. This config sits inside every one of
those limits, so it won't be silently reset or rejected.
## Verify
```bash
aws kinesis describe-stream-summary --stream-name xfusion-stream \
--query 'StreamDescriptionSummary.{Name:StreamName,Status:StreamStatus,Mode:StreamModeDetails.StreamMode,Shards:OpenShardCount,Retention:RetentionPeriodHours}'
```
Expected — name `xfusion-stream`, `StreamStatus: ACTIVE` (a few seconds after
create), `StreamMode: PROVISIONED`, `OpenShardCount: 1`, retention `24`.

92
terraform/task-19.md Normal file
View File

@@ -0,0 +1,92 @@
# Assignment
The Nautilus DevOps team needs to set up an SNS topic for sending notifications. They need to create an SNS topic with the following specifications:
1) The topic name should be nautilus-notifications.
Use Terraform to create this SNS topic. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# SNS Topic — `nautilus-notifications`
Terraform solution to create a standard SNS topic.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_sns_topic" "nautilus_notifications" {
name = "nautilus-notifications"
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_sns_topic`
One resource is all this task needs. The `name` argument sets the topic name to
`nautilus-notifications` exactly as required.
A few points on what's happening:
- **Standard topic by default.** SNS has two topic classes: **Standard** (high
throughput, best-effort ordering, at-least-once delivery) and **FIFO** (strict
ordering, exactly-once, name must end in `.fifo`). Since the name has no `.fifo`
suffix and `fifo_topic` isn't set, this creates a Standard topic — the right
choice for general notifications.
- **A topic is just the pub/sub channel.** Creating the topic gives you an endpoint
that publishers send messages to. Nothing receives those messages until you add
**subscriptions** (email, SMS, SQS, Lambda, HTTP, etc.). The task only asks for
the topic itself, so no `aws_sns_topic_subscription` resources are included — those
would be added later as delivery targets are decided.
- **No access policy specified.** By default SNS attaches a policy allowing the
topic owner (this account) to publish and manage it. That default is sufficient
for the task; a custom `policy` would only be needed to grant cross-account or
service-specific publish rights.
### Sandbox note
Constrained lab environments permit **basic SNS operations** — standard topics like
this one. Nothing advanced (FIFO, complex delivery policies) is in play, so the
create proceeds without restriction.
## Verify
```bash
TOPIC_ARN=$(aws sns list-topics \
--query "Topics[?ends_with(TopicArn, ':nautilus-notifications')].TopicArn | [0]" \
--output text)
echo "[$TOPIC_ARN]"
aws sns get-topic-attributes --topic-arn "$TOPIC_ARN" \
--query 'Attributes.{Name:DisplayName,Arn:TopicArn,Owner:Owner}'
```
Expected — a topic ARN of the form
`arn:aws:sns:us-east-1:<account-id>:nautilus-notifications`.

106
terraform/task-20.md Normal file
View File

@@ -0,0 +1,106 @@
# Assignment
The Nautilus DevOps team needs to create an SSM parameter in AWS with the following requirements:
1) The name of the parameter should be nautilus-ssm-parameter.
2) Set the parameter type to String.
3) Set the parameter value to nautilus-value.
4) The parameter should be created in the us-east-1 region.
5) Ensure the parameter is successfully created using terraform and can be retrieved when the task is completed.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# SSM Parameter — `nautilus-ssm-parameter`
Terraform solution to create a plaintext String parameter in AWS Systems Manager
Parameter Store.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_ssm_parameter" "nautilus" {
name = "nautilus-ssm-parameter"
type = "String"
value = "nautilus-value"
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### `aws_ssm_parameter`
One resource covers the whole task. SSM Parameter Store is a managed key/value store
for configuration data and secrets, and this creates a single entry in it.
- **`name = "nautilus-ssm-parameter"`** — the parameter's key/path. Names can be
hierarchical (e.g. `/app/prod/db-url`); a flat name like this one is stored at the
root.
- **`type = "String"`** — the parameter type. The three types are:
- `String` — a single plaintext value (used here).
- `StringList` — a comma-separated list of values.
- `SecureString` — an encrypted value backed by a KMS key.
The task wants a plain `String`, so no encryption or KMS key is involved.
- **`value = "nautilus-value"`** — the stored value. Because the type is `String`,
it's held as-is in plaintext and returned verbatim on read.
- **Region** comes from the provider (`us-east-1`). Unlike IAM, SSM parameters
**are** regional — a parameter created here is only visible in `us-east-1`, which
satisfies requirement #4.
### Retrieval (requirement #5)
The task asks that the parameter be retrievable once created. Nothing extra is
needed in the config for that — a `String` parameter is readable immediately via the
SSM API. The verification command below confirms it.
> Note: if this were a `SecureString`, you'd need `--with-decryption` on read to get
> the plaintext back. For a plain `String` that flag is unnecessary.
## Verify
```bash
aws ssm get-parameter --name nautilus-ssm-parameter \
--query 'Parameter.{Name:Name,Type:Type,Value:Value}'
```
Expected:
```json
{
"Name": "nautilus-ssm-parameter",
"Type": "String",
"Value": "nautilus-value"
}
```

103
terraform/task-21.md Normal file
View File

@@ -0,0 +1,103 @@
# Assignment
The Nautilus DevOps team needs to set up CloudWatch logging for their application. They need to create a CloudWatch log group and log stream with the following specifications:
1) The log group name should be xfusion-log-group.
2) The log stream name should be xfusion-log-stream.
Use Terraform to create the CloudWatch log group and log stream. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# CloudWatch Logs — `xfusion-log-group` + `xfusion-log-stream`
Terraform solution to create a CloudWatch log group and a log stream nested inside
it.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_cloudwatch_log_group" "xfusion" {
name = "xfusion-log-group"
}
resource "aws_cloudwatch_log_stream" "xfusion" {
name = "xfusion-log-stream"
log_group_name = aws_cloudwatch_log_group.xfusion.name
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
CloudWatch Logs has a two-level hierarchy: a **log group** is the container (where
you set retention, encryption, and access policy), and a **log stream** is an
ordered sequence of log events inside a group, typically one per source (an
instance, container, or function). This task creates one of each.
### `aws_cloudwatch_log_group`
- **`name = "xfusion-log-group"`** — the group name, exactly as required.
- No `retention_in_days` is set, so logs are retained **indefinitely** (the AWS
default). The task doesn't specify retention, so the default is fine; you'd add
`retention_in_days = N` if a policy required expiring logs.
### `aws_cloudwatch_log_stream`
- **`name = "xfusion-log-stream"`** — the stream name, exactly as required.
- **`log_group_name = aws_cloudwatch_log_group.xfusion.name`** — a stream can't
exist on its own; it must live inside a group. Referencing the group resource's
`name` attribute (rather than hardcoding the string) does two things:
1. It wires the value correctly, and
2. it creates an **implicit dependency** so Terraform provisions the group
**before** the stream. Without that ordering, the stream create would fail
because its parent group wouldn't exist yet.
This is why no explicit `depends_on` is needed — the attribute reference expresses
the dependency for you.
### Ordering matters
The dependency direction is one-way: group first, then stream. On `destroy`,
Terraform reverses it automatically — stream removed before group — so teardown is
clean too.
## Verify
```bash
# Confirm the group exists
aws logs describe-log-groups \
--log-group-name-prefix xfusion-log-group \
--query 'logGroups[0].{Name:logGroupName,Retention:retentionInDays}'
# Confirm the stream exists inside it
aws logs describe-log-streams \
--log-group-name xfusion-log-group \
--query 'logStreams[?logStreamName==`xfusion-log-stream`].logStreamName'
```
Expected — the group `xfusion-log-group` (retention `null` = never expire), and the
stream query returning `["xfusion-log-stream"]`.

116
terraform/task-22.md Normal file
View File

@@ -0,0 +1,116 @@
# Assignment
The Nautilus DevOps team is working on automating infrastructure deployment using AWS CloudFormation. As part of this effort, they need to create a CloudFormation stack that provisions an S3 bucket with versioning enabled.
Create a CloudFormation stack named xfusion-stack using Terraform. This stack should contain an S3 bucket named xfusion-bucket-20270 as a resource, and the bucket must have versioning enabled. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# CloudFormation Stack — `xfusion-stack` (S3 bucket with versioning)
Terraform provisions a **CloudFormation stack**; the stack's template, in turn,
creates a versioned S3 bucket. The bucket is a CloudFormation-managed resource, not a
native Terraform one.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_cloudformation_stack" "xfusion" {
name = "xfusion-stack"
template_body = <<-TEMPLATE
AWSTemplateFormatVersion: "2010-09-09"
Description: S3 bucket with versioning enabled
Resources:
XfusionBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: xfusion-bucket-20270
VersioningConfiguration:
Status: Enabled
TEMPLATE
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Two layers of provisioning
This task is deliberately indirect. Terraform does **not** create the bucket
directly with `aws_s3_bucket`. Instead:
1. Terraform creates an `aws_cloudformation_stack` resource.
2. AWS CloudFormation reads the template embedded in that stack and creates the S3
bucket from it.
So the ownership chain is **Terraform → CloudFormation stack → S3 bucket**. If you
inspect the bucket afterward it will show as managed by the `xfusion-stack` stack,
which is exactly what the task asks for.
### `aws_cloudformation_stack`
- **`name = "xfusion-stack"`** — the stack name, exactly as required.
- **`template_body`** — the CloudFormation template, inlined as a YAML heredoc. The
`<<-TEMPLATE` form strips leading indentation so the YAML parses cleanly despite
being nested inside HCL. You could also point at a file with `template_url` (S3) or
`file()`, but inlining keeps everything in the single `main.tf` the task requires.
### The CloudFormation template
- **`AWSTemplateFormatVersion`** — the CFN schema version; `"2010-09-09"` is the only
valid value and is effectively a constant.
- **`Resources.XfusionBucket`** — the logical ID (an internal name CloudFormation
uses to track the resource). It's arbitrary; the actual bucket name comes from the
properties below.
- **`Type: AWS::S3::Bucket`** — declares an S3 bucket.
- **`BucketName: xfusion-bucket-20270`** — sets the real, globally-unique bucket
name required by the task.
- **`VersioningConfiguration.Status: Enabled`** — turns on object versioning, so
overwritten or deleted objects are retained as prior versions. This is the CFN
equivalent of Terraform's `aws_s3_bucket_versioning` resource.
### Why versioning is inside the template
Because the bucket is owned by CloudFormation, its configuration must be expressed in
CFN syntax inside the template — not with a separate Terraform
`aws_s3_bucket_versioning` block. Mixing a native TF versioning resource against a
CFN-managed bucket would create two controllers fighting over the same bucket, so all
bucket config lives in the template.
## Verify
```bash
# Stack created successfully
aws cloudformation describe-stacks --stack-name xfusion-stack \
--query 'Stacks[0].{Name:StackName,Status:StackStatus}'
# Bucket exists with versioning enabled
aws s3api get-bucket-versioning --bucket xfusion-bucket-20270 \
--query 'Status'
```
Expected — stack `StackStatus: CREATE_COMPLETE`, and the versioning query returning
`"Enabled"`.

130
terraform/task-23.md Normal file
View File

@@ -0,0 +1,130 @@
# Assignment
The Nautilus DevOps team needs to set up an Amazon OpenSearch Service domain to store and search their application logs. The domain should have the following specification:
1) The domain name should be devops-es.
2) Use Terraform to create the OpenSearch domain. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Notes:
The Terraform working directory is /home/bob/terraform.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
Before submitting the task, ensure that terraform plan returns No changes. Your infrastructure matches the configuration.
The OpenSearch domain creation process may take several minutes. Please wait until the domain is fully created before submitting.
# Solution
# OpenSearch Domain — `devops-es`
Terraform solution to create a minimal, single-node Amazon OpenSearch Service domain
that comes up clean and reports no drift on re-plan.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_opensearch_domain" "devops_es" {
domain_name = "devops-es"
cluster_config {
instance_type = "t3.small.search"
instance_count = 1
}
ebs_options {
ebs_enabled = true
volume_type = "gp2"
volume_size = 10
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
# Domain creation takes ~10-20 minutes. Wait for apply to fully return.
# Required idempotency check:
terraform plan
# -> "No changes. Your infrastructure matches the configuration."
```
## How it works
### `aws_opensearch_domain`
- **`domain_name = "devops-es"`** — the domain name, exactly as required.
- **`cluster_config`** — defines the data nodes:
- `instance_type = "t3.small.search"` — the smallest current-generation
general-purpose node. OpenSearch instance types carry a `.search` suffix. Small
burstable nodes keep the domain inside sandbox cost limits (OpenSearch isn't in
the published KodeKloud caps, so the general "smallest viable instance" rule
applies).
- `instance_count = 1` — a single node. No `zone_awareness_enabled` and no
`dedicated_master_enabled`, which are unnecessary (and would cost more) for a
lab domain.
- **`ebs_options`** — OpenSearch nodes on this instance family use EBS for storage:
- `ebs_enabled = true` — required for non-instance-store types.
- `volume_type = "gp2"`**deliberately gp2, not gp3** (see idempotency note
below).
- `volume_size = 10` — 10 GiB, the minimum allowed per node.
### Why this config passes the "No changes" plan check
The task explicitly requires `terraform plan` to report no drift after apply.
OpenSearch domains are notorious for phantom diffs; two choices here prevent that:
1. **gp2 instead of gp3.** A gp3 volume has `throughput` and `iops` attributes that
AWS auto-populates with defaults when you don't specify them. Terraform then sees
values in the remote state that aren't in your config and reports a perpetual
diff. gp2 has no such tunables, so there's nothing to drift — the cleanest choice
for an idempotent lab domain.
2. **`engine_version` omitted.** When you don't pin the engine version, the argument
is treated as computed — AWS picks its current default at create, Terraform
records it in state, and because your config says nothing about it, there's
nothing to compare against on the next plan. Pinning a version can instead cause
drift when AWS applies an automatic minor-version patch.
Everything else (encryption blocks, endpoint options, auto-tune, off-peak window) is
left unset, so the provider treats those as computed defaults rather than managed
values — again, no diff.
### On creation time
OpenSearch domains provision slowly — the control plane spins up nodes, storage, and
networking, which typically takes 1020 minutes. `terraform apply` blocks until the
domain reaches `Active`, so when the command returns the domain is ready. Don't
submit until apply completes and the follow-up `plan` is clean.
## Verify
```bash
aws opensearch describe-domain --domain-name devops-es \
--query 'DomainStatus.{Name:DomainName,Processing:Processing,Type:ClusterConfig.InstanceType,Count:ClusterConfig.InstanceCount,Vol:EBSOptions.VolumeType,Size:EBSOptions.VolumeSize}'
```
Expected — name `devops-es`, `Processing: false` (creation finished), instance type
`t3.small.search`, count `1`, volume `gp2` size `10`.

112
terraform/task-24.md Normal file
View File

@@ -0,0 +1,112 @@
# Assignment
The Nautilus DevOps team needs to store sensitive data securely using AWS Secrets Manager. They need to create a secret with the following specifications:
1) The secret name should be xfusion-secret.
2) The secret value should contain a key-value pair with username: admin and password: Namin123.
3) Use Terraform to create the secret in AWS Secrets Manager.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Secrets Manager — `xfusion-secret`
Terraform solution to create an AWS Secrets Manager secret holding a username/password
key-value pair.
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_secretsmanager_secret" "xfusion" {
name = "xfusion-secret"
}
resource "aws_secretsmanager_secret_version" "xfusion" {
secret_id = aws_secretsmanager_secret.xfusion.id
secret_string = jsonencode({
username = "admin"
password = "Namin123"
})
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
Secrets Manager splits a secret into two parts, and Terraform mirrors that with two
resources:
### `aws_secretsmanager_secret`
The **container / metadata** — the named secret itself. It holds the name, optional
description, KMS key, rotation config, and resource policy, but **not** the secret
value.
- **`name = "xfusion-secret"`** — the secret name, exactly as required.
- No KMS key is specified, so Secrets Manager encrypts the value with the AWS-managed
`aws/secretsmanager` key by default — sufficient for the task.
### `aws_secretsmanager_secret_version`
The **actual value**. Secrets are versioned; every value you store is a version, and
one is marked `AWSCURRENT`. This resource writes that current version.
- **`secret_id = aws_secretsmanager_secret.xfusion.id`** — links the version to its
parent secret. Referencing the attribute (not hardcoding) creates the implicit
dependency so the container is created before the version is written into it.
- **`secret_string`** — the payload. The task wants a **key-value pair**, which in
Secrets Manager means a JSON object string. `jsonencode({ username = "admin",
password = "Namin123" })` produces `{"username":"admin","password":"Namin123"}`.
Using `jsonencode` (rather than hand-writing the JSON string) guarantees valid
escaping and is the idiomatic way to store structured secrets — it's also the
format the Secrets Manager console renders as separate key/value rows.
### Why JSON and not two plain values
Secrets Manager stores a single `SecretString` per version. To hold *multiple* fields
(username **and** password), you encode them as one JSON object. That's the standard
pattern and what lets the console and SDKs pull individual keys (e.g. via
`--query SecretString` then JSON parse). Storing raw `admin` / `Namin123` as two
separate things isn't possible — it's one string field, so JSON is the correct
container.
### Sandbox note
Secrets Manager **basic** operations (create secret, store value) are permitted.
Advanced features — automatic **rotation** and **cross-region replication** — are
blocked in these sandboxes, but this task uses neither, so it proceeds without issue.
## Verify
```bash
aws secretsmanager get-secret-value --secret-id xfusion-secret \
--query 'SecretString' --output text
```
Expected — `{"username":"admin","password":"Namin123"}`.

119
terraform/task-25.md Normal file
View File

@@ -0,0 +1,119 @@
# Assignment
During the migration process, the Nautilus DevOps team created several EC2 instances in different regions. They are currently in the process of identifying the correct resources and utilization and are making continuous changes to ensure optimal resource utilization. Recently, they discovered that one of the EC2 instances was underutilized, prompting them to decide to change the instance type. Please make sure the Status check is completed (if it's still in Initializing state) before making any changes to the instance.
Change the instance type from t2.micro to t2.nano for nautilus-ec2 instance using terraform.
Make sure the EC2 instance nautilus-ec2 is in running state after the change.
The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to change the instance type.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Change EC2 Instance Type — `nautilus-ec2` (t2.micro → t2.nano)
This is an **update** to an already Terraform-managed instance, not a new resource.
The instance definition already exists in `main.tf` from when it was created; the task
is to change its `instance_type` and re-apply.
## The change
Find the `aws_instance` block for `nautilus-ec2` in the existing `main.tf` and change
the single `instance_type` line:
```diff
resource "aws_instance" "nautilus_ec2" {
ami = "ami-xxxxxxxxxxxxxxxxx"
- instance_type = "t2.micro"
+ instance_type = "t2.nano"
# ... key_name, vpc_security_group_ids, tags, etc. unchanged ...
tags = {
Name = "nautilus-ec2"
}
}
```
For reference, the full block looks like this after the edit (leave every other
argument exactly as it already is — only `instance_type` changes):
```hcl
resource "aws_instance" "nautilus_ec2" {
ami = "ami-xxxxxxxxxxxxxxxxx" # keep the existing value
instance_type = "t2.nano" # was t2.micro
tags = {
Name = "nautilus-ec2"
}
}
```
> Do **not** rewrite the block from scratch or change the resource's Terraform name,
> AMI, key, or security groups. Altering those could force a **replacement** (destroy +
> recreate) instead of an in-place modify. Only the one line changes.
## How to run
```bash
cd /home/bob/terraform
# 1) Make sure status checks are done (not "Initializing") BEFORE changing anything.
aws ec2 describe-instance-status \
--filters Name=tag:Name,Values=nautilus-ec2 \
--query 'InstanceStatuses[0].{Instance:InstanceStatus.Status,System:SystemStatus.Status}'
# Wait until both report "ok" (2/2 checks passed). If it shows "initializing", wait and re-run.
# 2) Apply the instance-type change.
terraform plan # should show ~ instance_type "t2.micro" -> "t2.nano" (update in place)
terraform apply -auto-approve
```
## How it works
### Why it's an in-place update, not a replacement
`instance_type` is a mutable attribute in EC2. Terraform's AWS provider changes it
**without destroying the instance**: it stops the instance, issues
`ModifyInstanceAttribute` to set the new type, then starts it again. You'll see this
in the plan as `~ update in-place`, not `-/+ destroy and recreate`. The instance keeps
its ID, EBS volumes, private IP, and (if it has one) EIP association.
`t2.micro` and `t2.nano` are in the same family and share the same virtualization and
architecture, so the modify is fully compatible — no AMI or platform mismatch.
### Why wait for the status check first
The task calls this out specifically. Changing instance type requires the instance to
be **stopped** momentarily. If the instance is still `Initializing` (status checks
haven't reached 2/2), issuing a stop/modify mid-initialization can fail or leave the
instance in an inconsistent state. Waiting until both the system and instance status
checks report `ok` guarantees a clean stop → modify → start cycle.
### Why it ends up `running` (requirement #2)
After a type change, the AWS provider automatically starts the instance back up and
waits for it to reach the `running` state before `apply` returns. No extra
configuration is needed — an `aws_instance` resource's desired state is running, so
Terraform restores that after the modify. When `apply` completes, the instance is
running as `t2.nano`.
### If the instance isn't in Terraform state
The task says *update* `main.tf`, which means the instance is already managed here. If
for some reason `terraform plan` reports it wants to **create** `nautilus-ec2` (i.e.
it's not in state), stop — that means the instance was made outside Terraform and you'd
need to `terraform import aws_instance.nautilus_ec2 <instance-id>` first, then apply the
type change. In the normal lab flow this isn't necessary.
## Verify
```bash
aws ec2 describe-instances \
--filters Name=tag:Name,Values=nautilus-ec2 \
--query 'Reservations[0].Instances[0].{Id:InstanceId,Type:InstanceType,State:State.Name}'
```
Expected — the same instance ID as before, `InstanceType: t2.nano`, and
`State: running`.

103
terraform/task-26.md Normal file
View File

@@ -0,0 +1,103 @@
# Assignment
The Nautilus DevOps team has been creating a couple of services on AWS cloud. They have been breaking down the migration into smaller tasks, allowing for better control, risk mitigation, and optimization of resources throughout the migration process. Recently they came up with requirements mentioned below.
There is an instance named xfusion-ec2 and an elastic-ip named xfusion-ec2-eip in us-east-1 region. Attach the xfusion-ec2-eip elastic-ip to the xfusion-ec2 instance using Terraform only. The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to attach the specified Elastic IP to the instance.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Attach Elastic IP — `xfusion-ec2-eip` → `xfusion-ec2`
Both the instance (`aws_instance.ec2`) and the Elastic IP (`aws_eip.ec2_eip`) are
already managed in this `main.tf`. So there's no need for data sources — you reference
the existing resources directly and add a single association resource.
## `main.tf` (append this block; leave the existing two resources unchanged)
```hcl
# Provision EC2 instance
resource "aws_instance" "ec2" {
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
subnet_id = "subnet-bca4d7696cae064fb"
vpc_security_group_ids = [
"sg-62f0812f2b5774df4"
]
tags = {
Name = "xfusion-ec2"
}
}
# Provision Elastic IP
resource "aws_eip" "ec2_eip" {
tags = {
Name = "xfusion-ec2-eip"
}
}
# Associate the Elastic IP with the instance
resource "aws_eip_association" "ec2_eip_assoc" {
allocation_id = aws_eip.ec2_eip.id
instance_id = aws_instance.ec2.id
}
```
## How to run
```bash
cd /home/bob/terraform
terraform plan # should show only the new aws_eip_association to add
terraform apply -auto-approve
```
## How it works
### Referencing managed resources (not data sources)
Because both the instance and the EIP already live in this configuration, you point
the association straight at their resource attributes:
- **`allocation_id = aws_eip.ec2_eip.id`** — a VPC Elastic IP's `id` is its allocation
ID (`eipalloc-...`), which is what an association binds. (Data sources would only be
needed if these resources were created outside this config — here they aren't.)
- **`instance_id = aws_instance.ec2.id`** — the instance's `i-...` ID.
Referencing the resource attributes (rather than hardcoding IDs) also creates
**implicit dependencies**: Terraform knows the association depends on both the instance
and the EIP, so it orders creation correctly and the plan cleanly shows only the new
association being added — the two existing resources are untouched.
### `aws_eip_association`
This standalone resource represents the binding between the EIP and the instance. On
apply, Terraform calls `AssociateAddress`, and the instance's public-facing IP becomes
the Elastic IP. Keeping the association as its own resource (rather than folding an
`instance = ...` argument into the `aws_eip` block) is the cleaner, more explicit
pattern and keeps the attach/detach lifecycle isolated — destroying just this resource
would detach the EIP while leaving the instance and EIP intact.
> Note on the alternative: you *could* instead add `instance = aws_instance.ec2.id`
> directly to the `aws_eip` resource. That also works when Terraform owns the EIP, but
> a dedicated `aws_eip_association` is the recommended approach and avoids mixing
> allocation and association concerns in one resource.
## Verify
```bash
aws ec2 describe-addresses \
--filters Name=tag:Name,Values=xfusion-ec2-eip \
--query 'Addresses[0].{EIP:PublicIp,InstanceId:InstanceId,AssocId:AssociationId}'
```
Expected — the address shows a populated `InstanceId` (the `xfusion-ec2` instance) and
an `AssociationId`. Cross-check that the instance's `PublicIpAddress` now equals the
EIP:
```bash
aws ec2 describe-instances \
--filters Name=tag:Name,Values=xfusion-ec2 \
--query 'Reservations[0].Instances[0].PublicIpAddress'
```

107
terraform/task-27.md Normal file
View File

@@ -0,0 +1,107 @@
# Assignment
The Nautilus DevOps team has been creating a couple of services on AWS cloud. They have been breaking down the migration into smaller tasks, allowing for better control, risk mitigation, and optimization of resources throughout the migration process. Recently they came up with requirements mentioned below.
An IAM user named iamuser_james and a policy named iampolicy_james already exists. Use Terraform to attach the IAM policy iampolicy_james to the IAM user iamuser_james. The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to attach the specified IAM policy to the IAM user.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Attach IAM Policy — `iampolicy_james` → `iamuser_james`
The user (`aws_iam_user.user`) and the customer-managed policy
(`aws_iam_policy.policy`) already exist in this `main.tf`. The task is to bind them,
which is one appended `aws_iam_user_policy_attachment` resource referencing both.
## `main.tf` (append this block; leave the existing two resources unchanged)
```hcl
# Create IAM user
resource "aws_iam_user" "user" {
name = "iamuser_james"
tags = {
Name = "iamuser_james"
}
}
# Create IAM Policy
resource "aws_iam_policy" "policy" {
name = "iampolicy_james"
description = "IAM policy allowing EC2 read actions for james"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["ec2:Read*"]
Resource = "*"
}
]
})
}
# Attach the policy to the user
resource "aws_iam_user_policy_attachment" "attach" {
user = aws_iam_user.user.name
policy_arn = aws_iam_policy.policy.arn
}
```
## How to run
```bash
cd /home/bob/terraform
terraform plan # should show only the new attachment to add
terraform apply -auto-approve
```
## How it works
### `aws_iam_user_policy_attachment`
This resource attaches a **managed** policy (AWS-managed or customer-managed) to an
IAM user. It's the right resource here because `iampolicy_james` is a standalone
customer-managed policy with its own ARN — as opposed to an *inline* policy, which
would use `aws_iam_user_policy` and embed the JSON directly in the user.
- **`user = aws_iam_user.user.name`** — the attachment targets the user by name.
- **`policy_arn = aws_iam_policy.policy.arn`** — managed-policy attachments are made
by ARN. The `aws_iam_policy` resource exposes its generated `arn`
(`arn:aws:iam::<account>:policy/iampolicy_james`), so referencing the attribute
avoids hardcoding the account ID.
Referencing both resources by attribute creates **implicit dependencies**: Terraform
provisions/keeps the user and policy first, then the attachment. The plan shows only
the attachment being added — the existing user and policy are untouched.
### Managed vs. inline — why this resource
There are three ways to give an IAM user permissions in Terraform:
| Resource | Use when |
|----------|----------|
| `aws_iam_user_policy_attachment` | Attaching an existing **managed** policy (this task). |
| `aws_iam_policy_attachment` | Attaching one policy to many users/roles/groups at once — **avoid**; it's exclusive and will detach principals it doesn't know about. |
| `aws_iam_user_policy` | Defining an **inline** policy embedded in the user. |
`aws_iam_user_policy_attachment` is the correct, non-exclusive choice for wiring one
managed policy to one user.
### Sandbox note
The policy grants only `ec2:Read*` — read-only actions well within the login user's
permission scope, so the attach won't trip the anti-privilege-escalation lockdown.
Attaching a policy broader than your login user has is what triggers `AccessDenied`;
that isn't the case here.
## Verify
```bash
aws iam list-attached-user-policies --user-name iamuser_james \
--query 'AttachedPolicies[*].{Name:PolicyName,Arn:PolicyArn}'
```
Expected — a list containing `iampolicy_james` with its ARN.

93
terraform/task-28.md Normal file
View File

@@ -0,0 +1,93 @@
# Assignment
Data protection and recovery are fundamental aspects of data management. It's essential to have systems in place to ensure that data can be recovered in case of accidental deletion or corruption. The DevOps team has received a requirement for implementing such measures for one of the S3 buckets they are managing.
The S3 bucket name is xfusion-s3-1124, enable versioning for this bucket using Terraform.
The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Enable S3 Versioning — `xfusion-s3-1124`
The bucket (`aws_s3_bucket.s3_ran_bucket`) already exists in this `main.tf`. Enabling
versioning in modern AWS provider versions is done with a **separate**
`aws_s3_bucket_versioning` resource, not an inline `versioning {}` block.
## `main.tf` (append this block; leave the existing bucket unchanged)
```hcl
resource "aws_s3_bucket" "s3_ran_bucket" {
bucket = "xfusion-s3-1124"
acl = "private"
tags = {
Name = "xfusion-s3-1124"
}
}
# Enable versioning on the bucket
resource "aws_s3_bucket_versioning" "s3_ran_bucket" {
bucket = aws_s3_bucket.s3_ran_bucket.id
versioning_configuration {
status = "Enabled"
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform plan # should show only the new aws_s3_bucket_versioning to add
terraform apply -auto-approve
```
## How it works
### `aws_s3_bucket_versioning`
Since AWS provider v4, S3 sub-configurations (versioning, ACL, logging, lifecycle,
encryption, etc.) were split out of the `aws_s3_bucket` resource into dedicated
resources. Versioning is now its own resource:
- **`bucket = aws_s3_bucket.s3_ran_bucket.id`** — targets the existing bucket by
reference. Using the attribute (not the hardcoded name) creates an **implicit
dependency**, so Terraform manages the bucket first, then the versioning config, and
the plan shows only the new resource being added.
- **`versioning_configuration { status = "Enabled" }`** — turns on versioning. Valid
states are `Enabled`, `Suspended`, and `Disabled` (only meaningful on create).
`Enabled` means every overwrite or delete keeps prior object versions, which is
exactly the accidental-deletion/corruption protection the task calls for.
### Note on the existing inline `acl`
The existing bucket block sets `acl = "private"` inline. That inline argument is
**deprecated** in current provider versions (the modern equivalent is a separate
`aws_s3_bucket_acl` resource), but it still functions and — importantly — it's a
**different concern** from versioning. Adding `aws_s3_bucket_versioning` does **not**
conflict with the inline `acl`. Conflicts only arise if you configure the *same*
concern two ways (e.g. an inline `versioning {}` block **and** a standalone
`aws_s3_bucket_versioning` resource on the same bucket). Leave the `acl` line as-is —
the task only asks for versioning.
### How versioning behaves once enabled
- New writes to existing keys create new versions; the old bytes are retained under a
prior version ID.
- Deletes place a **delete marker** rather than removing data, so objects can be
recovered.
- Versioning, once enabled, can only be **suspended**, never fully turned off — that's
by design in S3.
## Verify
```bash
aws s3api get-bucket-versioning --bucket xfusion-s3-1124 \
--query 'Status'
```
Expected — `"Enabled"`.

130
terraform/task-29.md Normal file
View File

@@ -0,0 +1,130 @@
# Assignment
The Nautilus DevOps team is currently engaged in a cleanup process, focusing on removing unnecessary data and services from their AWS account. As part of the migration process, several resources were created for one-time use only, necessitating a cleanup effort to optimize their AWS environment.
A S3 bucket named devops-bck-28889 already exists.
1) Copy the contents of devops-bck-28889 S3 bucket to /opt/s3-backup/ directory on terraform-client host (the landing host once you load this lab).
2) Delete the S3 bucket devops-bck-28889.
3) Use the AWS CLI through Terraform to accomplish this task—for example, by running AWS CLI commands within Terraform. The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# S3 Backup + Delete via Terraform — `devops-bck-28889`
This task runs **AWS CLI commands through Terraform** using a `null_resource` with a
`local-exec` provisioner: sync the bucket's contents to a local directory on the
`terraform-client` host, then delete the bucket.
## `main.tf` (append this block)
```hcl
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
}
resource "null_resource" "s3_backup_and_delete" {
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = <<-EOT
mkdir -p /opt/s3-backup/ && \
aws s3 sync s3://devops-bck-28889 /opt/s3-backup/ && \
aws s3 rb s3://devops-bck-28889 --force
EOT
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Why a `null_resource` + `local-exec`
Terraform has no native resource for "copy bucket contents to a local path" or for a
force-delete of a populated bucket. When a task explicitly says *use the AWS CLI
through Terraform*, the idiomatic pattern is a `null_resource` whose `local-exec`
provisioner shells out to the CLI on the machine running Terraform (here, the
`terraform-client` landing host, which already has AWS credentials configured).
- **`null_resource`** — a resource that does nothing itself; it exists solely to hang
provisioners on. Creating it once (on `apply`) triggers its `local-exec` block.
- **`interpreter = ["/bin/bash", "-c"]`** — runs the script under bash. (The default
is `/bin/sh`; bash is set explicitly for predictable `&&` chaining and heredoc
behavior.)
### The command sequence
The three steps are chained with `&&`, so each runs only if the previous succeeded —
critical because step 3 is destructive:
1. **`mkdir -p /opt/s3-backup/`** — ensures the destination directory exists (and is a
no-op if it already does).
2. **`aws s3 sync s3://devops-bck-28889 /opt/s3-backup/`** — copies every object from
the bucket into the local directory, preserving key paths. `sync` only transfers
what's missing/changed, but on a fresh directory it pulls everything.
3. **`aws s3 rb s3://devops-bck-28889 --force`** — removes the bucket. `rb` (remove
bucket) normally refuses a non-empty bucket; `--force` first deletes all objects,
then the bucket itself.
The `&&` chaining is the safety mechanism: if the `sync` fails (network, permissions),
the `rb` never executes, so you don't delete data you failed to back up.
### Run-once semantics
A `null_resource` runs its provisioner when it's **created**. After a successful
apply it's in state and won't re-run on subsequent applies. That's the desired
behavior for a one-time cleanup. If you ever needed to run it again, you'd
`terraform taint null_resource.s3_backup_and_delete` (or
`terraform apply -replace=...`) to force recreation — but note the bucket is gone
after the first run, so a re-run would fail at the sync step anyway.
### Modern alternative
Terraform 1.4+ ships a built-in `terraform_data` resource that replaces
`null_resource` without needing the `hashicorp/null` provider. The same block written
with it:
```hcl
resource "terraform_data" "s3_backup_and_delete" {
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = <<-EOT
mkdir -p /opt/s3-backup/ && \
aws s3 sync s3://devops-bck-28889 /opt/s3-backup/ && \
aws s3 rb s3://devops-bck-28889 --force
EOT
}
}
```
Either works; `null_resource` is shown as the primary since it's the widely-recognized
form.
## Verify
```bash
# Backup landed locally
ls -la /opt/s3-backup/
# Bucket is gone (this should error with "NoSuchBucket" / "Not Found")
aws s3 ls s3://devops-bck-28889 || echo "Bucket deleted."
```
Expected — `/opt/s3-backup/` contains the former bucket contents, and the `s3 ls`
against the bucket fails because it no longer exists.

102
terraform/task-30.md Normal file
View File

@@ -0,0 +1,102 @@
# Assignment
During the migration process, several resources were created under the AWS account. Some of these test resources are no longer needed at the moment, so we need to clean them up temporarily. One such instance is currently unused and should be deleted.
1) Delete the ec2 instance named devops-ec2 present in us-east-1 region using terraform. Make sure to keep the provisioning code, as we might need to provision this instance again later.
2) Before submitting your task, make sure instance is in terminated state.
The Terraform working directory is /home/bob/terraform.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Delete EC2 Instance (Keep Code) — `devops-ec2`
The requirement has two halves that seem to conflict: **terminate** the instance, but
**keep** its provisioning code in `main.tf` for later. The resolution is a **targeted
destroy** — it operates on the real resource and Terraform state only, and never
touches your `.tf` files. So `main.tf` stays exactly as-is.
## `main.tf` — leave unchanged
Do **not** delete or comment out the block. It stays exactly as given:
```hcl
# Provision EC2 instance
resource "aws_instance" "ec2" {
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
vpc_security_group_ids = [
"sg-6593a59fe82db4400"
]
tags = {
Name = "devops-ec2"
}
}
```
## How to run
```bash
cd /home/bob/terraform
# Terminate ONLY this instance; leaves the code in main.tf intact.
terraform destroy -target=aws_instance.ec2 -auto-approve
```
## How it works
### Targeted destroy vs. the wrong approaches
Three ways you might try to delete the instance, and why only one fits:
| Approach | Result |
|----------|--------|
| Delete the resource block, then `apply` | Instance is destroyed, but the **code is gone** — violates "keep the provisioning code." |
| `terraform destroy` (no target) | Destroys **everything** in the config, not just this instance. |
| `terraform destroy -target=aws_instance.ec2` | Destroys **only** this instance; code stays in `main.tf`. ✅ |
The `-target` flag scopes the operation to a single resource address
(`aws_instance.ec2`). Terraform terminates that instance, removes its entry from
state, and leaves every line of your configuration file exactly where it was —
because destroy acts on infrastructure and state, never on source code.
### Why the code surviving matters
Keeping the block means the instance is trivially re-creatable later: a plain
`terraform apply` will see the resource declared in config but absent from state and
recreate it. That's the "we might need to provision this again" requirement — the
declaration is preserved as the reusable blueprint.
### Reaching `terminated` state (requirement #2)
`terraform destroy -target` calls `TerminateInstances` and **waits** until the
instance reaches `terminated` before returning. When the command completes, the
instance is terminated — no manual polling needed. (A terminated instance lingers as
a read-only entry in the console for a while before disappearing; that's normal.)
### Expected state afterward
After the targeted destroy, the config declares a resource that no longer exists in
state. So `terraform plan` will show Terraform wants to **create** `devops-ec2` again
(`+ 1 to add`). That's expected and correct — **do not apply it**. The task wants the
instance terminated with the code retained, which is exactly this state.
## Verify
```bash
aws ec2 describe-instances \
--filters Name=tag:Name,Values=devops-ec2 \
--query 'Reservations[*].Instances[*].{Id:InstanceId,State:State.Name}' \
--output table
```
Expected — the instance shows `State: terminated`. Also confirm the code is still
present:
```bash
grep -A12 'resource "aws_instance" "ec2"' /home/bob/terraform/main.tf
```

105
terraform/task-31.md Normal file
View File

@@ -0,0 +1,105 @@
# Assignment
The Nautilus DevOps team is currently engaged in a cleanup process, focusing on removing unnecessary data and services from their AWS account. As part of the migration process, several resources were created for one-time use only, necessitating a cleanup effort to optimize their AWS environment.
Delete an IAM group named iamgroup_siva using terraform. Make sure to keep the provisioning code, as we might need to provision this instance again later.
The Terraform working directory is /home/bob/terraform.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Delete IAM Group (Keep Code) — `iamgroup_siva`
Same shape as the "delete but keep the code" pattern: **destroy** the group while
leaving its provisioning block in `main.tf` for later reuse. A **targeted destroy**
does exactly this — it acts on the real resource and Terraform state only, never on
your `.tf` source.
## `main.tf` — leave unchanged
Do **not** delete or comment out the block. It stays exactly as given:
```hcl
resource "aws_iam_group" "this" {
name = "iamgroup_siva"
}
```
## How to run
```bash
cd /home/bob/terraform
# Destroy ONLY this IAM group; leaves the code in main.tf intact.
terraform destroy -target=aws_iam_group.this -auto-approve
```
## How it works
### Targeted destroy vs. the wrong approaches
| Approach | Result |
|----------|--------|
| Delete the resource block, then `apply` | Group is destroyed, but the **code is gone** — violates "keep the provisioning code." |
| `terraform destroy` (no target) | Destroys **everything** in the config, not just this group. |
| `terraform destroy -target=aws_iam_group.this` | Destroys **only** this group; code stays in `main.tf`. ✅ |
The `-target` flag scopes the operation to the single resource address
(`aws_iam_group.this` — note this is the **Terraform resource name** `this`, not the
AWS group name `iamgroup_siva`). Terraform calls `DeleteGroup`, removes the resource
from state, and leaves your configuration file untouched, because destroy operates on
infrastructure and state, never on source code.
### Why the code surviving matters
Keeping the block means the group is trivially re-creatable later: a plain
`terraform apply` will see it declared in config but absent from state and recreate
it. That satisfies "we might need to provision this again" — the declaration remains
the reusable blueprint.
### One caveat for IAM groups
`DeleteGroup` only succeeds if the group is **empty** — no users as members and no
attached/inline policies. If the destroy errors with `DeleteConflict`, detach
everything first, then re-run:
```bash
# Remove any members
aws iam get-group --group-name iamgroup_siva \
--query 'Users[].UserName' --output text | \
xargs -r -n1 -I{} aws iam remove-user-from-group --group-name iamgroup_siva --user-name {}
# Detach any managed policies
aws iam list-attached-group-policies --group-name iamgroup_siva \
--query 'AttachedPolicies[].PolicyArn' --output text | \
xargs -r -n1 -I{} aws iam detach-group-policy --group-name iamgroup_siva --policy-arn {}
# Then retry
terraform destroy -target=aws_iam_group.this -auto-approve
```
For a bare group like this one (created empty), the plain targeted destroy works
directly.
### Expected state afterward
After the destroy, the config still declares a resource that no longer exists in
state, so `terraform plan` will show Terraform wants to **create** `iamgroup_siva`
again (`+ 1 to add`). That's expected — **do not apply it**. The task wants the group
deleted with the code retained, which is exactly this state.
## Verify
```bash
aws iam get-group --group-name iamgroup_siva 2>&1 | grep -q 'NoSuchEntity' \
&& echo "Group deleted." \
|| echo "Group still exists."
# Confirm the code is still present
grep -A2 'resource "aws_iam_group" "this"' /home/bob/terraform/main.tf
```
Expected — `get-group` fails with `NoSuchEntity` (group gone), and the resource block
is still present in `main.tf`.

129
terraform/task-32.md Normal file
View File

@@ -0,0 +1,129 @@
# Assignment
The Nautilus DevOps team is currently engaged in a cleanup process, focusing on removing unnecessary data and services from their AWS account. As part of the migration process, several resources were created for one-time use only, necessitating a cleanup effort to optimize their AWS environment.
Delete the IAM role named iamrole_ammar using Terraform. Make sure to keep the provisioning code, as we might need to provision this instance again later.
The Terraform working directory is /home/bob/terraform.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Delete IAM Role (Keep Code) — `iamrole_ammar`
Same "delete but keep the code" pattern: **destroy** the role while leaving its
provisioning block in `main.tf` for later reuse. A **targeted destroy** does exactly
this — it acts on the real resource and Terraform state only, never on your `.tf`
source.
## `main.tf` — leave unchanged
Do **not** delete or comment out the block. It stays exactly as given:
```hcl
resource "aws_iam_role" "role" {
name = "iamrole_ammar"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = {
Name = "iamrole_ammar"
}
}
```
## How to run
```bash
cd /home/bob/terraform
# Destroy ONLY this IAM role; leaves the code in main.tf intact.
terraform destroy -target=aws_iam_role.role -auto-approve
```
## How it works
### Targeted destroy vs. the wrong approaches
| Approach | Result |
|----------|--------|
| Delete the resource block, then `apply` | Role is destroyed, but the **code is gone** — violates "keep the provisioning code." |
| `terraform destroy` (no target) | Destroys **everything** in the config, not just this role. |
| `terraform destroy -target=aws_iam_role.role` | Destroys **only** this role; code stays in `main.tf`. ✅ |
The `-target` flag scopes the operation to the single resource address
(`aws_iam_role.role` — the **Terraform resource name** `role`, not the AWS role name
`iamrole_ammar`). Terraform calls `DeleteRole`, removes the resource from state, and
leaves your configuration untouched, because destroy operates on infrastructure and
state, never on source code.
### Why the code surviving matters
Keeping the block means the role is trivially re-creatable later: a plain
`terraform apply` will see it declared in config but absent from state and recreate
it — assume-role policy and all. That satisfies "we might need to provision this
again."
### One caveat for IAM roles
`DeleteRole` only succeeds if the role has **nothing attached**: no managed policies,
no inline policies, and no instance profile referencing it. The `assume_role_policy`
(trust policy) shown here is part of the role itself and does **not** block deletion —
only *permission* policies and instance-profile links do. If the destroy errors with
`DeleteConflict`, clear those first, then re-run:
```bash
# Detach managed policies
aws iam list-attached-role-policies --role-name iamrole_ammar \
--query 'AttachedPolicies[].PolicyArn' --output text | \
xargs -r -n1 -I{} aws iam detach-role-policy --role-name iamrole_ammar --policy-arn {}
# Delete inline policies
aws iam list-role-policies --role-name iamrole_ammar \
--query 'PolicyNames[]' --output text | \
xargs -r -n1 -I{} aws iam delete-role-policy --role-name iamrole_ammar --policy-name {}
# Remove from any instance profiles
aws iam list-instance-profiles-for-role --role-name iamrole_ammar \
--query 'InstanceProfiles[].InstanceProfileName' --output text | \
xargs -r -n1 -I{} aws iam remove-role-from-instance-profile --instance-profile-name {} --role-name iamrole_ammar
# Then retry
terraform destroy -target=aws_iam_role.role -auto-approve
```
For a bare role like this one (only a trust policy, no permissions attached), the
plain targeted destroy works directly.
### Expected state afterward
After the destroy, the config still declares a resource that no longer exists in
state, so `terraform plan` will show Terraform wants to **create** `iamrole_ammar`
again (`+ 1 to add`). That's expected — **do not apply it**. The task wants the role
deleted with the code retained, which is exactly this state.
## Verify
```bash
aws iam get-role --role-name iamrole_ammar 2>&1 | grep -q 'NoSuchEntity' \
&& echo "Role deleted." \
|| echo "Role still exists."
# Confirm the code is still present
grep -A18 'resource "aws_iam_role" "role"' /home/bob/terraform/main.tf
```
Expected — `get-role` fails with `NoSuchEntity` (role gone), and the resource block is
still present in `main.tf`.

104
terraform/task-33.md Normal file
View File

@@ -0,0 +1,104 @@
# Assignment
The Nautilus DevOps team is strategically planning the migration of a portion of their infrastructure to the AWS cloud. Acknowledging the magnitude of this endeavor, they have chosen to tackle the migration incrementally rather than as a single, massive transition. They created some services in different regions and later found that some of those can be deleted now.
Delete a VPC named devops-vpc present in us-east-1 region using Terraform. Make sure to keep the provisioning code, as we might need to provision this instance again later.
The Terraform working directory is /home/bob/terraform.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Delete VPC (Keep Code) — `devops-vpc`
Same "delete but keep the code" pattern: **destroy** the VPC while leaving its
provisioning block in `main.tf` for later reuse. A **targeted destroy** does exactly
this — it acts on the real resource and Terraform state only, never on your `.tf`
source.
## `main.tf` — leave unchanged
Do **not** delete or comment out the block. It stays exactly as given:
```hcl
resource "aws_vpc" "this" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "devops-vpc"
}
}
```
## How to run
```bash
cd /home/bob/terraform
# Destroy ONLY this VPC; leaves the code in main.tf intact.
terraform destroy -target=aws_vpc.this -auto-approve
```
## How it works
### Targeted destroy vs. the wrong approaches
| Approach | Result |
|----------|--------|
| Delete the resource block, then `apply` | VPC is destroyed, but the **code is gone** — violates "keep the provisioning code." |
| `terraform destroy` (no target) | Destroys **everything** in the config, not just this VPC. |
| `terraform destroy -target=aws_vpc.this` | Destroys **only** this VPC; code stays in `main.tf`. ✅ |
The `-target` flag scopes the operation to the single resource address
(`aws_vpc.this` — the **Terraform resource name** `this`, not the Name tag
`devops-vpc`). Terraform calls `DeleteVpc`, removes the resource from state, and
leaves your configuration untouched, because destroy operates on infrastructure and
state, never on source code.
### Why the code surviving matters
Keeping the block means the VPC is trivially re-creatable later: a plain
`terraform apply` will see it declared in config but absent from state and recreate
it with the same `10.0.0.0/16` CIDR. That satisfies "we might need to provision this
again."
### One caveat for VPCs — dependencies
`DeleteVpc` only succeeds if the VPC is **empty** of dependent resources. AWS refuses
to delete a VPC that still has subnets, internet/NAT gateways, route tables (beyond
the main one), non-default security groups, ENIs, or running instances attached — the
call fails with `DependencyViolation`.
- If those dependents are **Terraform-managed** in this same config, targeting the VPC
will normally also plan destruction of what depends on it. If not, or if the delete
errors with `DependencyViolation`, remove the dependents first (or run a full
`terraform destroy` if the whole config is just this VPC's stack).
- The **default** security group, default route table, and default NACL are deleted
automatically *with* the VPC and don't block it.
For a bare VPC like this one (just the CIDR, nothing provisioned inside it), the plain
targeted destroy works directly.
### Expected state afterward
After the destroy, the config still declares a resource that no longer exists in
state, so `terraform plan` will show Terraform wants to **create** `devops-vpc` again
(`+ 1 to add`). That's expected — **do not apply it**. The task wants the VPC deleted
with the code retained, which is exactly this state.
## Verify
```bash
aws ec2 describe-vpcs \
--filters Name=tag:Name,Values=devops-vpc \
--query 'Vpcs' --output text | grep -q . \
&& echo "VPC still exists." \
|| echo "VPC deleted."
# Confirm the code is still present
grep -A6 'resource "aws_vpc" "this"' /home/bob/terraform/main.tf
```
Expected — the describe returns nothing (VPC gone), and the resource block is still
present in `main.tf`.

89
terraform/task-34.md Normal file
View File

@@ -0,0 +1,89 @@
# Assignment
The Nautilus DevOps team is presently immersed in data migrations, transferring data from on-premise storage systems to AWS S3 buckets. They have recently received some data that they intend to copy to one of the S3 buckets.
S3 bucket named nautilus-cp-32671 already exists. Copy the file /tmp/nautilus.txt to s3 bucket nautilus-cp-32671 using Terraform. The Terraform working directory is /home/bob/terraform. Update the main.tf file (do not create a separate .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Upload File to S3 — `/tmp/nautilus.txt` → `nautilus-cp-32671`
The bucket (`aws_s3_bucket.my_bucket`) already exists in this `main.tf`. Uploading a
file is a native Terraform operation via the `aws_s3_object` resource — no CLI needed.
Append one block.
## `main.tf` (append this block; leave the existing bucket unchanged)
```hcl
resource "aws_s3_bucket" "my_bucket" {
bucket = "nautilus-cp-32671"
acl = "private"
tags = {
Name = "nautilus-cp-32671"
}
}
# Upload the local file into the bucket
resource "aws_s3_object" "nautilus_file" {
bucket = aws_s3_bucket.my_bucket.id
key = "nautilus.txt"
source = "/tmp/nautilus.txt"
etag = filemd5("/tmp/nautilus.txt")
}
```
## How to run
```bash
cd /home/bob/terraform
terraform plan # should show only the new aws_s3_object to add
terraform apply -auto-approve
```
## How it works
### `aws_s3_object`
This resource represents a single object (a file) stored in an S3 bucket. It's the
idiomatic, native way to put a file into S3 with Terraform — the `aws_s3_object` name
is the current one (it replaced the older `aws_s3_bucket_object`, which still works but
is deprecated).
- **`bucket = aws_s3_bucket.my_bucket.id`** — the target bucket, referenced by
attribute rather than hardcoded. This creates an **implicit dependency** so
Terraform ensures the bucket exists before uploading, and the plan shows only the
new object being added.
- **`key = "nautilus.txt"`** — the object key, i.e. its name/path *inside* the bucket.
A bare `nautilus.txt` places it at the bucket root. (You could use
`some/prefix/nautilus.txt` to nest it under a "folder".)
- **`source = "/tmp/nautilus.txt"`** — the path to the local file on the machine
running Terraform. Terraform reads this file and uploads its bytes. Use `source` for
file uploads; the alternative `content = "..."` is for inline string data instead of
a file.
- **`etag = filemd5("/tmp/nautilus.txt")`** — the MD5 of the local file. S3 stores an
object's ETag as its MD5, so wiring this lets Terraform detect **content changes**:
if `/tmp/nautilus.txt` is edited later, the `etag` changes and Terraform will
re-upload on the next apply. Without it, Terraform only tracks the object's
existence, not its contents.
### Why native resource over CLI
Some tasks require shelling out to the AWS CLI (e.g. syncing a whole bucket or a
force-delete). A single-file upload isn't one of them — `aws_s3_object` handles it
declaratively, tracks the object in state, and gives clean create/update/delete
lifecycle management. Reaching for `null_resource` + `local-exec` here would be
unnecessary and would put the upload outside Terraform's state tracking.
## Verify
```bash
aws s3 ls s3://nautilus-cp-32671/
aws s3api head-object --bucket nautilus-cp-32671 --key nautilus.txt \
--query '{Key:`nautilus.txt`,Size:ContentLength,ETag:ETag}'
```
Expected — `nautilus.txt` listed in the bucket, with a size matching the local file.

125
terraform/task-35.md Normal file
View File

@@ -0,0 +1,125 @@
# Assignment
The Nautilus DevOps team is automating VPC creation using Terraform to manage networking efficiently. As part of this task, they need to create a VPC with specific requirements.
For this task, create an AWS VPC using Terraform with the following requirements:
The VPC name datacenter-vpc should be stored in a variable named KKE_vpc.
The VPC should have a CIDR block of 10.0.0.0/16.
Note:
The configuration values should be stored in a variables.tf file.
The Terraform script should be structured with a main.tf file referencing variables.tf.
The Terraform working directory is /home/bob/terraform.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# VPC with Variables — `datacenter-vpc` (`KKE_vpc`)
This task requires a **two-file** Terraform structure: `variables.tf` holds the
configuration values, and `main.tf` references them. The VPC name must be stored in a
variable named exactly `KKE_vpc`.
## `variables.tf`
```hcl
variable "KKE_vpc" {
description = "Name of the VPC"
type = string
default = "datacenter-vpc"
}
variable "KKE_vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
```
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "datacenter_vpc" {
cidr_block = var.KKE_vpc_cidr
tags = {
Name = var.KKE_vpc
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Splitting config from logic
Terraform loads **every** `.tf` file in the working directory and merges them into one
configuration, so splitting declarations across files is purely organizational — there
are no imports to wire up. The convention this task asks for:
- **`variables.tf`** — declares input variables (the "what": names, CIDRs, sizes).
- **`main.tf`** — declares resources (the "how"), referencing the variables with
`var.<name>`.
This separation is standard practice: it centralizes the values people tweak, keeps
resource blocks generic and reusable, and makes the config easier to parameterize
later (per-environment `.tfvars`, overrides, etc.).
### The variables
- **`variable "KKE_vpc"`** — holds the VPC name, exactly as required. The `default`
of `datacenter-vpc` means no value needs to be passed at apply time; Terraform uses
the default.
- **`variable "KKE_vpc_cidr"`** — holds the CIDR. Putting it here (rather than
hardcoding in `main.tf`) satisfies the "configuration values should be stored in
variables.tf" note. Its default is the required `10.0.0.0/16`.
- **`type = string`** constrains each to a string, so Terraform validates the input
type at plan time.
### Referencing variables in `main.tf`
- **`cidr_block = var.KKE_vpc_cidr`** — resolves to `10.0.0.0/16`.
- **`tags = { Name = var.KKE_vpc }`** — resolves the VPC's `Name` tag to
`datacenter-vpc`. A VPC has no native name field, so the `Name` tag is what the
console and graders read as its name.
The `var.` prefix is how Terraform interpolates a declared variable's value. Because
both variables have defaults, `terraform apply` runs non-interactively; without
defaults, Terraform would prompt for the values (or you'd pass `-var` / a `.tfvars`
file).
## Verify
```bash
aws ec2 describe-vpcs \
--filters Name=tag:Name,Values=datacenter-vpc \
--query 'Vpcs[0].{Id:VpcId,Cidr:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value}'
```
Expected — the VPC ID, `CidrBlock: 10.0.0.0/16`, and `Name: datacenter-vpc`.

116
terraform/task-36.md Normal file
View File

@@ -0,0 +1,116 @@
# Assignment
The Nautilus DevOps team is enhancing infrastructure automation and needs to provision a Security Group using Terraform with specific configurations.
For this task, create an AWS Security Group using Terraform with the following requirements:
The Security Group name xfusion-sg should be stored in a variable named KKE_sg.
Note:
1. The configuration values should be stored in a variables.tf file.
2. The Terraform script should be structured with a main.tf file referencing variables.tf.
The Terraform working directory is /home/bob/terraform.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Security Group with Variables — `xfusion-sg` (`KKE_sg`)
Two-file structure again: `variables.tf` holds the config, `main.tf` references it. The
Security Group name must be stored in a variable named exactly `KKE_sg`.
## `variables.tf`
```hcl
variable "KKE_sg" {
description = "Name of the Security Group"
type = string
default = "xfusion-sg"
}
```
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Default VPC to place the security group in
data "aws_vpc" "default" {
default = true
}
resource "aws_security_group" "xfusion_sg" {
name = var.KKE_sg
description = "Security group xfusion-sg managed by Terraform"
vpc_id = data.aws_vpc.default.id
tags = {
Name = var.KKE_sg
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Splitting config from logic
Terraform merges every `.tf` file in the working directory into one configuration, so
`variables.tf` and `main.tf` act as a single unit — no imports needed. The convention:
`variables.tf` declares inputs, `main.tf` declares resources that consume them via
`var.<name>`.
### The variable
- **`variable "KKE_sg"`** — holds the Security Group name, exactly as required. The
`default` of `xfusion-sg` lets `terraform apply` run without prompting for a value.
- **`type = string`** validates the input type at plan time.
### The Security Group
- **`name = var.KKE_sg`** — resolves to `xfusion-sg`. The `var.` prefix interpolates
the variable's value.
- **`description`** — **required** by AWS on every security group. If you omit it, the
provider defaults it to "Managed by Terraform"; it's set explicitly here for clarity.
Note the description is **immutable** — AWS won't let you change it after creation, so
it must be right the first time.
- **`vpc_id = data.aws_vpc.default.id`** — places the SG in the account's default VPC.
The `data "aws_vpc" "default"` block reads the existing default VPC without managing
it. (If `vpc_id` is omitted entirely, the SG lands in the default VPC anyway, but
wiring it explicitly is clearer and deterministic.)
- **No `ingress`/`egress` blocks** — the task specifies no rules, so none are defined.
Be aware that when Terraform manages a security group with no `egress` block, it
**removes** the default allow-all outbound rule, leaving the SG with no rules at all.
That matches the task (which asks only for the named SG to exist); add an `egress`
block if outbound traffic is later needed.
## Verify
```bash
aws ec2 describe-security-groups \
--filters Name=group-name,Values=xfusion-sg \
--query 'SecurityGroups[0].{Name:GroupName,Desc:Description,Vpc:VpcId,Id:GroupId}'
```
Expected — `GroupName: xfusion-sg`, the description, the default VPC ID, and a
generated group ID.

104
terraform/task-37.md Normal file
View File

@@ -0,0 +1,104 @@
# Assignment
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. As part of this phased migration approach, they need to allocate an Elastic IP address to support external access for specific workloads.
For this task, create an AWS Elastic IP using Terraform with the following requirement:
The Elastic IP name xfusion-eip should be stored in a variable named KKE_eip. The Terraform working directory is /home/bob/terraform.
Note:
The configuration values should be stored in a variables.tf file.
The Terraform script should be structured with a main.tf file referencing variables.tf.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Elastic IP with Variables — `xfusion-eip` (`KKE_eip`)
Two-file structure: `variables.tf` holds the config, `main.tf` references it. The
Elastic IP name must be stored in a variable named exactly `KKE_eip`.
## `variables.tf`
```hcl
variable "KKE_eip" {
description = "Name of the Elastic IP"
type = string
default = "xfusion-eip"
}
```
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_eip" "xfusion_eip" {
domain = "vpc"
tags = {
Name = var.KKE_eip
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Splitting config from logic
Terraform merges every `.tf` file in the directory into one configuration, so
`variables.tf` and `main.tf` work as a single unit with no imports. `variables.tf`
declares inputs; `main.tf` declares resources that consume them via `var.<name>`.
### The variable
- **`variable "KKE_eip"`** — holds the Elastic IP name, exactly as required. The
`default` of `xfusion-eip` lets `terraform apply` run without prompting.
- **`type = string`** validates the input type at plan time.
### The Elastic IP
- **`domain = "vpc"`** — allocates the EIP for use in a VPC. This is the modern
argument; the old `vpc = true` was removed in AWS provider v5+, so on `~> 6.0` you
must use `domain`. Since the task only asks to *allocate* the address (no instance or
ENI association), no `instance` or `network_interface` argument is set.
- **`tags = { Name = var.KKE_eip }`** — an Elastic IP has no native name field, so the
`Name` tag (resolved from the variable to `xfusion-eip`) is what the console and
graders read as its name.
The `var.` prefix interpolates the declared variable's value. Because the variable has
a default, `terraform apply` runs non-interactively.
> Cost note: an unassociated Elastic IP incurs an hourly charge, but that's the
> sandbox's concern — the task only requires the allocation to exist.
## Verify
```bash
aws ec2 describe-addresses \
--filters Name=tag:Name,Values=xfusion-eip \
--query 'Addresses[0].{IP:PublicIp,AllocId:AllocationId,Name:Tags[?Key==`Name`]|[0].Value}'
```
Expected — a public IP, an allocation ID, and `Name: xfusion-eip`.

103
terraform/task-38.md Normal file
View File

@@ -0,0 +1,103 @@
# Assignment
The Nautilus DevOps team is automating IAM user creation using Terraform for better identity management.
For this task, create an AWS IAM User using Terraform with the following requirements:
The IAM User name iamuser_mark should be stored in a variable named KKE_user.
Note:
1. The configuration values should be stored in a variables.tf file.
2. The Terraform script should be structured with a main.tf file referencing variables.tf.
The Terraform working directory is /home/bob/terraform.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# IAM User with Variables — `iamuser_mark` (`KKE_user`)
Two-file structure: `variables.tf` holds the config, `main.tf` references it. The IAM
User name must be stored in a variable named exactly `KKE_user`.
## `variables.tf`
```hcl
variable "KKE_user" {
description = "Name of the IAM user"
type = string
default = "iamuser_mark"
}
```
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "mark" {
name = var.KKE_user
tags = {
Name = var.KKE_user
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Splitting config from logic
Terraform merges every `.tf` file in the directory into one configuration, so
`variables.tf` and `main.tf` act as a single unit — no imports needed. `variables.tf`
declares inputs; `main.tf` declares resources that consume them via `var.<name>`.
### The variable
- **`variable "KKE_user"`** — holds the IAM user name, exactly as required. The
`default` of `iamuser_mark` lets `terraform apply` run without prompting.
- **`type = string`** validates the input type at plan time.
### The IAM user
- **`name = var.KKE_user`** — resolves to `iamuser_mark`. The `var.` prefix
interpolates the variable's value.
- **`iamuser_` prefix matters.** These locked-down sandboxes require IAM user names to
start with `iamuser_`. The variable's value already satisfies that, so the create
stays inside the login user's permission scope and won't be denied.
- **IAM is global.** The `region` in the provider is needed to initialize and
authenticate, but IAM resources aren't regional — the user is visible account-wide.
- **No policies, keys, or login profile.** The task asks only for the user to exist, so
nothing is attached. This keeps the config minimal and avoids the custom-policy /
attach restrictions these sandboxes enforce (attaching permissions broader than the
login user has would fail with `AccessDenied`).
## Verify
```bash
aws iam get-user --user-name iamuser_mark \
--query 'User.{Name:UserName,Id:UserId,Arn:Arn}'
```
Expected — the user's name, a unique ID, and an ARN of the form
`arn:aws:iam::<account-id>:user/iamuser_mark`.

125
terraform/task-39.md Normal file
View File

@@ -0,0 +1,125 @@
# Assignment
The Nautilus DevOps team is automating IAM role creation using Terraform to streamline permissions management. As part of this task, they need to create an IAM role with specific requirements.
For this task, create an AWS IAM role using Terraform with the following requirements:
The IAM role name iamrole_rose should be stored in a variable named KKE_iamrole.
Note:
1. The configuration values should be stored in a variables.tf file.
2. The Terraform script should be structured with a main.tf file referencing variables.tf.
The Terraform working directory is /home/bob/terraform.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# IAM Role with Variables — `iamrole_rose` (`KKE_iamrole`)
Two-file structure: `variables.tf` holds the config, `main.tf` references it. The IAM
role name must be stored in a variable named exactly `KKE_iamrole`.
## `variables.tf`
```hcl
variable "KKE_iamrole" {
description = "Name of the IAM role"
type = string
default = "iamrole_rose"
}
```
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_role" "rose" {
name = var.KKE_iamrole
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = {
Name = var.KKE_iamrole
}
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Splitting config from logic
Terraform merges every `.tf` file in the directory into one configuration, so
`variables.tf` and `main.tf` act as a single unit — no imports. `variables.tf` declares
inputs; `main.tf` declares resources that consume them via `var.<name>`.
### The variable
- **`variable "KKE_iamrole"`** — holds the IAM role name, exactly as required. The
`default` of `iamrole_rose` lets `terraform apply` run without prompting.
- **`type = string`** validates the input type at plan time.
### The IAM role
- **`name = var.KKE_iamrole`** — resolves to `iamrole_rose` via the `var.` prefix.
- **`assume_role_policy` is mandatory.** Unlike a user or group, an IAM role
**requires** a trust policy at creation — it defines *who* (which principal) is
allowed to assume the role. Terraform's `aws_iam_role` will error without it. This
one trusts the EC2 service (`ec2.amazonaws.com`) to assume the role via
`sts:AssumeRole`, which is the standard trust for a role you'd attach to EC2
instances. Any valid trust works; EC2 is a sensible, common default.
- **`jsonencode({...})`** builds the trust-policy JSON from an HCL object, keeping it
readable and correctly escaped rather than hand-writing a raw JSON string.
- **No permission policies attached.** The trust policy governs *who can assume* the
role; it grants no AWS permissions itself. The task only asks for the role to exist,
so no `aws_iam_role_policy_attachment` is added — which also keeps the config within
the sandbox's IAM restrictions (attaching a broad policy could trigger
`AccessDenied`).
- **IAM is global.** The provider `region` is only for auth; the role is account-wide.
## Verify
```bash
aws iam get-role --role-name iamrole_rose \
--query 'Role.{Name:RoleName,Id:RoleId,Arn:Arn,Trust:AssumeRolePolicyDocument}'
```
Expected — the role name, a unique ID, an ARN of the form
`arn:aws:iam::<account-id>:role/iamrole_rose`, and the trust policy allowing
`ec2.amazonaws.com` to assume it.

136
terraform/task-40.md Normal file
View File

@@ -0,0 +1,136 @@
# Assignment
The Nautilus DevOps team is automating IAM policy creation using Terraform to enhance security and access management. As part of this task, they need to create an IAM policy with specific requirements.
For this task, create an AWS IAM policy using Terraform with the following requirements:
The IAM policy name iampolicy_javed should be stored in a variable named KKE_iampolicy.
Note:
The configuration values should be stored in a variables.tf file.
The Terraform script should be structured with a main.tf file referencing variables.tf.
The Terraform working directory is /home/bob/terraform.
Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# IAM Policy with Variables — `iampolicy_javed` (`KKE_iampolicy`)
Two-file structure: `variables.tf` holds the config, `main.tf` references it. The IAM
policy name must be stored in a variable named exactly `KKE_iampolicy`.
## `variables.tf`
```hcl
variable "KKE_iampolicy" {
description = "Name of the IAM policy"
type = string
default = "iampolicy_javed"
}
```
## `main.tf`
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_policy" "javed" {
name = var.KKE_iampolicy
description = "IAM policy managed by Terraform"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["ec2:Describe*"]
Resource = "*"
}
]
})
}
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### Splitting config from logic
Terraform merges every `.tf` file in the directory into one configuration, so
`variables.tf` and `main.tf` act as a single unit — no imports. `variables.tf` declares
inputs; `main.tf` declares resources that consume them via `var.<name>`.
### The variable
- **`variable "KKE_iampolicy"`** — holds the IAM policy name, exactly as required. The
`default` of `iampolicy_javed` lets `terraform apply` run without prompting.
- **`type = string`** validates the input type at plan time.
### The IAM policy
- **`name = var.KKE_iampolicy`** — resolves to `iampolicy_javed` via the `var.` prefix.
This creates a **customer-managed policy** — a standalone, reusable permission
document with its own ARN.
- **`policy` is mandatory.** An IAM policy is defined by its JSON document, so the
`policy` argument is required. The task doesn't specify what the policy should
grant, so a minimal, safe statement is used: `ec2:Describe*` (read-only) on all
resources. Read-only actions stay within the login user's permission scope, which
avoids the anti-privilege-escalation restrictions these sandboxes enforce — granting
actions broader than your own would fail with `AccessDenied`.
- **`jsonencode({...})`** builds the policy JSON from an HCL object — readable and
correctly escaped, and validated by Terraform at plan time.
- **`Version = "2012-10-17"`** is the current IAM policy language version — always this
literal date, not today's date.
- **`Resource = "*"`** — `ec2:Describe*` actions are account-wide list/read operations
that don't support resource-level scoping, so `*` is the correct value.
- **Policy created, not attached.** The task only asks for the policy to exist, so it
isn't attached to any user, group, or role. Attachment is a separate resource
(`aws_iam_user_policy_attachment` etc.) added when needed.
### Sandbox note
Creating a customer-managed policy uses `iam:CreatePolicy`, which is restricted in some
lab environments. Because this task explicitly requires a named custom policy, the lab
provisions that permission for this scenario, so the create succeeds. Keeping the
document read-only further ensures it stays within the allowed scope.
## Verify
```bash
POLICY_ARN=$(aws iam list-policies --scope Local \
--query "Policies[?PolicyName=='iampolicy_javed'].Arn | [0]" --output text)
echo "[$POLICY_ARN]"
aws iam get-policy-version --policy-arn "$POLICY_ARN" --version-id v1 \
--query 'PolicyVersion.Document'
```
Expected — a local policy ARN for `iampolicy_javed`, and its document showing the
`ec2:Describe*` allow statement.