Skip to content

Commit a6323b3

Browse files
author
Avritt Rohwer
committed
feat: bloom-dev initial configuration
1 parent b761b62 commit a6323b3

10 files changed

Lines changed: 1043 additions & 6 deletions

File tree

docker-compose.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ services:
107107
DATABASE_URL: "postgres://postgres:example@db:5432/bloom_prisma"
108108
healthcheck:
109109
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3100/"]
110-
interval: "2s"
111-
timeout: "1s"
112-
retries: 20
113-
start_period: "1s"
110+
interval: "5s"
111+
timeout: "2s"
112+
retries: 10
113+
start_period: "5s"
114114
depends_on:
115115
db:
116116
condition: service_healthy

infra/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,19 @@ Cloud Native Computing Foundation.
1010
is a set of resources that are all managed together. Each root module has a state file that
1111
records the results of the latest apply operation.
1212

13+
- [bloom_dev](./tofu_root_modules/bloom_dev/README.md): Configures the bloom-dev AWS account.
1314
- [bloom_dev_deployer_permission_set](./tofu_root_modules/bloom_dev_deployer_permission_set/README.md):
1415
Configures the bloom-dev-deployer permission set that is assigned on the bloom-dev account.
1516

17+
- [tofu_importable_modules](./tofu_importable_modules): Contains all the Open Tofu importable
18+
modules. An importable module is a reusable set of resources configured through input
19+
parameters. Root modules import importable modules.
20+
21+
- [bloom_deployment](./tofu_importable_modules/bloom_deployment/README.md): Configures all the
22+
resources needed for a Bloom deployment in a single AWS account.
23+
24+
25+
1626
## Infrastructure-as-code mental model
1727

