Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS App Runner Terraform Module

A secure-by-default, fully managed container/source-to-URL compute service — deploy an ECR image or build straight from a GitHub repo, with derived VPC egress, X-Ray tracing on by default, and no servers to patch. Built for the AWS provider v6.x.

Terraform >= 1.12 aws 6.x module v1.0.0 type=composite resources=9


🧩 Overview

  • 🚀 Deploys AWS App Runner — a fully managed service that builds and runs a web application or API from either a container image (ECR/ECR Public) or a GitHub source repository, with automatic TLS, scaling, and a public HTTPS endpoint out of the box.
  • 🔒 Derives egress_type automatically — wiring a VPC connector (created here or supplied externally) flips the service to private VPC egress; omitting one leaves the AWS default public-internet egress. No separate toggle to forget.
  • 🕵️ X-Ray observability ON by default — every service gets an attached aws_apprunner_observability_configuration unless explicitly opted out, giving a regulated FI's compute an audit trail from day one.
  • 🔑 Supports a customer-managed KMS key for the source-repo copy and service logs App Runner maintains internally (AWS-managed key by default).
  • 🌐 Composable child resources — auto scaling configurations, VPC connectors, VPC Ingress Connections (private ingress), custom domain associations, and GitHub connections — all for_each over stable-keyed maps.
  • 🏷️ Universal tags on every taggable child resource, with tags_all surfaced for governance/cost allocation.

💡 Why it matters: App Runner is the fastest path from a container or a GitHub branch to a running, TLS-terminated, auto-scaled HTTPS endpoint — without provisioning a single EC2 instance, ALB, or ECS service. For, that speed can't come at the cost of an accidentally public database dependency or an untraceable production service — this module makes the secure path the default path.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

flowchart LR
 subgraph Foundations
 IAM[terraform-aws-iam-role]
 KMS[terraform-aws-kms]
 VPC[terraform-aws-vpc]
 SG[terraform-aws-security-group]
 end

 subgraph Supporting
 ECR[terraform-aws-ecr]
 SM[terraform-aws-secrets-manager]
 SSM[terraform-aws-ssm]
 VPCE[terraform-aws-vpc-endpoint]
 R53[terraform-aws-route53-zone]
 end

 APPR[["terraform-aws-apprunner"]]

 subgraph Serverless family - Phase 7
 LAMBDA[terraform-aws-lambda]
 SFN[terraform-aws-sfn]
 end

 IAM -->|access_role_arn / instance_role_arn| APPR
 KMS -->|kms_key_arn| APPR
 VPC -->|subnets| APPR
 SG -->|security_groups| APPR
 ECR -->|image_identifier| APPR
 SM -->|runtime_environment_secrets| APPR
 SSM -->|runtime_environment_secrets| APPR
 VPCE -->|vpc_endpoint_id| APPR
 APPR -->|service_url CNAME target| R53
 APPR -.siblings.-> LAMBDA
 APPR -.siblings.-> SFN

 style APPR fill:#FF9900,color:#fff
Loading

Rendered and validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) during authoring.


🧬 What this module builds

flowchart TB
 SVC[["aws_apprunner_service.this (keystone)"]]

 CONN[aws_apprunner_connection]
 ASV[aws_apprunner_auto_scaling_configuration_version]
 OBS[aws_apprunner_observability_configuration]
 VPCC[aws_apprunner_vpc_connector]

 CDA[aws_apprunner_custom_domain_association]
 VIC[aws_apprunner_vpc_ingress_connection]
 DASV[aws_apprunner_default_auto_scaling_configuration_version]
 DEP[aws_apprunner_deployment]

 CONN -->|connection_arn| SVC
 ASV -->|auto_scaling_configuration_arn| SVC
 OBS -->|observability_configuration_arn| SVC
 VPCC -->|vpc_connector_arn egress| SVC

 SVC -->|service_arn| CDA
 SVC -->|service_arn| VIC
 SVC -->|service_arn| DEP
 ASV -->|promote to account default| DASV

 style SVC fill:#FF9900,color:#fff
Loading

Rendered and validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) during authoring.


✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws provider >= 6.0, < 7.0

No provider {} block ships inside this module — the caller's root module configures the aws provider (region, credentials, default_tags).


🔑 Required IAM Permissions

Action Required for Notes
apprunner:CreateService, DescribeService, UpdateService, DeleteService, ListServices, TagResource/UntagResource Service lifecycle Core keystone actions
apprunner:CreateAutoScalingConfiguration, DescribeAutoScalingConfiguration, DeleteAutoScalingConfiguration, ListAutoScalingConfigurations auto_scaling_configurations Each config revision is immutable
apprunner:UpdateDefaultAutoScalingConfiguration default_auto_scaling_configuration_key Account/Region-wide side effect — grant sparingly
apprunner:CreateConnection, DescribeConnection, DeleteConnection, ListConnections connections Connection still needs a manual console handshake
apprunner:AssociateCustomDomain, DescribeCustomDomains, DisassociateCustomDomain custom_domain_associations
apprunner:CreateVpcConnector, DescribeVpcConnector, DeleteVpcConnector, ListVpcConnectors vpc_connectors
apprunner:CreateVpcIngressConnection, DescribeVpcIngressConnection, UpdateVpcIngressConnection, DeleteVpcIngressConnection vpc_ingress_connections
apprunner:CreateObservabilityConfiguration, DescribeObservabilityConfiguration, DeleteObservabilityConfiguration observability_configuration (ON by default)
apprunner:StartDeployment trigger_deployment
iam:PassRole on the access-role and instance-role ARNs authentication_configuration.access_role_arn, instance_configuration.instance_role_arn Scope to the specific role ARNs, never iam:PassRole *
kms:CreateGrant, kms:DescribeKey encryption_configuration.kms_key_arn Only when a customer-managed CMK is supplied
ec2:DescribeNetworkInterfaces, DescribeSubnets, DescribeSecurityGroups vpc_connectors read-back ENI provisioning itself is performed by App Runner's own service-linked role

App Runner auto-creates AWSServiceRoleForAppRunner on first use — the executing principal should have iam:CreateServiceLinkedRole scoped to apprunner.amazonaws.com for a clean first apply.


📋 AWS Prerequisites

  • Service-linked roleAWSServiceRoleForAppRunner is created automatically on first use in an account/Region; no manual step required beyond the IAM permission above.
  • ECR private repositories need an access role. Set source_configuration.authentication_configuration.access_role_arn to a role trusting build.apprunner.amazonaws.com with ECR pull permissions. ECR Public repositories need none.
  • GitHub code_repository sources need a completed connection handshake. aws_apprunner_connection creates the connection object in PENDING_HANDSHAKE; a human must approve it once in the AWS Console (App Runner → GitHub connections) before it can back a service. Terraform cannot automate this interactive OAuth step.
  • VPC egress is opt-in. Without a vpc_connectors entry wired through network_configuration, the service can only reach the public internet — not a private RDS/ElastiCache/internal endpoint.
  • Region availability — App Runner is not available in every AWS Region; confirm availability in the target Region. No us-east-1 global-resource coupling (unlike CloudFront/ACM/WAFv2).
  • Quotas — 60 services per account/Region (soft), 100 vCPUs / 100 running instances per account/Region (soft), 100 VPC connectors per account/Region, 5 custom domains per service (soft).

📁 Module Structure

terraform-aws-apprunner/
├── providers.tf
├── variables.tf
├── main.tf
├── outputs.tf
├── README.md
└── SCOPE.md

⚙️ Quick Start

module "apprunner" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  instance_configuration = {
    instance_role_arn = module.apprunner_instance_role.arn
  }

  tags = {
    Environment = "prod"
    Application = "loan-api"
  }
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
source_configuration.image_repository.image_identifier string (ECR URI) terraform-aws-ecr
source_configuration.authentication_configuration.access_role_arn string (IAM ARN) terraform-aws-iam-role
instance_configuration.instance_role_arn string (IAM ARN) terraform-aws-iam-role
vpc_connectors[*].subnets list(string) terraform-aws-vpc
vpc_connectors[*].security_groups list(string) terraform-aws-security-group
vpc_ingress_connections[*].vpc_endpoint_id string terraform-aws-vpc-endpoint
encryption_configuration.kms_key_arn string (ARN) terraform-aws-kms
*.runtime_environment_secrets values string (ARN) terraform-aws-secrets-manager / terraform-aws-ssm

