Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# aws-tf-modules
# aws-tf-modules

A collection of opinionated Terraform modules for AWS.

## Modules

| Module | Description |
|---|---|
| [acm-certificate](acm-certificate/) | ACM certificate with optional Route53 DNS validation |
| [github-oidc-iam-role](github-oidc-iam-role/) | IAM role for GitHub Actions OIDC authentication |
| [github-oidc-provider](github-oidc-provider/) | GitHub OIDC identity provider |
| [guardrails](guardrails/) | Account-level security guardrails (EBS, S3, IAM) |
| [http-api](http-api/) | API Gateway v2 HTTP API with Lambda integrations and authorizer |
| [http-api-domain](http-api-domain/) | Custom domain name for HTTP APIs (shared across multiple APIs) |
| [http-api-lambda-authorizer](http-api-lambda-authorizer/) | Lambda function pre-configured as an HTTP API REQUEST authorizer |
| [lambda-function](lambda-function/) | Lambda function with IAM role, CloudWatch logging, and permissions |
| [lambda-layer](lambda-layer/) | Lambda layer packaging |
| [ses-domain-identity](ses-domain-identity/) | SES domain identity with DNS records |
| [sqs](sqs/) | SQS queue with optional dead-letter queue |
| [static-site](static-site/) | Static site hosting via S3 + CloudFront + Route53 |

## HTTP API modules

The `http-api`, `http-api-domain`, and `http-api-lambda-authorizer` modules are designed to work together. See [http-api/README.md](http-api/README.md) for the complete multi-module wiring example.
71 changes: 71 additions & 0 deletions http-api-domain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# HTTP API Domain

This module sets up a custom domain name for API Gateway v2 HTTP APIs.

## Features

- `aws_apigatewayv2_domain_name` with TLS 1.2 and REGIONAL endpoint
- Optional Route53 A alias record (omit `zone_id` to manage DNS externally)
- Designed to be shared across multiple `http-api` instances at different base paths

## Design notes

**Why is the domain separate from the API?** A single domain (`api.mysite.com`) can serve multiple independent APIs mounted at different base paths (`/auth`, `/posts`, `/members`). If the domain lived inside each `http-api` module instance, every API would try to create the same domain resource. Keeping the domain here lets you create it once and map as many APIs to it as you need.

**Bring-your-own cert.** Use the `acm-certificate` module to provision the ACM certificate, then pass its ARN here. ACM certificates for API Gateway regional endpoints must be in the same AWS region as the API (unlike CloudFront, which requires `us-east-1`).

## Usage

See `variables.tf` for the full argument reference.

```hcl
module "cert" {
source = "github.com/script47/aws-tf-modules//acm-certificate"
domains = ["api.mysite.com"]
zone_id = "Z123456789ABCDEF"
tags = local.tags
}

module "api_domain" {
source = "github.com/script47/aws-tf-modules//http-api-domain"

domain_name = "api.mysite.com"
certificate_arn = module.cert.arn
zone_id = "Z123456789ABCDEF"

tags = {
Project = "my-project"
Environment = "production"
}
}
```

Then pass the domain to each `http-api` module:

```hcl
module "posts_api" {
source = "github.com/script47/aws-tf-modules//http-api"
name = "posts-api"

domain = {
name = module.api_domain.domain.name
base_path = "posts"
}

# ...
}

module "auth_api" {
source = "github.com/script47/aws-tf-modules//http-api"
name = "auth-api"

domain = {
name = module.api_domain.domain.name
base_path = "auth"
}

# ...
}
```

See `http-api/README.md` for the full multi-module wiring example.
10 changes: 10 additions & 0 deletions http-api-domain/domain.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource "aws_apigatewayv2_domain_name" "this" {
domain_name = var.domain_name
tags = var.tags

domain_name_configuration {
certificate_arn = var.certificate_arn
endpoint_type = "REGIONAL"
security_policy = "TLS_1_2"
}
}
8 changes: 8 additions & 0 deletions http-api-domain/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
output "domain" {
description = "Custom domain details"
value = {
name = aws_apigatewayv2_domain_name.this.domain_name
target = aws_apigatewayv2_domain_name.this.domain_name_configuration[0].target_domain_name
hosted_zone_id = aws_apigatewayv2_domain_name.this.domain_name_configuration[0].hosted_zone_id
}
}
10 changes: 10 additions & 0 deletions http-api-domain/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.13"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6"
}
}
}
13 changes: 13 additions & 0 deletions http-api-domain/route53.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
resource "aws_route53_record" "this" {
count = var.zone_id != null ? 1 : 0

zone_id = var.zone_id
name = var.domain_name
type = "A"

alias {
name = aws_apigatewayv2_domain_name.this.domain_name_configuration[0].target_domain_name
zone_id = aws_apigatewayv2_domain_name.this.domain_name_configuration[0].hosted_zone_id
evaluate_target_health = false
}
}
21 changes: 21 additions & 0 deletions http-api-domain/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
variable "domain_name" {
type = string
description = "The custom domain name (e.g. api.mysite.com)"
}

variable "certificate_arn" {
type = string
description = "ARN of the ACM certificate for this domain (must be in the same region as the API)"
}

