Multi-Environment Setup

Manage dev, staging, and production environments with shared modules and cross-environment planning

advanced 35 minutes

Prerequisites

  • Terry-Form MCP installed and configured
  • Completed the "Module Development" tutorial
  • Understanding of Terraform modules and workspaces

Multi-Environment Setup

This tutorial shows you how to structure a multi-environment Terraform project with shared modules, environment-specific configurations, and cross-environment validation using Terry-Form MCP.

What You’ll Learn

  • How to structure a multi-environment project
  • How to share modules across environments
  • How to use per-environment variable files
  • How to validate and plan all environments
  • How to compare plans across environments

Architecture

graph TB
    subgraph "Shared Modules"
        M1[modules/vpc]
        M2[modules/security-group]
        M3[modules/compute]
    end

    subgraph "Environments"
        subgraph "dev"
            D1[main.tf] --> M1
            D1 --> M2
            D1 --> M3
            D2[terraform.tfvars]
        end

        subgraph "staging"
            S1[main.tf] --> M1
            S1 --> M2
            S1 --> M3
            S2[terraform.tfvars]
        end

        subgraph "prod"
            P1[main.tf] --> M1
            P1 --> M2
            P1 --> M3
            P2[terraform.tfvars]
        end
    end

Step 1: Project Structure

Create the workspace layout:

workspace/
└── multi-env/
    ├── modules/
    │   ├── vpc/
    │   │   ├── main.tf
    │   │   ├── variables.tf
    │   │   └── outputs.tf
    │   ├── security-group/
    │   │   ├── main.tf
    │   │   ├── variables.tf
    │   │   └── outputs.tf
    │   └── compute/
    │       ├── main.tf
    │       ├── variables.tf
    │       └── outputs.tf
    └── environments/
        ├── dev/
        │   ├── main.tf
        │   ├── variables.tf
        │   ├── outputs.tf
        │   └── terraform.tfvars
        ├── staging/
        │   ├── main.tf
        │   ├── variables.tf
        │   ├── outputs.tf
        │   └── terraform.tfvars
        └── prod/
            ├── main.tf
            ├── variables.tf
            ├── outputs.tf
            └── terraform.tfvars

Step 2: Create the VPC Module

If you completed the Module Development tutorial, you already have a VPC module. Otherwise, create one:

modules/vpc/variables.tf:

variable "name" {
  description = "Name prefix for VPC resources"
  type        = string
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
}

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
}

variable "public_subnet_cidrs" {
  description = "CIDR blocks for public subnets"
  type        = list(string)
}

variable "private_subnet_cidrs" {
  description = "CIDR blocks for private subnets"
  type        = list(string)
}

variable "enable_nat_gateway" {
  description = "Whether to create a NAT gateway"
  type        = bool
  default     = false
}

variable "tags" {
  description = "Additional tags for all resources"
  type        = map(string)
  default     = {}
}

modules/vpc/main.tf:

resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name = "${var.name}-vpc"
  })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(var.tags, {
    Name = "${var.name}-igw"
  })
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name}-public-${count.index + 1}"
  })
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(var.tags, {
    Name = "${var.name}-private-${count.index + 1}"
  })
}

modules/vpc/outputs.tf:

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "IDs of public subnets"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "IDs of private subnets"
  value       = aws_subnet.private[*].id
}

Step 3: Create the Security Group Module

modules/security-group/variables.tf:

variable "name" {
  description = "Name prefix for security groups"
  type        = string
}

variable "vpc_id" {
  description = "VPC ID where security groups are created"
  type        = string
}

variable "allowed_ssh_cidrs" {
  description = "CIDR blocks allowed SSH access"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}

modules/security-group/main.tf:

resource "aws_security_group" "web" {
  name_prefix = "${var.name}-web-"
  description = "Web server security group"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS"
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP"
  }

  dynamic "ingress" {
    for_each = length(var.allowed_ssh_cidrs) > 0 ? [1] : []
    content {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = var.allowed_ssh_cidrs
      description = "SSH from allowed ranges"
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound"
  }

  tags = merge(var.tags, {
    Name = "${var.name}-web-sg"
  })
}

modules/security-group/outputs.tf:

output "web_security_group_id" {
  description = "ID of the web security group"
  value       = aws_security_group.web.id
}

Step 4: Create the Compute Module

modules/compute/variables.tf:

variable "name" {
  description = "Name prefix for compute resources"
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "instance_count" {
  description = "Number of instances to create"
  type        = number
  default     = 1
}

variable "subnet_ids" {
  description = "Subnet IDs for instances"
  type        = list(string)
}

variable "security_group_ids" {
  description = "Security group IDs"
  type        = list(string)
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}

modules/compute/main.tf:

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "this" {
  count = var.instance_count

  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = var.subnet_ids[count.index % length(var.subnet_ids)]

  vpc_security_group_ids = var.security_group_ids

  tags = merge(var.tags, {
    Name = "${var.name}-${count.index + 1}"
  })
}

modules/compute/outputs.tf:

output "instance_ids" {
  description = "IDs of created instances"
  value       = aws_instance.this[*].id
}

output "private_ips" {
  description = "Private IP addresses of instances"
  value       = aws_instance.this[*].private_ip
}

Step 5: Create Environment Configurations

Each environment uses the same modules but with different parameters.

Shared Variables

All environments share the same variables.tf:

environments/*/variables.tf:

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "project_name" {
  description = "Project name for resource tagging"
  type        = string
  default     = "multi-env-tutorial"
}

Shared Main Configuration

environments/*/main.tf:

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

locals {
  name = "${var.project_name}-${var.environment}"

  env_config = {
    dev = {
      vpc_cidr             = "10.0.0.0/16"
      azs                  = ["us-east-1a", "us-east-1b"]
      public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
      private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24"]
      enable_nat           = false
      instance_type        = "t3.micro"
      instance_count       = 1
      allowed_ssh_cidrs    = ["10.0.0.0/8"]
    }
    staging = {
      vpc_cidr             = "10.1.0.0/16"
      azs                  = ["us-east-1a", "us-east-1b"]
      public_subnet_cidrs  = ["10.1.1.0/24", "10.1.2.0/24"]
      private_subnet_cidrs = ["10.1.10.0/24", "10.1.11.0/24"]
      enable_nat           = true
      instance_type        = "t3.small"
      instance_count       = 2
      allowed_ssh_cidrs    = ["10.0.0.0/8"]
    }
    prod = {
      vpc_cidr             = "10.2.0.0/16"
      azs                  = ["us-east-1a", "us-east-1b", "us-east-1c"]
      public_subnet_cidrs  = ["10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"]
      private_subnet_cidrs = ["10.2.10.0/24", "10.2.11.0/24", "10.2.12.0/24"]
      enable_nat           = true
      instance_type        = "t3.medium"
      instance_count       = 3
      allowed_ssh_cidrs    = []
    }
  }

  config = local.env_config[var.environment]
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
    }
  }
}