1828
Let's say that you need to deploy Bloom to an AWS account. A straight-forward way of achieving this
@@ -122,6 +132,8 @@ directory.
122132
(Log in via https://d-9067ac8222.awsapps.com/start). If there are unexpected results, go back to
123133
step 1. In some cases you may have to manually modify or delete resources directly to 'unstick'
124134
Open Tofu.
135+
6. To delete only the resources provisioned by the bloom-dev module, run `tofu destroy
136+
-target=module.bloom-dev`.
125137

126138
## AWS setup done manually
127139

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Create a database.
2+
locals {
3+
# Pricing: https://aws.amazon.com/rds/postgresql/pricing/?pg=pr&loc=3
4+
# Machine specs: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.Summary.html#hardware-specifications.burstable-inst-classes
5+
db_instance_class = local.is_prod ? "db.t4g.medium" : "db.t4g.micro"
6+
db_multi_az = local.is_prod ? true : false
7+
8+
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp2-storage
9+
# Unit: GiB
10+
db_start_storage = local.is_prod ? 10 : 5
11+
db_max_storage = local.is_prod ? 50 : 10
12+
13+
# Unit: days
14+
db_backup_retention = local.is_prod ? 30 : 7
15+
}
16+
resource "aws_db_subnet_group" "bloom" {
17+
region = var.aws_region
18+
name = "bloom"
19+
subnet_ids = [for s in aws_subnet.db : s.id]
20+
}
21+
resource "aws_db_instance" "bloom" {
22+
identifier = "bloom"
23+
deletion_protection = local.is_prod
24+
engine = "postgres"
25+
engine_version = "17"
26+
instance_class = local.db_instance_class
27+
multi_az = local.db_multi_az
28+
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade", "iam-db-auth-error"]
29+
username = "master"
30+
manage_master_user_password = true
31+
32+
# Networking
33+
vpc_security_group_ids = [aws_security_group.db.id]
34+
iam_database_authentication_enabled = true
35+
db_subnet_group_name = aws_db_subnet_group.bloom.id
36+
37+
# Updates
38+
apply_immediately = true # If false, any changes are applied in the next maintenance window instead of when tofu apply runs.
39+
engine_lifecycle_support = "open-source-rds-extended-support"
40+
allow_major_version_upgrade = false
41+
auto_minor_version_upgrade = true
42+
43+
# Storage
44+
storage_encrypted = true
45+
storage_type = "gp2"
46+
allocated_storage = local.db_start_storage
47+
max_allocated_storage = local.db_max_storage
48+
backup_retention_period = local.db_backup_retention
49+
final_snapshot_identifier = "bloom-db-finalsnapshot"
50+
skip_final_snapshot = !local.is_prod
51+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Create an ECS cluster and services for each Bloom binary.
2+
resource "aws_iam_service_linked_role" "ecs" {
3+
aws_service_name = "ecs.amazonaws.com"
4+
}
5+
resource "aws_ecs_cluster" "bloom" {
6+
region = var.aws_region
7+
name = "bloom"
8+
depends_on = [aws_iam_service_linked_role.ecs]
9+
setting {
10+
name = "containerInsights"
11+
value = "enabled"
12+
}
13+
}
14+
15+
# API service.
16+
resource "aws_iam_role" "bloom_api_ecs" {
17+
name = "bloom-api-ecs"
18+
description = "Role the ECS service uses when launching the Bloom api container."
19+
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html#create_task_iam_policy_and_role
20+
assume_role_policy = jsonencode({
21+
Version = "2012-10-17"
22+
Statement = [{
23+
Action = "sts:AssumeRole"
24+
Effect = "Allow"
25+
Principal = {
26+
Service = "ecs-tasks.amazonaws.com"
27+
}
28+
Condition = {
29+
ArnLike = {
30+
"aws:SourceArn" = "arn:aws:ecs:${var.aws_region}:${var.aws_account_number}:*"
31+
}
32+
StringEquals = {
33+
"aws:SourceAccount" = var.aws_account_number
34+
}
35+
}
36+
}]
37+
})
38+
}
39+
resource "aws_iam_role_policy" "bloom_api_ecs" {
40+
name = "bloom-api-ecs"
41+
role = aws_iam_role.bloom_api_ecs.id
42+
policy = jsonencode({
43+
Version = "2012-10-17"
44+
Statement = [
45+
{
46+
Action = "secretsmanager:GetSecretValue"
47+
Effect = "Allow"
48+
Resource = aws_db_instance.bloom.master_user_secret[0].secret_arn
49+
},
50+
{
51+
Action = [
52+
"logs:CreateLogStream",
53+
"logs:PutLogEvents",
54+
]
55+
Effect = "Allow"
56+
Resource = "${aws_cloudwatch_log_group.bloom_api.arn}:log-stream:*"
57+
},
58+
]
59+
})
60+
}
61+
resource "aws_iam_role" "bloom_api_container" {
62+
name = "bloom-api-container"
63+
description = "Role the Bloom API container runs as."
64+
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html#create_task_iam_policy_and_role
65+
assume_role_policy = jsonencode({
66+
Version = "2012-10-17"
67+
Statement = [{
68+
Action = "sts:AssumeRole"
69+
Effect = "Allow"
70+
Principal = {
71+
Service = "ecs-tasks.amazonaws.com"
72+
}
73+
Condition = {
74+
ArnLike = {
75+
"aws:SourceArn" = "arn:aws:ecs:${var.aws_region}:${var.aws_account_number}:*"
76+
}
77+
StringEquals = {
78+
"aws:SourceAccount" = var.aws_account_number
79+
}
80+
}
81+
}]
82+
})
83+
}
84+
resource "aws_iam_role_policy" "bloom_api_container" {
85+
name = "bloom-api-container"
86+
role = aws_iam_role.bloom_api_container.id
87+
policy = jsonencode({
88+
Version = "2012-10-17"
89+
Statement = [{
90+
Action = "*"
91+
Effect = "Deny"
92+
Resource = "*"
93+
}]
94+
})
95+
}
96+
resource "aws_cloudwatch_log_group" "bloom_api" {
97+
region = var.aws_region
98+
name = "bloom-api"
99+
log_group_class = "STANDARD"
100+
retention_in_days = local.is_prod ? 30 : 7
101+
}
102+
resource "aws_ecs_task_definition" "bloom_api" {
103+
region = var.aws_region
104+
family = "bloom-api"
105+
network_mode = "awsvpc"
106+
requires_compatibilities = ["FARGATE"]
107+
runtime_platform {
108+
operating_system_family = "LINUX"
109+
cpu_architecture = "X86_64"
110+
}
111+
112+
execution_role_arn = aws_iam_role.bloom_api_ecs.arn
113+
task_role_arn = aws_iam_role.bloom_api_container.arn
114+
115+
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size
116+
cpu = 1024 # 1 vCPU
117+
memory = 2 * 1024 # 2 GiB in MiB
118+
119+
container_definitions = jsonencode([
120+
{
121+
Name = "bloom-api"
122+
image = var.bloom_api_image
123+
command = [
124+
"/bin/bash",
125+
"-c",
126+
# TODO: https://www.prisma.io/docs/orm/more/help-and-troubleshooting/dataguide/connection-uris#percent-encoding-values
127+
"export DATABASE_URL=postgres://$DB_USER:$(echo $DB_PASSWORD | sed 's/:/%3A/')@$DB_HOST/bloomprisma && env && yarn db:migration:run && yarn start:prod",
128+
]
129+
secrets = [
130+
{
131+
name = "DB_PASSWORD"
132+
# The master_user_secret tofu attribute is a block, so it is exposed as a list even though
133+
# there is only one element.
134+
#
135+
# The RDS managed secret has a username key and a password key. Docs for how to read
136+
# a specific key out of a secret:
137+
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/secrets-envvar-secrets-manager.html#secrets-envvar-secrets-manager-update-container-definition
138+
valueFrom = "${aws_db_instance.bloom.master_user_secret[0].secret_arn}:password::"
139+
}
140+
]
141+
environment = [
142+
{
143+
name = "PORT"
144+
value = "3100"
145+
},
146+
{
147+
name = "NODE_ENV"
148+
value = "production"
149+
},
150+
{
151+
name = "APP_SECRET"
152+
value = "totally a secret"
153+
},
154+
{
155+
name = "DB_USER"
156+
value = aws_db_instance.bloom.username
157+
},
158+
{
159+
name = "DB_HOST"
160+
value = aws_db_instance.bloom.endpoint
161+
},
162+
]
163+
portMappings = [
164+
{
165+
containerPort = 3100
166+
appProtocol = "http"
167+
}
168+
]
169+
restartPolicy = {
170+
enabled = false
171+
}
172+
healthCheck = {
173+
command = [
174+
"curl",
175+
"--fail",
176+
"http://127.0.0.1:3100/"
177+
]
178+
interval = 5 # seconds
179+
timeout = 2 # seconds
180+
retries = 10
181+
startPeriod = 5 # seconds
182+
}
183+
logConfiguration = {
184+
logDriver = "awslogs"
185+
options = {
186+
"awslogs-region" = var.aws_region
187+
"awslogs-group" = aws_cloudwatch_log_group.bloom_api.name
188+
"awslogs-stream-prefix" = "bloom-api"
189+
}
190+
}
191+
}
192+
])
193+
}
194+
resource "aws_ecs_service" "bloom_api" {
195+
region = var.aws_region
196+
cluster = aws_ecs_cluster.bloom.arn
197+
name = "bloom-api"
198+
force_delete = true # allow deletion of the service without scaling down tasks to 0 first.
199+
task_definition = aws_ecs_task_definition.bloom_api.arn
200+
wait_for_steady_state = false
201+
depends_on = [
202+
aws_db_instance.bloom,
203+
aws_vpc_endpoint.secrets_manager,
204+
aws_route_table_association.api_to_nat,
205+
]
206+
207+
network_configuration {
208+
security_groups = [aws_security_group.api.id]
209+
subnets = [for s in aws_subnet.api : s.id]
210+
assign_public_ip = false
211+
}
212+
# load_balancer {}
213+
# health_check_grace_period_seconds - LB
214+
215+
launch_type = "FARGATE"
216+
scheduling_strategy = "REPLICA"
217+
availability_zone_rebalancing = "ENABLED"
218+
desired_count = local.is_prod ? 2 : 1
219+
deployment_configuration {
220+
strategy = "ROLLING"
221+
}
222+
deployment_circuit_breaker {
223+
enable = true
224+
rollback = true
225+
}
226+
deployment_controller {
227+
type = "ECS"
228+
}
229+
deployment_maximum_percent = 200 # allow surge of up to twice desired task count.
230+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
terraform {
2+
required_providers {
3+
time = {
4+
source = "hashicorp/time"
5+
version = "0.13.1"
6+
}
7+
aws = {
8+
version = "6.21.0"
9+
source = "hashicorp/aws"
10+
}
11+
}
12+
}
13+
14+
variable "env_type" {
15+
type = string
16+
description = "Type of environment this deployment is going in."
17+
validation {
18+
condition = (
19+
var.env_type == "dev" ||
20+
var.env_type == "production"
21+
)
22+
error_message = "Must be 'dev' or 'production'."
23+
}
24+
}
25+
variable "aws_account_number" {
26+
type = number
27+
description = "AWS account number that is being configured."
28+
}
29+
variable "aws_region" {
30+
type = string
31+
description = "Region to deploy AWS resources to"
32+
validation {
33+
condition = (
34+
var.aws_region == "us-east-1" ||
35+
var.aws_region == "us-east-2" ||
36+
var.aws_region == "us-west-1" ||
37+
var.aws_region == "us-west-2"
38+
)
39+
error_message = "Must be 'us-east-1', 'us-east-2', 'us-west-1', or 'us-west-2'."
40+
}
41+
}
42+
locals {
43+
is_prod = var.env_type == "production"
44+
}
45+
variable "bloom_api_image" {
46+
type = string
47+
description = "Container image for the Bloom API."
48+
}
49+
variable "bloom_partners_site_image" {
50+
type = string
51+
description = "Container image for the Bloom partners site."
52+
}
53+
variable "bloom_public_site_image" {
54+
type = string
55+
description = "Container image for the Bloom public site."
56+
}
57+
58+
# Create a CloudTrail data store so that audit events are query-able in SQL.
59+
resource "aws_cloudtrail_event_data_store" "audit" {
60+
count = local.is_prod ? 1 : 0
61+
62+
region = var.aws_region
63+
name = "audit"
64+
multi_region_enabled = true
65+
retention_period = 30
66+
termination_protection_enabled = true
67+
}
68+

0 commit comments

Comments
 (0)