Terraform Practice Exercises
Hands-on exercises to reinforce Terraform concepts for MLOps
These exercises align with the basics and examples to provide practical, hands-on experience. Each exercise includes objectives, requirements, hints, and references to relevant examples.
Exercise Roadmap
┌─────────────────────────────────────────────────────────────────────┐
│ Terraform Learning Path │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Basics Examples Exercises │
│ ────── ──────── ───────── │
│ │
│ ✓ Providers data-sources → Exercise 1: Data Sources │
│ ✓ Resources for-each → Exercise 2: For Each │
│ ✓ Variables locals → Exercise 3: Locals │
│ ✓ Outputs modules → Exercise 4: Modules │
│ ✓ Basic workflow conditionals → Exercise 5: Conditionals │
│ lifecycle → Exercise 6: Lifecycle │
│ remote-state → Exercise 7: Remote State │
│ │
└─────────────────────────────────────────────────────────────────────┘Exercise 1: Query AWS Resources with Data Sources
Prerequisite: Complete basics/ and review data-sources example
Objectives
- Use data sources to query existing AWS infrastructure
- Reference data source outputs in new resources
- Filter resources by tags and attributes
Requirements
Create a Terraform configuration that:
Query existing infrastructure:
- Get AWS caller identity (account ID, region, ARN)
- Find the default VPC
- Query the latest Amazon Linux 2023 AMI
- Get all subnets in the default VPC
Create a security group:
- Place it in the default VPC (from data source)
- Allow SSH (port 22) from your IP
- Allow HTTPS (port 443) from anywhere
- Use tags for identification
Create an S3 bucket:
- Use a unique prefix with your account ID
- Include the region in tags
Outputs:
- Account ID and region
- Default VPC ID and CIDR
- Latest AMI ID
- Security group ID
- S3 bucket name and ARN
Acceptance Criteria
- [ ]
terraform validatepasses without errors - [ ]
terraform planshows correct data source queries - [ ] Security group is created in the default VPC
- [ ] All outputs display correct information
- [ ] Configuration works in any AWS account
Starter Template
# providers.tf
terraform {
required_version = ">= 1.14.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "my_ip_address" {
description = "Your IP address for SSH access (CIDR notation)"
type = string
default = "0.0.0.0/0"
}
# TODO: Add main.tf with data sources and resources
# TODO: Add outputs.tf with all required outputsHints
Click to reveal hints
Data sources needed:
data "aws_caller_identity" "current"data "aws_region" "current"data "aws_vpc" "default"withdefault = truedata "aws_ami"with filters for Amazon Linux 2023data "aws_subnets"with VPC filter
Security group reference:
- Use
data.aws_vpc.default.idfor the VPC
- Use
AMIs filter:
owners = ["amazon"]- Filter by name pattern:
al2023-ami-2023.*-x86_64
Outputs:
- Reference data source values like
data.aws_caller_identity.current.account_id
- Reference data source values like
Solution Reference
See data-sources example for similar patterns.
Exercise 2: Multi-Environment S3 Buckets with for_each
Prerequisite: Review for-each example
Objectives
- Use
for_eachto create multiple resources from a map - Use
countto create resources from a list - Access resource instances with indices and keys
Requirements
Create a Terraform configuration that:
Create environment buckets with
for_each:- Define a map with environments: dev, staging, prod
- Each environment has a lifecycle retention period (dev: 30, staging: 90, prod: 365 days)
- Each bucket should have:
- Name pattern:
{project}-{env}-ml-data - Environment-specific tag
- Appropriate lifecycle rule
- Name pattern:
Create model type buckets with
count:- Create buckets for: ["tensorflow", "pytorch", "sklearn", "xgboost"]
- Each bucket name:
{project}-models-{type}
Create a models bucket with dynamic blocks:
- Use
for_eachto add lifecycle rules for different prefix transitions - Rules: training-data → IA after 30 days, artifacts → Glacier after 90 days
- Use
Outputs:
- All environment buckets as a map
- All model type buckets as a list
- Models bucket with all lifecycle rules
Acceptance Criteria
- [ ] Creates exactly 3 environment buckets
- [ ] Creates exactly 4 model type buckets
- [ ] Each bucket has correct naming and tags
- [ ] Lifecycle rules are properly configured
- [ ] Outputs show all resources correctly
Starter Template
# variables.tf
variable "project_name" {
description = "Project name for resource naming"
type = string
default = "mlops-training"
}
variable "environments" {
description = "Environment configurations"
type = map(object({
retention_days = number
}))
default = {
dev = {
retention_days = 30
}
staging = {
retention_days = 90
}
prod = {
retention_days = 365
}
}
}
variable "model_types" {
description = "ML model types"
type = list(string)
default = ["tensorflow", "pytorch", "sklearn", "xgboost"]
}
# TODO: Add main.tf with for_each and count resources
# TODO: Add outputs.tfHints
Click to reveal hints
For-each with map:
hclresource "aws_s3_bucket" "env_buckets" { for_each = var.environments bucket_prefix = "${var.project_name}-${each.key}-ml-data-" }Count with list:
hclresource "aws_s3_bucket" "model_buckets" { count = length(var.model_types) bucket_prefix = "${var.project_name}-models-${var.model_types[count.index]}-" }Dynamic blocks:
hcldynamic "rule" { for_each = var.lifecycle_rules content { id = rule.value.id status = "Enabled" # ... rule configuration } }Accessing resources:
aws_s3_bucket.env_buckets["dev"].idaws_s3_bucket.model_buckets[0].id
Solution Reference
See for-each example for similar patterns.
Exercise 3: ML Infrastructure with Locals
Prerequisite: Review locals example
Objectives
- Use locals for reusable values and expressions
- Create naming conventions with locals
- Build complex tag combinations
- Transform and filter data with locals
Requirements
Create an ML infrastructure configuration that:
Define local values:
name_prefix: Combines project and environmentcommon_tags: Standard tags (Project, Environment, ManagedBy)bucket_names: Map of bucket purposes to nameslifecycle_rules: Common lifecycle configurations
Create resources using locals:
- S3 buckets for models, data, and artifacts
- Security group for ML inference
- CloudWatch log groups
Use local expressions:
- String interpolation for naming
- Merge function for combining tags
- Conditional expressions for environment differences
Outputs:
- All resource identifiers
- Combined information using local expressions
Acceptance Criteria
- [ ] All locals are properly defined and used
- [ ] Resources follow consistent naming convention
- [ ] Tags are applied correctly using merge()
- [ ] No hardcoded values in resources
- [ ] Configuration is DRY (Don't Repeat Yourself)
Starter Template
# TODO: Define locals for:
# - name_prefix
# - common_tags
# - bucket_configs
# - lifecycle_rules
# - environment_overrides
locals {
# Your locals here
}
# TODO: Create resources using locals
resource "aws_s3_bucket" "models" {
bucket_prefix = local.name_prefix
# Use local configs
}Hints
Click to reveal hints
Name prefix:
hcllocals { name_prefix = "${var.project_name}-${var.environment}" }Merging tags:
hclresource "aws_s3_bucket" "example" { tags = merge(local.common_tags, { Purpose = "ml-models" }) }Bucket configurations:
hcllocals { bucket_configs = { models = { prefix = "ml-models" versioning = true } data = { prefix = "ml-data" versioning = true } } }Conditional locals:
hcllocals { is_production = var.environment == "prod" retention_days = local.is_production ? 365 : 90 }
Solution Reference
See locals example for similar patterns.
Exercise 4: Build a Reusable ML Storage Module
Prerequisite: Review modules example
Objectives
- Structure a Terraform module properly
- Define module inputs and outputs
- Use a module to create resources
- Understand module composition
Requirements
Part 1: Create the Module
Create a module at modules/ml-storage/ with:
Inputs (variables.tf):
project_name(string, required)environment(string, required)bucket_prefix(string, required)enable_versioning(bool, default: true)enable_lifecycle_rules(bool, default: false)retention_days(number, default: 90)custom_tags(map, default: {})
Resources (main.tf):
- S3 bucket with unique naming
- Optional versioning configuration
- Optional lifecycle rules
- Block public access enabled
- Server-side encryption
Outputs (outputs.tf):
bucket_name- Bucket name/IDbucket_arn- Bucket ARNbucket_region- Bucket regionversioning_enabled- Whether versioning is enabled
Part 2: Use the Module
Create a root configuration that:
Uses the module to create three buckets:
- Models (with versioning and lifecycle)
- Data (with versioning, no lifecycle)
- Artifacts (no versioning, with lifecycle)
Outputs combined information about all buckets
Acceptance Criteria
- [ ] Module structure follows best practices
- [ ] All variables have descriptions and types
- [ ] Module works independently
- [ ] Root configuration creates all buckets
- [ ] Outputs display correct information
Module Structure
exercise-4/
├── main.tf # Root configuration using modules
├── variables.tf # Root variables
├── outputs.tf # Root outputs
└── modules/
└── ml-storage/
├── main.tf # Module resources
├── variables.tf # Module inputs
├── outputs.tf # Module outputs
└── README.md # Module documentationStarter Template
# modules/ml-storage/variables.tf
variable "project_name" {
description = "Project name"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
# TODO: Add remaining variables
# modules/ml-storage/main.tf
resource "aws_s3_bucket" "storage" {
# TODO: Configure bucket using variables
}
# TODO: Add optional resources based on variables
# modules/ml-storage/outputs.tf
output "bucket_name" {
value = aws_s3_bucket.storage.id
}
# TODO: Add remaining outputsHints
Click to reveal hints
Module usage:
hclmodule "ml_models" { source = "./modules/ml-storage" project_name = var.project_name environment = var.environment bucket_prefix = "ml-models" enable_versioning = true enable_lifecycle_rules = true }Conditional resources:
hclresource "aws_s3_bucket_versioning" "this" { count = var.enable_versioning ? 1 : 0 bucket = aws_s3_bucket.storage.id # ... }Merging tags:
hcllocals { default_tags = { Project = var.project_name Environment = var.environment } } tags = merge(local.default_tags, var.custom_tags)Module outputs:
hcloutput "bucket_name" { value = aws_s3_bucket.storage.id description = "S3 bucket name" }
Solution Reference
See modules example for complete module patterns.
Exercise 5: Environment-Specific Configurations with Conditionals
Prerequisite: Review conditionals example
Objectives
- Use ternary operators for value selection
- Conditionally create resources with
count - Filter resources with
for_eachconditionals - Use dynamic blocks with conditionals
Requirements
Ternary Operators:
- Instance type:
t3.xlargefor prod,t3.mediumotherwise - Compliance level:
highfor prod,standardotherwise - Retention days: 365 for prod, 90 for staging, 30 for dev
- Instance type:
Conditional Resource Creation:
- Versioning: only for prod/staging
- KMS encryption: only for prod
- Requester pays: only for dev environment
Filtered For-Each:
- Create monitoring buckets only where
monitoring.enabled == true - Filter model types based on environment
- Create monitoring buckets only where
Dynamic Blocks:
- Add grant configurations only when specified
- Add lifecycle rules based on conditions
Acceptance Criteria
- [ ] Ternary operators select correct values per environment
- [ ] Conditional resources create/don't create as expected
- [ ] Filtered for-each creates only matching items
- [ ] Dynamic blocks conditionally iterate
- [ ] Configuration works for all environments
Starter Template
variable "environment" {
description = "Environment (dev, staging, prod)"
type = string
default = "dev"
}
variable "model_types" {
description = "Model types with creation flags"
type = map(object({
create = bool
size_gb = number
}))
default = {
tensorflow = { create = true, size_gb = 500 }
pytorch = { create = true, size_gb = 200 }
sklearn = { create = false, size_gb = 10 }
}
}
# TODO: Use conditionals throughoutHints
Click to reveal hints
Ternary operator:
hcllocals { instance_type = var.environment == "prod" ? "t3.xlarge" : "t3.medium" }Conditional count:
hclresource "aws_s3_bucket_versioning" "this" { count = var.environment != "dev" ? 1 : 0 # ... }Filtered for-each:
hclresource "aws_s3_bucket" "filtered" { for_each = { for k, v in var.model_types : k => v if v.create == true } # ... }Dynamic with condition:
hcldynamic "grant" { for_each = var.enable_grants ? var.grants : [] content { # ... grant configuration } }
Solution Reference
See conditionals example for similar patterns.
Exercise 6: Safe ML Infrastructure with Lifecycle Rules
Prerequisite: Review lifecycle example
Objectives
- Use
create_before_destroyfor zero-downtime updates - Use
prevent_destroyto protect critical resources - Use
ignore_changesto handle dynamic updates - Use
replace_triggered_byfor dependency-driven replacement
Requirements
Zero-Downtime Updates:
- Security group for ML inference (create_before_destroy)
- IAM role for Lambda execution (create_before_destroy)
Critical Resource Protection:
- Production models bucket with
prevent_destroy - Only apply when environment is "prod"
- Production models bucket with
Ignore External Changes:
- Artifacts bucket ignores tag changes from ML pipeline
- Training log group ignores retention changes
Conditional Replacement:
- Model cache bucket replacement triggers config bucket replacement
- Model version variable changes trigger model storage replacement
Acceptance Criteria
- [ ] Security groups update without downtime
- [ ] Production bucket is protected from deletion
- [ ] Tag changes don't cause drift detection
- [ ] Dependencies trigger proper replacements
- [ ] Configuration works across environments
Starter Template
variable "environment" {
description = "Environment (dev, staging, prod)"
type = string
default = "dev"
}
variable "model_version" {
description = "Current model version"
type = string
default = "v1.0"
}
# TODO: Add lifecycle rules to resourcesHints
Click to reveal hints
create_before_destroy:
hclresource "aws_security_group" "ml_inference" { lifecycle { create_before_destroy = true } }prevent_destroy:
hclresource "aws_s3_bucket" "prod_models" { count = var.environment == "prod" ? 1 : 0 lifecycle { prevent_destroy = true } }ignore_changes:
hcllifecycle { ignore_changes = [ tags, tags["LastModified"], tags["PipelineRunID"] ] }replace_triggered_by:
hclresource "aws_s3_bucket" "dependent" { lifecycle { replace_triggered_by = [ aws_s3_bucket.trigger.id ] } }
Solution Reference
See lifecycle example for complete patterns.
Exercise 7: Configure Remote State with S3 Backend
Prerequisite: Review remote-state example
Objectives
- Configure S3 backend for remote state storage
- Enable state locking with modern lock file
- Support both AWS and LocalStack
- Handle state encryption and versioning
Requirements
S3 Backend Configuration:
- Create an S3 bucket for state storage
- Enable versioning on the state bucket
- Enable default encryption
- Configure backend in
terraformblock
State Locking:
- Enable
use_lockfile = true(modern approach) - No DynamoDB table needed
- Enable
Dual Support:
- Support both AWS and LocalStack
- Use variables to switch between providers
- Conditional backend configuration
State Management:
- Create a state management guide
- Document state migration from local to remote
- Document state recovery procedures
Acceptance Criteria
- [ ] S3 bucket is created for state storage
- [ ] Backend configuration works with AWS
- [ ] Backend configuration works with LocalStack
- [ ] State persists across runs
- [ ] Lock file prevents concurrent modifications
Starter Template
terraform {
required_version = ">= 1.14.0"
backend "s3" {
# TODO: Configure backend
# Note: Some values must use -backend-config
# bucket = "..."
# key = "..."
# region = "..."
# encrypt = true
# use_lockfile = true
}
}
# TODO: Create state bucket resource
# TODO: Configure provider with LocalStack supportHints
Click to reveal hints
Backend with variables:
bashterraform init \ -backend-config="bucket=${STATE_BUCKET}" \ -backend-config="key=mlops-training/terraform.tfstate" \ -backend-config="region=us-east-1"State bucket resource:
hclresource "aws_s3_bucket" "terraform_state" { bucket_prefix = "terraform-state-" } resource "aws_s3_bucket_versioning" "state" { bucket = aws_s3_bucket.terraform_state.id versioning_configuration { status = "Enabled" } }LocalStack provider:
hclprovider "aws" { region = var.use_localstack ? var.localstack_region : var.aws_region endpoints = var.use_localstack ? { s3 = var.localstack_endpoint } : {} }State migration:
bash# Migrate local state to remote terraform init -migrate-state
Solution Reference
See remote-state example for complete configuration.
General Guidelines
Before Starting an Exercise
- Complete the related basics section and example
- Ensure Terraform is installed:
terraform version - Ensure AWS credentials are configured:
aws sts get-caller-identity - Create a new directory for each exercise
- Always start with
terraform planbeforeterraform apply
During an Exercise
- Organize code into:
main.tf,variables.tf,outputs.tf - Run
terraform validatefrequently - Run
terraform fmtto keep code clean - Use descriptive names for resources and variables
- Add comments explaining complex logic
After Completing an Exercise
- Always clean up:
terraform destroy - Verify resources are deleted in AWS Console
- Review what you learned
- Try variations of the exercise
- Move to the next exercise
Troubleshooting
| Error | Solution |
|---|---|
configuration conflicts | Run terraform fmt and check for duplicate blocks |
Failed to load provider | Run terraform init |
Invalid for_each argument | Ensure your map has unique keys |
Invalid count argument | Ensure count value is known at apply time |
Resource already managed | Check state with terraform show |
Backend configuration changed | Run terraform init -migrate-state |
Credentials error | Verify AWS credentials with aws sts get-caller-identity |
Progress Tracking
Track your progress:
| Exercise | Status | Completed Date |
|---|---|---|
| Exercise 1: Data Sources | Not Started | |
| Exercise 2: For Each | Not Started | |
| Exercise 3: Locals | Not Started | |
| Exercise 4: Modules | Not Started | |
| Exercise 5: Conditionals | Not Started | |
| Exercise 6: Lifecycle | Not Started | |
| Exercise 7: Remote State | Not Started |
Next Steps
After completing these exercises:
- Build your own ML infrastructure from scratch
- Explore CI/CD integration with Terraform
- Learn about Terraform Cloud/Enterprise for team collaboration
- Study advanced topics: workspaces, state operations, testing
Need Help? Refer to: