Skip to content

Commit 5267621

Browse files
committed
feat(app-runner): add AWS App Runner stack with CloudFront integration
Add complete Terraform configuration for deploying applications using AWS App Runner with CloudFront distribution. The stack includes: - ECR repository for Docker images - App Runner service with auto-scaling - CloudFront distribution with custom domain - ACM certificate for HTTPS - Route53 DNS configuration - SSM parameter integration for secrets
1 parent ede5855 commit 5267621

File tree

11 files changed

+776
-9
lines changed

11 files changed

+776
-9
lines changed

app-runner/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# App Runner stack
2+
3+
```mermaid
4+
---
5+
config:
6+
layout: fixed
7+
---
8+
flowchart TD
9+
RG["Resource Group"] --> AppRunner["AWS App Runner Service"] & ECR["ECR Repository"] & AutoScaling["Auto Scaling Configuration"] & CloudFront["CloudFront Distribution"] & ACM["ACM Certificate"] & Route53["Route53 Records"] & SSM["SSM Parameters"]
10+
ECR --> AppRunner
11+
AutoScaling --> AppRunner
12+
AppRunner --> CloudFront
13+
SSM --> AppRunner
14+
ACM --> CloudFront & Route53Cert["Route53 Certificate Records"]
15+
CloudFront --> Route53
16+
```
17+
18+
> [!NOTE]
19+
> Use Terraform remote state setting values to `backend.tf` file.
20+
> Use `terraform-backend` to create resources for remote state.
21+
22+
## Resources
23+
24+
- **Resource Group**: Groups all resources related to the service
25+
- **AWS App Runner Service**: Main service to run the application
26+
- **Auto Scaling Configuration**: Auto-scaling configuration for App Runner
27+
- **ECR Repository**: Repository to store Docker images
28+
- **CloudFront Distribution**: CDN to distribute content
29+
- **ACM Certificate**: SSL/TLS certificate for HTTPS
30+
- **Route53 Records**: DNS records to point to CloudFront
31+
- **SSM Parameters**: Store configuration values for the App Runner service
32+
33+
## Data flow
34+
35+
1. The code is packaged into a Docker image and sent to ECR
36+
2. App Runner uses this image to deploy the service
37+
3. CloudFront connects to App Runner as the origin
38+
4. Route53 directs traffic to CloudFront
39+
5. ACM provides the SSL certificate for CloudFront
40+
41+
## Get started
42+
43+
1. Create ECR setting `create_apprunner_service` input as `false`
44+
1. Build and push the image to ECR
45+
2. Create App Runner service using `create_apprunner_service` input as `true`
46+
47+
### ECR
48+
49+
Use the following commands to build and push a Docker image to ECR. Make sure you have the AWS CLI configured with the necessary permissions to access ECR.
50+
51+
```bash
52+
ECR_URL=$(terraform output -raw ecr_repository_url 2>/dev/null)
53+
54+
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URL
55+
56+
docker pull --platform=linux/amd64 traefik/whoami:latest
57+
docker tag traefik/whoami:latest "${ECR_URL}:latest"
58+
docker push "${ECR_URL}:latest"
59+
```
60+
61+
### App Runner
62+
63+
Set `create_apprunner_service` input to `true` to create the App Runner service. This will use the Docker image from ECR.

app-runner/backend.tf.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
terraform {
2+
backend "s3" {
3+
key = "my-org/my-service/terraform.tfstate" # Path to the state file inside the bucket
4+
bucket = "tf-prod-terraform-state" # Replace with your S3 bucket name
5+
region = "us-east-1" # S3 bucket region
6+
encrypt = true # Encrypt the state file
7+
dynamodb_table = "tf-prod-terraform-state-lock" # DynamoDB table for locking
8+
}
9+
}

app-runner/certificate.tf

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
resource "aws_acm_certificate" "certificate" {
2+
domain_name = var.api_domain != "" ? var.api_domain : data.aws_route53_zone.domain_zone.name
3+
validation_method = "DNS"
4+
5+
tags = {
6+
Name = "${local.service_full_name}-certificate"
7+
}
8+
}
9+
10+
resource "aws_acm_certificate_validation" "certificate_validation" {
11+
certificate_arn = aws_acm_certificate.certificate.arn
12+
validation_record_fqdns = [for record in aws_route53_record.certificate_records : record.fqdn]
13+
14+
depends_on = [
15+
aws_acm_certificate.certificate
16+
]
17+
18+
timeouts {
19+
create = "10m"
20+
}
21+
}
22+
23+
# Add Certificate Validation Records on Route53
24+
resource "aws_route53_record" "certificate_records" {
25+
for_each = {
26+
for dvo in aws_acm_certificate.certificate.domain_validation_options : dvo.domain_name => {
27+
name = dvo.resource_record_name
28+
record = dvo.resource_record_value
29+
type = dvo.resource_record_type
30+
}
31+
}
32+
33+
allow_overwrite = true
34+
name = each.value.name
35+
records = [each.value.record]
36+
ttl = 60
37+
type = each.value.type
38+
zone_id = data.aws_route53_zone.domain_zone.zone_id
39+
}

