Supercharge GitHub Copilot for Terraform with Custom Repository Instructions

·

11 min read

Cover Image for Supercharge GitHub Copilot for Terraform with Custom Repository Instructions

GitHub Copilot becomes significantly more powerful when it understands your project's specific conventions, patterns, and architectural decisions. Custom repository instructions provide Copilot with the context it needs to generate code that aligns with your team's standards and best practices, rather than generic suggestions that might not fit your workflow.

What Are Custom Repository Instructions?

Custom repository instructions are markdown files (.github/copilot-instructions.md) that you place in your repository to guide Copilot's code generation. Think of them as a persistent conversation with Copilot about how your project works, what patterns you prefer, and what conventions your team follows.

For Terraform projects, these instructions are particularly valuable because infrastructure-as-code has numerous architectural decisions that vary between organizations: naming conventions, module structures, state management approaches, and resource organization patterns. Without guidance, Copilot might suggest patterns that conflict with your established standards.

Why Custom Instructions Matter for Terraform

Terraform projects benefit from custom instructions because they help Copilot:

  • Generate reusable module code with consistent patterns for conditional resource creation

  • Follow your team's naming conventions for resources, variables, and outputs

  • Understand your state management strategy, especially when using remote backends

  • Apply your preferred dependency management approaches

  • Use data sources appropriately versus hard-coded values

  • Implement your organization's tagging and labeling standards

  • Follow security and compliance requirements specific to your infrastructure

Essential Elements for Terraform Instructions

Project Context and Architecture

Start by explaining your project's purpose and how Terraform is organized. This helps Copilot understand the big picture.

markdown

## Project Overview
This repository manages AWS infrastructure for production and staging environments.
We use a workspace-based approach with remote state in S3.

Module Reusability with Ternary Operators

Instruct Copilot to create flexible modules using conditional logic for optional resources.

## Module Patterns

### Conditional Resource Creation
Use ternary operators and `count` or `for_each` to make resources optional:
- Prefer `count = var.enable_feature ? 1 : 0` for single optional resources
- Use `for_each` for multiple conditional resources based on maps or sets
- Always provide sensible defaults in variable definitions

Locals for Computed Values

Guide Copilot on when and how to use locals blocks.

## Using Locals

Use `locals` blocks for:
- Computed values used multiple times
- Complex expressions that would clutter resource definitions
- Combining variables into standardized formats (e.g., naming conventions)
- Environment-specific configurations

Example pattern:
```hcl
locals {
  common_tags = merge(
    var.tags,
    {
      Environment = var.environment
      ManagedBy   = "Terraform"
      Repository  = "github.com/org/repo"
    }
  )

  resource_name = "${var.project}-${var.environment}-${var.component}"
}
```

State Management with Removed Blocks

For teams managing state with removed blocks rather than CLI commands, this is crucial context.

## State Management

### Remote State
- Backend configuration is in `backend.tf`
- Use S3 backend with DynamoDB locking
- Never commit `.tfstate` files

### Removing Resources
When removing resources from management, use `removed` blocks instead of `terraform state rm`:
```hcl
removed {
  from = aws_instance.legacy_server

  lifecycle {
    destroy = false
  }
}
```

This ensures state changes are tracked in version control and applied consistently across the team.

Dependency Management

Explain how to handle implicit and explicit dependencies.

## Managing Dependencies

### Implicit Dependencies
Prefer implicit dependencies through resource references:
```hcl
subnet_id = aws_subnet.private.id
```

### Explicit Dependencies
Use `depends_on` only when Terraform cannot infer the dependency:
- Cross-module dependencies that aren't captured by outputs
- Timing issues where resources must be created in sequence
- When destroying resources in specific order matters

Always add a comment explaining why explicit dependency is needed.

Data Sources vs. Hard-Coded Values

Provide guidance on using data sources for dynamic lookups.

## Data Sources

Prefer data sources over hard-coded values for:
- AMI IDs (use latest with filters)
- Availability zones
- VPC and subnet IDs when working across modules
- IAM policies and service principals
- Route53 zone IDs

Example:
```hcl
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}
```

Avoid data sources for values that should be explicitly versioned in code.

Variable and Output Conventions

Define naming and documentation standards.

## Variables and Outputs

### Variable Definitions
- Use snake_case for all variable names
- Always include description and type
- Provide defaults for optional variables
- Use validation blocks for constrained values
```hcl
variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}
```

### Outputs
- Output resource IDs and ARNs that other modules might need
- Include descriptions explaining the output's purpose
- Use sensitive = true for secrets

Tagging Strategy

Specify your organization's tagging requirements.

## Resource Tagging

All resources that support tags must include:
```hcl
tags = merge(
  local.common_tags,
  {
    Name = local.resource_name
  }
)
```

Required tags in common_tags:
- Environment
- ManagedBy
- Repository
- CostCenter
- Owner

Security and Compliance

Include security-specific guidance.

## Security Requirements

- Never hard-code credentials or secrets
- Use AWS Secrets Manager or SSM Parameter Store for sensitive values
- Enable encryption at rest for all storage resources
- Use private subnets for compute resources when possible
- Enable logging and monitoring for all resources
- Follow principle of least privilege for IAM roles and policies

Module Organization

Explain how modules should be structured.

## Module Structure

Standard module layout: modules/ <module-name>/ main.tf # Primary resource definitions variables.tf # Input variables outputs.tf # Output values versions.tf # Provider and Terraform version constraints locals.tf # Local values (if needed) data.tf # Data sources (if needed) README.md # Module documentation

#module guildelines
Each module should be self-contained and reusable.

Testing and Validation

Provide instructions for testing patterns.

## Testing

- Use `terraform fmt` to format all .tf files
- Run `terraform validate` before committing
- Use `tflint` with the AWS ruleset
- Include examples/ directory with working examples of module usage
- Use `terraform-docs` to generate module documentation

Complete Sample Instructions File

Here's a comprehensive example bringing all these elements together:

markdown

# Terraform Custom Instructions for GitHub Copilot

## Project Overview
This repository manages multi-environment AWS infrastructure using Terraform modules.
We deploy to dev, staging, and production environments with separate AWS accounts.

## Code Style and Conventions

### Naming
- Use snake_case for resources, variables, outputs, and locals
- Resource names: `<resource_type>_<descriptive_name>`
- Variable names should be descriptive and unabbreviated when possible
- Module directory names: kebab-case

### File Organization
- `main.tf`: Primary resource definitions
- `variables.tf`: Input variables only
- `outputs.tf`: Output values only
- `locals.tf`: Local value computations
- `data.tf`: Data source lookups
- `versions.tf`: Terraform and provider version constraints
- `backend.tf`: Backend configuration

## Module Design Patterns

### Conditional Resources
Use ternary operators with count for optional resources:
```hcl
resource "aws_cloudwatch_log_group" "this" {
  count = var.enable_logging ? 1 : 0

  name              = "/aws/lambda/${var.function_name}"
  retention_in_days = var.log_retention_days

  tags = local.common_tags
}
```

For multiple conditional resources, prefer for_each:
```hcl
resource "aws_subnet" "private" {
  for_each = var.create_private_subnets ? var.private_subnet_cidrs : {}

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value
  availability_zone = each.key

  tags = merge(
    local.common_tags,
    {
      Name = "${local.resource_prefix}-private-${each.key}"
      Tier = "private"
    }
  )
}
```

### Locals Usage
Use locals for:
- Repeated computed values
- Name prefixes following our convention
- Merging tags
- Complex conditional logic
```hcl
locals {
  resource_prefix = "${var.project_name}-${var.environment}"

  common_tags = merge(
    var.additional_tags,
    {
      Environment  = var.environment
      ManagedBy    = "Terraform"
      Repository   = "github.com/myorg/infrastructure"
      CostCenter   = var.cost_center
      Owner        = var.owner_email
    }
  )

  # Complex logic in locals keeps resources clean
  enable_enhanced_monitoring = var.environment == "prod" ? true : var.enable_monitoring

  backup_retention = {
    dev     = 7
    staging = 14
    prod    = 30
  }
  retention_days = local.backup_retention[var.environment]
}
```

## State Management

### Backend Configuration
- Use S3 backend with DynamoDB state locking
- Backend config in `backend.tf`
- State file path: `<environment>/<component>/terraform.tfstate`

### Removing Resources from State
Use `removed` blocks instead of CLI commands for team consistency:
```hcl
removed {
  from = aws_instance.deprecated_server

  lifecycle {
    destroy = false  # Keep the resource, just remove from state
  }
}

# Or to track actual resource deletion
removed {
  from = module.legacy_database

  lifecycle {
    destroy = true
  }
}
```

This ensures state changes are version-controlled and reviewable.

## Dependency Management

### Prefer Implicit Dependencies
```hcl
# Good - implicit dependency
resource "aws_eip" "nat" {
  vpc = true
  tags = local.common_tags
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id  # Implicit dependency
  subnet_id     = aws_subnet.public.id
}
```

### Explicit Dependencies (Use Sparingly)
Only use `depends_on` when necessary:
```hcl
resource "aws_iam_role_policy_attachment" "lambda" {
  role       = aws_iam_role.lambda.name
  policy_arn = aws_iam_policy.lambda.arn

  # Explicit dependency needed for eventual consistency
  depends_on = [aws_iam_role.lambda]
}
```

Always include a comment explaining why explicit dependency is required.

## Data Sources

Use data sources for dynamic lookups, not for values that should be versioned:
```hcl
# Good - dynamic lookup of latest AMI
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Good - lookup existing VPC
data "aws_vpc" "main" {
  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# Good - get available AZs
data "aws_availability_zones" "available" {
  state = "available"
}

# Bad - hard-code when data source is appropriate
resource "aws_instance" "web" {
  ami = "ami-0c55b159cbfafe1f0"  # Don't do this
  # ...
}
```

