Files
terraform-foundation/terraform/modules/app-account/main.tf
Greg Hendrickson 6136cde9bb feat: Terraform Foundation - AWS Landing Zone
Enterprise-grade multi-tenant AWS cloud foundation.

Modules:
- GitHub OIDC for keyless CI/CD authentication
- IAM account settings and security baseline
- AWS Config Rules for compliance
- ABAC (Attribute-Based Access Control)
- SCPs (Service Control Policies)

Features:
- Multi-account architecture
- Cost optimization patterns
- Security best practices
- Comprehensive documentation

Tech: Terraform, AWS Organizations, IAM Identity Center
2026-02-02 02:57:23 +00:00

223 lines
6.1 KiB
HCL

################################################################################
# App Account Module
#
# Account vending machine for provisioning new workload accounts:
# - Creates AWS account via Organizations
# - Applies account baseline
# - Sets up cross-account IAM roles
# - Configures budget alerts
################################################################################
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
data "aws_organizations_organization" "this" {}
locals {
# Generate account email if not provided
account_email = var.account_email != "" ? var.account_email : "${var.email_prefix}+${var.account_name}@${var.email_domain}"
# Standard account tags
account_tags = {
AccountName = var.account_name
Environment = var.environment
Owner = var.owner
CostCenter = var.cost_center
OrganizationUnit = var.organizational_unit
ManagedBy = "terraform"
}
}
################################################################################
# AWS Account
################################################################################
resource "aws_organizations_account" "this" {
name = var.account_name
email = local.account_email
parent_id = var.organizational_unit_id
# IAM user access to billing (usually disabled)
iam_user_access_to_billing = var.iam_user_access_to_billing ? "ALLOW" : "DENY"
# Role name for cross-account access from management account
role_name = var.admin_role_name
# Don't close account on destroy (safety)
close_on_deletion = var.close_on_deletion
tags = merge(var.tags, local.account_tags)
lifecycle {
# Prevent accidental deletion
prevent_destroy = false # Set to true in production
# Email cannot be changed
ignore_changes = [email, role_name]
}
}
################################################################################
# Cross-Account IAM Role (in new account)
# Note: This creates a role that can be assumed from the management account
################################################################################
# Provider for the new account (assumes role created during account creation)
provider "aws" {
alias = "new_account"
region = var.region
assume_role {
role_arn = "arn:aws:iam::${aws_organizations_account.this.id}:role/${var.admin_role_name}"
session_name = "terraform-account-setup"
}
}
# Readonly role for cross-account access
resource "aws_iam_role" "cross_account_readonly" {
provider = aws.new_account
count = var.create_cross_account_roles ? 1 : 0
name = "cross-account-readonly"
path = "/cross-account/"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = var.readonly_trusted_principals
}
}]
})
tags = merge(var.tags, {
Name = "cross-account-readonly"
})
depends_on = [aws_organizations_account.this]
}
resource "aws_iam_role_policy_attachment" "cross_account_readonly" {
provider = aws.new_account
count = var.create_cross_account_roles ? 1 : 0
role = aws_iam_role.cross_account_readonly[0].name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
# Admin role for cross-account access (requires MFA)
resource "aws_iam_role" "cross_account_admin" {
provider = aws.new_account
count = var.create_cross_account_roles ? 1 : 0
name = "cross-account-admin"
path = "/cross-account/"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = var.admin_trusted_principals
}
Condition = {
Bool = {
"aws:MultiFactorAuthPresent" = "true"
}
}
}]
})
max_session_duration = 3600
tags = merge(var.tags, {
Name = "cross-account-admin"
})
depends_on = [aws_organizations_account.this]
}
resource "aws_iam_role_policy_attachment" "cross_account_admin" {
provider = aws.new_account
count = var.create_cross_account_roles ? 1 : 0
role = aws_iam_role.cross_account_admin[0].name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
################################################################################
# Account Baseline (in new account)
################################################################################
module "account_baseline" {
source = "../account-baseline"
count = var.apply_baseline ? 1 : 0
providers = {
aws = aws.new_account
}
name = var.account_name
enable_ebs_encryption = true
enable_s3_block_public = true
enable_password_policy = true
enable_access_analyzer = true
# Security services typically managed by delegated admin
enable_securityhub = false
enable_guardduty = false
enable_config = false
tags = merge(var.tags, local.account_tags)
depends_on = [aws_organizations_account.this]
}
################################################################################
# Budget (in new account)
################################################################################
resource "aws_budgets_budget" "this" {
provider = aws.new_account
count = var.budget_limit > 0 ? 1 : 0
name = "${var.account_name}-monthly-budget"
budget_type = "COST"
limit_amount = tostring(var.budget_limit)
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = [var.owner_email != "" ? var.owner_email : local.account_email]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = [var.owner_email != "" ? var.owner_email : local.account_email]
}
tags = merge(var.tags, {
Name = "${var.account_name}-monthly-budget"
})
depends_on = [aws_organizations_account.this]
}