mirror of
https://github.com/ghndrx/terraform-foundation.git
synced 2026-02-10 06:45:06 +00:00
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
This commit is contained in:
466
terraform/04-tenants/_template/main.tf
Normal file
466
terraform/04-tenants/_template/main.tf
Normal file
@@ -0,0 +1,466 @@
|
||||
################################################################################
|
||||
# Layer 04: Tenant - <TENANT_NAME>
|
||||
#
|
||||
# Creates tenant-specific resources:
|
||||
# - Security Groups (tenant-scoped, blocks cross-tenant traffic)
|
||||
# - IAM Roles with ABAC (can only access Tenant=X resources)
|
||||
# - Budgets with alerts
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/new-tenant.sh acme
|
||||
# cd terraform/04-tenants/acme
|
||||
# # Edit locals below
|
||||
# terraform init -backend-config=../../00-bootstrap/backend.hcl
|
||||
# terraform apply -var="state_bucket=YOUR_BUCKET"
|
||||
################################################################################
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
key = "04-tenants/<TENANT_NAME>/terraform.tfstate"
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Configuration - UPDATE THESE
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
# Tenant name (max 20 chars, lowercase, alphanumeric + hyphen)
|
||||
tenant = "<TENANT_NAME>"
|
||||
|
||||
# Environment
|
||||
env = "prod" # prod, staging, dev
|
||||
|
||||
# Short prefix for resources (tenant-env, max 28 chars total)
|
||||
prefix = "${local.tenant}-${local.env}"
|
||||
|
||||
# Apps with ports and budgets
|
||||
apps = {
|
||||
api = {
|
||||
port = 8080
|
||||
budget = 200
|
||||
owner = "team@example.com"
|
||||
}
|
||||
web = {
|
||||
port = 3000
|
||||
budget = 100
|
||||
owner = "team@example.com"
|
||||
}
|
||||
}
|
||||
|
||||
# Budget
|
||||
budget = 500
|
||||
alert_emails = ["ops@example.com"]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Variables
|
||||
################################################################################
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "state_bucket" {
|
||||
type = string
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Provider
|
||||
################################################################################
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
Tenant = local.tenant
|
||||
Environment = local.env
|
||||
ManagedBy = "terraform"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Data Sources
|
||||
################################################################################
|
||||
|
||||
data "terraform_remote_state" "network" {
|
||||
backend = "s3"
|
||||
config = {
|
||||
bucket = var.state_bucket
|
||||
key = "02-network/terraform.tfstate"
|
||||
region = var.region
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
################################################################################
|
||||
# Security Group - Base (intra-tenant)
|
||||
################################################################################
|
||||
|
||||
resource "aws_security_group" "base" {
|
||||
name = "${local.prefix}-base"
|
||||
description = "Base SG for ${local.tenant} - intra-tenant only"
|
||||
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
|
||||
|
||||
ingress {
|
||||
description = "Self"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
self = true
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = { Name = "${local.prefix}-base" }
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Security Group - Web (public)
|
||||
################################################################################
|
||||
|
||||
resource "aws_security_group" "web" {
|
||||
name = "${local.prefix}-web"
|
||||
description = "Web SG for ${local.tenant}"
|
||||
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
|
||||
|
||||
ingress {
|
||||
description = "HTTPS"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
ingress {
|
||||
description = "HTTP"
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = { Name = "${local.prefix}-web" }
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Security Group - Database
|
||||
################################################################################
|
||||
|
||||
resource "aws_security_group" "db" {
|
||||
name = "${local.prefix}-db"
|
||||
description = "DB SG for ${local.tenant}"
|
||||
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
|
||||
|
||||
ingress {
|
||||
description = "PostgreSQL"
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.base.id]
|
||||
}
|
||||
|
||||
ingress {
|
||||
description = "MySQL"
|
||||
from_port = 3306
|
||||
to_port = 3306
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.base.id]
|
||||
}
|
||||
|
||||
ingress {
|
||||
description = "Redis"
|
||||
from_port = 6379
|
||||
to_port = 6379
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.base.id]
|
||||
}
|
||||
|
||||
tags = { Name = "${local.prefix}-db" }
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Security Groups - Per App
|
||||
################################################################################
|
||||
|
||||
resource "aws_security_group" "app" {
|
||||
for_each = { for k, v in local.apps : k => v if v.port > 0 }
|
||||
|
||||
name = "${local.prefix}-${each.key}"
|
||||
description = "SG for ${local.tenant} ${each.key}"
|
||||
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
|
||||
|
||||
ingress {
|
||||
description = "App port"
|
||||
from_port = each.value.port
|
||||
to_port = each.value.port
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.base.id]
|
||||
}
|
||||
|
||||
ingress {
|
||||
description = "Self"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
self = true
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${local.prefix}-${each.key}"
|
||||
App = each.key
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# IAM Role - Admin (ABAC)
|
||||
################################################################################
|
||||
|
||||
resource "aws_iam_role" "admin" {
|
||||
name = "${local.prefix}-admin"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRole"
|
||||
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
|
||||
}]
|
||||
})
|
||||
|
||||
tags = { Name = "${local.prefix}-admin" }
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "admin" {
|
||||
name = "abac"
|
||||
role = aws_iam_role.admin.id
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "AllowTagged"
|
||||
Effect = "Allow"
|
||||
Action = ["ec2:*", "ecs:*", "ecr:*", "lambda:*", "rds:*", "s3:*", "dynamodb:*", "logs:*", "cloudwatch:*", "ssm:*", "secretsmanager:*", "elasticloadbalancing:*"]
|
||||
Resource = "*"
|
||||
Condition = { StringEquals = { "aws:ResourceTag/Tenant" = local.tenant } }
|
||||
},
|
||||
{
|
||||
Sid = "AllowDescribe"
|
||||
Effect = "Allow"
|
||||
Action = ["ec2:Describe*", "ecs:Describe*", "ecs:List*", "rds:Describe*", "s3:ListAllMyBuckets", "lambda:List*", "logs:Describe*", "elasticloadbalancing:Describe*"]
|
||||
Resource = "*"
|
||||
},
|
||||
{
|
||||
Sid = "AllowCreateTagged"
|
||||
Effect = "Allow"
|
||||
Action = ["ec2:RunInstances", "ec2:CreateVolume", "rds:CreateDBInstance", "s3:CreateBucket", "lambda:CreateFunction", "ecs:CreateCluster"]
|
||||
Resource = "*"
|
||||
Condition = { StringEquals = { "aws:RequestTag/Tenant" = local.tenant } }
|
||||
},
|
||||
{
|
||||
Sid = "AllowTagging"
|
||||
Effect = "Allow"
|
||||
Action = ["ec2:CreateTags", "rds:AddTagsToResource", "s3:PutBucketTagging", "lambda:TagResource"]
|
||||
Resource = "*"
|
||||
Condition = { StringEquals = { "aws:RequestTag/Tenant" = local.tenant } }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# IAM Role - Developer (limited)
|
||||
################################################################################
|
||||
|
||||
resource "aws_iam_role" "developer" {
|
||||
name = "${local.prefix}-dev"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRole"
|
||||
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
|
||||
}]
|
||||
})
|
||||
|
||||
tags = { Name = "${local.prefix}-dev" }
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "developer" {
|
||||
name = "dev-access"
|
||||
role = aws_iam_role.developer.id
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "ReadOnly"
|
||||
Effect = "Allow"
|
||||
Action = ["ec2:Describe*", "ecs:Describe*", "ecs:List*", "logs:*", "cloudwatch:Get*", "cloudwatch:List*", "ssm:GetParameter*"]
|
||||
Resource = "*"
|
||||
},
|
||||
{
|
||||
Sid = "DeployLambda"
|
||||
Effect = "Allow"
|
||||
Action = ["lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration"]
|
||||
Resource = "arn:aws:lambda:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:function:${local.tenant}-*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# IAM Role - ReadOnly
|
||||
################################################################################
|
||||
|
||||
resource "aws_iam_role" "readonly" {
|
||||
name = "${local.prefix}-ro"
|
||||
managed_policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRole"
|
||||
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
|
||||
}]
|
||||
})
|
||||
|
||||
tags = { Name = "${local.prefix}-ro" }
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Budget - Tenant Total
|
||||
################################################################################
|
||||
|
||||
resource "aws_budgets_budget" "tenant" {
|
||||
name = "${local.prefix}-total"
|
||||
budget_type = "COST"
|
||||
limit_amount = tostring(local.budget)
|
||||
limit_unit = "USD"
|
||||
time_unit = "MONTHLY"
|
||||
|
||||
cost_filter {
|
||||
name = "TagKeyValue"
|
||||
values = ["Tenant$${local.tenant}"]
|
||||
}
|
||||
|
||||
notification {
|
||||
comparison_operator = "GREATER_THAN"
|
||||
notification_type = "ACTUAL"
|
||||
threshold = 50
|
||||
threshold_type = "PERCENTAGE"
|
||||
subscriber_email_addresses = local.alert_emails
|
||||
}
|
||||
|
||||
notification {
|
||||
comparison_operator = "GREATER_THAN"
|
||||
notification_type = "ACTUAL"
|
||||
threshold = 80
|
||||
threshold_type = "PERCENTAGE"
|
||||
subscriber_email_addresses = local.alert_emails
|
||||
}
|
||||
|
||||
notification {
|
||||
comparison_operator = "GREATER_THAN"
|
||||
notification_type = "FORECASTED"
|
||||
threshold = 100
|
||||
threshold_type = "PERCENTAGE"
|
||||
subscriber_email_addresses = local.alert_emails
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Budget - Per App
|
||||
################################################################################
|
||||
|
||||
resource "aws_budgets_budget" "app" {
|
||||
for_each = local.apps
|
||||
|
||||
name = "${local.prefix}-${each.key}"
|
||||
budget_type = "COST"
|
||||
limit_amount = tostring(each.value.budget)
|
||||
limit_unit = "USD"
|
||||
time_unit = "MONTHLY"
|
||||
|
||||
cost_filter {
|
||||
name = "TagKeyValue"
|
||||
values = ["App$${each.key}"]
|
||||
}
|
||||
|
||||
notification {
|
||||
comparison_operator = "GREATER_THAN"
|
||||
notification_type = "ACTUAL"
|
||||
threshold = 90
|
||||
threshold_type = "PERCENTAGE"
|
||||
subscriber_email_addresses = [each.value.owner]
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Outputs
|
||||
################################################################################
|
||||
|
||||
output "tenant" {
|
||||
value = local.tenant
|
||||
}
|
||||
|
||||
output "security_groups" {
|
||||
value = {
|
||||
base = aws_security_group.base.id
|
||||
web = aws_security_group.web.id
|
||||
db = aws_security_group.db.id
|
||||
apps = { for k, v in aws_security_group.app : k => v.id }
|
||||
}
|
||||
}
|
||||
|
||||
output "iam_roles" {
|
||||
value = {
|
||||
admin = aws_iam_role.admin.arn
|
||||
developer = aws_iam_role.developer.arn
|
||||
readonly = aws_iam_role.readonly.arn
|
||||
}
|
||||
}
|
||||
|
||||
output "subnets" {
|
||||
value = data.terraform_remote_state.network.outputs.private_subnet_ids
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
value = data.terraform_remote_state.network.outputs.vpc_id
|
||||
}
|
||||
Reference in New Issue
Block a user