module "vpc" {
  source = "../../modules/vpc"

  name                 = local.name
  cidr_block           = local.config.vpc_cidr
  availability_zones   = local.config.azs
  public_subnet_cidrs  = local.config.public_subnet_cidrs
  private_subnet_cidrs = local.config.private_subnet_cidrs
  enable_nat_gateway   = local.config.enable_nat

  tags = {
    Environment = var.environment
  }
}

module "security" {
  source = "../../modules/security-group"

  name              = local.name
  vpc_id            = module.vpc.vpc_id
  allowed_ssh_cidrs = local.config.allowed_ssh_cidrs

  tags = {
    Environment = var.environment
  }
}

module "compute" {
  source = "../../modules/compute"

  name               = local.name
  instance_type      = local.config.instance_type
  instance_count     = local.config.instance_count
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.security.web_security_group_id]

  tags = {
    Environment = var.environment
  }
}

Per-Environment tfvars

environments/dev/terraform.tfvars:

environment = "dev"

environments/staging/terraform.tfvars:

environment = "staging"

environments/prod/terraform.tfvars:

environment = "prod"

Shared Outputs

environments/*/outputs.tf:

output "vpc_id" {
  description = "VPC ID"
  value       = module.vpc.vpc_id
}

output "instance_count" {
  description = "Number of instances"
  value       = length(module.compute.instance_ids)
}

output "instance_private_ips" {
  description = "Private IPs of instances"
  value       = module.compute.private_ips
}

Step 6: Validate All Environments

Use Terry-Form MCP to validate each environment. Ask your AI assistant:

“Initialize and validate all three environments: dev, staging, and prod under multi-env/environments/”

The assistant will run for each environment:

{
  "tool": "terry",
  "arguments": {
    "path": "multi-env/environments/dev",
    "actions": ["init", "validate"],
    "vars": {"environment": "dev"}
  }
}
{
  "tool": "terry",
  "arguments": {
    "path": "multi-env/environments/staging",
    "actions": ["init", "validate"],
    "vars": {"environment": "staging"}
  }
}
{
  "tool": "terry",
  "arguments": {
    "path": "multi-env/environments/prod",
    "actions": ["init", "validate"],
    "vars": {"environment": "prod"}
  }
}

Step 7: Compare Plans Across Environments

Generate plans for each environment to compare:

“Generate Terraform plans for dev, staging, and prod and summarize the differences”

The assistant calls terry with ["init", "plan"] for each environment.

Expected Differences

Resource Dev Staging Prod
AZs 2 2 3
NAT Gateway No Yes Yes
Instance type t3.micro t3.small t3.medium
Instance count 1 2 3
SSH access Internal only Internal only None

Step 8: Security Scan All Environments

Run security scans across all environments:

“Run security scans on all three environments”

{
  "tool": "terry_security_scan",
  "arguments": {
    "path": "multi-env/environments/dev",
    "severity": "medium"
  }
}

Repeat for staging and prod. Compare findings — production should have stricter security (no SSH access, NAT gateway for private subnet internet access).

Step 9: Analyze Shared Modules

Also analyze the shared modules for best practices:

“Analyze the VPC, security-group, and compute modules for best practices”

{
  "tool": "terry_analyze",
  "arguments": {
    "path": "multi-env/modules/vpc"
  }
}

Well-structured modules should pass with minimal warnings.

Step 10: Workspace Discovery

Use Terry-Form’s workspace tools to explore the structure:

“List all workspaces and show info about the multi-env project”

{
  "tool": "terry_workspace_list",
  "arguments": {}
}
{
  "tool": "terry_workspace_info",
  "arguments": {
    "path": "multi-env/environments/prod"
  }
}

Multi-Environment Patterns

Pattern 1: Environment Map (Used Above)

Define all environment configs in a locals map. Simple, everything in one file.

Pattern 2: Separate tfvars Files

Same main.tf everywhere, different terraform.tfvars per environment. Better for large configs.

Pattern 3: Terragrunt

Use Terragrunt for DRY configuration across environments. Terry-Form MCP works with the generated Terraform files.

Summary

In this tutorial, you learned how to:

  • Structure a multi-environment project with shared modules
  • Use a locals map for environment-specific configuration
  • Validate and plan all environments with Terry-Form MCP
  • Compare plans across dev, staging, and production
  • Run cross-environment security scans
  • Analyze shared modules for best practices

Next Steps