This repository was archived by the owner on May 16, 2026. It is now read-only.
Upgrade Terraform lock file#278
Merged
Merged
Conversation
State
|
| Success | 🟢 Add | Change | Destroy |
|---|---|---|---|
| ✅ | 11 | 0 | 0 |
Affected resources by action
| Action | Resources |
|---|---|
| 🟢 | module.repos["github-control"].github_actions_secret.secret["ANTHROPIC_API_KEY"] |
| 🟢 | module.repos["github-control"].github_branch_default.main[0] |
| 🟢 | module.repos["github-control"].github_branch_protection.main[0] |
| 🟢 | module.repos["github-control"].github_repository.repo |
| 🟢 | module.repos["github-control"].github_repository_file.claude_instructions |
| 🟢 | module.repos["github-control"].github_repository_file.coding_standard |
| 🟢 | module.repos["github-control"].github_repository_file.renovate_json |
| 🟢 | module.repos["github-control"].github_repository_file.vuln_scanner_workflow |
| 🟢 | module.repos["github-control"].github_repository_ruleset.main[0] |
| 🟢 | module.repos["github-control"].github_team_repository.admin |
| 🟢 | module.repos["github-control"].github_team_repository.dev |
STDOUT
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.repos["github-control"].github_actions_secret.secret["ANTHROPIC_API_KEY"] will be created
+ resource "github_actions_secret" "secret" {
+ created_at = (known after apply)
+ id = (known after apply)
+ key_id = (known after apply)
+ plaintext_value = (sensitive value)
+ remote_updated_at = (known after apply)
+ repository = "github-control"
+ repository_id = (known after apply)
+ secret_name = "ANTHROPIC_API_KEY"
+ updated_at = (known after apply)
}
# module.repos["github-control"].github_branch_default.main[0] will be created
+ resource "github_branch_default" "main" {
+ branch = "main"
+ etag = (known after apply)
+ id = (known after apply)
+ rename = false
+ repository = "github-control"
}
# module.repos["github-control"].github_branch_protection.main[0] will be created
+ resource "github_branch_protection" "main" {
+ allows_deletions = false
+ allows_force_pushes = false
+ enforce_admins = false
+ id = (known after apply)
+ lock_branch = false
+ pattern = "main"
+ repository_id = (known after apply)
+ require_conversation_resolution = false
+ require_signed_commits = false
+ required_linear_history = false
+ required_pull_request_reviews {
+ dismiss_stale_reviews = true
+ pull_request_bypassers = [
+ "A_kwHOB0VVB84AD4Ir",
]
+ require_code_owner_reviews = true
+ require_last_push_approval = false
+ required_approving_review_count = 1
}
}
# module.repos["github-control"].github_repository.repo will be created
+ resource "github_repository" "repo" {
+ allow_auto_merge = false
+ allow_forking = (known after apply)
+ allow_merge_commit = true
+ allow_rebase_merge = true
+ allow_squash_merge = true
+ archived = false
+ default_branch = (known after apply)
+ delete_branch_on_merge = true
+ description = "InfraHouse GitHub configuration."
+ etag = (known after apply)
+ fork = (known after apply)
+ full_name = (known after apply)
+ git_clone_url = (known after apply)
+ has_issues = true
+ has_projects = true
+ html_url = (known after apply)
+ http_clone_url = (known after apply)
+ id = (known after apply)
+ ignore_vulnerability_alerts_during_read = false
+ is_template = false
+ merge_commit_message = "PR_TITLE"
+ merge_commit_title = "MERGE_MESSAGE"
+ name = "github-control"
+ node_id = (known after apply)
+ primary_language = (known after apply)
+ private = (known after apply)
+ repo_id = (known after apply)
+ source_owner = (known after apply)
+ source_repo = (known after apply)
+ squash_merge_commit_message = "COMMIT_MESSAGES"
+ squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
+ ssh_clone_url = (known after apply)
+ svn_url = (known after apply)
+ topics = [
+ "infrahouse",
]
+ visibility = "public"
+ vulnerability_alerts = true
+ web_commit_signoff_required = (known after apply)
# (1 unchanged attribute hidden)
+ security_and_analysis (known after apply)
}
# module.repos["github-control"].github_repository_file.claude_instructions will be created
+ resource "github_repository_file" "claude_instructions" {
+ autocreate_branch_source_sha = (known after apply)
+ branch = (known after apply)
+ commit_message = "Add Claude Code instructions"
+ commit_sha = (known after apply)
+ content = <<-EOT
# Project Instructions for Claude Code
## First Steps (Do This Immediately)
### Session set up
- [ ] *ALWAYS* Read `.claude/CODING_STANDARD.md`
- [ ] Confirm to the user that you have read it.
### Generating CLAUDE.md
When generating or updating CLAUDE.md, include this text verbatim as the first section after the intro paragraph:
```markdown
## First Steps
**Your first tool call in this repository MUST be reading .claude/CODING_STANDARD.md.
Do not read any other files, search, or take any actions until you have read it.**
This contains InfraHouse's comprehensive coding standards for Terraform, Python, and general formatting rules.
```
End the CLAUDE.md file with a newline.
EOT
+ file = "./.claude/instructions.md"
+ id = (known after apply)
+ overwrite_on_create = true
+ ref = (known after apply)
+ repository = "github-control"
+ repository_id = (known after apply)
+ sha = (known after apply)
}
# module.repos["github-control"].github_repository_file.coding_standard will be created
+ resource "github_repository_file" "coding_standard" {
+ autocreate_branch_source_sha = (known after apply)
+ branch = (known after apply)
+ commit_message = "Add CODING_STANDARD.md"
+ commit_sha = (known after apply)
+ content = <<-EOT
<!--
This file is managed by Terraform in github-control repository
Do not edit this file, all changes will be overwritten
If you need to change this file, create a pull request in
https://github.com/infrahouse8/github-control
-->
# Coding Standards
This document defines coding standards for InfraHouse projects.
## General Formatting (All Files)
* **Maximum line length: 120 characters**
- Applies to all files: code (Python, Terraform, Puppet), documentation (Markdown, plans), configuration files, etc.
- Break long lines for readability
- Exception: URLs or other content that cannot be split
* **All files must end with a newline character**
## Python Standards
### Code Formatting
* **Use Black for code formatting**
- Format code: `black .`
- Check formatting in CI: `black --check .`
- Black is the uncompromising Python code formatter
- Ensures consistent formatting across all projects
### Linting
* **Use pylint for code linting**
- Enforces code quality standards
- Run: `pylint <module_name>`
- Configuration: `.pylintrc` in project root
### Type Hinting
* **Type hints are required for all functions**
- Include type hints for all function parameters and return values
- Use standard library `typing` module
- Example:
```python
from typing import List, Dict, Optional
def process_items(items: List[str], config: Optional[Dict[str, str]] = None) -> bool:
"""
Process a list of items with optional configuration.
:param items: List of item names to process
:param config: Optional configuration dictionary
:return: True if processing succeeded
"""
# Implementation
return True
```
### Documentation
* **Use RST (reStructuredText) docstrings** for all functions, classes, and modules
- Follow Sphinx docstring format
- Include parameter descriptions, return values, and exceptions
### Testing
* **Use pytest for all tests**
- Tests must be meaningful and test specific behaviors
- **Cover both happy and unhappy paths**
- No coverage requirements (quality over quantity)
- Don't write tests just to achieve coverage percentage
- Example:
```python
def test_function_success():
"""Test the happy path - function succeeds with valid input."""
result = my_function("valid_input")
assert result == expected_output
def test_function_invalid_input():
"""Test the unhappy path - function fails gracefully with invalid input."""
with pytest.raises(ValueError) as exc_info:
my_function("invalid_input")
assert "expected error message" in str(exc_info.value)
```
### Error Handling
* **Never use bare `except Exception:`**
- It's better to crash than to mute an error silently
- Only catch specific exceptions you can actually handle
- If you catch an exception, either fix the problem or re-raise it
- Example:
```python
# WRONG - mutes all errors:
try:
risky_operation()
except Exception:
pass # Don't do this
# CORRECT - catch specific exceptions:
try:
risky_operation()
except FileNotFoundError as e:
logger.error(f"Required file not found: {e}")
raise # Re-raise if you can't fix the problem
except ValueError as e:
# Fix the problem if you can handle it
return default_value
```
* **Never return booleans or exit codes to signal errors**
- Returning `(True/False, result)` tuples for error signaling is an anti-pattern
- Callers can forget to check the return value, silently ignoring failures
- Exceptions propagate naturally up the call stack with full context (message, traceback)
- CLI entry points (`@click.command`) should be the only place that catches exceptions and converts them
to `sys.exit()`
- Example:
```python
# WRONG - returning booleans to signal errors:
def execute_sql(sql: str) -> Tuple[bool, str]:
exit_code, stdout, stderr = run_command(sql)
if exit_code != 0:
return False, stderr # caller must remember to check
return True, stdout
# Caller:
success, output = execute_sql(sql)
if not success:
LOG.error("Failed: %s", output)
sys.exit(1)
# CORRECT - raise exceptions on failure:
def execute_sql(sql: str) -> str:
exit_code, stdout, stderr = run_command(sql)
if exit_code != 0:
raise SQLExecutionError(f"SQL failed: {stderr}")
return stdout
# Caller — no need to check, exception propagates:
output = execute_sql(sql)
```
### Class Member Ordering
* **Order members by visibility, then by type**
- By visibility (top to bottom): public, protected (`_`), private (`__`)
- By type within each visibility group: properties first, then methods
- Within each group, order alphabetically unless grouping related methods makes more sense
- Example:
```python
class MyService:
def __init__(self, client):
self._client = client
# --- Public properties ---
@property
def name(self) -> str:
...
@property
def status(self) -> str:
...
# --- Public methods ---
def connect(self) -> None:
...
def disconnect(self) -> None:
...
# --- Private properties ---
@property
def _endpoint(self) -> str:
...
# --- Private methods ---
def _validate(self) -> bool:
...
```
### Logging
* **Use `setup_logging()` from infrahouse-core**
- Provides consistent logging configuration across projects
- Example:
```python
import logging
from infrahouse_core.logging import setup_logging
LOG = logging.getLogger(__name__)
setup_logging(LOG)
def my_function():
LOG.info("Starting operation")
LOG.debug("Debug details here")
LOG.error("Error occurred")
```
### Dependencies
* Pin Python dependencies to a major version. Use ~= syntax. I.e. `requests ~= 2.31` instead of `requests>=2.31.0,<3.0.0`
- Trusts semantic versioning for patch and minor updates
- Libraries (like infrahouse-core) require version ranges; exact versions cause dependency conflicts
- Applications benefit from automatic security/bug fixes while unit tests provide safety
### InfraHouse Packages
* **infrahouse-core** (https://pypi.org/project/infrahouse-core/)
- Library with reusable classes and functions
- Use for imports in your Python code
- Examples: AWS helper classes, validation utilities, `setup_logging()` function
- When creating reusable code, add it to infrahouse-core
* **infrahouse-toolkit** (https://pypi.org/project/infrahouse-toolkit/)
- End-user CLI toolkit for provisioning, operational tasks, and CI/CD workflows
- Use as command-line tool (not for imports)
- Depends on infrahouse-core
- Note: Some classes exist in infrahouse-toolkit for historical reasons; reusable classes should migrate to
infrahouse-core
* **pytest-infrahouse** (https://pypi.org/project/pytest-infrahouse/)
- Pytest fixtures for Terraform module testing
- Provides fixtures for AWS infrastructure testing
- Use in test files with pytest
## Terraform Standards
### General
* When a variable or output description is too long, use HEREDOC construction to wrap lines
```hcl
variable "vpc_cidr" {
type = string
description = <<-EOT
The CIDR block for the VPC. Must be a valid IPv4 CIDR block.
Should not overlap with other VPCs or on-premises networks.
Recommended ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
EOT
}
```
* The module should only require providers it actually uses to create direct resources.
Child modules should take care of their required providers
- Separation of concerns: each module knows which providers it needs
- A module shouldn't dictate provider requirements for resources it doesn't directly create
* **InfraHouse modules must use registry.infrahouse.com** (private but publicly available registry)
* **All modules must use exact version pinning** (no ranges)
- Makes only one infrastructure change at a time (your change, not unrelated module updates)
- Version ranges are dangerous: unrelated updates can trigger risky operations
(e.g., adding a secret shouldn't trigger ElasticSearch instance refresh)
- Renovate manages version updates via individual PRs for explicit review and testing
```hcl
# InfraHouse modules - use registry.infrahouse.com:
module "vpc" {
source = "registry.infrahouse.com/infrahouse/service-network/aws"
version = "3.2.1" # Always exact version
# ...
}
# Non-InfraHouse modules - also exact version:
module "external" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2" # Always exact version
# ...
}
# WRONG - version range (applies to any module):
module "bad" {
source = "..."
version = "~> 3.2" # Don't do this
# ...
}
```
### Naming Conventions
* **Use snake_case everywhere**: Variables, resources, data sources, locals, outputs
- Use underscores (`_`), not dashes (`-`)
- Use lowercase letters and numbers only
* **Variable naming:**
- Boolean variables with verbs: `enable_*` or `create_*` (e.g., `enable_monitoring`, `create_bucket`)
- Boolean variables with nouns: `is_*` (e.g., `is_public`, `is_encrypted`)
- Lists: use plural form (e.g., `subnet_ids`, `availability_zones`)
* **Resource naming:**
- Single resource of a type: use `this` or `main` (e.g., `aws_nat_gateway.this`)
- Multiple resources of same type: use descriptive names (e.g., `aws_route_table.private`,
`aws_route_table.public`)
- Don't include resource type in name (GOOD: `aws_instance.web`, BAD: `aws_instance.ec2_web`)
* **Locals naming:**
- Use descriptive snake_case names
* **Output naming:**
- Group related outputs with prefixes (e.g., `vpc_id`, `vpc_cidr`, `vpc_arn`)
### Variables
* **Always specify explicit type constraints**
- Use `type = string`, `type = number`, `type = list(string)`, etc.
- Don't rely on Terraform's type inference
* **Default values - when to use:**
- **Required (no default)**: Critical for compliance/governance (e.g., `alert_emails`), no sensible default exists
(e.g., `environment`, `service`), need to force explicit user decision
- **Optional (with default)**: Meaningful, recommended default exists (e.g., `instance_type = "t3.micro"` for
specific workload)
- **Never default**: Arbitrary/meaningless values (e.g., `environment = "dev"`, `managed_by = "terraform"`)
* **Validation:**
- Goal: Catch definitely wrong input values, not enforce arbitrary constraints
- Add validation for: Format checks (CIDR, email), range checks (percentages 0-100, non-negative values)
- Error messages must: Explain what's wrong with specifics, include instructions on how to fix
- Use appropriate methods: Regex for formats, range checks, contains() for valid sets
- Don't enforce arbitrary lists (like approved instance types) unless there's a strong reason
* **Sensitive variables:**
- Always mark as `sensitive = true`
- Use `infrahouse/secret/aws` module for secrets management
- Specify allowed readers in the `readers` input value
- Never put secret values in user data; pass secret name/ARN instead
* **Complete variable examples:**
```hcl
# Example 1: Required variable with validation
variable "environment" {
type = string
description = "Environment name (development, staging, production, etc.)"
validation {
condition = can(regex("^[a-z0-9_]+$", var.environment))
error_message = "environment must contain only lowercase letters, numbers, and underscores (no hyphens)"
}
}
# Example 2: Optional variable with meaningful default
variable "instance_type" {
type = string
description = "EC2 instance type for the application servers"
default = "t3.micro"
}
# Example 3: Complex variable with all elements
variable "max_instance_count" {
type = number
description = <<-EOT
Maximum number of instances in the Auto Scaling Group.
Must be between 1 and 100. Set to null to use the recommended default based on workload.
EOT
default = null
validation {
condition = var.max_instance_count == null ? true : (var.max_instance_count >= 1 && var.max_instance_count <= 100)
error_message = "max_instance_count must be between 1 and 100, or null. Got: ${var.max_instance_count}"
}
}
# Example 4: Sensitive variable (receives secret value from infrahouse/secret/aws module)
variable "database_password" {
type = string
description = "Database password value (from module.my_secret.secret_value)"
sensitive = true
}
# WRONG - never hardcode secrets:
variable "bad_password" {
type = string
description = "Database password"
default = "MyP@ssw0rd123" # NEVER DO THIS
sensitive = true
}
```
### Outputs
* **When to create outputs:**
- Use reasonable judgment: output values with high chance to be needed by another module
- Better to under-do than over-do - don't output everything "just in case"
* **Always provide descriptions** for all outputs
* **Sensitive outputs:**
- Mark as `sensitive = true` when they contain secrets or sensitive data
### Documentation
* **Inline comments:**
- Add comments to explain why, not what
- Document non-obvious decisions and reasoning
* **README.md (required):**
- Must include terraform-docs markers: `<!-- BEGIN_TF_DOCS -->` and `<!-- END_TF_DOCS -->`
- Pre-commit hook uses terraform-docs to auto-generate documentation
- **Required badges** (in this order):
- Contact: `[](https://infrahouse.com/contact)`
- Documentation: `[](https://infrahouse.github.io/repo-name/)`
- Terraform Registry: `[](https://registry.terraform.io/modules/infrahouse/module-name/aws/latest)`
- Latest Release: `[](https://github.com/infrahouse/repo-name/releases/latest)`
- AWS Service Badge(s): `[](https://aws.amazon.com/ec2/)` (link to relevant AWS service(s))
- Security: `[](https://github.com/infrahouse/repo-name/actions/workflows/vuln-scanner-pr.yml)`
- License: `[](LICENSE)`
- **Required sections:**
1. Brief description (what it does, why it exists)
2. Features (bullet list)
3. Quick Start (minimal working example)
4. Documentation (links to GitHub Pages)
5. Requirements (Terraform version, providers)
6. Usage (terraform-docs auto-generated)
7. Examples (link to examples/)
8. Contributing (link to CONTRIBUTING.md)
9. License (link to LICENSE)
* **examples/ directory:**
- Desired but optional
- Provide working examples when included
* **terraform-docs configuration:**
- `.terraform-docs.yml` is managed by github-control
- README.md uses centrally-managed configuration
### GitHub Pages Documentation (terraform_module)
* **Deployment:** Automated via `.github/workflows/docs.yml` (managed by github-control)
* **Built with:** MkDocs with Material theme
* **Required pages:**
- `docs/index.md` - Overview, features, quick start
- `docs/getting-started.md` - Prerequisites, first deployment
- `docs/configuration.md` - All variables explained with examples
- `docs/architecture.md` - How it works, diagrams
- `docs/examples.md` - Common use cases with explanations
- `docs/troubleshooting.md` - Common issues and solutions
- `docs/changelog.md` - Symlink to CHANGELOG.md (`ln -fs ../CHANGELOG.md docs/changelog.md`)
* **Optional pages:**
- `docs/comparison.md` - vs alternatives
- `docs/security.md` - Security considerations
- `docs/monitoring.md` - Observability setup
- `docs/upgrading.md` - Migration guides between versions
### Repository Files
* **Must have:**
- `README.md` - Module documentation (see above)
- `LICENSE` - Apache 2.0 (recommended for patent protection and enterprise adoption)
- `CHANGELOG.md` - Auto-generated with git-cliff
- `.terraform-docs.yml` - Managed by github-control
- `mkdocs.yml` - MkDocs configuration (managed by github-control)
- `cliff.toml` - git-cliff configuration (managed by github-control)
- `.github/workflows/release.yml` - Auto-create GitHub Releases from tags (managed by github-control)
- `examples/` directory - Working examples
* **Should have:**
- `CONTRIBUTING.md` - Contribution guidelines
- `SECURITY.md` - Security policy, how to report vulnerabilities
- `CODEOWNERS` - Auto-assign reviewers
### Release Automation
* **Automated via `.github/workflows/release.yml`** (managed by github-control)
* **Trigger:** Push of version tags (e.g., `0.1.0`, `v1.0.0`)
* **Process:**
1. git-cliff generates changelog for the release
2. GitHub Release is created automatically with release notes
3. "Latest Release" badge in README reflects the new version
* **Manual release process** (via Makefile targets):
```makefile
release-patch:
git-cliff --tag $(shell bumpversion --dry-run --list patch | grep new_version | cut -d= -f2) -o CHANGELOG.md
bumpversion patch
git push && git push --tags
# GitHub Actions workflow automatically creates the release
```
* **Available targets:** `release-patch`, `release-minor`, `release-major`
### Resource Organization
* **`count` vs `for_each`:**
- Use `count` for simple create/don't create scenarios (e.g., `count = var.enable_feature ? 1 : 0`)
- Use `for_each` for pre-calculated values (lists, sets, maps) - handles computed values better and avoids
index-shifting issues
* **`locals`:**
- Extract expressions to locals to make code readable and troubleshoot-able
- Follow usual programming best practices (DRY, meaningful names, avoid complex inline expressions)
* **File organization:**
- Always create at minimum: `main.tf`, `variables.tf`, `outputs.tf`
- Create additional files to break module into logical pieces by function (e.g., `iam.tf`, `networking.tf`)
- Don't inflate `main.tf` - split into focused files
* **Dependencies:**
- Prefer implicit dependencies (Terraform references)
- Use explicit `depends_on` only when Terraform struggles with ordering (usually as a bugfix)
- Acceptable to create indirect dependencies (e.g., use module output as tag value or resource name)
### Security
#### General Principles
* Follow principle of least privilege
* Comply with ISO27001 and SOC2 requirements
* Use AWS Control Tower for account governance
#### Secrets Management
* **Always use `infrahouse/secret/aws` module** for secrets storage
- Provides secure access control
- Enables auditing: "who has access to this secret?"
- See: https://infrahouse.com/blog/2024-09-29-compliant-secrets/
* Never hardcode secret values in code or user data
* Pass secret names/ARNs instead of values
#### Security Groups
* Avoid `0.0.0.0/0` for ingress rules (except specific cases like public ALBs)
* Prefer security group references over CIDR ranges for internal communication
- Example: Put ALB and ASG in same security group, allow inter-security-group traffic
* **ICMP rules:**
- Within VPC: Allow all ICMP types (for troubleshooting)
- From internet (0.0.0.0/0): Allow only types 3 (Destination Unreachable), 8 (Echo Request), 0 (Echo Reply),
11 (Time Exceeded). Block all other types
#### Encryption at Rest
* Enable encryption by default for all resources (S3, EBS, RDS, etc.)
* Use AWS-managed keys (SSE-S3, aws/ebs, etc.) by default
* **Exception - CloudTrail**: Must use Customer-Managed Keys (CMK)
* **S3 buckets**: Use `infrahouse/s3-bucket/aws` module - configures encryption per ISO/SOC requirements
#### Encryption in Transit
* **Require HTTPS/TLS** for all load balancers and APIs
* Allow HTTP only to redirect to HTTPS
* **TLS Policy for ALB:**
- Default: `ELBSecurityPolicy-TLS13-1-2-Res-2021-06` (Restrictive)
- Allowed: `ELBSecurityPolicy-TLS13-1-2-Ext1-2021-06` (if compatibility required) or more restrictive policies
- Do not allow weaker/older policies
#### Logging and Auditing
* **Standard log retention**: 365 days (ISO requirement)
* **CloudTrail:**
- Must be enabled via Control Tower
- Must be encrypted with CMK
- Minimum retention: 365 days
* **VPC Flow Logs:**
- Always enable for all VPCs
- Destination: S3
- Retention: 365 days
- Use `infrahouse/service-network/aws` module for configuration
* **Application Logs:**
- Use CloudWatch Logs
- AWS-managed keys
- Retention: 365 days
* **AWS Config:**
- Required for ISO/SOC compliance
- Use default conformance packs
### Validation Blocks
* **CRITICAL: Always use ternary operators instead of logical OR when dealing with nullable variables.**
Terraform's OR operator doesn't properly short-circuit null comparisons, causing validation errors.
```hcl
# WRONG - will fail if var.value is null:
condition = var.value == null || var.value <= 100
# CORRECT - use ternary:
condition = var.value == null ? true : var.value <= 100
```
* **Additional validation patterns:**
```hcl
# Puppet environment name validation (lowercase letters, numbers, underscores only):
variable "environment" {
type = string
description = "Environment name (must follow Puppet naming rules)"
validation {
condition = can(regex("^[a-z0-9_]+$", var.environment))
error_message = "environment must contain only lowercase letters, numbers, and underscores (no hyphens). Got: ${var.environment}"
}
}
# Format validation (CIDR block):
variable "vpc_cidr" {
type = string
description = "The CIDR block for the VPC"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr must be a valid IPv4 CIDR block (e.g., 10.0.0.0/16)"
}
}
# Range validation (percentage):
variable "cpu_threshold" {
type = number
description = "CPU threshold percentage for alerting"
default = 80
validation {
condition = var.cpu_threshold >= 0 && var.cpu_threshold <= 100
error_message = "cpu_threshold must be between 0 and 100. Got: ${var.cpu_threshold}"
}
}
# List validation (minimum length):
variable "availability_zones" {
type = list(string)
description = "List of availability zones"
validation {
condition = length(var.availability_zones) >= 2
error_message = "At least 2 availability zones required for HA. Provided: ${length(var.availability_zones)}"
}
}
# Complex validation (nullable with additional check):
variable "max_size" {
type = number
description = "Maximum instance count"
default = null
validation {
condition = var.max_size == null ? true : (var.max_size > 0 && var.max_size <= 100)
error_message = "max_size must be between 1 and 100, or null. Got: ${var.max_size}"
}
}
```
### IAM Policies
* For IAM policies, use data source policy document, don't use a generated json
- Data sources handle policy versioning and formatting correctly
- User-generated JSON can fail with wrong version or formatting issues
- Delegates policy generation to Terraform, which does a better job than manual JSON
```hcl
# CORRECT - use data source:
data "aws_iam_policy_document" "lambda_assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role" "lambda" {
name = "lambda-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}
# WRONG - generated JSON string:
resource "aws_iam_role" "lambda" {
name = "lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
```
### Tagging
* Use lowercase tags except `Name`
- AWS tags are case-sensitive (`environment` ≠ `Environment`), so lowercase prevents confusion and accidental duplicates
- `Name` is capitalized to follow AWS console convention (displayed in resource tables)
- Note: AWS-generated tags (prefixed with `aws:`) are managed by AWS and ignored by this standard
* Use underscores for multi-word tags (e.g., `created_by_module`, not `created-by-module`)
* **Resource provenance tags** - Track what created each resource:
- `created_by`: GitHub repository that deployed the resource (format: `org/repo_name`)
Example: `"infrahouse/aws-control-289256138624"`
- `created_by_module`: Terraform module where resource is defined (format: `org/module_name/provider`)
Example: `"infrahouse/jumphost/aws"`
- `created_by_fixture`: Pytest fixture that created test resources (format: `org/package/fixture`)
Example: `"infrahouse/pytest-infrahouse/postgres"` (only present on test resources)
* Create one selected "main" resource with `module_version` tag
* **Require environment tag from user** - Do not provide defaults
- Prevents nonsense like `environment=dev` in production AWS accounts
- Forces explicit environment declaration, bringing order and preventing deployment mistakes
```hcl
# Example of proper tagging on a resource:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = {
Name = "web-server" # Capitalized per AWS convention
environment = var.environment # Required from user
service = var.service # Required from user
created_by = "infrahouse/aws-control-289256138624"
created_by_module = "infrahouse/ec2-instance/aws"
module_version = var.module_version # On selected "main" resource only
}
}
```
### Testing
#### Testing Philosophy
* Tests are called "unit tests" but are actually **integration tests**
- Create real infrastructure in AWS
- Validate the infrastructure behaves correctly
- Clean up resources after testing
#### Testing Framework
* Use **pytest** for all Terraform module tests
* Use Terraform fixtures from **pytest-infrahouse** (https://pypi.org/project/pytest-infrahouse/)
* Use **infrahouse-core** (https://pypi.org/project/infrahouse-core/) for validation
- Provides AWS and infrastructure helper classes
#### Test Coverage Requirements
* Test both happy path and edge cases
* Validate resource creation, outputs, and behavior
#### Makefile Requirements
* Implement **test-keep** and **test-clean** targets
* Support configurable test parameters:
```makefile
TEST_REGION ?= us-west-2
TEST_ROLE ?= arn:aws:iam::303467602807:role/ecs-tester
TEST_SELECTOR ?= tests/
TEST_PATH ?= tests/test_httpd.py
TEST_FILTER ?= "test_ and aws-6"
```
* `test-keep`: Run tests and keep infrastructure for debugging
* `test-clean`: Run tests and clean up all resources (use before PRs)
#### Development Workflow
1. **Local development**: Run tests with `test-keep` during feature development
2. **Before PR**: Run `make test-clean` for final validation
3. **Create PR**: GitHub Actions runs full test suite
4. **Merge**: When all tests pass
#### CI/CD Requirements
* The root module must define variables in terraform.tfvars
```hcl
# Example terraform.tfvars for testing:
environment = "development"
service = "my-service"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-west-2a", "us-west-2b"]
instance_type = "t3.micro"
enable_monitoring = true
```
* **Test root module must set `created_by` tag** in provider default_tags:
```hcl
provider "aws" {
default_tags {
tags = {
created_by = "infrahouse/terraform-aws-ecs" # GitHub repository that created the resource
}
}
}
```
* GitHub Actions must run on self-hosted runner (GitHub runners don't have ih-registry command)
* Test role session time in CI: **12 hours**
* .github/workflows/terraform-CD.yml configures CI test execution
## Puppet Standards
### General
* TBD - Standards to be documented
## Makefile Standards
All InfraHouse projects (Python libraries, Terraform modules, etc.) must include a Makefile following these standards.
### Help Target (Default)
* **`make help` must be the default target**
- Running `make` without arguments should display help
- Help output should list all user-facing targets with descriptions
- Implementation: Use `.DEFAULT_GOAL := help` or list `help` as first target
### Required Targets
#### `bootstrap`
* Installs all project dependencies
* Assumes it's running in a virtualenv
* Installs pip, setuptools, and dependencies from requirements.txt (or equivalent)
* Must indirectly call the `install-hooks` target
* Example:
```makefile
bootstrap: install-hooks
pip install --upgrade pip setuptools
pip install -r requirements.txt
```
#### `install-hooks`
* Installs pre-commit hooks and commit-msg hook
* The `hooks/commit-msg` file is managed by github-control
* Example:
```makefile
install-hooks:
pre-commit install
pre-commit install --hook-type commit-msg
```
#### `test`
* Runs the full test suite
* Should run all tests with default configuration
#### `test-keep`
* Runs tests and keeps infrastructure for debugging
* Useful during local development
* Resources remain after tests complete for troubleshooting
#### `test-clean`
* Runs tests and cleans up all resources
* Must be run before creating PRs to ensure proper cleanup
* Validates that infrastructure can be cleanly destroyed
#### `clean`
* Cleans all build artifacts, temporary files, caches, etc.
* **Do NOT remove Claude Code reviews** (preserve .claude/ directory contents)
* Common cleanups:
- Python: `__pycache__`, `*.pyc`, `.pytest_cache`, `dist/`, `build/`, `*.egg-info`
- Terraform: `.terraform/`, `terraform.tfstate*`, `.terraform.lock.hcl` (in test directories)
* Example:
```makefile
clean:
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name '*.pyc' -delete
rm -rf .pytest_cache dist build *.egg-info
```
#### `format`
* Reformats code to match coding standards
* **For Terraform modules**: Use both `black .` and `terraform fmt -recursive`
* **For Python projects**: Use `black .`
* Should be safe to run repeatedly (idempotent)
* Example:
```makefile
format:
black .
terraform fmt -recursive
```
#### `lint`
* Runs project-specific linters and formatting tools in check mode
* Should NOT modify files, only report issues
* Exit with non-zero status if issues found (for CI/CD)
* **For Python**: Check with `black --check .` and `pylint`
* **For Terraform**: Use `terraform fmt -check -recursive` and `terraform validate`
* Example:
```makefile
lint:
black --check .
pylint my_module
terraform fmt -check -recursive
```
#### `release-*` targets
* Automate the release process
* Run git-cliff for changelog generation
* Run bumpversion to update version numbers
* Common targets:
- `release-patch`: Patch version bump (e.g., 1.2.3 → 1.2.4)
- `release-minor`: Minor version bump (e.g., 1.2.3 → 1.3.0)
- `release-major`: Major version bump (e.g., 1.2.3 → 2.0.0)
* Example:
```makefile
release-patch:
git-cliff --tag $(shell bumpversion --dry-run --list patch | grep new_version | cut -d= -f2) -o CHANGELOG.md
bumpversion patch
git push && git push --tags
```
### Target Naming Conventions
* Use lowercase with hyphens for multi-word targets (e.g., `test-clean`, `install-hooks`)
* Use descriptive names that clearly indicate the action
* Group related targets with common prefixes (e.g., `release-*`, `test-*`)
### PHONY Targets
* Declare all targets that don't produce files as `.PHONY`
* Prevents conflicts with files of the same name
* Improves performance by skipping timestamp checks
* Example:
```makefile
.PHONY: help bootstrap install-hooks test test-keep test-clean clean format lint
```
### Best Practices
* Use `?=` for variables that users can override (e.g., `TEST_REGION ?= us-west-2`)
* Document complex targets with comments
* Keep targets focused - one clear purpose per target
* Chain related targets using dependencies (e.g., `bootstrap: install-hooks`)
* Use consistent indentation (tabs, not spaces, per Make syntax requirements)
EOT
+ file = "./.claude/CODING_STANDARD.md"
+ id = (known after apply)
+ overwrite_on_create = true
+ ref = (known after apply)
+ repository = "github-control"
+ repository_id = (known after apply)
+ sha = (known after apply)
}
# module.repos["github-control"].github_repository_file.renovate_json will be created
+ resource "github_repository_file" "renovate_json" {
+ autocreate_branch_source_sha = (known after apply)
+ branch = (known after apply)
+ commit_message = "Configure renovate"
+ commit_sha = (known after apply)
+ content = jsonencode(
{
+ "$schema" = "https://docs.renovatebot.com/renovate-schema.json"
+ dependencyDashboardApproval = true
+ extends = [
+ "config:recommended",
]
+ packageRules = [
+ {
+ enabled = false
+ matchFileNames = [
+ ".github/workflows/checkov.yml",
+ ".github/workflows/terraform-CI.yml",
+ ".github/workflows/terraform-CD.yml",
+ ".github/workflows/vuln-scanner-pr-public.yml",
+ ".github/workflows/vuln-scanner-pr-private.yml",
+ ".github/workflows/vuln-scanner-pr.yml",
+ ".github/workflows/terraform-review.yml",
+ ".github/workflows/docs.yml",
+ ".github/workflows/release.yml",
+ ".github/workflows/notify-on-failure.yml",
]
+ matchManagers = [
+ "github-actions",
]
},
]
+ prConcurrentLimit = 1
+ rebaseWhen = "conflicted"
}
)
+ file = "renovate.json"
+ id = (known after apply)
+ overwrite_on_create = true
+ ref = (known after apply)
+ repository = "github-control"
+ repository_id = (known after apply)
+ sha = (known after apply)
}
# module.repos["github-control"].github_repository_file.vuln_scanner_workflow will be created
+ resource "github_repository_file" "vuln_scanner_workflow" {
+ autocreate_branch_source_sha = (known after apply)
+ branch = (known after apply)
+ commit_message = "Update vuln-scanner-pr.yml workflow"
+ commit_sha = (known after apply)
+ content = <<-EOT
# This file is managed by Terraform in github-control repository
# Do not edit this file, all changes will be overwritten
# If you need to change this file, create a pull request in
# https://github.com/infrahouse8/github-control
---
name: OSV-Scanner PR Scan
on: # yamllint disable-line rule:truthy
pull_request:
branches: [main]
permissions:
# Required to upload SARIF file to CodeQL. See: https://github.com/github/codeql-action/issues/2117
actions: read
# Require writing security events to upload SARIF file to security tab
security-events: write
# Only need to read contents
contents: read
jobs:
vulnerability-check:
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.0"
with:
scan-args: --config osv-scanner.toml
upload-sarif: false
EOT
+ file = "./.github/workflows/vuln-scanner-pr.yml"
+ id = (known after apply)
+ overwrite_on_create = true
+ ref = (known after apply)
+ repository = "github-control"
+ repository_id = (known after apply)
+ sha = (known after apply)
}
# module.repos["github-control"].github_repository_ruleset.main[0] will be created
+ resource "github_repository_ruleset" "main" {
+ enforcement = "active"
+ etag = (known after apply)
+ id = (known after apply)
+ name = "Main Branch Protection"
+ node_id = (known after apply)
+ repository = "github-control"
+ ruleset_id = (known after apply)
+ target = "branch"
+ bypass_actors {
+ actor_id = 1016363
+ actor_type = "Integration"
+ bypass_mode = "always"
}
+ bypass_actors {
+ actor_id = 14268696
+ actor_type = "Team"
+ bypass_mode = "always"
}
+ conditions {
+ ref_name {
+ exclude = []
+ include = [
+ "~DEFAULT_BRANCH",
]
}
}
+ rules {
+ update_allows_fetch_and_merge = false
+ pull_request {
+ allowed_merge_methods = (known after apply)
+ dismiss_stale_reviews_on_push = true
+ require_code_owner_review = true
+ require_last_push_approval = false
+ required_approving_review_count = 1
+ required_review_thread_resolution = false
}
+ required_status_checks {
+ do_not_enforce_on_create = false
+ strict_required_status_checks_policy = true
+ required_check {
+ context = "vulnerability-check / osv-scan"
+ integration_id = 0
}
}
}
}
# module.repos["github-control"].github_team_repository.admin will be created
+ resource "github_team_repository" "admin" {
+ etag = (known after apply)
+ id = (known after apply)
+ permission = "admin"
+ repository = "github-control"
+ team_id = "14268696"
}
# module.repos["github-control"].github_team_repository.dev will be created
+ resource "github_team_repository" "dev" {
+ etag = (known after apply)
+ id = (known after apply)
+ permission = "push"
+ repository = "github-control"
+ team_id = "7332815"
}
Plan: 11 to add, 0 to change, 0 to destroy.
Warning: Argument is deprecated
with module.ih_8_repos.github_repository.repo,
on modules/local-repo/repos.tf line 4, in resource "github_repository" "repo":
4: has_downloads = false
This attribute is no longer in use, but it hasn't been removed yet. It will
be removed in a future version. See
https://github.com/orgs/community/discussions/102145#discussioncomment-8351756
(and 7 more similar warnings elsewhere)
Warning: Deprecated attribute
on .terraform/modules/actions-runner-pem-493370826424-uw1/data_sources.tf line 11, in data "external" "secret_value":
11: "python", "${path.module}/assets/get_secret.py", data.aws_region.current.name, aws_secretsmanager_secret.secret.id, data.aws_iam_role.caller_role.arn
The attribute "name" is deprecated. Refer to the provider documentation for
details.
(and 5 more similar warnings elsewhere)
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tf.plan
To perform exactly these actions, run the following command to apply:
terraform apply "tf.plan"
Releasing state lock. This may take a few moments...
metadata
eyJzMzovL2luZnJhaG91c2UtZ2l0aHViLWNvbnRyb2wtc3RhdGUvdGVycmFmb3JtLnRmc3RhdGUiOiB7InN1Y2Nlc3MiOiB0cnVlLCAiYWRkIjogMTEsICJjaGFuZ2UiOiAwLCAiZGVzdHJveSI6IDB9fQ==
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.