Emits

Output Description Consumed by
id Service id Root-module cross-references
arn Service ARN IAM policies, this module's own vpc_ingress_connections/custom_domain_associations
service_url Default HTTPS subdomain Route 53 CNAME target
egress_type Resolved DEFAULT/VPC egress mode Network audit tooling
vpc_connector_arns Map name → ARN Sibling module calls sharing a connector
custom_domain_certificate_validation_records / custom_domain_dns_targets DNS records to create Route 53 automation
connection_arns / connection_statuses GitHub connection state Runbook gating on AVAILABLE
tags_all Merged tags Governance/cost allocation

📚 Example Library

1 · Minimal — ECR image-based service
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }
}
2 · GitHub source-repository (code_repository) service
module "apprunner_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-marketing-site"

  connections = {
    github = { provider_type = "GITHUB" }
  }

  source_configuration = {
    code_repository = {
      repository_url = "https://github.com/microsoftexpert/marketing-site"
      source_code_version = {
        value = "main"
      }
      code_configuration = {
        configuration_source = "REPOSITORY" # reads apprunner.yaml
      }
    }
    authentication_configuration = {
      connection_arn = module.apprunner_connection.connection_arns["github"]
    }
  }
}

# NOTE: complete the OAuth handshake once in the AWS Console (App Runner ->
# GitHub connections) before this service can deploy successfully.
3 · VPC-connected service reaching a private RDS instance
module "apprunner_backoffice" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-backoffice"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  vpc_connectors = {
    private = {
      subnets         = module.vpc.private_subnet_ids
      security_groups = [module.apprunner_sg.id]
    }
  }

  # egress_type is derived to "VPC" automatically because vpc_connector_key is set.
  network_configuration = {
    egress_configuration = {
      vpc_connector_key = "private"
    }
  }

  instance_configuration = {
    instance_role_arn = module.apprunner_instance_role.arn
  }
}

# module.apprunner_sg would allow egress to module.rds's security group on 5432/3306.
4 · Custom domain association
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  custom_domain_associations = {
    api = {
      domain_name          = "api.farmcreditcasey.example"
      enable_www_subdomain = false
    }
  }
}

# After apply, create the CNAME records from
# module.apprunner_api.custom_domain_certificate_validation_records["api"]
# plus a CNAME from api.farmcreditcasey.example to
# module.apprunner_api.custom_domain_dns_targets["api"].
5 · Private ingress via VPC endpoint (no public access)
module "apprunner_internal" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-internal-tool"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  network_configuration = {
    ingress_configuration = {
      is_publicly_accessible = false
    }
  }

  vpc_ingress_connections = {
    internal = {
      vpc_id          = module.vpc.id
      vpc_endpoint_id = module.apprunner_vpc_endpoint.id
    }
  }
}
6 · X-Ray observability — explicit naming
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  observability_configuration = {
    name = "casey-loan-api-xray"
  }
}

# observability_configuration.enabled defaults to true — this example only
# customizes the resource's name; tracing is already ON without this block.
7 · Secure-by-default opt-out — disable X-Ray tracing
module "apprunner_lowrisk" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-public-docs"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  # Discouraged for regulated workloads — documented exception required.
  observability_configuration = {
    enabled = false
  }
}
8 · Customer-managed KMS key for source/log encryption
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  encryption_configuration = {
    kms_key_arn = module.kms.arn
  }
}
9 · Auto scaling configuration — custom concurrency/instance bounds
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  auto_scaling_configurations = {
    api-scaling = {
      max_concurrency = 50
      min_size        = 2
      max_size        = 10
    }
  }

  auto_scaling_configuration_key = "api-scaling"
}
10 · Environment variables and secrets from Secrets Manager
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
      image_configuration = {
        port = "8080"
        runtime_environment_variables = {
          LOG_LEVEL = "info"
        }
        runtime_environment_secrets = {
          DATABASE_URL = module.secrets_manager.arn
        }
      }
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }
}
11 · `for_each` pattern — one service per environment
locals {
  apprunner_services = {
    dev  = { image_tag = "dev-latest" }
    test = { image_tag = "test-latest" }
    prod = { image_tag = "latest" }
  }
}

