Skip to content
Merged
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
79 changes: 79 additions & 0 deletions AppRunner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# AppRunner module

Terraform module for an AWS App Runner service from an ECR or ECR Public image.
Optional: VPC connector, custom domain association, and automatic IAM role for ECR access.

## Features

- **Image source:** ECR or ECR Public (`image_identifier`, `image_repository_type`)
- **Access role:** Pass `access_role_arn` or set `ecr_repository_arn` and the module creates an IAM role for App Runner to pull from ECR
- **VPC connector:** Optional `vpc_connector_subnet_ids` (and optionally `vpc_connector_security_group_ids`) to create a connector, or pass `vpc_connector_arn`
- **Custom domain:** Optional `custom_domain_name` to associate a domain with the service (use with the AcmCustomDomain module for ACM + Route53)
- **Naming:** Uses the terraform-accelerator Label module via `context`

## Usage

### Minimal (ECR Public image)

```hcl
module "apprunner" {
source = "./AppRunner"

context = module.this.context

image_identifier = "public.ecr.aws/aws-containers/hello-app-runner:latest"
image_repository_type = "ECR_PUBLIC"
port = "8000"
}
```

### With private ECR (module creates access role)

```hcl
module "apprunner" {
source = "./AppRunner"

context = module.this.context

image_identifier = "${module.ecr.url}:latest"
image_repository_type = "ECR"
port = "5000"
ecr_repository_arn = module.ecr.arn
}
```

### With custom domain

Set `custom_domain_name` (and optionally `certificate_arn` if your provider supports it).
Use the **AcmCustomDomain** module to create the ACM cert and Route53 CNAME/alias; pass its `certificate_arn` and use `service_url` for the target hostname.

### With VPC connector

```hcl
module "apprunner" {
source = "./AppRunner"

context = module.this.context

image_identifier = "..."
image_repository_type = "ECR"
port = "8080"
ecr_repository_arn = module.ecr.arn

vpc_connector_subnet_ids = module.vpc.private_subnet_ids
vpc_connector_security_group_ids = [aws_security_group.apprunner_egress.id]
}
```

## Example

See [example/](example/) for a full runnable setup: ECR, Docker build/push, and App Runner.
Run from `infra/AppRunner/example`:

```bash
terraform init && terraform apply
```

## Inputs / Outputs

