mirror of
https://github.com/ghndrx/terraform-foundation.git
synced 2026-02-10 06:45:06 +00:00
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
410 lines
12 KiB
HCL
410 lines
12 KiB
HCL
################################################################################
|
|
# Workload: Lambda Function
|
|
#
|
|
# Deploys a serverless function:
|
|
# - Lambda function with VPC access (optional)
|
|
# - API Gateway HTTP API (optional)
|
|
# - CloudWatch logging & X-Ray tracing
|
|
# - EventBridge rules for scheduled invocation (optional)
|
|
#
|
|
# Usage:
|
|
# Copy this folder to 05-workloads/<tenant>-<app>/
|
|
# Update locals and variables
|
|
# terraform init -backend-config=../../00-bootstrap/backend.hcl
|
|
# terraform apply
|
|
################################################################################
|
|
|
|
terraform {
|
|
required_version = ">= 1.5"
|
|
|
|
required_providers {
|
|
aws = {
|
|
source = "hashicorp/aws"
|
|
version = ">= 5.0"
|
|
}
|
|
}
|
|
|
|
backend "s3" {
|
|
key = "05-workloads/<TENANT>-<APP>/terraform.tfstate"
|
|
}
|
|
}
|
|
|
|
################################################################################
|
|
# Configuration - UPDATE THESE
|
|
################################################################################
|
|
|
|
locals {
|
|
# Naming
|
|
tenant = "<TENANT>"
|
|
app = "<APP>"
|
|
env = "prod" # prod, staging, dev
|
|
name = "${local.tenant}-${local.app}-${local.env}"
|
|
|
|
# Lambda config
|
|
runtime = "python3.12" # python3.12, nodejs20.x, go1.x, etc.
|
|
handler = "main.handler"
|
|
memory_size = 256
|
|
timeout = 30
|
|
|
|
# Source - provide ONE of these
|
|
source_dir = null # Path to source directory (will be zipped)
|
|
s3_bucket = null # S3 bucket containing deployment package
|
|
s3_key = null # S3 key for deployment package
|
|
image_uri = null # Container image URI
|
|
|
|
# VPC - set to true for database access
|
|
enable_vpc = false
|
|
|
|
# API Gateway
|
|
enable_api = true
|
|
api_path = "/{proxy+}"
|
|
|
|
# Scheduled execution (cron or rate expression)
|
|
schedule_expression = null # "rate(5 minutes)" or "cron(0 12 * * ? *)"
|
|
|
|
# Environment variables
|
|
environment = {
|
|
APP_ENV = local.env
|
|
LOG_LEVEL = "INFO"
|
|
}
|
|
|
|
# Secrets (ARNs to SSM/Secrets Manager)
|
|
secrets = {}
|
|
}
|
|
|
|
################################################################################
|
|
# 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
|
|
App = local.app
|
|
Environment = local.env
|
|
ManagedBy = "terraform"
|
|
}
|
|
}
|
|
}
|
|
|
|
################################################################################
|
|
# Data Sources
|
|
################################################################################
|
|
|
|
data "terraform_remote_state" "network" {
|
|
count = local.enable_vpc ? 1 : 0
|
|
backend = "s3"
|
|
config = {
|
|
bucket = var.state_bucket
|
|
key = "02-network/terraform.tfstate"
|
|
region = var.region
|
|
}
|
|
}
|
|
|
|
data "terraform_remote_state" "tenant" {
|
|
count = local.enable_vpc ? 1 : 0
|
|
backend = "s3"
|
|
config = {
|
|
bucket = var.state_bucket
|
|
key = "04-tenants/${local.tenant}/terraform.tfstate"
|
|
region = var.region
|
|
}
|
|
}
|
|
|
|
data "aws_caller_identity" "current" {}
|
|
data "aws_region" "current" {}
|
|
|
|
################################################################################
|
|
# CloudWatch Log Group
|
|
################################################################################
|
|
|
|
resource "aws_cloudwatch_log_group" "main" {
|
|
name = "/aws/lambda/${local.name}"
|
|
retention_in_days = 30
|
|
|
|
tags = { Name = local.name }
|
|
}
|
|
|
|
################################################################################
|
|
# IAM Role
|
|
################################################################################
|
|
|
|
resource "aws_iam_role" "lambda" {
|
|
name = "${local.name}-lambda"
|
|
|
|
assume_role_policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [{
|
|
Effect = "Allow"
|
|
Action = "sts:AssumeRole"
|
|
Principal = { Service = "lambda.amazonaws.com" }
|
|
}]
|
|
})
|
|
|
|
tags = { Name = "${local.name}-lambda" }
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "lambda_basic" {
|
|
role = aws_iam_role.lambda.name
|
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "lambda_vpc" {
|
|
count = local.enable_vpc ? 1 : 0
|
|
role = aws_iam_role.lambda.name
|
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "lambda_xray" {
|
|
role = aws_iam_role.lambda.name
|
|
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
}
|
|
|
|
resource "aws_iam_role_policy" "lambda_app" {
|
|
name = "app-permissions"
|
|
role = aws_iam_role.lambda.id
|
|
|
|
policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Sid = "AllowTaggedResources"
|
|
Effect = "Allow"
|
|
Action = ["s3:GetObject", "s3:PutObject", "dynamodb:*", "sqs:*", "sns:Publish"]
|
|
Resource = "*"
|
|
Condition = { StringEquals = { "aws:ResourceTag/Tenant" = local.tenant } }
|
|
},
|
|
{
|
|
Sid = "SecretsAccess"
|
|
Effect = "Allow"
|
|
Action = ["secretsmanager:GetSecretValue", "ssm:GetParameter", "ssm:GetParameters"]
|
|
Resource = length(local.secrets) > 0 ? values(local.secrets) : ["arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${local.tenant}/*"]
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
################################################################################
|
|
# Security Group (VPC mode)
|
|
################################################################################
|
|
|
|
resource "aws_security_group" "lambda" {
|
|
count = local.enable_vpc ? 1 : 0
|
|
name = "${local.name}-lambda"
|
|
description = "Lambda function ${local.name}"
|
|
vpc_id = data.terraform_remote_state.network[0].outputs.vpc_id
|
|
|
|
egress {
|
|
from_port = 0
|
|
to_port = 0
|
|
protocol = "-1"
|
|
cidr_blocks = ["0.0.0.0/0"]
|
|
}
|
|
|
|
tags = { Name = "${local.name}-lambda" }
|
|
}
|
|
|
|
################################################################################
|
|
# Lambda Function
|
|
################################################################################
|
|
|
|
# Create zip from source directory if provided
|
|
data "archive_file" "lambda" {
|
|
count = local.source_dir != null ? 1 : 0
|
|
type = "zip"
|
|
source_dir = local.source_dir
|
|
output_path = "${path.module}/lambda.zip"
|
|
}
|
|
|
|
resource "aws_lambda_function" "main" {
|
|
function_name = local.name
|
|
description = "${local.tenant} ${local.app} function"
|
|
role = aws_iam_role.lambda.arn
|
|
|
|
# Source - exactly one must be specified
|
|
filename = local.source_dir != null ? data.archive_file.lambda[0].output_path : null
|
|
source_code_hash = local.source_dir != null ? data.archive_file.lambda[0].output_base64sha256 : null
|
|
s3_bucket = local.s3_bucket
|
|
s3_key = local.s3_key
|
|
image_uri = local.image_uri
|
|
package_type = local.image_uri != null ? "Image" : "Zip"
|
|
|
|
# Only for Zip packages
|
|
runtime = local.image_uri == null ? local.runtime : null
|
|
handler = local.image_uri == null ? local.handler : null
|
|
|
|
memory_size = local.memory_size
|
|
timeout = local.timeout
|
|
|
|
environment {
|
|
variables = merge(local.environment, {
|
|
for k, v in local.secrets : k => v
|
|
})
|
|
}
|
|
|
|
dynamic "vpc_config" {
|
|
for_each = local.enable_vpc ? [1] : []
|
|
content {
|
|
subnet_ids = data.terraform_remote_state.network[0].outputs.private_subnet_ids
|
|
security_group_ids = [
|
|
aws_security_group.lambda[0].id,
|
|
data.terraform_remote_state.tenant[0].outputs.security_groups.base
|
|
]
|
|
}
|
|
}
|
|
|
|
tracing_config {
|
|
mode = "Active"
|
|
}
|
|
|
|
depends_on = [aws_cloudwatch_log_group.main]
|
|
|
|
tags = { Name = local.name }
|
|
}
|
|
|
|
################################################################################
|
|
# API Gateway HTTP API
|
|
################################################################################
|
|
|
|
resource "aws_apigatewayv2_api" "main" {
|
|
count = local.enable_api ? 1 : 0
|
|
name = local.name
|
|
protocol_type = "HTTP"
|
|
|
|
cors_configuration {
|
|
allow_origins = ["*"]
|
|
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
allow_headers = ["Content-Type", "Authorization"]
|
|
max_age = 300
|
|
}
|
|
|
|
tags = { Name = local.name }
|
|
}
|
|
|
|
resource "aws_apigatewayv2_stage" "main" {
|
|
count = local.enable_api ? 1 : 0
|
|
api_id = aws_apigatewayv2_api.main[0].id
|
|
name = "$default"
|
|
auto_deploy = true
|
|
|
|
access_log_settings {
|
|
destination_arn = aws_cloudwatch_log_group.api[0].arn
|
|
format = jsonencode({
|
|
requestId = "$context.requestId"
|
|
ip = "$context.identity.sourceIp"
|
|
requestTime = "$context.requestTime"
|
|
httpMethod = "$context.httpMethod"
|
|
routeKey = "$context.routeKey"
|
|
status = "$context.status"
|
|
responseLength = "$context.responseLength"
|
|
integrationError = "$context.integrationErrorMessage"
|
|
})
|
|
}
|
|
|
|
tags = { Name = local.name }
|
|
}
|
|
|
|
resource "aws_cloudwatch_log_group" "api" {
|
|
count = local.enable_api ? 1 : 0
|
|
name = "/aws/apigateway/${local.name}"
|
|
retention_in_days = 30
|
|
|
|
tags = { Name = "${local.name}-api" }
|
|
}
|
|
|
|
resource "aws_apigatewayv2_integration" "main" {
|
|
count = local.enable_api ? 1 : 0
|
|
api_id = aws_apigatewayv2_api.main[0].id
|
|
integration_type = "AWS_PROXY"
|
|
integration_uri = aws_lambda_function.main.invoke_arn
|
|
payload_format_version = "2.0"
|
|
}
|
|
|
|
resource "aws_apigatewayv2_route" "main" {
|
|
count = local.enable_api ? 1 : 0
|
|
api_id = aws_apigatewayv2_api.main[0].id
|
|
route_key = "ANY ${local.api_path}"
|
|
target = "integrations/${aws_apigatewayv2_integration.main[0].id}"
|
|
}
|
|
|
|
resource "aws_lambda_permission" "api" {
|
|
count = local.enable_api ? 1 : 0
|
|
statement_id = "AllowAPIGateway"
|
|
action = "lambda:InvokeFunction"
|
|
function_name = aws_lambda_function.main.function_name
|
|
principal = "apigateway.amazonaws.com"
|
|
source_arn = "${aws_apigatewayv2_api.main[0].execution_arn}/*/*"
|
|
}
|
|
|
|
################################################################################
|
|
# EventBridge Schedule
|
|
################################################################################
|
|
|
|
resource "aws_cloudwatch_event_rule" "schedule" {
|
|
count = local.schedule_expression != null ? 1 : 0
|
|
name = "${local.name}-schedule"
|
|
description = "Schedule for ${local.name}"
|
|
schedule_expression = local.schedule_expression
|
|
|
|
tags = { Name = "${local.name}-schedule" }
|
|
}
|
|
|
|
resource "aws_cloudwatch_event_target" "schedule" {
|
|
count = local.schedule_expression != null ? 1 : 0
|
|
rule = aws_cloudwatch_event_rule.schedule[0].name
|
|
target_id = "lambda"
|
|
arn = aws_lambda_function.main.arn
|
|
}
|
|
|
|
resource "aws_lambda_permission" "schedule" {
|
|
count = local.schedule_expression != null ? 1 : 0
|
|
statement_id = "AllowEventBridge"
|
|
action = "lambda:InvokeFunction"
|
|
function_name = aws_lambda_function.main.function_name
|
|
principal = "events.amazonaws.com"
|
|
source_arn = aws_cloudwatch_event_rule.schedule[0].arn
|
|
}
|
|
|
|
################################################################################
|
|
# Outputs
|
|
################################################################################
|
|
|
|
output "function_name" {
|
|
value = aws_lambda_function.main.function_name
|
|
}
|
|
|
|
output "function_arn" {
|
|
value = aws_lambda_function.main.arn
|
|
}
|
|
|
|
output "invoke_arn" {
|
|
value = aws_lambda_function.main.invoke_arn
|
|
}
|
|
|
|
output "api_endpoint" {
|
|
value = local.enable_api ? aws_apigatewayv2_api.main[0].api_endpoint : null
|
|
}
|
|
|
|
output "log_group" {
|
|
value = aws_cloudwatch_log_group.main.name
|
|
}
|
|
|
|
output "role_arn" {
|
|
value = aws_iam_role.lambda.arn
|
|
}
|