module "apprunner" {
  source   = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"
  for_each = local.apprunner_services

  service_name = "casey-loan-api-${each.key}"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:${each.value.image_tag}"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  tags = {
    Environment = each.key
  }
}
12 · Manual deployment trigger (redeploy the same image tag)
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  trigger_deployment = true
}
13 · Import an existing App Runner service
import {
  to = module.apprunner_api.aws_apprunner_service.this
  id = "arn:aws:apprunner:us-east-2:123456789012:service/casey-loan-api/abc123def456"
}
14 · Promote a custom auto scaling configuration to the account default (opt-in)
module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  auto_scaling_configurations = {
    org-standard = {
      min_size = 1
      max_size = 25
    }
  }

  # Account/Region-wide side effect: every future service with no explicit
  # auto_scaling_configuration_arn inherits this configuration.
  default_auto_scaling_configuration_key = "org-standard"
}
15 · End-to-end composition — ECR + VPC + RDS + App Runner
module "vpc" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-vpc?ref=v1.0.0"
  #...
}

module "ecr" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-ecr?ref=v1.0.0"
  #...
}

module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  #...
}

module "rds" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds?ref=v1.0.0"

  identifier             = "casey-loan-db"
  vpc_security_group_ids = [module.rds_sg.id]
  db_subnet_group_name   = module.vpc.database_subnet_group_name
  kms_key_arn            = module.kms.arn
  #...
}

module "apprunner_ecr_access_role" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  # trust policy: build.apprunner.amazonaws.com
  # inline policy: ecr:GetDownloadUrlForLayer / BatchGetImage / GetAuthorizationToken on module.ecr.arn
}

module "apprunner_instance_role" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  # trust policy: tasks.apprunner.amazonaws.com
}

module "apprunner_sg" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-security-group?ref=v1.0.0"

  vpc_id = module.vpc.id
  egress_rules = {
    to_rds = {
      referenced_security_group_id = module.rds_sg.id
      from_port                    = 5432
      to_port                      = 5432
      ip_protocol                  = "tcp"
    }
  }
}

module "apprunner_api" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-apprunner?ref=v1.0.0"

  service_name = "casey-loan-api"

  source_configuration = {
    image_repository = {
      image_identifier      = "${module.ecr.repository_url}:latest"
      image_repository_type = "ECR"
      image_configuration = {
        runtime_environment_secrets = {
          DATABASE_URL = module.rds.master_user_secret_arn
        }
      }
    }
    authentication_configuration = {
      access_role_arn = module.apprunner_ecr_access_role.arn
    }
  }

  instance_configuration = {
    instance_role_arn = module.apprunner_instance_role.arn
  }

  vpc_connectors = {
    private = {
      subnets         = module.vpc.private_subnet_ids
      security_groups = [module.apprunner_sg.id]
    }
  }

  network_configuration = {
    egress_configuration = {
      vpc_connector_key = "private"
    }
  }

  encryption_configuration = {
    kms_key_arn = module.kms.arn
  }

  tags = {
    Environment = "prod"
    Application = "loan-api"
  }
}

📥 Inputs

Core

  • service_name — service name (FORCE-NEW)
  • source_configuration — exactly one of image_repository or code_repository, plus authentication_configuration and auto_deployments_enabled

Compute

  • instance_configurationcpu (default "1024"), memory (default "2048"), instance_role_arn

Networking

  • network_configurationip_address_type, ingress_configuration.is_publicly_accessible, egress_configuration.vpc_connector_arn/vpc_connector_key (derives egress_type)
  • vpc_connectors — map of private-egress VPC connectors to create
  • vpc_ingress_connections — map of private-ingress PrivateLink connections