app-runner/cloudfront.tf

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Data source for AWS managed CloudFront cache policy
2+
data "aws_cloudfront_cache_policy" "managed_caching_disabled" {
3+
name = "Managed-CachingDisabled"
4+
}
5+
6+
data "aws_cloudfront_origin_request_policy" "managed_all_viewer_except_host_header" {
7+
name = "Managed-AllViewerExceptHostHeader"
8+
}
9+
10+
resource "aws_cloudfront_response_headers_policy" "response_headers_policy" {
11+
name = "${local.service_full_name}-response-headers-policy"
12+
comment = "Response headers policy for ${local.service_full_name} to handle CORS"
13+
14+
cors_config {
15+
origin_override = false
16+
access_control_max_age_sec = var.cors_max_age_seconds
17+
access_control_allow_credentials = var.cors_allow_credentials
18+
access_control_allow_origins {
19+
items = var.cors_allow_origins
20+
}
21+
22+
access_control_allow_headers {
23+
items = var.cors_allow_headers
24+
}
25+
26+
access_control_allow_methods {
27+
items = var.cors_allow_methods
28+
}
29+
30+
access_control_expose_headers {
31+
items = var.cors_expose_headers
32+
}
33+
}
34+
}
35+
36+
resource "aws_cloudfront_distribution" "cloudfront" {
37+
count = var.create_apprunner_service ? 1 : 0
38+
39+
enabled = true
40+
is_ipv6_enabled = true
41+
comment = "Edge for ${local.service_full_name}"
42+
default_root_object = var.cloudfront_default_root_object
43+
http_version = var.cloudfront_http_version
44+
aliases = var.api_domain != "" ? [var.api_domain] : var.route53_zone_domain != "" ? [var.route53_zone_domain] : []
45+
46+
origin {
47+
origin_id = "apprunner"
48+
domain_name = aws_apprunner_service.service[0].service_url
49+
50+
custom_origin_config {
51+
http_port = 80
52+
https_port = 443
53+
origin_protocol_policy = "https-only"
54+
origin_ssl_protocols = ["TLSv1.2"]
55+
}
56+
}
57+
58+
default_cache_behavior {
59+
target_origin_id = "apprunner"
60+
smooth_streaming = true
61+
compress = true
62+
allowed_methods = var.cloudfront_allowed_methods
63+
cached_methods = var.cloudfront_cached_methods
64+
cache_policy_id = data.aws_cloudfront_cache_policy.managed_caching_disabled.id
65+
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.managed_all_viewer_except_host_header.id
66+
response_headers_policy_id = aws_cloudfront_response_headers_policy.response_headers_policy.id
67+
viewer_protocol_policy = "redirect-to-https"
68+
min_ttl = 0
69+
default_ttl = 0
70+
max_ttl = 0
71+
}
72+
73+
restrictions {
74+
geo_restriction {
75+
restriction_type = "none"
76+
}
77+
}
78+
79+
viewer_certificate {
80+
cloudfront_default_certificate = false
81+
minimum_protocol_version = "TLSv1.2_2021"
82+
ssl_support_method = "sni-only"
83+
acm_certificate_arn = aws_acm_certificate_validation.certificate_validation.certificate_arn
84+
}
85+
86+
tags = {
87+
Name = "${local.service_full_name}-cloudfront"
88+
}
89+
90+
depends_on = [
91+
aws_apprunner_service.service
92+
]
93+
}
94+
95+
# Create Route53 Record to CloudFront
96+
resource "aws_route53_record" "domain_record" {
97+
count = var.create_apprunner_service ? 1 : 0
98+
99+
name = var.api_domain != "" ? var.api_domain : data.aws_route53_zone.domain_zone.name
100+
type = "A"
101+
zone_id = data.aws_route53_zone.domain_zone.zone_id
102+
103+
alias {
104+
name = aws_cloudfront_distribution.cloudfront[0].domain_name
105+
zone_id = aws_cloudfront_distribution.cloudfront[0].hosted_zone_id
106+
evaluate_target_health = false
107+
}
108+
109+
depends_on = [
110+
aws_cloudfront_distribution.cloudfront
111+
]
112+
}

0 commit comments

Comments
 (0)