Skip to content

Sample — Medium project environment

First PublishedLast UpdatedByAtif Alam

This page shows a medium project layout: separate directories per environment (dev, staging, prod) that share the same modules. Each environment has its own state, backend config, and variable values.

Typical exercise scope: “Spin up an AWS EC2 instance with an attached security group, in a non-default VPC, in us-east-1.” This sample supports that: the vpc module creates a dedicated (non-default) VPC and subnets; the app module creates an EC2 instance and security group in that VPC. Region is set via variables (e.g. us-east-1 in terraform.tfvars). No hardcoded region or CIDRs in resource blocks — all parameterized so you can judge modularization, variable use, and reuse across environments.

terraform/
modules/
vpc/
main.tf
variables.tf
outputs.tf
app/
main.tf
variables.tf
outputs.tf
environments/
dev/
main.tf
variables.tf
terraform.tfvars
backend.tf
staging/
...
prod/
...

Typical contents for one environment (e.g. environments/dev/).

Provider and module calls; pass env-specific variables into shared modules.

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
module "vpc" {
source = "../../modules/vpc"
vpc_cidr = var.vpc_cidr
environment = var.environment
public_subnets = var.public_subnets
}
module "app" {
source = "../../modules/app"
environment = var.environment
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
instance_type = var.instance_type
}

Root module inputs. Each environment can use the same variable definitions and only change .tfvars.

variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
}
variable "aws_region" {
description = "AWS region"
type = string
}
variable "vpc_cidr" {
description = "CIDR for the VPC"
type = string
}
variable "public_subnets" {
description = "List of public subnet CIDRs"
type = list(string)
}
variable "instance_type" {
description = "EC2 instance type for app servers"
type = string
}

Dev-specific values. Do not put secrets here; use environment variables or a secret store for sensitive values.

environment = "dev"
aws_region = "us-east-1"
vpc_cidr = "10.0.0.0/16"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
instance_type = "t3.micro"

Remote state so each environment has its own state file. Use a different key per environment (e.g. environments/prod/terraform.tfstate for prod).

terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "environments/dev/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}

From environments/dev/ run:

Terminal window
terraform init
terraform plan
terraform apply

Staging and prod use the same structure under environments/staging/ and environments/prod/ with different terraform.tfvars and backend key (e.g. environments/prod/terraform.tfstate).


The app module is expected to create at least an EC2 instance and a security group attached to it, in the VPC and subnets provided by the vpc module. Example shape (actual implementation lives in modules/app/):

# modules/app/main.tf (conceptual)
resource "aws_security_group" "app" {
name_prefix = "${var.environment}-app-"
vpc_id = var.vpc_id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.ingress_cidr] # variable, not 0.0.0.0/0
}
egress { from_port = 0; to_port = 0; protocol = "-1"; cidr_blocks = ["0.0.0.0/0"] }
}
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.subnet_ids[0]
vpc_security_group_ids = [aws_security_group.app.id]
tags = { Name = "${var.environment}-app", Environment = var.environment }
}

Using a variable for ingress CIDR (e.g. var.ingress_cidr or a list of allowed prefixes) instead of hardcoding 0.0.0.0/0 avoids overly permissive security groups — a common review point. For more on tooling that flags this, see Best Practices — Testing and policy.