Health & Reliability

  • health_check_configuration — protocol/path/interval/timeout/thresholds

Encryption

  • encryption_configurationkms_key_arn (FORCE-NEW; null = AWS-managed key)

Scaling

  • auto_scaling_configurations — map of named scaling profiles to create
  • auto_scaling_configuration_arn / auto_scaling_configuration_key — which one the service uses
  • default_auto_scaling_configuration_key — promote one to the account/Region default (opt-in)

Observability

  • observability_configurationenabled (default true), name, trace_configuration.vendor

Domains & Source

  • custom_domain_associations — map of custom domains
  • connections — map of GitHub source-repository connections

Operational

  • trigger_deployment — force a redeploy
  • tags, timeouts

🧾 Outputs

  • id, arn — primary identifiers
  • service_name, service_id, service_url, status
  • auto_scaling_configuration_arns, service_auto_scaling_configuration_arn
  • vpc_connector_arns, egress_type
  • vpc_ingress_connection_arns, vpc_ingress_connection_domain_names
  • custom_domain_association_ids, custom_domain_certificate_validation_records, custom_domain_dns_targets
  • connection_arns, connection_statuses
  • observability_configuration_arnnull when observability is disabled
  • tags_all

No output is sensitive = true — this module emits no secrets; runtime_environment_secrets values are ARNs pointing at Secrets Manager/SSM, not the secret values themselves.


🧠 Architecture Notes

  • ARN format: arn:aws:apprunner:<region>:<account>:service/<service_name>/<service_id>. Child resources (VPC connector, auto scaling configuration, observability configuration) share the pattern arn:aws:apprunner:<region>:<account>:<type>/<name>/<id>/<revision>.
  • service_name and encryption_configuration are FORCE-NEW. Changing either destroys and recreates the service, generating a new service_id/service_url and breaking any DNS CNAMEs pointed at the old URL.
  • No timeouts block on the service itself — a real provider gotcha for this module; only aws_apprunner_deployment supports a create timeout.
  • Auto scaling configurations and observability configurations are immutable, versioned resources. Every field is force-new; a change registers a new revision under the same name, and the old revision goes INACTIVE (reclaimed by AWS after a delay, not immediately deleted).
  • egress_typevpc_connector_arn coupling is handled for you: this module derives egress_type from whether a connector is wired, closing the common "connector created but never actually used" misconfiguration.
  • tagstags_all ↔ provider default_tags — standard interaction; resource tags win on key conflict. aws_apprunner_default_auto_scaling_configuration_version and aws_apprunner_deployment are not taggable.
  • Eventual consistency / async validation — custom domain association validation is DNS-based and asynchronous; certificate_validation_records/dns_target are available immediately, but the domain doesn't reach ACTIVE until the caller creates the DNS records and App Runner observes them.
  • Destroy orderingvpc_ingress_connections and custom_domain_associations reference the service by ARN; Terraform's implicit dependency graph destroys them before the service. A vpc_connectors entry still attached to a running service cannot be deleted — detach (switch egress back to DEFAULT or another connector) first.
  • No us-east-1 global-resource requirement — App Runner is a regional service; unlike CloudFront/ACM/WAFv2, there is no global-resource coupling to document here.