## Variables

### Variable Definitions
Always include description, type, and defaults when appropriate:
```hcl
variable "environment" {
  description = "Environment name: dev, staging, or prod"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

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

variable "enable_monitoring" {
  description = "Enable detailed CloudWatch monitoring"
  type        = bool
  default     = false
}

variable "subnet_cidrs" {
  description = "Map of availability zone to subnet CIDR blocks"
  type        = map(string)
  default     = {}
}

variable "allowed_cidr_blocks" {
  description = "List of CIDR blocks allowed to access resources"
  type        = list(string)
  default     = []

  validation {
    condition = alltrue([
      for cidr in var.allowed_cidr_blocks : can(cidrhost(cidr, 0))
    ])
    error_message = "All elements must be valid CIDR blocks."
  }
}
```

### Variable Precedence
Variables should be provided in this order:
1. Environment-specific `.tfvars` files
2. Common `terraform.tfvars`
3. Defaults in `variables.tf`

## Outputs

Include clear descriptions and mark sensitive values:
```hcl
output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs for compute resources"
  value       = [for subnet in aws_subnet.private : subnet.id]
}

output "database_endpoint" {
  description = "Connection endpoint for the RDS instance"
  value       = aws_db_instance.main.endpoint
  sensitive   = true
}
```

## Tagging Strategy

All taggable resources must include common_tags:
```hcl
resource "aws_instance" "web" {
  # ... other configuration ...

  tags = merge(
    local.common_tags,
    {
      Name      = "${local.resource_prefix}-web-${count.index + 1}"
      Component = "web-server"
      Backup    = "daily"
    }
  )
}
```

Required tags (enforced via SCPs):
- Environment
- ManagedBy
- Repository
- CostCenter
- Owner

## Security Best Practices

### Secrets Management
Never hard-code secrets. Use AWS Secrets Manager or SSM Parameter Store:
```hcl
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "${var.project_name}/${var.environment}/db-password"
}

resource "aws_db_instance" "main" {
  # ... other configuration ...
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
```

### Encryption
Enable encryption for all storage:
```hcl
resource "aws_s3_bucket" "data" {
  bucket = "${local.resource_prefix}-data"

  tags = local.common_tags
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
  }
}
```

### Network Security
- Place compute resources in private subnets
- Use security groups with minimal required access
- Enable VPC flow logs
- Use AWS PrivateLink for AWS service access when possible

## Module Structure

Standard module organization:

Sample .github/copilot-instructions.md for VS Code

For VS Code Copilot customization, create a similar file that focuses on editor-specific workflows:

# Terraform Development with GitHub Copilot

## Project Context
AWS infrastructure managed with Terraform in a multi-environment setup.
Follow the patterns and conventions defined in our terraform modules.

## When Writing Terraform Code

### Always Include
- Type definitions for all variables
- Descriptions for variables and outputs
- Validation blocks for constrained variables
- Common tags merged with resource-specific tags
- Comments explaining complex conditionals

### Naming Patterns
Generate resource names using this pattern:
```hcl
locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_xxx" "example" {
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-descriptive-name"
  })
}
```

### Conditional Resources
When I ask for optional resources, use count with ternary:
```hcl
count = var.enable_feature ? 1 : 0
```

Reference with: `aws_resource.example[0].id`

For multiple resources based on a map, use for_each.

### Data Sources
Suggest data sources for:
- Latest AMIs (with filters)
- Availability zones
- Existing VPCs/subnets
- IAM policy documents

### Security Defaults
When creating resources:
- Enable encryption by default
- Use private subnets for compute
- Apply least-privilege IAM policies
- Enable logging and monitoring

### Suggest Modules
If I'm writing repetitive resource configurations, suggest creating a reusable module.

## Code Completion Preferences

When I start typing:
- `variable` - include description, type, and validation if constrained
- `output` - include description
- `resource` - include common_tags
- `data` - include relevant filters
- `locals` - use for name prefixes and tag merging

## Testing
Remind me to run:
- `terraform fmt` before committing
- `terraform validate` to check syntax
- `terraform plan` to preview changes

Conclusion

Custom repository instructions transform GitHub Copilot from a generic code completion tool into a knowledgeable team member that understands your specific Terraform patterns and conventions. By investing time in creating comprehensive instructions, you'll receive more relevant suggestions, reduce code review cycles, and maintain consistency across your infrastructure codebase.

Start with the essential elements outlined above, then refine your instructions based on the patterns and challenges unique to your organization. As your team's conventions evolve, keep your instructions updated to ensure Copilot continues to provide valuable, context-aware assistance.

Reference

if you are intesrested in learning more about how you can leverage Copilot then I would recomment you to check Repo below

awesome-copilot

copilot-in-a-box