variable "zone_id" {
type = string
description = "Route53 hosted zone ID. If provided, an A record alias will be created automatically"
default = null
}

variable "tags" {
type = map(string)
description = "The tags to apply to all resources created"
default = {}
}
62 changes: 62 additions & 0 deletions http-api-lambda-authorizer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# HTTP API Lambda Authorizer

This module sets up a Lambda function pre-configured as an HTTP API Gateway REQUEST authorizer.

## Features

- All features of the `lambda-function` module (IAM role, CloudWatch logs, layers, environment variables)
- Purpose-built for HTTP API Gateway v2 REQUEST authorizers
- The `http-api` module owns the Lambda permissions to avoid circular dependencies

## Design notes

**REQUEST type only.** The Lambda receives the full HTTP request and is responsible for validating the JWT from the `Authorization: Bearer <token>` header itself. This is different from the API Gateway-native JWT type, where API GW validates tokens directly against a JWKS endpoint (e.g. Cognito, Auth0). Use this module when you want full control over token validation logic in your Lambda code.

**No Lambda permissions created here.** When you wire this authorizer into an `http-api` module instance, that module creates the `aws_lambda_permission` granting `apigateway.amazonaws.com` the right to invoke this function - scoped to that API's `execution_arn`. Keeping permissions in the API module avoids a circular dependency: if this module created its own permission, it would need to reference the API's ARN, and the API already references this module's function ARN.

## Usage

See `variables.tf` for the full argument reference.

```hcl
module "authorizer" {
source = "github.com/script47/aws-tf-modules//http-api-lambda-authorizer"

name = "my-api-authorizer"
src = abspath("${path.module}/../dist/authorizer")
handler = "index.handler"

logs = {
enabled = true
retention_in_days = 14
}

tags = {
Project = "my-project"
Environment = "production"
}
}
```

Then pass the function ARN to `http-api`:

```hcl
module "api" {
source = "github.com/script47/aws-tf-modules//http-api"
name = "my-api"

authorizer = {
function_arn = module.authorizer.arn
}

routes = {
"GET /items" = {
function_arn = module.items_fn.arn
}
}

# ...
}
```

See `http-api/README.md` for the full multi-module wiring example.
20 changes: 20 additions & 0 deletions http-api-lambda-authorizer/lambda.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module "fn" {
source = "../lambda-function"

name = var.name
description = var.description
role_arn = var.role_arn
policy_arns = var.policy_arns
inline_policies = var.inline_policies
layer_arns = var.layer_arns
runtime = var.runtime
architectures = var.architectures
memory = var.memory
timeout = var.timeout
concurrency = var.concurrency
vars = var.vars
src = var.src
handler = var.handler
logs = var.logs
tags = var.tags
}
21 changes: 21 additions & 0 deletions http-api-lambda-authorizer/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
output "arn" {
value = module.fn.arn
}

output "function_name" {
value = module.fn.function_name
}

output "invoke_arn" {
value = module.fn.invoke_arn
}

output "fn" {
description = "Lambda function details"
value = module.fn.fn
}

output "log_group" {
description = "CloudWatch log group details (if enabled)"
value = module.fn.log_group
}
15 changes: 15 additions & 0 deletions http-api-lambda-authorizer/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
terraform {
required_version = ">= 1.13"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6"
}

archive = {
source = "hashicorp/archive"
version = ">= 2.0.0, < 3.0.0"
}
}
}
97 changes: 97 additions & 0 deletions http-api-lambda-authorizer/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
variable "name" {
type = string
description = "The function's name"
}

variable "description" {
type = string
default = ""
description = "The function's description"
}

variable "role_arn" {
type = string
description = "ARN of the role assumed by the function. If unspecified a role will be created"
default = null
}

variable "policy_arns" {
type = list(string)
description = "Optional list of policy ARNs to attach to the execution role"
default = []
}

variable "inline_policies" {
type = map(any)
description = "Map of inline IAM policy documents"
default = {}
}

variable "layer_arns" {
type = list(string)
default = []
description = "ARNs of Lambda layers to attach"
}

variable "runtime" {
type = string
default = "nodejs24.x"
description = "Lambda runtime environment identifier"
}

variable "architectures" {
type = set(string)
default = ["arm64"]
description = "A list of the supported architectures"
}

variable "memory" {
type = number
default = 128
description = "Allocated memory for the function"
}

variable "timeout" {
type = number
default = 3
description = "Lambda execution timeout in seconds"
}

variable "concurrency" {
type = number
default = -1
description = "Set the maximum execution concurrency"
}

variable "vars" {
type = map(string)
default = {}
description = "Environment variables available to the function"
}

variable "src" {
type = string
description = "The path to your function code"
}

variable "handler" {
type = string
description = "The function's entrypoint"
}

variable "logs" {
type = object({
enabled = optional(bool, true)
format = optional(string, "Text")
retention_in_days = optional(number, 30)
app_log_level = optional(string, null)
system_log_level = optional(string, null)
})
default = {}
}

variable "tags" {
type = map(string)
description = "The tags to apply to all resources created"
default = {}
}
Loading
Loading