🧱 Design Principles

  • Egress network path defaults to public (DEFAULT) unless a VPC connector is wired — but the moment one is wired (via vpc_connector_arn or vpc_connector_key), the module automatically switches to "VPC" egress. Opt-out: simply don't wire a connector.
  • Encryption at rest defaults to the AWS-managed key (encryption_configuration = null). Opt-out (toward stronger posture): supply encryption_configuration.kms_key_arn for an auditable, revocable CMK.
  • X-Ray observability defaults ON (observability_configuration.enabled = true). Opt-out (discouraged): set observability_configuration.enabled = false.
  • Public reachability defaults to true (App Runner's core purpose). Opt-out toward a private posture: set network_configuration.ingress_configuration.is_publicly_accessible = false and add a vpc_ingress_connections entry.
  • VPC connector security groups default to the VPC's default security group (allows all outbound) when security_groups is omitted. Opt-out toward least privilege: always supply an explicit security_groups list for PII-adjacent workloads.
  • Account-wide default scaling profile is never touched unless explicitly requested via default_auto_scaling_configuration_key — a side effect this module refuses to trigger silently.

🚀 Runbook

cd terraform-aws-apprunner
terraform init -backend=false
terraform validate
terraform fmt -check

⚠️ Pin the module source with ?ref=v1.0.0 — never a branch — for reproducible applies.

terraform plan/apply additionally require valid AWS credentials (profile, SSO session, or OIDC role) and a configured Region with App Runner availability.

terraform plan
terraform apply
terraform output

🧪 Testing

  • terraform init -backend=false / validate / fmt -check — pure static validation, no AWS credentials required.
  • tflint (AWS ruleset) and checkov via the shared validate_all_modules.ps1 harness.
  • Live testing (a human, out of band): apply in a non-production account with a least-privilege role, confirm service_url responds over HTTPS, confirm status = RUNNING, and (if vpc_connectors is used) confirm connectivity to the private dependency from inside the service.

💬 Example Output

$ terraform output
arn = "arn:aws:apprunner:us-east-2:123456789012:service/casey-loan-api/6b1f2e6b3c9a4d9e8f0a1b2c3d4e5f60"
egress_type = "VPC"
id = "arn:aws:apprunner:us-east-2:123456789012:service/casey-loan-api/6b1f2e6b3c9a4d9e8f0a1b2c3d4e5f60"
service_id = "6b1f2e6b3c9a4d9e8f0a1b2c3d4e5f60"
service_name = "casey-loan-api"
service_url = "abc123def456.us-east-2.awsapprunner.com"
status = "RUNNING"
observability_configuration_arn = "arn:aws:apprunner:us-east-2:123456789012:observabilityconfiguration/casey-loan-api-observability/1/..."

🔍 Troubleshooting

  • Tag drift from default_tags overlap — if a key exists in both the provider's default_tags and this module's tags, tags_all reflects the resource-tag value (it wins); avoid duplicating the same key in both places.
  • Credential-chain failuresterraform plan/apply fail with NoCredentialProviders or similar when no AWS_PROFILE/SSO session/OIDC role is active; this module never accepts credentials as variables, so fix the chain at the shell/CI level.
  • Service stuck in CREATE_FAILED — almost always a missing/incorrect authentication_configuration.access_role_arn (private ECR) or an unapproved aws_apprunner_connection handshake (GitHub). Check apprunner:DescribeService events in the console.
  • Service can't reach a private RDS/ElastiCache endpoint — confirm network_configuration.egress_configuration actually resolves to "VPC" (check the egress_type output); a vpc_connectors entry that's defined but never referenced by vpc_connector_arn/vpc_connector_key leaves the service on public egress.
  • IAM AccessDenied on apprunner:CreateService/etc. — the executing role is missing an action from the Required IAM Permissions table above; add the specific action, don't broaden to apprunner:*.
  • iam:PassRole deniedaccess_role_arn/instance_role_arn require the Terraform principal to have iam:PassRole scoped to those exact role ARNs.
  • Destroy hangs on a vpc_connectors entry — the connector is still referenced by a running service's egress configuration; switch the service to DEFAULT egress (or another connector) before removing the entry.
  • Custom domain stuck PENDING_CERTIFICATE_DNS_VALIDATION — the caller hasn't yet created the CNAME records from custom_domain_certificate_validation_records/custom_domain_dns_targets; App Runner polls DNS asynchronously and there is no Terraform-side wait for it.
  • GitHub connection stuck PENDING_HANDSHAKE — a human must approve it once in the AWS Console; no Terraform action will complete this.

🔗 Related Docs

  • Terraform AWS provider resource reference: App Runner Service, Auto Scaling Configuration Version, Connection, Custom Domain Association, Default Auto Scaling Configuration Version, Deployment, Observability Configuration, VPC Connector, VPC Ingress Connection
  • AWS App Runner Developer Guide — source configuration, networking, and security concepts
  • AWS App Runner service quotas reference

🧡 "Infrastructure as Code should be standardized, consistent, and secure."