See [SPECS.md](SPECS.md) (generated with `terraform-docs markdown table --output-file SPECS.md --output-mode inject ./`).
72 changes: 72 additions & 0 deletions AppRunner/SPECS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.1.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 5.18 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 5.18 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_apprunner_access_role"></a> [apprunner\_access\_role](#module\_apprunner\_access\_role) | git::git@github.com:generalui/terraform-accelerator.git//IamRole | 1.0.1-IamRole |
| <a name="module_ecr_access_policy"></a> [ecr\_access\_policy](#module\_ecr\_access\_policy) | git::git@github.com:ohgod-ai/eo-terraform.git//IamPolicy | 1.0.1-IamPolicy |
| <a name="module_this"></a> [this](#module\_this) | git::git@github.com:generalui/terraform-accelerator.git//Label | 1.0.1-Label |

## Resources

| Name | Type |
|------|------|
| [aws_apprunner_custom_domain_association.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apprunner_custom_domain_association) | resource |
| [aws_apprunner_service.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apprunner_service) | resource |
| [aws_apprunner_vpc_connector.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apprunner_vpc_connector) | resource |
| [aws_iam_role_policy_attachment.ecr_access_attach](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_access_role_arn"></a> [access\_role\_arn](#input\_access\_role\_arn) | ARN of IAM role for App Runner to pull the image from ECR. Required for private ECR when not creating a role. When null and ecr\_repository\_arn is set, the module creates a role. | `string` | `null` | no |
| <a name="input_attributes"></a> [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,<br>in the order they appear in the list. New attributes are appended to the<br>end of the list. The elements of the list are joined by the `delimiter`<br>and treated as a single ID element. | `list(string)` | `[]` | no |
| <a name="input_auto_deployments_enabled"></a> [auto\_deployments\_enabled](#input\_auto\_deployments\_enabled) | Whether automatic deployments from the source repository are enabled (e.g. new image tag). | `bool` | `false` | no |
| <a name="input_certificate_arn"></a> [certificate\_arn](#input\_certificate\_arn) | ARN of ACM certificate for the custom domain. Required when custom\_domain\_name is set if the certificate is not in us-east-1 (App Runner uses us-east-1 for cert validation in some cases). | `string` | `null` | no |
| <a name="input_context"></a> [context](#input\_context) | Single object for setting entire context at once.<br>See description of individual variables for details.<br>Leave string and numeric variables as `null` to use default value.<br>Individual variable settings (non-null) override settings in context object,<br>except for attributes and tags, which are merged. | `any` | <pre>{<br> "attributes": [],<br> "enabled": true,<br> "name": null,<br> "namespace": null,<br> "stage": null,<br> "tags": {}<br>}</pre> | no |
| <a name="input_cpu"></a> [cpu](#input\_cpu) | Number of CPU units for each instance. Valid values: 256, 512, 1024, 2048, 4096 (or 0.25, 0.5, 1, 2, 4 vCPU). | `string` | `"1024"` | no |
| <a name="input_custom_domain_name"></a> [custom\_domain\_name](#input\_custom\_domain\_name) | Custom domain name to associate with the App Runner service (e.g. app.example.com). | `string` | `null` | no |
| <a name="input_ecr_repository_arn"></a> [ecr\_repository\_arn](#input\_ecr\_repository\_arn) | ARN of the ECR repository. Used only when access\_role\_arn is null to create an access role with least-privilege ECR pull for this repository. | `string` | `null` | no |
| <a name="input_enabled"></a> [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
| <a name="input_environment_name"></a> [environment\_name](#input\_environment\_name) | Environment name, e.g. prod, staging, dev. | `string` | `null` | no |
| <a name="input_health_check_path"></a> [health\_check\_path](#input\_health\_check\_path) | URL path for HTTP health checks. | `string` | `"/"` | no |
| <a name="input_health_check_protocol"></a> [health\_check\_protocol](#input\_health\_check\_protocol) | Protocol for health checks. Valid values: TCP, HTTP. | `string` | `"HTTP"` | no |
| <a name="input_image_identifier"></a> [image\_identifier](#input\_image\_identifier) | Full image identifier (e.g. ECR URL or ECR Public URL). For ECR: `account.dkr.ecr.region.amazonaws.com/repo:tag`. For ECR Public: `public.ecr.aws/.../image:tag`. | `string` | n/a | yes |
| <a name="input_image_repository_type"></a> [image\_repository\_type](#input\_image\_repository\_type) | Type of the image repository. Valid values: `ECR`, `ECR_PUBLIC`. | `string` | `"ECR"` | no |
| <a name="input_instance_role_arn"></a> [instance\_role\_arn](#input\_instance\_role\_arn) | Optional ARN of IAM role for the running service (permissions your code needs when calling AWS APIs). | `string` | `null` | no |
| <a name="input_memory"></a> [memory](#input\_memory) | Memory reserved for each instance (e.g. 512, 1024, 2048, 3072, 4096, 6144, 8192, 10240, 12288 MB, or 0.5, 1, 2, 3, 4, 6, 8, 10, 12 GB). | `string` | `"2048"` | no |
| <a name="input_namespace"></a> [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no |
| <a name="input_port"></a> [port](#input\_port) | Port that your application listens to in the container. | `string` | `"8080"` | no |
| <a name="input_project"></a> [project](#input\_project) | Project name. | `string` | `"apprunner"` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).<br>Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
| <a name="input_vpc_connector_arn"></a> [vpc\_connector\_arn](#input\_vpc\_connector\_arn) | ARN of an existing App Runner VPC connector. If set, used instead of creating one (vpc\_connector\_subnet\_ids and vpc\_connector\_security\_group\_ids are ignored). | `string` | `null` | no |
| <a name="input_vpc_connector_security_group_ids"></a> [vpc\_connector\_security\_group\_ids](#input\_vpc\_connector\_security\_group\_ids) | List of security group IDs for the VPC connector. Required when vpc\_connector\_subnet\_ids is set. | `list(string)` | `null` | no |
| <a name="input_vpc_connector_subnet_ids"></a> [vpc\_connector\_subnet\_ids](#input\_vpc\_connector\_subnet\_ids) | List of subnet IDs for the VPC connector. When set, a VPC connector is created and the service egress is set to VPC. | `list(string)` | `null` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_access_role_arn"></a> [access\_role\_arn](#output\_access\_role\_arn) | ARN of the IAM access role used for ECR pull (if created by this module). |
| <a name="output_custom_domain_certificate_validation_records"></a> [custom\_domain\_certificate\_validation\_records](#output\_custom\_domain\_certificate\_validation\_records) | Certificate validation records for the custom domain (add CNAME to DNS). |
| <a name="output_service_arn"></a> [service\_arn](#output\_service\_arn) | ARN of the App Runner service. |
| <a name="output_service_id"></a> [service\_id](#output\_service\_id) | App Runner service ID (unique within the region). |
| <a name="output_service_name"></a> [service\_name](#output\_service\_name) | Name of the App Runner service. |
| <a name="output_service_status"></a> [service\_status](#output\_service\_status) | Current state of the App Runner service. |
| <a name="output_service_url"></a> [service\_url](#output\_service\_url) | Subdomain URL that App Runner generated for this service (HTTPS). |
| <a name="output_vpc_connector_arn"></a> [vpc\_connector\_arn](#output\_vpc\_connector\_arn) | ARN of the VPC connector (if created by this module). |
<!-- END_TF_DOCS -->
89 changes: 89 additions & 0 deletions AppRunner/context.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Module should access the whole context as `module.this.context`
# to get the input variables with nulls for defaults,
# for example `context = module.this.context`,
# and access individual variables as `module.this.<var>`,
# with final values filled in.
#

module "this" {
source = "git::git@github.com:generalui/terraform-accelerator.git//Label?ref=1.0.1-Label"

attributes = var.attributes
enabled = var.enabled
name = var.project
namespace = var.namespace
stage = var.environment_name
tags = var.tags

context = var.context
}

variable "attributes" {
type = list(string)
default = []
description = <<-EOT
ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element.
EOT
}

variable "context" {
type = any
default = {
attributes = []
enabled = true
name = null
namespace = null
stage = null
tags = {}
# Note: we have to use [] instead of null for unset lists due to
# https://github.com/hashicorp/terraform/issues/28137
# which was not fixed until Terraform 1.0.0.
}
description = <<-EOT
Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes and tags, which are merged.
EOT
}

variable "enabled" {
type = bool
default = null
description = "Set to false to prevent the module from creating any resources"
}

variable "environment_name" {
type = string
default = null
description = "Environment name, e.g. prod, staging, dev."
validation {
condition = var.environment_name == null || length(var.environment_name) < 8
error_message = "environment_name must be null or less than 8 characters."
}
}

variable "namespace" {
type = string
default = null
description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique"
}

variable "project" {
type = string
default = "apprunner"
description = "Project name."
}

variable "tags" {
type = map(string)
default = {}
description = <<-EOT
Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module.
EOT
}
11 changes: 11 additions & 0 deletions AppRunner/example/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.14-slim

WORKDIR /usr/src/app

COPY PythonExample/requirements.txt PythonExample/app.py ./

RUN pip install --no-cache-dir -r requirements.txt

EXPOSE 5000

CMD ["python", "app.py"]
21 changes: 21 additions & 0 deletions AppRunner/example/PythonExample/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Minimal Flask app for App Runner example. Listens on port 5000."""
import os

from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
return "App Runner example is running.\n", 200


@app.route("/health")
def health():
return "ok\n", 200


if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.run(host="0.0.0.0", port=port)
1 change: 1 addition & 0 deletions AppRunner/example/PythonExample/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
flask==3.1.2
49 changes: 49 additions & 0 deletions AppRunner/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# App Runner example

Runnable example: ECR repository, Docker image build/push, and App Runner service.

## Prerequisites

- AWS CLI configured (profile or default)
- Terraform >= 1.1
- Docker (for building and pushing the image)

## Run the app in Docker locally

Build and run the same image used by App Runner to test the PythonExample app locally:

```bash
cd infra/AppRunner/example
docker build -t apprunner-example .
docker run --rm -p 5001:5000 apprunner-example
```

Open <http://localhost:5001> (and <http://localhost:5001/health> for the health check).

## Run

```bash
cd infra/AppRunner/example
terraform init
terraform apply
```

After apply, use the `service_url` output to open the app in a browser.

## Clean up

```bash
terraform destroy
```

## What it creates

- **Label** (context / naming)
- **ECR** repository (via terraform-accelerator ECR module)
- **Docker image** built from the local Dockerfile (Flask app on port 5000), pushed to ECR
- **App Runner service** using that image; module creates the ECR access role automatically

## Variables

Override as needed, e.g. `-var="aws_region=us-east-1"` or a `.tfvars` file.
Key variables: `aws_profile`, `aws_region`, `project`, `namespace`, `environment_name`.
Loading