From 7ea81ce5b818b7835f0773aa67d80cb25f64ee81 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 23 Jan 2024 11:29:01 -0800 Subject: [PATCH 01/16] initial infrastructure --- .dockerignore | 34 ++++ Dockerfile | 99 ++++++++++ bin/render-build.sh | 0 infrastructure/terraform/.gitignore | 5 + .../terraform/aws/.terraform.lock.hcl | 44 +++++ infrastructure/terraform/aws/README.md | 26 +++ infrastructure/terraform/aws/main.tf | 42 ++++ .../terraform/aws/modules/v7-cluster/ecr.tf | 9 + .../terraform/aws/modules/v7-cluster/ecs.tf | 118 ++++++++++++ .../terraform/aws/modules/v7-cluster/main.tf | 180 ++++++++++++++++++ .../aws/modules/v7-cluster/outputs.tf | 7 + .../terraform/aws/modules/v7-cluster/rds.tf | 59 ++++++ .../aws/modules/v7-cluster/variables.tf | 58 ++++++ infrastructure/terraform/aws/outputs.tf | 0 infrastructure/terraform/aws/variables.tf | 15 ++ .../terraform/globals/.terraform.lock.hcl | 25 +++ infrastructure/terraform/globals/README.md | 15 ++ .../terraform/globals/github_actions_iam.tf | 83 ++++++++ infrastructure/terraform/globals/main.tf | 38 ++++ infrastructure/terraform/globals/outputs.tf | 20 ++ 20 files changed, 877 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile mode change 100644 => 100755 bin/render-build.sh create mode 100644 infrastructure/terraform/.gitignore create mode 100644 infrastructure/terraform/aws/.terraform.lock.hcl create mode 100644 infrastructure/terraform/aws/README.md create mode 100644 infrastructure/terraform/aws/main.tf create mode 100644 infrastructure/terraform/aws/modules/v7-cluster/ecr.tf create mode 100644 infrastructure/terraform/aws/modules/v7-cluster/ecs.tf create mode 100644 infrastructure/terraform/aws/modules/v7-cluster/main.tf create mode 100644 infrastructure/terraform/aws/modules/v7-cluster/outputs.tf create mode 100644 infrastructure/terraform/aws/modules/v7-cluster/rds.tf create mode 100644 infrastructure/terraform/aws/modules/v7-cluster/variables.tf create mode 100644 infrastructure/terraform/aws/outputs.tf create mode 100644 infrastructure/terraform/aws/variables.tf create mode 100644 infrastructure/terraform/globals/.terraform.lock.hcl create mode 100644 infrastructure/terraform/globals/README.md create mode 100644 infrastructure/terraform/globals/github_actions_iam.tf create mode 100644 infrastructure/terraform/globals/main.tf create mode 100644 infrastructure/terraform/globals/outputs.tf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..fd00eaa962 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.next +**/.cache +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/build +**/dist +LICENSE +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..4511f448e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,99 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +ARG NODE_VERSION=20.6.0 +ARG PNPM_VERSION=8.14.3 + +################################################################################ +# Use node image for base image for all stages. +FROM node:${NODE_VERSION}-alpine as base + +# Install python deps for node-gyp +RUN apk add g++ make py3-pip + +# Set working directory for all build stages. +WORKDIR /usr/src/app + +# Install pnpm. +RUN --mount=type=cache,target=/root/.npm \ + npm install -g pnpm@${PNPM_VERSION} + +################################################################################ +# Create a stage for installing production dependecies. +FROM base as deps + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds. +# Leverage bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them +# into this layer. +RUN --mount=type=bind,source=package.json,target=package.json \ + --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ + --mount=type=bind,source=core/package.json,target=core/package.json \ + --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ + --mount=type=cache,target=/root/.local/share/pnpm/store \ + --mount=type=cache,target=/pnpm/store \ + pnpm install --prod --frozen-lockfile + +################################################################################ +# Create a stage for building the application. +FROM deps as build + +# Download additional development dependencies before building, as some projects require +# "devDependencies" to be installed to build. If you don't need this, remove this step. +RUN --mount=type=bind,source=package.json,target=package.json \ + --mount=type=bind,source=core/package.json,target=core/package.json \ + --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ + --mount=type=cache,target=/root/.local/share/pnpm/store \ + --mount=type=cache,target=/pnpm/store \ + pnpm install --frozen-lockfile + +# Copy the rest of the source files into the image. +COPY . . + +# Run the build script. +RUN ./bin/render-build.sh core + +################################################################################ +# Create a new stage to run the application with minimal runtime dependencies +# where the necessary files are copied from the build stage. +FROM base as final + +# Use production node environment by default. +ENV NODE_ENV production + +# Run the application as a non-root user. +USER node + +# Copy package.json so that package manager commands can be used. +COPY package.json \ + pnpm-workspace.yaml \ + . + +COPY ./core/package.json \ + ./core/.env.template \ + ./core/postcss.config.js \ + ./core/tailwind.config.js \ + ./core/ + + # TODO: These may be needed, but are TS currently: + # ./core/next.config.js \ + # ./core/sentry.client.config.ts \ + # ./core/sentry.edge.config.ts \ + # ./core/sentry.server.config.ts \ + +# Copy the production dependencies from the deps stage and also +# the built application from the build stage into the image. + +COPY --from=deps /usr/src/app/node_modules ./node_modules +COPY --from=build /usr/src/app/core/node_modules ./core/node_modules +COPY --from=build /usr/src/app/core/.next ./core/.next + + +# Expose the port that the application listens on. +EXPOSE 3000 + +# Run the application. +CMD pnpm --filter core start diff --git a/bin/render-build.sh b/bin/render-build.sh old mode 100644 new mode 100755 diff --git a/infrastructure/terraform/.gitignore b/infrastructure/terraform/.gitignore new file mode 100644 index 0000000000..d4f7de49c1 --- /dev/null +++ b/infrastructure/terraform/.gitignore @@ -0,0 +1,5 @@ +**/*.tfstate +**/*.tfstate.* +**/.terraform +**/*.tfvars +**/*.tfbackend diff --git a/infrastructure/terraform/aws/.terraform.lock.hcl b/infrastructure/terraform/aws/.terraform.lock.hcl new file mode 100644 index 0000000000..1a136df7fd --- /dev/null +++ b/infrastructure/terraform/aws/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.33.0" + constraints = ">= 4.0.0, >= 5.0.0" + hashes = [ + "h1:kPm7PkwHh6tZ74pUj5C/QRPtauxdnzrEG2yhCJla/4o=", + "zh:10bb683f2a9306e881f51a971ad3b2bb654ac94b54945dd63769876a343b5b04", + "zh:3916406db958d5487ea0c2d2320012d1907c29e6d01bf693560fe05e38ee0601", + "zh:3cb54b76b2f9e30620f3281ab7fb20633b1e4584fc84cc4ecd5752546252e86f", + "zh:513bcfd6971482215c5d64725189f875cbcbd260c6d11f0da4d66321efd93a92", + "zh:545a34427ebe7a950056627e7c980c9ba16318bf086d300eb808ffc41c52b7a8", + "zh:5a44b90faf1c8e8269f389c04bfac25ad4766d26360e7f7ac371be12a442981c", + "zh:64e1ef83162f78538dccad8b035577738851395ba774d6919cb21eb465a21e3a", + "zh:7315c70cb6b7f975471ea6129474639a08c58c071afc95a36cfaa41a13ae7fb9", + "zh:9806faae58938d638b757f54414400be998dddb45edfd4a29c85e827111dc93d", + "zh:997fa2e2db242354d9f772fba7eb17bd6d18d28480291dd93f85a18ca0a67ac2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f9e076b7e9752971f39eead6eda69df1c5e890c82ba2ca95f56974af7adfe79", + "zh:b1d6af047f96de7f97d38b685654f1aed4356d5060b0e696d87d0270f5d49f75", + "zh:bfb0654b6f34398aeffdf907b744af06733d168db610a2c5747263380f817ac7", + "zh:e25203ee8cedccf60bf450950d533d3c172509bda8af97dbc3bc817d2a503c57", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.6.0" + hashes = [ + "h1:I8MBeauYA8J8yheLJ8oSMWqB0kovn16dF/wKZ1QTdkk=", + "zh:03360ed3ecd31e8c5dac9c95fe0858be50f3e9a0d0c654b5e504109c2159287d", + "zh:1c67ac51254ba2a2bb53a25e8ae7e4d076103483f55f39b426ec55e47d1fe211", + "zh:24a17bba7f6d679538ff51b3a2f378cedadede97af8a1db7dad4fd8d6d50f829", + "zh:30ffb297ffd1633175d6545d37c2217e2cef9545a6e03946e514c59c0859b77d", + "zh:454ce4b3dbc73e6775f2f6605d45cee6e16c3872a2e66a2c97993d6e5cbd7055", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:91df0a9fab329aff2ff4cf26797592eb7a3a90b4a0c04d64ce186654e0cc6e17", + "zh:aa57384b85622a9f7bfb5d4512ca88e61f22a9cea9f30febaa4c98c68ff0dc21", + "zh:c4a3e329ba786ffb6f2b694e1fd41d413a7010f3a53c20b432325a94fa71e839", + "zh:e2699bc9116447f96c53d55f2a00570f982e6f9935038c3810603572693712d0", + "zh:e747c0fd5d7684e5bfad8aa0ca441903f15ae7a98a737ff6aca24ba223207e2c", + "zh:f1ca75f417ce490368f047b63ec09fd003711ae48487fba90b4aba2ccf71920e", + ] +} diff --git a/infrastructure/terraform/aws/README.md b/infrastructure/terraform/aws/README.md new file mode 100644 index 0000000000..3eebe1fde3 --- /dev/null +++ b/infrastructure/terraform/aws/README.md @@ -0,0 +1,26 @@ +# ECS Cluster Environment + +## Dependencies + +### State file bucket/config +You must have some way of storing terraform state files. +We use and recommend the s3 backend, but you can change +that configuration. In order to keep this code generic, however, +we omit the specific bucket config. + +You can fill in your own backend config in a file such as +`demo-env.s3.tfbackend` and supply this config on command-line: + +```bash +terraform init -backend-config=demo-env.s3.tfbackend +``` + +This file is only needed for `terraform init`. Once that is done, +you don't need to supply the backend config to `terraform plan/apply`. +If you need to change the backend, update this file and `init` again. + +### Vars file + +the module exposes its configuration area, but those configurations +need to be supplied at plan/apply time using the flag `-var-file=demo-env.tfvars`. + diff --git a/infrastructure/terraform/aws/main.tf b/infrastructure/terraform/aws/main.tf new file mode 100644 index 0000000000..d1811ec379 --- /dev/null +++ b/infrastructure/terraform/aws/main.tf @@ -0,0 +1,42 @@ +# aws terraform provider config + +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + backend "s3" { + # contents provided in NAME.s3.tfbackend + } +} + +provider "aws" { + region = var.region +} + +module "cluster" { + source = "./modules/v7-cluster" + + name = var.name + environment = var.environment + region = var.region + + availability_zones = ["us-east-1a", "us-east-1c"] + + core_configuration = { + container_port = 3000 + environment = { + API_KEY = "undefined" + JWT_SECRET = "undefined" + MAILGUN_SMTP_USERNAME = "undefined" + NEXT_PUBLIC_PUBPUB_URL = "undefined" + NEXT_PUBLIC_SUPABASE_URL = "undefined" + SENTRY_AUTH_TOKEN = "undefined" + SUPABASE_SERVICE_ROLE_KEY = "undefined" + SUPABASE_WEBHOOKS_API_KEY = "undefined" + } + } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf b/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf new file mode 100644 index 0000000000..e2bf8fc8e6 --- /dev/null +++ b/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf @@ -0,0 +1,9 @@ +# ecr repositories for all containers +resource "aws_ecr_repository" "pubpub_v7" { + name = "pubpub-v7" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = false # can set this to true if we want + } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf b/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf new file mode 100644 index 0000000000..1bdf14c52e --- /dev/null +++ b/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf @@ -0,0 +1,118 @@ +locals { + db_user = aws_db_instance.core_postgres.username + db_pw = random_password.rds_db_password.result + db_name = aws_db_instance.core_postgres.db_name + db_host = aws_db_instance.core_postgres.address + db_sslmode = "require" +} + +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + + cluster_name = "${var.name}-ecs-cluster-${var.environment}" + + cluster_configuration = { + execute_command_configuration = { + logging = "OVERRIDE" + log_configuration = { + cloud_watch_log_group_name = "/aws/ecs/aws-ec2" + } + } + } + + tags = { + Environment = "${var.name}-${var.environment}" + Project = "Pubpub-v7" + } +} + +module "ecs_service_core" { + source = "terraform-aws-modules/ecs/aws//modules/service" + name = "${var.name}-core" + + cluster_arn = module.ecs_cluster.arn + enable_execute_command = true + + cpu = 512 + memory = 1024 + desired_count = 1 + # execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + # task_role_arn = aws_iam_role.ecs_task_role.arn + + # Container definition(s) + container_definitions = { + + core = { + essential = true + image = "${aws_ecr_repository.pubpub_v7.repository_url}:latest" + port_mappings = [{ + protocol = "tcp" + containerPort = var.core_configuration.container_port + hostPort = var.core_configuration.container_port + }] + + environment = [ + # watch out for issues with a string-wrapped number in this context + { name = "PORT" + value = "${var.core_configuration.container_port}" + }, + { name = "DATABASE_URL" + value = "postgresql://${ + local.db_user}:${local.db_pw + }@${ + local.db_host + }:5432/${local.db_name}?sslmode=${local.db_sslmode}" + }, + { name = "API_KEY" , value = var.core_configuration.environment.API_KEY }, + # { name = ASSETS_REGION , value = var.core_configuration.environment.ASSETS_REGION + # { name = ASSETS_UPLOAD_KEY , value = var.core_configuration.environment.ASSETS_UPLOAD_KEY + # { name = ASSETS_UPLOAD_SECRET_KEY , value = var.core_configuration.environment.ASSETS_UPLOAD_SECRET_KEY + { name = "JWT_SECRET" , value = var.core_configuration.environment.JWT_SECRET}, + { name = "MAILGUN_SMTP_USERNAME" , value = var.core_configuration.environment.MAILGUN_SMTP_USERNAME}, + { name = "NEXT_PUBLIC_PUBPUB_URL" , value = var.core_configuration.environment.NEXT_PUBLIC_PUBPUB_URL}, + { name = "NEXT_PUBLIC_SUPABASE_URL" , value = var.core_configuration.environment.NEXT_PUBLIC_SUPABASE_URL}, + { name = "SENTRY_AUTH_TOKEN" , value = var.core_configuration.environment.SENTRY_AUTH_TOKEN}, + { name = "SUPABASE_SERVICE_ROLE_KEY" , value = var.core_configuration.environment.SUPABASE_SERVICE_ROLE_KEY}, + { name = "SUPABASE_WEBHOOKS_API_KEY" , value = var.core_configuration.environment.SUPABASE_WEBHOOKS_API_KEY}, + ] + + + # Example image used requires access to write to root filesystem + readonly_root_filesystem = false + + log_configuration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.ecs.name, + awslogs-region = var.region, + awslogs-stream-prefix = "ecs" + } + } + # memory_reservation = 100 + } + } + + + load_balancer = { + service = { + target_group_arn = aws_lb_target_group.main.arn + container_name = "core" # TODO: validate + container_port = var.core_configuration.container_port + } + } + + subnet_ids = aws_subnet.private.*.id + security_group_ids = [aws_security_group.ecs_tasks.id] + assign_public_ip = false + + tags = { + Environment = "${var.name}-${var.environment}" + Project = "Pubpub-v7" + } + + # this lifecycle property allows us to update the version of the container image without terraform clobbering it later + # changing the container image creates a "revision" of the task definition + # lifecycle { + # ignore_changes = [services.core.container_definitions.core.image] + # } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/main.tf b/infrastructure/terraform/aws/modules/v7-cluster/main.tf new file mode 100644 index 0000000000..73cf58d9e0 --- /dev/null +++ b/infrastructure/terraform/aws/modules/v7-cluster/main.tf @@ -0,0 +1,180 @@ +# aws terraform provider config + +terraform { + required_version = ">= 0.12.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} + +# Network configuration +resource "aws_vpc" "main" { + cidr_block = var.cidr +} + +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id +} + +resource "aws_subnet" "private" { + vpc_id = aws_vpc.main.id + cidr_block = element(var.private_subnets, count.index) + availability_zone = element(var.availability_zones, count.index) + count = length(var.private_subnets) +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = element(var.public_subnets, count.index) + availability_zone = element(var.availability_zones, count.index) + count = length(var.public_subnets) + map_public_ip_on_launch = true +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id +} + +resource "aws_route" "public" { + route_table_id = aws_route_table.public.id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id +} + +resource "aws_route_table_association" "public" { + count = length(var.public_subnets) + subnet_id = element(aws_subnet.public.*.id, count.index) + route_table_id = aws_route_table.public.id +} + +resource "aws_nat_gateway" "main" { + count = length(var.private_subnets) + allocation_id = element(aws_eip.nat.*.id, count.index) + # note that there is one nat gateway per private subnet, + # but the gateway must live in the public subnet + subnet_id = element(aws_subnet.public.*.id, count.index) + depends_on = [aws_internet_gateway.main] +} + +resource "aws_eip" "nat" { + count = length(var.private_subnets) + domain = "vpc" +} + +resource "aws_route_table" "private" { + count = length(var.private_subnets) + vpc_id = aws_vpc.main.id +} + +resource "aws_route" "private" { + count = length(compact(var.private_subnets)) + route_table_id = element(aws_route_table.private.*.id, count.index) + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = element(aws_nat_gateway.main.*.id, count.index) +} + +resource "aws_route_table_association" "private" { + count = length(var.private_subnets) + subnet_id = element(aws_subnet.private.*.id, count.index) + route_table_id = element(aws_route_table.private.*.id, count.index) +} + +# security groups for the load balancer and task +resource "aws_security_group" "alb" { + name = "${var.name}-sg-alb-${var.environment}" + vpc_id = aws_vpc.main.id + + ingress { + protocol = "tcp" + from_port = 80 + to_port = 80 + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "tcp" + from_port = 443 + to_port = 443 + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "ecs_tasks" { + name = "${var.name}-sg-task-${var.environment}" + vpc_id = aws_vpc.main.id + + ingress { + protocol = "tcp" + from_port = var.core_configuration.container_port + to_port = var.core_configuration.container_port + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + + egress { + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } +} + +# load balancer +resource "aws_lb" "main" { + name = "${var.name}-lb-${var.environment}" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.alb.id] + subnets = aws_subnet.public.*.id + + enable_deletion_protection = false +} + +resource "aws_lb_target_group" "main" { + name = "${var.name}-tg-${var.environment}" + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.main.id + target_type = "ip" + + # health_check { + # path = var.health_check_path + # interval = "30" + # protocol = "HTTP" + # matcher = "200" + # timeout = "3" + # unhealthy_threshold = "2" + # } +} + +resource "aws_lb_listener" "http" { + load_balancer_arn = aws_lb.main.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.main.arn + } +} + +# logging + +resource "aws_cloudwatch_log_group" "ecs" { + name = "${var.name}-ecs-${var.environment}-container-logs" + + tags = { + Environment = var.environment + } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf b/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf new file mode 100644 index 0000000000..6ea41c1226 --- /dev/null +++ b/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf @@ -0,0 +1,7 @@ +output "ecr_repository_url" { + value = aws_ecr_repository.pubpub_v7.repository_url +} + +output "rds_db_password_id" { + value = aws_secretsmanager_secret.rds_db_password.id +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/rds.tf b/infrastructure/terraform/aws/modules/v7-cluster/rds.tf new file mode 100644 index 0000000000..960c3a10ee --- /dev/null +++ b/infrastructure/terraform/aws/modules/v7-cluster/rds.tf @@ -0,0 +1,59 @@ +# generate password and make it accessible through aws secrets manager +resource "random_password" "rds_db_password" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret" "rds_db_password" { + name = "rds-db-password-${var.name}-${var.environment}" +} + +resource "aws_secretsmanager_secret_version" "password" { + secret_id = aws_secretsmanager_secret.rds_db_password.id + secret_string = random_password.rds_db_password.result +} + +# network config +resource "aws_db_subnet_group" "ecs_dbs" { + name = "${var.name}_ecs_db_${var.environment}" + subnet_ids = aws_subnet.private.*.id + + tags = { + Name = "subnet group for ECS RDS instances" + } +} + +resource "aws_security_group" "ecs_tasks_rds_instances" { + name = "${var.name}-sg-rds-${var.environment}" + vpc_id = aws_vpc.main.id + + ingress { + protocol = "tcp" + from_port = 5432 + to_port = 5432 + security_groups = [aws_security_group.ecs_tasks.id] + } +} + +# the actual database instance +resource "aws_db_instance" "core_postgres" { + identifier = "${var.name}-core-postgres-${var.environment}" + allocated_storage = 20 + db_name = "${var.name}_${var.environment}_core_postgres" + db_subnet_group_name = aws_db_subnet_group.ecs_dbs.name + engine = "postgres" + engine_version = "14" + instance_class = "db.t3.small" + vpc_security_group_ids = [aws_security_group.ecs_tasks_rds_instances.id] + username = var.name + password = random_password.rds_db_password.result + parameter_group_name = "default.postgres14" + skip_final_snapshot = true + + lifecycle { + ignore_changes = [ + password, + ] + } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/variables.tf b/infrastructure/terraform/aws/modules/v7-cluster/variables.tf new file mode 100644 index 0000000000..860bb11e54 --- /dev/null +++ b/infrastructure/terraform/aws/modules/v7-cluster/variables.tf @@ -0,0 +1,58 @@ +variable "name" { + description = "Familiar name of the stack" + default = "pubpub" +} + +variable "environment" { + description = "Name of the version/layer of the stack" +} + +variable "cidr" { + description = "The CIDR block for the VPC" + default = "10.0.0.0/16" +} + +variable "private_subnets" { + description = "a list of CIDRs for private subnets in the VPC, one for each availability zone" + default = ["10.0.128.0/20", "10.0.144.0/20"] +} + +variable "public_subnets" { + description = "a list of CIDRs for public subnets in the VPC, one for each availability zone" + default = ["10.0.0.0/20", "10.0.16.0/20"] +} + +variable "availability_zones" { + description = "a list of availability zones" +} + +variable "region" { + description = "Region for all resources (MUST agree with provider config)" + default = "us-east-1" +} + +variable "core_configuration" { + description = "Container configurations for `core`" + sensitive = true + + type = object({ + container_port = number + + # This might become too cumbersome, but for now it is nice to + # make the surface area clear everywhere + environment = object({ + # DATABASE_URL = string + API_KEY = string + # ASSETS_REGION = string + # ASSETS_UPLOAD_KEY = string + # ASSETS_UPLOAD_SECRET_KEY = string + JWT_SECRET = string + MAILGUN_SMTP_USERNAME = string + NEXT_PUBLIC_PUBPUB_URL = string + NEXT_PUBLIC_SUPABASE_URL = string + SENTRY_AUTH_TOKEN = string + SUPABASE_SERVICE_ROLE_KEY = string + SUPABASE_WEBHOOKS_API_KEY = string + }) + }) +} diff --git a/infrastructure/terraform/aws/outputs.tf b/infrastructure/terraform/aws/outputs.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/infrastructure/terraform/aws/variables.tf b/infrastructure/terraform/aws/variables.tf new file mode 100644 index 0000000000..43bf7735d0 --- /dev/null +++ b/infrastructure/terraform/aws/variables.tf @@ -0,0 +1,15 @@ +variable "region" { + description = "AWS region shortname" + type = string + default = "us-east-1" +} + +variable "name" { + description = "Proper name for this environment" + type = string +} + +variable "environment" { + description = "Functional name for this environment" + type = string +} diff --git a/infrastructure/terraform/globals/.terraform.lock.hcl b/infrastructure/terraform/globals/.terraform.lock.hcl new file mode 100644 index 0000000000..ec30c9d73c --- /dev/null +++ b/infrastructure/terraform/globals/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.33.0" + constraints = ">= 2.0.0" + hashes = [ + "h1:kPm7PkwHh6tZ74pUj5C/QRPtauxdnzrEG2yhCJla/4o=", + "zh:10bb683f2a9306e881f51a971ad3b2bb654ac94b54945dd63769876a343b5b04", + "zh:3916406db958d5487ea0c2d2320012d1907c29e6d01bf693560fe05e38ee0601", + "zh:3cb54b76b2f9e30620f3281ab7fb20633b1e4584fc84cc4ecd5752546252e86f", + "zh:513bcfd6971482215c5d64725189f875cbcbd260c6d11f0da4d66321efd93a92", + "zh:545a34427ebe7a950056627e7c980c9ba16318bf086d300eb808ffc41c52b7a8", + "zh:5a44b90faf1c8e8269f389c04bfac25ad4766d26360e7f7ac371be12a442981c", + "zh:64e1ef83162f78538dccad8b035577738851395ba774d6919cb21eb465a21e3a", + "zh:7315c70cb6b7f975471ea6129474639a08c58c071afc95a36cfaa41a13ae7fb9", + "zh:9806faae58938d638b757f54414400be998dddb45edfd4a29c85e827111dc93d", + "zh:997fa2e2db242354d9f772fba7eb17bd6d18d28480291dd93f85a18ca0a67ac2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f9e076b7e9752971f39eead6eda69df1c5e890c82ba2ca95f56974af7adfe79", + "zh:b1d6af047f96de7f97d38b685654f1aed4356d5060b0e696d87d0270f5d49f75", + "zh:bfb0654b6f34398aeffdf907b744af06733d168db610a2c5747263380f817ac7", + "zh:e25203ee8cedccf60bf450950d533d3c172509bda8af97dbc3bc817d2a503c57", + ] +} diff --git a/infrastructure/terraform/globals/README.md b/infrastructure/terraform/globals/README.md new file mode 100644 index 0000000000..bd137d7a9f --- /dev/null +++ b/infrastructure/terraform/globals/README.md @@ -0,0 +1,15 @@ +# Global terraform state module + +This module should generally be created by an admin, +and not applied or updated by a machine user. + +1. Uncomment the code creating this bucket; comment the backend block +1. terraform init +1. Set the bucket name +1. terraform apply +1. `terraform state rm aws_s3_bucket.terraform_state` +1. comment the bucket definition; uncomment the backend block +1. terraform init ("yes" to copying the state file) +1. destroy local copies of the state file + +This bucket name can now be in your s3.tfbackend files everywhere. diff --git a/infrastructure/terraform/globals/github_actions_iam.tf b/infrastructure/terraform/globals/github_actions_iam.tf new file mode 100644 index 0000000000..116850b7f3 --- /dev/null +++ b/infrastructure/terraform/globals/github_actions_iam.tf @@ -0,0 +1,83 @@ +# iam user for Github Actions + +resource "aws_iam_user" "github_actions" { + name = "github_actions" + path = "/" +} + +resource "aws_iam_access_key" "github_actions" { + user = aws_iam_user.github_actions.name +} + +resource "aws_iam_policy" "ecr" { + name = "ECRAdmin" + policy = jsonencode({ + Version: "2012-10-17", + Statement: [ + { + Sid: "EcrAdmin", + Effect: "Allow", + Action: [ + "ecr:*" + ], + Resource: [ + "*" + ] + } + ] + }) +} + +// iam policy to allow aws ecs update-service +resource "aws_iam_policy" "ecs" { + name = "ECSUpdateService" + policy = jsonencode({ + Version: "2012-10-17", + Statement: [ + { + Sid: "EcsUpdateService", + Effect: "Allow", + Action: [ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:DescribeClusters" + ], + Resource: [ + "*" + ] + } + ] + }) +} + +resource "aws_iam_role" "github_actions_role" { + name = "github_actions_role" + + # Terraform's "jsonencode" function converts a + # Terraform expression result to valid JSON syntax. + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "sts:AssumeRole", + "sts:TagSession", // specifically used by AWS provided GH action modules + ] + Effect = "Allow" + Principal = { + AWS = [aws_iam_user.github_actions.arn] + } + }, + ] + }) +} + +resource "aws_iam_role_policy_attachment" "gha_attach_ecr" { + role = aws_iam_role.github_actions_role.name + policy_arn = aws_iam_policy.ecr.arn +} + +resource "aws_iam_role_policy_attachment" "gha_attach_ecs" { + role = aws_iam_role.github_actions_role.name + policy_arn = aws_iam_policy.ecs.arn +} diff --git a/infrastructure/terraform/globals/main.tf b/infrastructure/terraform/globals/main.tf new file mode 100644 index 0000000000..5c66e9362d --- /dev/null +++ b/infrastructure/terraform/globals/main.tf @@ -0,0 +1,38 @@ +# aws terraform provider config + +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 2.0" + } + } + backend "s3" { + bucket = "pubpub-tfstates" + key = "global.tfstate" + region = "us-east-1" + } +} + + +provider "aws" { + region = "us-east-1" +} + +## s3 bucket for terraform state +# +# This resource was created using terraform with these fields. +# However, it is dangerous to manage terraform state files in +# a bucket that is itself Terraform-configured. +# +# it has been removed with `terraform state rm`, but this +# declaration is left here for posterity. +# +# resource "aws_s3_bucket" "terraform_state" { +# bucket = "pubpub-tfstates" +# acl = "private" +# versioning { +# enabled = true +# } +# } diff --git a/infrastructure/terraform/globals/outputs.tf b/infrastructure/terraform/globals/outputs.tf new file mode 100644 index 0000000000..cc28776c7b --- /dev/null +++ b/infrastructure/terraform/globals/outputs.tf @@ -0,0 +1,20 @@ +# if resources are needed in an environment's inputs, +# then you can use terraform_remote_state to get this module's output + +# put these creds in github actions secrets config +output "github_actions_user_credential" { + value = { + id = aws_iam_access_key.github_actions.id + secret = aws_iam_access_key.github_actions.secret + } + + # prevents this secret value from appearing accidentally + # NOTE - it is still saved in the state file. + sensitive = true +} + +# Provide this value to github actions +output "github_actions_role_to_assume_arn" { + value = aws_iam_role.github_actions_role.arn +} + From bf94603e7210fff2a660f1be29e7242ff7dd2e3d Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Thu, 25 Jan 2024 13:31:21 -0800 Subject: [PATCH 02/16] migrate ecs service to module --- infrastructure/terraform/aws/main.tf | 34 ++++--- .../terraform/aws/modules/v7-cluster/ecs.tf | 98 ------------------- .../terraform/aws/modules/v7-cluster/main.tf | 6 +- .../aws/modules/v7-cluster/outputs.tf | 24 +++++ .../aws/modules/v7-cluster/variables.tf | 50 +++++----- 5 files changed, 77 insertions(+), 135 deletions(-) diff --git a/infrastructure/terraform/aws/main.tf b/infrastructure/terraform/aws/main.tf index d1811ec379..313b9c909a 100644 --- a/infrastructure/terraform/aws/main.tf +++ b/infrastructure/terraform/aws/main.tf @@ -24,19 +24,31 @@ module "cluster" { environment = var.environment region = var.region + container_ingress_port = 3000 + availability_zones = ["us-east-1a", "us-east-1c"] +} + +module "service_core" { + source = "./modules/ecs-service" + + service_name = "core" + cluster_info = module.cluster.cluster_info - core_configuration = { + + repository_url = module.cluster.ecr_repository_url + + configuration = { container_port = 3000 - environment = { - API_KEY = "undefined" - JWT_SECRET = "undefined" - MAILGUN_SMTP_USERNAME = "undefined" - NEXT_PUBLIC_PUBPUB_URL = "undefined" - NEXT_PUBLIC_SUPABASE_URL = "undefined" - SENTRY_AUTH_TOKEN = "undefined" - SUPABASE_SERVICE_ROLE_KEY = "undefined" - SUPABASE_WEBHOOKS_API_KEY = "undefined" - } + environment = [ + {name = "API_KEY", value = "undefined"}, + {name = "JWT_SECRET", value = "undefined"}, + {name = "MAILGUN_SMTP_USERNAME", value = "undefined"}, + {name = "NEXT_PUBLIC_PUBPUB_URL", value = "undefined"}, + {name = "NEXT_PUBLIC_SUPABASE_URL", value = "undefined"}, + {name = "SENTRY_AUTH_TOKEN", value = "undefined"}, + {name = "SUPABASE_SERVICE_ROLE_KEY", value = "undefined"}, + {name = "SUPABASE_WEBHOOKS_API_KEY", value = "undefined"}, + ] } } diff --git a/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf b/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf index 1bdf14c52e..69426ef799 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/ecs.tf @@ -1,11 +1,3 @@ -locals { - db_user = aws_db_instance.core_postgres.username - db_pw = random_password.rds_db_password.result - db_name = aws_db_instance.core_postgres.db_name - db_host = aws_db_instance.core_postgres.address - db_sslmode = "require" -} - module "ecs_cluster" { source = "terraform-aws-modules/ecs/aws//modules/cluster" @@ -26,93 +18,3 @@ module "ecs_cluster" { } } -module "ecs_service_core" { - source = "terraform-aws-modules/ecs/aws//modules/service" - name = "${var.name}-core" - - cluster_arn = module.ecs_cluster.arn - enable_execute_command = true - - cpu = 512 - memory = 1024 - desired_count = 1 - # execution_role_arn = aws_iam_role.ecs_task_execution_role.arn - # task_role_arn = aws_iam_role.ecs_task_role.arn - - # Container definition(s) - container_definitions = { - - core = { - essential = true - image = "${aws_ecr_repository.pubpub_v7.repository_url}:latest" - port_mappings = [{ - protocol = "tcp" - containerPort = var.core_configuration.container_port - hostPort = var.core_configuration.container_port - }] - - environment = [ - # watch out for issues with a string-wrapped number in this context - { name = "PORT" - value = "${var.core_configuration.container_port}" - }, - { name = "DATABASE_URL" - value = "postgresql://${ - local.db_user}:${local.db_pw - }@${ - local.db_host - }:5432/${local.db_name}?sslmode=${local.db_sslmode}" - }, - { name = "API_KEY" , value = var.core_configuration.environment.API_KEY }, - # { name = ASSETS_REGION , value = var.core_configuration.environment.ASSETS_REGION - # { name = ASSETS_UPLOAD_KEY , value = var.core_configuration.environment.ASSETS_UPLOAD_KEY - # { name = ASSETS_UPLOAD_SECRET_KEY , value = var.core_configuration.environment.ASSETS_UPLOAD_SECRET_KEY - { name = "JWT_SECRET" , value = var.core_configuration.environment.JWT_SECRET}, - { name = "MAILGUN_SMTP_USERNAME" , value = var.core_configuration.environment.MAILGUN_SMTP_USERNAME}, - { name = "NEXT_PUBLIC_PUBPUB_URL" , value = var.core_configuration.environment.NEXT_PUBLIC_PUBPUB_URL}, - { name = "NEXT_PUBLIC_SUPABASE_URL" , value = var.core_configuration.environment.NEXT_PUBLIC_SUPABASE_URL}, - { name = "SENTRY_AUTH_TOKEN" , value = var.core_configuration.environment.SENTRY_AUTH_TOKEN}, - { name = "SUPABASE_SERVICE_ROLE_KEY" , value = var.core_configuration.environment.SUPABASE_SERVICE_ROLE_KEY}, - { name = "SUPABASE_WEBHOOKS_API_KEY" , value = var.core_configuration.environment.SUPABASE_WEBHOOKS_API_KEY}, - ] - - - # Example image used requires access to write to root filesystem - readonly_root_filesystem = false - - log_configuration = { - logDriver = "awslogs", - options = { - awslogs-group = aws_cloudwatch_log_group.ecs.name, - awslogs-region = var.region, - awslogs-stream-prefix = "ecs" - } - } - # memory_reservation = 100 - } - } - - - load_balancer = { - service = { - target_group_arn = aws_lb_target_group.main.arn - container_name = "core" # TODO: validate - container_port = var.core_configuration.container_port - } - } - - subnet_ids = aws_subnet.private.*.id - security_group_ids = [aws_security_group.ecs_tasks.id] - assign_public_ip = false - - tags = { - Environment = "${var.name}-${var.environment}" - Project = "Pubpub-v7" - } - - # this lifecycle property allows us to update the version of the container image without terraform clobbering it later - # changing the container image creates a "revision" of the task definition - # lifecycle { - # ignore_changes = [services.core.container_definitions.core.image] - # } -} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/main.tf b/infrastructure/terraform/aws/modules/v7-cluster/main.tf index 73cf58d9e0..302547a61e 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/main.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/main.tf @@ -115,8 +115,8 @@ resource "aws_security_group" "ecs_tasks" { ingress { protocol = "tcp" - from_port = var.core_configuration.container_port - to_port = var.core_configuration.container_port + from_port = var.container_ingress_port + to_port = var.container_ingress_port cidr_blocks = ["0.0.0.0/0"] ipv6_cidr_blocks = ["::/0"] } @@ -130,6 +130,7 @@ resource "aws_security_group" "ecs_tasks" { } } + # load balancer resource "aws_lb" "main" { name = "${var.name}-lb-${var.environment}" @@ -169,6 +170,7 @@ resource "aws_lb_listener" "http" { } } + # logging resource "aws_cloudwatch_log_group" "ecs" { diff --git a/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf b/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf index 6ea41c1226..5d5b18aead 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf @@ -1,3 +1,10 @@ +locals { + db_user = aws_db_instance.core_postgres.username + db_name = aws_db_instance.core_postgres.db_name + db_host = aws_db_instance.core_postgres.address + db_sslmode = "require" +} + output "ecr_repository_url" { value = aws_ecr_repository.pubpub_v7.repository_url } @@ -5,3 +12,20 @@ output "ecr_repository_url" { output "rds_db_password_id" { value = aws_secretsmanager_secret.rds_db_password.id } + +output "rds_connection_string_sans_password" { + value = "postgresql://${local.db_user}@${local.db_host}:5432/${local.db_name}?sslmode=${local.db_sslmode}" +} + +output "cluster_info" { + value = { + region = var.region + name = var.name + environment = var.environment + cluster_arn = module.ecs_cluster.arn + private_subnet_ids = aws_subnet.private.*.id + container_security_group_ids = [aws_security_group.ecs_tasks.id] + cloudwatch_log_group_name = aws_cloudwatch_log_group.ecs.name + lb_target_group_arn = aws_lb_target_group.main.arn + } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/variables.tf b/infrastructure/terraform/aws/modules/v7-cluster/variables.tf index 860bb11e54..4045e3b8a3 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/variables.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/variables.tf @@ -26,33 +26,35 @@ variable "availability_zones" { description = "a list of availability zones" } +# variable "container_port" { +# description = "The port the containers are listening on" +# default = 5050 +# } +# +# variable "container_environment" { +# description = "Environment variables for the containers" +# default = [] +# } +# +# variable "health_check_path" { +# description = "The path for the health check" +# default = "/v1/debug/health" +# } +# +# variable "hosted_zone_id" { +# description = "The ID of the hosted zone for the domain" +# } +# +# variable "subdomain" { +# description = "Prefix to domain name of hosted zone above, so serve app from" +# } + variable "region" { description = "Region for all resources (MUST agree with provider config)" default = "us-east-1" } -variable "core_configuration" { - description = "Container configurations for `core`" - sensitive = true - - type = object({ - container_port = number - - # This might become too cumbersome, but for now it is nice to - # make the surface area clear everywhere - environment = object({ - # DATABASE_URL = string - API_KEY = string - # ASSETS_REGION = string - # ASSETS_UPLOAD_KEY = string - # ASSETS_UPLOAD_SECRET_KEY = string - JWT_SECRET = string - MAILGUN_SMTP_USERNAME = string - NEXT_PUBLIC_PUBPUB_URL = string - NEXT_PUBLIC_SUPABASE_URL = string - SENTRY_AUTH_TOKEN = string - SUPABASE_SERVICE_ROLE_KEY = string - SUPABASE_WEBHOOKS_API_KEY = string - }) - }) +variable "container_ingress_port" { + description = "port to allow traffic in private security group" + type = number } From db06834fc4af287f0888c864f46651ed92f1d725 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Thu, 25 Jan 2024 14:04:47 -0800 Subject: [PATCH 03/16] add module for services --- .../aws/modules/ecs-service/README.md | 0 .../terraform/aws/modules/ecs-service/main.tf | 65 +++++++++++++++++++ .../aws/modules/ecs-service/outputs.tf | 0 .../aws/modules/ecs-service/variables.tf | 50 ++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 infrastructure/terraform/aws/modules/ecs-service/README.md create mode 100644 infrastructure/terraform/aws/modules/ecs-service/main.tf create mode 100644 infrastructure/terraform/aws/modules/ecs-service/outputs.tf create mode 100644 infrastructure/terraform/aws/modules/ecs-service/variables.tf diff --git a/infrastructure/terraform/aws/modules/ecs-service/README.md b/infrastructure/terraform/aws/modules/ecs-service/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/infrastructure/terraform/aws/modules/ecs-service/main.tf b/infrastructure/terraform/aws/modules/ecs-service/main.tf new file mode 100644 index 0000000000..0bd48bfb17 --- /dev/null +++ b/infrastructure/terraform/aws/modules/ecs-service/main.tf @@ -0,0 +1,65 @@ +module "ecs_service" { + source = "terraform-aws-modules/ecs/aws//modules/service" + name = "${var.cluster_info.name}-${var.service_name}" + + cluster_arn = var.cluster_info.cluster_arn + enable_execute_command = true + + cpu = var.resources.cpu + memory = var.resources.memory + desired_count = var.resources.desired_count + # execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + # task_role_arn = aws_iam_role.ecs_task_role.arn + + # Container definition(s) + container_definitions = { + + "${var.service_name}" = { + essential = true + image = "${var.repository_url}:latest" + port_mappings = [{ + protocol = "tcp" + containerPort = var.configuration.container_port + hostPort = var.configuration.container_port + }] + + environment = var.configuration.environment + + readonly_root_filesystem = false + + log_configuration = { + logDriver = "awslogs", + options = { + awslogs-group = var.cluster_info.cloudwatch_log_group_name, + awslogs-region = var.cluster_info.region, + awslogs-stream-prefix = "ecs" + } + } + # memory_reservation = 100 + } + } + + + load_balancer = { + service = { + target_group_arn = var.cluster_info.lb_target_group_arn + container_name = var.service_name + container_port = var.configuration.container_port + } + } + + subnet_ids = var.cluster_info.private_subnet_ids + security_group_ids = var.cluster_info.container_security_group_ids + assign_public_ip = false + + tags = { + Environment = "${var.cluster_info.name}-${var.cluster_info.environment}" + Project = "Pubpub-v7" + } + + # this lifecycle property allows us to update the version of the container image without terraform clobbering it later + # changing the container image creates a "revision" of the task definition + # lifecycle { + # ignore_changes = [services.core.container_definitions.core.image] + # } +} diff --git a/infrastructure/terraform/aws/modules/ecs-service/outputs.tf b/infrastructure/terraform/aws/modules/ecs-service/outputs.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/infrastructure/terraform/aws/modules/ecs-service/variables.tf b/infrastructure/terraform/aws/modules/ecs-service/variables.tf new file mode 100644 index 0000000000..fabd602883 --- /dev/null +++ b/infrastructure/terraform/aws/modules/ecs-service/variables.tf @@ -0,0 +1,50 @@ +variable "cluster_info" { + description = "infrastructure values output from v7-cluster" + + type = object({ + region = string + name = string + cluster_arn = string + environment = string + private_subnet_ids = list(string) + container_security_group_ids = list(string) + cloudwatch_log_group_name = string + lb_target_group_arn = string + }) +} + +variable "service_name" { + description = "name for this service" +} + +variable "repository_url" { + description = "url to the image repository (excluding tag)" +} + +variable "resources" { + description = "resources available to this container service" + type = object({ + cpu = number + memory = number + desired_count = number + }) + + default = { + cpu = 512 + memory = 1024 + desired_count = 1 + } +} + +variable "configuration" { + description = "Container configuration options" + + type = object({ + container_port = number + + environment = list(object({ + name = string + value = string + })) + }) +} From cd49d0d4bf61ae5369ee847496a94524b3feceee Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Thu, 25 Jan 2024 14:34:01 -0800 Subject: [PATCH 04/16] parametric docker build --- Dockerfile | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4511f448e5..6a7d0a467c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,9 @@ ARG NODE_VERSION=20.6.0 ARG PNPM_VERSION=8.14.3 +ARG PACKAGE_DIR=core +ARG PACKAGE=core + ################################################################################ # Use node image for base image for all stages. FROM node:${NODE_VERSION}-alpine as base @@ -24,6 +27,7 @@ RUN --mount=type=cache,target=/root/.npm \ ################################################################################ # Create a stage for installing production dependecies. FROM base as deps +ARG PACKAGE_DIR # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds. @@ -31,7 +35,7 @@ FROM base as deps # into this layer. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ - --mount=type=bind,source=core/package.json,target=core/package.json \ + --mount=type=bind,source=${PACKAGE_DIR}/package.json,target=${PACKAGE_DIR}/package.json \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=cache,target=/root/.local/share/pnpm/store \ --mount=type=cache,target=/pnpm/store \ @@ -40,11 +44,12 @@ RUN --mount=type=bind,source=package.json,target=package.json \ ################################################################################ # Create a stage for building the application. FROM deps as build +ARG PACKAGE_DIR PACKAGE # Download additional development dependencies before building, as some projects require # "devDependencies" to be installed to build. If you don't need this, remove this step. RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=core/package.json,target=core/package.json \ + --mount=type=bind,source=${PACKAGE_DIR}/package.json,target=${PACKAGE_DIR}/package.json \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=cache,target=/root/.local/share/pnpm/store \ --mount=type=cache,target=/pnpm/store \ @@ -54,12 +59,13 @@ RUN --mount=type=bind,source=package.json,target=package.json \ COPY . . # Run the build script. -RUN ./bin/render-build.sh core +RUN ./bin/render-build.sh ${PACKAGE} ################################################################################ # Create a new stage to run the application with minimal runtime dependencies # where the necessary files are copied from the build stage. FROM base as final +ARG PACKAGE_DIR PACKAGE # Use production node environment by default. ENV NODE_ENV production @@ -72,28 +78,19 @@ COPY package.json \ pnpm-workspace.yaml \ . -COPY ./core/package.json \ - ./core/.env.template \ - ./core/postcss.config.js \ - ./core/tailwind.config.js \ - ./core/ - - # TODO: These may be needed, but are TS currently: - # ./core/next.config.js \ - # ./core/sentry.client.config.ts \ - # ./core/sentry.edge.config.ts \ - # ./core/sentry.server.config.ts \ +COPY ./${PACKAGE_DIR}/package.json \ + ./${PACKAGE_DIR}/ # Copy the production dependencies from the deps stage and also # the built application from the build stage into the image. COPY --from=deps /usr/src/app/node_modules ./node_modules -COPY --from=build /usr/src/app/core/node_modules ./core/node_modules -COPY --from=build /usr/src/app/core/.next ./core/.next +COPY --from=build /usr/src/app/${PACKAGE_DIR}/node_modules ./${PACKAGE_DIR}/node_modules +COPY --from=build /usr/src/app/${PACKAGE_DIR}/.next ./${PACKAGE_DIR}/.next # Expose the port that the application listens on. EXPOSE 3000 # Run the application. -CMD pnpm --filter core start +CMD pnpm --filter ${PACKAGE} start From b19fd082245e74bf04a4fa04f196eaaca08fb77d Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Thu, 1 Feb 2024 14:30:14 -0700 Subject: [PATCH 05/16] add Github Actions workflow --- .github/workflows/awsdeploy.yml | 117 ++++++++++++++++++ README.md | 71 +++++++++++ infrastructure/terraform/aws/main.tf | 2 +- .../terraform/aws/modules/ecs-service/main.tf | 11 +- 4 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/awsdeploy.yml diff --git a/.github/workflows/awsdeploy.yml b/.github/workflows/awsdeploy.yml new file mode 100644 index 0000000000..bc4351911f --- /dev/null +++ b/.github/workflows/awsdeploy.yml @@ -0,0 +1,117 @@ +# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service + +name: aws ecs deploy + +on: + push: + branches: + - main + +env: + AWS_REGION: us-east-1 # set this to your preferred AWS region, e.g. us-west-1 + ECR_REPOSITORY: pubpub-v7 # set this to your Amazon ECR repository name + ECS_SERVICE: blake-core # set this to your Amazon ECS service name + ECS_CLUSTER: blake-ecs-cluster-staging # set this to your Amazon ECS cluster name + ECS_TASK_DEFINITION_TEMPLATE: blake-core # set this to the FAMILY of your task definition + CONTAINER_NAME: core # set this to the name of the container in the + # containerDefinitions section of your task definition + +jobs: + build: + name: Build + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Get short sha + id: shortsha + run: echo "sha_short=$(git describe --always --abbrev=40 --dirty)" >> $GITHUB_OUTPUT + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} + run: | + # Build a docker container and + # push it to ECR so that it can + # be deployed to ECS. + docker build \ + --platform linux/amd64 \ + --build-arg PACKAGE=core \ + --build-arg PACKAGE_DIR=core \ + -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ + . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + + deploy: + name: Deploy + runs-on: ubuntu-latest + environment: production + needs: + - build + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Get short sha + id: shortsha + run: echo "sha_short=$(git describe --always --abbrev=40 --dirty)" >> $GITHUB_OUTPUT + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Retrieve Task Definition contents from template + id: get-taskdef + run: | + aws ecs describe-task-definition \ + --task-definition $ECS_TASK_DEFINITION_TEMPLATE \ + --query taskDefinition >> template_task_def.json + + - name: Interpolate image tag + id: write-tag + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} + run: | + echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc + with: + task-definition: template_task_def.json + container-name: ${{ env.CONTAINER_NAME }} + image: ${{ steps.write-tag.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true diff --git a/README.md b/README.md index feb8c883b6..ec073f7e49 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,74 @@ We currently have a race condition where dev will sometimes fail because we can' - https://github.com/vercel/turbo/issues/460 `core` depends on `ui` which depends on `utils`. `utils` often takes longer to build than it does for `ui` to start building, which causes an error to be thrown because `utils` d.ts file has been cleared out during its build and hasn't been replaced yet. This generates an error, but is quick to resolve, so doesn't break actual dev work from beginning. It does make the console output messier though. + + +## Building and deploying for AWS environments + +All change management to Knowledge Futures' production environment is done through github actions. +This environment runs on AWS ECS and leverages terraform to allow reproducible, parametric environments. + +Services running in AWS ECS are scheduled using "Task Definitions", which are CRUDdy resources +including all details for a container. We don't want to tie code releases to terraform "infrastructure" changes, +but the service "declaration" relies on this Task Definition to exist. + +Therefore based on community patterns we have seen, the flow is roughly this: +1. The infrastructure code in terraform declares a "template" Task Definition. +2. Terraform is told not to change the "service" based on changes to the Task Definition. +3. Any changes to the template will be picked up by the next deploy, which is done outside of Terraform. +4. Github Actions builds new containers on merge, and will use AWS-provided Actions to template the literal correct Task Definition and update the Service. + +### Updating deployment topology and/or environment variables/container settings + +To change "infrastructure settings", which include anything from networking to env vars, +make changes to `./infrastructure/terraform/aws`. Use `terraform apply` there to update +the infrastructure and/or Task Definition Template. See that directory for more info. + +Then you must perform a Github Actions Deploy, either by pushing your changes to main or +with local `act` CLI. + +### Updating container versions with github actions + +The core automation workflow can be examined in [`awsdeploy.yml`](./.github/workflows/awsdeploy.yml) + +There is a Dockerfile in this repository that builds a container for one package. You can use it like: + +``` +docker build \ + --platform linux/amd64 \ + --build-arg PACKAGE=core \ + --build-arg PACKAGE_DIR=core \ + -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ + . +``` + +Note how this matches the invocation used in the Actions workflow file. + +We automatically build and push a container to AWS ECR with the github SHA as a tag. + +### Using `act` to run the deploy events locally + +If you need to build or validate a change and deploy to production, you can use the [`act` cli](https://github.com/nektos/act): + +``` +act \ + -W .github/workflows/awsdeploy.yml \ + --container-architecture linux/amd64 \ + --secret-file ~/.aws/pubpub.secrets \ + -j deploy +``` + +**Secrets:** Though you will have an `~/.aws/credentials` file, this is not the format for secrets access that +`act` requires, so I copy the key-value pairs in that file into a file that matches the Github +secrets called `~/.aws/pubpub.secrets`: + +``` +AWS_ACCESS_KEY_ID=... +AWS_SECRET_ACCESS_KEY... +``` + +**Dirty worktree:** If you run `act` like this, the image will be conveniently tagged with the latest SHA plus `-dirty`. +Images tagged with a SHA alone should be idempotently built, but `-dirty` can be changed/overwritten. + +**TODO:** +- [ ] allow deploying without a rebuild, so that a rollback is convenient diff --git a/infrastructure/terraform/aws/main.tf b/infrastructure/terraform/aws/main.tf index 313b9c909a..1a43a5ed16 100644 --- a/infrastructure/terraform/aws/main.tf +++ b/infrastructure/terraform/aws/main.tf @@ -44,7 +44,7 @@ module "service_core" { {name = "API_KEY", value = "undefined"}, {name = "JWT_SECRET", value = "undefined"}, {name = "MAILGUN_SMTP_USERNAME", value = "undefined"}, - {name = "NEXT_PUBLIC_PUBPUB_URL", value = "undefined"}, + {name = "NEXT_PUBLIC_PUBPUB_URL", value = "https://v7.pubpub.org"}, {name = "NEXT_PUBLIC_SUPABASE_URL", value = "undefined"}, {name = "SENTRY_AUTH_TOKEN", value = "undefined"}, {name = "SUPABASE_SERVICE_ROLE_KEY", value = "undefined"}, diff --git a/infrastructure/terraform/aws/modules/ecs-service/main.tf b/infrastructure/terraform/aws/modules/ecs-service/main.tf index 0bd48bfb17..c2de9f63b1 100644 --- a/infrastructure/terraform/aws/modules/ecs-service/main.tf +++ b/infrastructure/terraform/aws/modules/ecs-service/main.tf @@ -5,13 +5,16 @@ module "ecs_service" { cluster_arn = var.cluster_info.cluster_arn enable_execute_command = true + # allow github actions to update the service without confusing TF + ignore_task_definition_changes = true + cpu = var.resources.cpu memory = var.resources.memory desired_count = var.resources.desired_count # execution_role_arn = aws_iam_role.ecs_task_execution_role.arn # task_role_arn = aws_iam_role.ecs_task_role.arn - # Container definition(s) + # TEMPLATE Container definition(s). container_definitions = { "${var.service_name}" = { @@ -56,10 +59,4 @@ module "ecs_service" { Environment = "${var.cluster_info.name}-${var.cluster_info.environment}" Project = "Pubpub-v7" } - - # this lifecycle property allows us to update the version of the container image without terraform clobbering it later - # changing the container image creates a "revision" of the task definition - # lifecycle { - # ignore_changes = [services.core.container_definitions.core.image] - # } } From 0fcd13a5dde212b1b12dfde4aa206a74c0174ded Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Thu, 1 Feb 2024 14:40:25 -0700 Subject: [PATCH 06/16] add comment about AWS CLI in act --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index ec073f7e49..cd29025ebb 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,19 @@ act \ -j deploy ``` +**AWS CLI access in `act`:** +When you setup `act` locally for the first time, you can choose whether to do a Small, Medium, or Large install. +The Large install is *very large*, but the medium install does not include the AWS CLI. + +If you choose to work with the medium install, you will need to customize afterward by editing your `~/.actrc` file +to use an image that includes the AWS CLI, for example your .actrc might look like: +``` +-P ubuntu-latest=eveships/act-plusplus:latest +-P ubuntu-22.04=catthehacker/ubuntu:full-22.04 +-P ubuntu-20.04=catthehacker/ubuntu:full-20.04 +-P ubuntu-18.04=catthehacker/ubuntu:full-18.04 +``` + **Secrets:** Though you will have an `~/.aws/credentials` file, this is not the format for secrets access that `act` requires, so I copy the key-value pairs in that file into a file that matches the Github secrets called `~/.aws/pubpub.secrets`: From 54cc9d8a3e588a6ee064c512db3aea4cfd9f1214 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 6 Feb 2024 15:21:59 -0700 Subject: [PATCH 07/16] add repositories for component-based containers --- .../terraform/aws/modules/v7-cluster/ecr.tf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf b/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf index e2bf8fc8e6..8c72a849d7 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf @@ -7,3 +7,21 @@ resource "aws_ecr_repository" "pubpub_v7" { scan_on_push = false # can set this to true if we want } } + +resource "aws_ecr_repository" "pubpub_v7_core" { + name = "pubpub-v7-core" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = false # can set this to true if we want + } +} + +resource "aws_ecr_repository" "pubpub_v7_intg_submissions" { + name = "pubpub-v7-integration-submissions" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = false # can set this to true if we want + } +} From 00afa3afa4967b7cacb61e240dd6b23e18ccd22f Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 6 Feb 2024 15:24:41 -0700 Subject: [PATCH 08/16] add ecr-build workflows --- .github/workflows/ecrbuild-all.yml | 25 +++++++++ .github/workflows/ecrbuild-template.yml | 68 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 .github/workflows/ecrbuild-all.yml create mode 100644 .github/workflows/ecrbuild-template.yml diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml new file mode 100644 index 0000000000..0b79cb30ab --- /dev/null +++ b/.github/workflows/ecrbuild-all.yml @@ -0,0 +1,25 @@ +# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service + +name: aws ecs deploy + +on: + push: + +jobs: + build-core: + uses: ./.github/workflows/ecrbuild-template.yml + with: + package-name: core + package-src: core + secrets: + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + + build-intg-submissions: + uses: ./.github/workflows/ecrbuild-template.yaml + with: + package-name: integration-submissions + package-src: integrations/submissions + secrets: + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml new file mode 100644 index 0000000000..894a815a88 --- /dev/null +++ b/.github/workflows/ecrbuild-template.yml @@ -0,0 +1,68 @@ +# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service + +name: aws ecr build template + +on: + workflow_call: + inputs: + package-name: + required: true + type: string + package-src: + required: true + type: string + secrets: + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true + +env: + AWS_REGION: us-east-1 # set this to your preferred AWS region, e.g. us-west-1 + ECR_REPOSITORY_PREFIX: pubpub-v7 # set this to your Amazon ECR repository name + +jobs: + build: + name: Build + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Get short sha + id: shortsha + run: echo "sha_short=$(git describe --always --abbrev=40 --dirty)" >> $GITHUB_OUTPUT + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} + PACKAGE_DIR: ${{ inputs.package-src }} + PACKAGE: ${{ inputs.package-name }} + run: | + # Build a docker container and + # push it to ECR so that it can + # be deployed to ECS. + docker build \ + --platform linux/amd64 \ + --build-arg PACKAGE=$PACKAGE \ + --build-arg PACKAGE_DIR=$PACKAGE_DIR \ + -t $ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG \ + . + docker push $ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG + echo "image=$ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG" >> $GITHUB_OUTPUT + From 9386110ffc7e10ed2545862e4b88574857f74423 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 6 Feb 2024 15:34:48 -0700 Subject: [PATCH 09/16] fix typo in workflow --- .github/workflows/ecrbuild-all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml index 0b79cb30ab..5b1b6545eb 100644 --- a/.github/workflows/ecrbuild-all.yml +++ b/.github/workflows/ecrbuild-all.yml @@ -16,7 +16,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} build-intg-submissions: - uses: ./.github/workflows/ecrbuild-template.yaml + uses: ./.github/workflows/ecrbuild-template.yml with: package-name: integration-submissions package-src: integrations/submissions From e61aab891073ea53156c020ea9c07420543d7978 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 6 Feb 2024 15:58:41 -0700 Subject: [PATCH 10/16] use assume-role flow --- .github/workflows/ecrbuild-template.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml index 894a815a88..bb381d29a4 100644 --- a/.github/workflows/ecrbuild-template.yml +++ b/.github/workflows/ecrbuild-template.yml @@ -34,6 +34,7 @@ jobs: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: + role-to-assume: ${{ vars.IAM_ROLE_TO_ASSUME }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ env.AWS_REGION }} From ec24b685747be0956fb20685b3033d143733636a Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 6 Feb 2024 16:00:11 -0700 Subject: [PATCH 11/16] remove environment key --- .github/workflows/ecrbuild-template.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml index bb381d29a4..271247b564 100644 --- a/.github/workflows/ecrbuild-template.yml +++ b/.github/workflows/ecrbuild-template.yml @@ -25,7 +25,6 @@ jobs: build: name: Build runs-on: ubuntu-latest - environment: production steps: - name: Checkout From 8509fd721743a430a84df2fcf97afb428366bc7e Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Wed, 7 Feb 2024 12:40:03 -0800 Subject: [PATCH 12/16] update modularization --- infrastructure/terraform/aws/main.tf | 50 ++++++++-- .../aws/modules/core-services/README.md | 98 +++++++++++++++++++ .../aws/modules/core-services/main.tf | 91 +++++++++++++++++ .../aws/modules/core-services/outputs.tf | 18 ++++ .../aws/modules/core-services/variables.tf | 15 +++ .../aws/modules/ecs-service/variables.tf | 1 + .../terraform/aws/modules/v7-cluster/ecr.tf | 10 ++ .../aws/modules/v7-cluster/outputs.tf | 25 ++--- .../terraform/aws/modules/v7-cluster/rds.tf | 59 ----------- infrastructure/terraform/aws/variables.tf | 27 +++++ 10 files changed, 309 insertions(+), 85 deletions(-) create mode 100644 infrastructure/terraform/aws/modules/core-services/README.md create mode 100644 infrastructure/terraform/aws/modules/core-services/main.tf create mode 100644 infrastructure/terraform/aws/modules/core-services/outputs.tf create mode 100644 infrastructure/terraform/aws/modules/core-services/variables.tf delete mode 100644 infrastructure/terraform/aws/modules/v7-cluster/rds.tf diff --git a/infrastructure/terraform/aws/main.tf b/infrastructure/terraform/aws/main.tf index 1a43a5ed16..df090ffdfd 100644 --- a/infrastructure/terraform/aws/main.tf +++ b/infrastructure/terraform/aws/main.tf @@ -29,26 +29,58 @@ module "cluster" { availability_zones = ["us-east-1a", "us-east-1c"] } +module "core_dependency_services" { + source = "./modules/core-services" + + cluster_info = module.cluster.cluster_info +} + module "service_core" { source = "./modules/ecs-service" service_name = "core" cluster_info = module.cluster.cluster_info + repository_url = module.cluster.ecr_repository_urls.core - repository_url = module.cluster.ecr_repository_url + configuration = { + container_port = 3000 + environment = [ + { name = "DATABASE_URL", value = module.core_dependency_services.rds_connection_string_sans_password }, + { name = "ASSETS_REGION", value = var.region }, + { name = "ASSETS_BUCKET_NAME", value = var.ASSETS_BUCKET_NAME }, + { name = "ASSETS_UPLOAD_KEY", value = var.ASSETS_UPLOAD_KEY }, + { name = "NEXT_PUBLIC_PUBPUB_URL", value = var.pubpub_url }, + { name = "MAILGUN_SMTP_USERNAME", value = var.MAILGUN_SMTP_USERNAME }, + { name = "NEXT_PUBLIC_SUPABASE_URL", value = var.NEXT_PUBLIC_SUPABASE_URL }, + ] + + secrets = [ + { name = "DATABASE_PASSWORD", valueFrom = module.core_dependency_services.rds_db_password_id }, + { name = "API_KEY", valueFrom = module.core_dependency_services.api_key_secret_id }, + ] + } +} + +module "service_flock" { + source = "./modules/ecs-service" + + service_name = "jobs" + cluster_info = module.cluster.cluster_info + + repository_url = module.cluster.ecr_repository_urls.jobs configuration = { container_port = 3000 environment = [ - {name = "API_KEY", value = "undefined"}, - {name = "JWT_SECRET", value = "undefined"}, - {name = "MAILGUN_SMTP_USERNAME", value = "undefined"}, - {name = "NEXT_PUBLIC_PUBPUB_URL", value = "https://v7.pubpub.org"}, - {name = "NEXT_PUBLIC_SUPABASE_URL", value = "undefined"}, - {name = "SENTRY_AUTH_TOKEN", value = "undefined"}, - {name = "SUPABASE_SERVICE_ROLE_KEY", value = "undefined"}, - {name = "SUPABASE_WEBHOOKS_API_KEY", value = "undefined"}, + {name = "PUBPUB_URL", value = var.pubpub_url }, + {name = "DATABASE_URL", value = module.core_dependency_services.rds_connection_string_sans_password }, + # Secrets - TODO move these to aws secrets + ] + + secrets = [ + { name = "DATABASE_PASSWORD", valueFrom = module.core_dependency_services.rds_db_password_id }, + { name = "API_KEY", valueFrom = module.core_dependency_services.api_key_secret_id }, ] } } diff --git a/infrastructure/terraform/aws/modules/core-services/README.md b/infrastructure/terraform/aws/modules/core-services/README.md new file mode 100644 index 0000000000..1620c6aa0e --- /dev/null +++ b/infrastructure/terraform/aws/modules/core-services/README.md @@ -0,0 +1,98 @@ +# Setup +In a `main.tf` file for a workspace that needs a cluster, +you can use this module like: +``` +module "cluster" { + source = "../path/to/this/directory" + // version is disallowed when using path-based modules + + environment = "staging" + region = "us-east-2" + hosted_zone_id = "SOME-ZONE-ID" + // all other variables are optional +} +``` + +then +``` +terraform init +terraform apply +``` + +You will see these resources under `module.cluster.xyz`. + +## Managing the ECS Task Definition +Working with ECS task definitions in Terraform +is kind of awkward. + +This module creates an ECS task definition, +so that it can set up the ECS service that uses that task definition, +but expects that future task revisions will be created by a CI pipeline +as new images are created and pushed. +The `deploy_on_merge.yml` has an example of such a pipeline. +In this pipeline, +we get the ECS task definition from a file, +and interpolate the image, +to create a new revision. +Terraform ignores changes +made by the pipeline, +due to the `lifecycle` setting +on the ECS service resource. + +If you make changes to the task definition resource in this module, +and run `terraform apply` in the `ecs-staging` directory, +Terraform will update the task, +but the next time a new commit is pushed to git, +that change will be overwritten +by the definition in the `ecs-staging` directory, +that's used by the pipeline. + +Ideally these definitions would come from the same source +so if you're reading this +perhaps today is the day +to make that refactor! + +More information about the general wonkiness +of managing ECS with Terraform +can be found in [this Terraform issue.](https://github.com/hashicorp/terraform-provider-aws/issues/632) + +## Rotating the RDS Password +The RDS password is retrieved from AWS Secrets Manager +but that password is managed manually, +and rotating it requires downtime. + +To rotate it, you'll need to perform the following steps: +- Update the value of the Secrets Manager entry through the AWS console +- Update the value in the RDS instance through the AWS console. (At this point, the core container will stop being able to access the database.) +- Recreate the core container's service with `aws update-service cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment` + +In the future the RDS should probably [manage its own password](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-secrets-manager.html) +which will probably require changing the service's code +to fetch the password from Secrets Manager itself +rather than getting it passed in from an environment variable. + +## Rotating the ACM Certificate + +ACM issues certs that last for 1 year. They send you an email prior to renewing, but will automatically rotate the cert. + +Since we rely on DNS validation, it is necessary that our validation CNAMEs are present in route53, provided by this module. +The required records MAY not change, but if they have changed behind the scenes, this will catch up our terraform state: + +```bash +# from clean state , no changes to code + +# updates our state file's records of the Domain Validation Options on the cert (DVOs). +terraform apply + +# should show that new DNS records need to be created/updated, matching the DVOs. +terraform plan -out TMP.tfplan +terraform apply TMP.tfplan +``` + +For more info: see [AWS Docs](https://docs.aws.amazon.com/acm/latest/userguide/dns-renewal-validation.html). + +## Development + +When you change the resources in this directory, you must run `terraform apply` in the calling workspace to see changes. + +More info on developing [terraform modules](https://developer.hashicorp.com/terraform/language/modules/develop). diff --git a/infrastructure/terraform/aws/modules/core-services/main.tf b/infrastructure/terraform/aws/modules/core-services/main.tf new file mode 100644 index 0000000000..dbc568ce95 --- /dev/null +++ b/infrastructure/terraform/aws/modules/core-services/main.tf @@ -0,0 +1,91 @@ +# aws terraform provider config + +terraform { + required_version = ">= 0.12.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} + +# Static secrets +resource "random_password" "api_key" { + length = 32 + special = true + override_special = "-_.~!#$&'()*+,/:;=?@[]" +} + +resource "aws_secretsmanager_secret" "api_key" { + name = "api-key-${var.cluster_info.name}-${var.cluster_info.environment}" +} + +resource "aws_secretsmanager_secret_version" "api_key" { + secret_id = aws_secretsmanager_secret.api_key.id + secret_string = random_password.api_key.result +} + +# generate password and make it accessible through aws secrets manager +resource "random_password" "rds_db_password" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret" "rds_db_password" { + name = "rds-db-password-${var.cluster_info.name}-${var.cluster_info.environment}" +} + +resource "aws_secretsmanager_secret_version" "password" { + secret_id = aws_secretsmanager_secret.rds_db_password.id + secret_string = random_password.rds_db_password.result +} + +# network config +resource "aws_db_subnet_group" "ecs_dbs" { + name = "${var.cluster_info.name}_ecs_db_${var.cluster_info.environment}" + subnet_ids = var.cluster_info.private_subnet_ids + + tags = { + Name = "subnet group for ECS RDS instances" + } +} + +resource "aws_security_group" "ecs_tasks_rds_instances" { + name = "${var.cluster_info.name}-sg-rds-${var.cluster_info.environment}" + vpc_id = var.cluster_info.vpc_id + + ingress { + protocol = "tcp" + from_port = 5432 + to_port = 5432 + security_groups = var.cluster_info.container_security_group_ids + } +} + +# the actual database instance +resource "aws_db_instance" "core_postgres" { + identifier = "${var.cluster_info.name}-core-postgres-${var.cluster_info.environment}" + allocated_storage = 20 + db_name = "${var.cluster_info.name}_${var.cluster_info.environment}_core_postgres" + db_subnet_group_name = aws_db_subnet_group.ecs_dbs.name + engine = "postgres" + engine_version = "14" + instance_class = "db.t3.small" + vpc_security_group_ids = [aws_security_group.ecs_tasks_rds_instances.id] + username = var.cluster_info.name + password = random_password.rds_db_password.result + parameter_group_name = "default.postgres14" + skip_final_snapshot = true + + lifecycle { + ignore_changes = [ + password, + ] + } +} + + +### TODO : add Sentry? other stuff? + diff --git a/infrastructure/terraform/aws/modules/core-services/outputs.tf b/infrastructure/terraform/aws/modules/core-services/outputs.tf new file mode 100644 index 0000000000..f3590935ef --- /dev/null +++ b/infrastructure/terraform/aws/modules/core-services/outputs.tf @@ -0,0 +1,18 @@ +locals { + db_user = aws_db_instance.core_postgres.username + db_name = aws_db_instance.core_postgres.db_name + db_host = aws_db_instance.core_postgres.address + db_sslmode = "require" +} + +output "api_key_secret_id" { + value = aws_secretsmanager_secret.api_key.id +} + +output "rds_db_password_id" { + value = aws_secretsmanager_secret.rds_db_password.id +} + +output "rds_connection_string_sans_password" { + value = "postgresql://${local.db_user}@${local.db_host}:5432/${local.db_name}?sslmode=${local.db_sslmode}" +} diff --git a/infrastructure/terraform/aws/modules/core-services/variables.tf b/infrastructure/terraform/aws/modules/core-services/variables.tf new file mode 100644 index 0000000000..ecd025c2af --- /dev/null +++ b/infrastructure/terraform/aws/modules/core-services/variables.tf @@ -0,0 +1,15 @@ +variable "cluster_info" { + description = "infrastructure values output from v7-cluster" + + type = object({ + region = string + name = string + vpc_id = string + cluster_arn = string + environment = string + private_subnet_ids = list(string) + container_security_group_ids = list(string) + cloudwatch_log_group_name = string + lb_target_group_arn = string + }) +} diff --git a/infrastructure/terraform/aws/modules/ecs-service/variables.tf b/infrastructure/terraform/aws/modules/ecs-service/variables.tf index fabd602883..a5482c797c 100644 --- a/infrastructure/terraform/aws/modules/ecs-service/variables.tf +++ b/infrastructure/terraform/aws/modules/ecs-service/variables.tf @@ -4,6 +4,7 @@ variable "cluster_info" { type = object({ region = string name = string + vpc_id = string cluster_arn = string environment = string private_subnet_ids = list(string) diff --git a/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf b/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf index 8c72a849d7..c3d58c2a10 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/ecr.tf @@ -17,6 +17,7 @@ resource "aws_ecr_repository" "pubpub_v7_core" { } } +# TODO: integrations may want to be one image alone ...? resource "aws_ecr_repository" "pubpub_v7_intg_submissions" { name = "pubpub-v7-integration-submissions" image_tag_mutability = "MUTABLE" @@ -25,3 +26,12 @@ resource "aws_ecr_repository" "pubpub_v7_intg_submissions" { scan_on_push = false # can set this to true if we want } } + +resource "aws_ecr_repository" "pubpub_v7_jobs" { + name = "pubpub-v7-jobs" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = false # can set this to true if we want + } +} diff --git a/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf b/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf index 5d5b18aead..80e2e8b1a7 100644 --- a/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf +++ b/infrastructure/terraform/aws/modules/v7-cluster/outputs.tf @@ -1,26 +1,17 @@ -locals { - db_user = aws_db_instance.core_postgres.username - db_name = aws_db_instance.core_postgres.db_name - db_host = aws_db_instance.core_postgres.address - db_sslmode = "require" -} - -output "ecr_repository_url" { - value = aws_ecr_repository.pubpub_v7.repository_url -} - -output "rds_db_password_id" { - value = aws_secretsmanager_secret.rds_db_password.id -} - -output "rds_connection_string_sans_password" { - value = "postgresql://${local.db_user}@${local.db_host}:5432/${local.db_name}?sslmode=${local.db_sslmode}" +output "ecr_repository_urls" { + value = { + root = aws_ecr_repository.pubpub_v7.repository_url + core = aws_ecr_repository.pubpub_v7_core.repository_url + intg_submissions = aws_ecr_repository.pubpub_v7_intg_submissions.repository_url + jobs = aws_ecr_repository.pubpub_v7_jobs.repository_url + } } output "cluster_info" { value = { region = var.region name = var.name + vpc_id = aws_vpc.main.id environment = var.environment cluster_arn = module.ecs_cluster.arn private_subnet_ids = aws_subnet.private.*.id diff --git a/infrastructure/terraform/aws/modules/v7-cluster/rds.tf b/infrastructure/terraform/aws/modules/v7-cluster/rds.tf deleted file mode 100644 index 960c3a10ee..0000000000 --- a/infrastructure/terraform/aws/modules/v7-cluster/rds.tf +++ /dev/null @@ -1,59 +0,0 @@ -# generate password and make it accessible through aws secrets manager -resource "random_password" "rds_db_password" { - length = 16 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -resource "aws_secretsmanager_secret" "rds_db_password" { - name = "rds-db-password-${var.name}-${var.environment}" -} - -resource "aws_secretsmanager_secret_version" "password" { - secret_id = aws_secretsmanager_secret.rds_db_password.id - secret_string = random_password.rds_db_password.result -} - -# network config -resource "aws_db_subnet_group" "ecs_dbs" { - name = "${var.name}_ecs_db_${var.environment}" - subnet_ids = aws_subnet.private.*.id - - tags = { - Name = "subnet group for ECS RDS instances" - } -} - -resource "aws_security_group" "ecs_tasks_rds_instances" { - name = "${var.name}-sg-rds-${var.environment}" - vpc_id = aws_vpc.main.id - - ingress { - protocol = "tcp" - from_port = 5432 - to_port = 5432 - security_groups = [aws_security_group.ecs_tasks.id] - } -} - -# the actual database instance -resource "aws_db_instance" "core_postgres" { - identifier = "${var.name}-core-postgres-${var.environment}" - allocated_storage = 20 - db_name = "${var.name}_${var.environment}_core_postgres" - db_subnet_group_name = aws_db_subnet_group.ecs_dbs.name - engine = "postgres" - engine_version = "14" - instance_class = "db.t3.small" - vpc_security_group_ids = [aws_security_group.ecs_tasks_rds_instances.id] - username = var.name - password = random_password.rds_db_password.result - parameter_group_name = "default.postgres14" - skip_final_snapshot = true - - lifecycle { - ignore_changes = [ - password, - ] - } -} diff --git a/infrastructure/terraform/aws/variables.tf b/infrastructure/terraform/aws/variables.tf index 43bf7735d0..be212d1bc1 100644 --- a/infrastructure/terraform/aws/variables.tf +++ b/infrastructure/terraform/aws/variables.tf @@ -13,3 +13,30 @@ variable "environment" { description = "Functional name for this environment" type = string } + +variable "pubpub_url" { + description = "URL where pubpub will be addressable (include https://)" + type = string +} + +variable "MAILGUN_SMTP_USERNAME" { + description = "SMTP Username for Mailgun service" + type = string +} + +variable "NEXT_PUBLIC_SUPABASE_URL" { + description = "URL to Supabase public address for this install" + type = string +} + +# TODO deprecate this in favor of a Terraformed bucket +variable "ASSETS_BUCKET_NAME" { + description = "Name of the S3 bucket to store assets" + type = string +} + +# TODO: deprecate this in favor of terraformed iam/service roles +variable "ASSETS_UPLOAD_KEY" { + description = "AWS access key ID for uploading to s3" + type = string +} From 6ec77759e244cae56d119c522f66bc22ff202903 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Mon, 12 Feb 2024 09:56:04 -0800 Subject: [PATCH 13/16] split workflows into templates --- .github/workflows/awsdeploy.yml | 129 ++++-------------------- .github/workflows/deploy-template.yml | 85 ++++++++++++++++ .github/workflows/ecrbuild-all.yml | 10 ++ .github/workflows/ecrbuild-template.yml | 5 + Dockerfile | 23 +++-- 5 files changed, 138 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/deploy-template.yml diff --git a/.github/workflows/awsdeploy.yml b/.github/workflows/awsdeploy.yml index bc4351911f..9d73404ad6 100644 --- a/.github/workflows/awsdeploy.yml +++ b/.github/workflows/awsdeploy.yml @@ -1,117 +1,30 @@ # Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service -name: aws ecs deploy +name: aws ecs deploy - blake on: push: branches: - main -env: - AWS_REGION: us-east-1 # set this to your preferred AWS region, e.g. us-west-1 - ECR_REPOSITORY: pubpub-v7 # set this to your Amazon ECR repository name - ECS_SERVICE: blake-core # set this to your Amazon ECS service name - ECS_CLUSTER: blake-ecs-cluster-staging # set this to your Amazon ECS cluster name - ECS_TASK_DEFINITION_TEMPLATE: blake-core # set this to the FAMILY of your task definition - CONTAINER_NAME: core # set this to the name of the container in the - # containerDefinitions section of your task definition - jobs: - build: - name: Build - runs-on: ubuntu-latest - environment: production - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Get short sha - id: shortsha - run: echo "sha_short=$(git describe --always --abbrev=40 --dirty)" >> $GITHUB_OUTPUT - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Build, tag, and push image to Amazon ECR - id: build-image - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} - run: | - # Build a docker container and - # push it to ECR so that it can - # be deployed to ECS. - docker build \ - --platform linux/amd64 \ - --build-arg PACKAGE=core \ - --build-arg PACKAGE_DIR=core \ - -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ - . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - - deploy: - name: Deploy - runs-on: ubuntu-latest - environment: production - needs: - - build - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Get short sha - id: shortsha - run: echo "sha_short=$(git describe --always --abbrev=40 --dirty)" >> $GITHUB_OUTPUT - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Retrieve Task Definition contents from template - id: get-taskdef - run: | - aws ecs describe-task-definition \ - --task-definition $ECS_TASK_DEFINITION_TEMPLATE \ - --query taskDefinition >> template_task_def.json - - - name: Interpolate image tag - id: write-tag - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} - run: | - echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc - with: - task-definition: template_task_def.json - container-name: ${{ env.CONTAINER_NAME }} - image: ${{ steps.write-tag.outputs.image }} - - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: ${{ env.ECS_SERVICE }} - cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true + deploy-core: + uses: ./.github/workflows/deploy-template.yml + with: + service-name: core + environment: staging + env-proper-name: blake + secrets: + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + + deploy-jobs: + uses: ./.github/workflows/deploy-template.yml + needs: deploy-core + with: + service-name: jobs + environment: staging + env-proper-name: blake + secrets: + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/deploy-template.yml b/.github/workflows/deploy-template.yml new file mode 100644 index 0000000000..17daa25c84 --- /dev/null +++ b/.github/workflows/deploy-template.yml @@ -0,0 +1,85 @@ +# Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service + +name: aws ecs deploy template + +on: + workflow_call: + inputs: + service-name: # example: core + required: true + type: string + env-proper-name: # example: blake + required: true + type: string + environment: # example: staging + required: true + type: string + secrets: + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true + +env: + AWS_REGION: us-east-1 + ECR_REPOSITORY: pubpub-v7-${{ inputs.service-name }} + ECS_SERVICE: ${{ inputs.env-proper-name }}-${{inputs.service-name}} + ECS_CLUSTER: ${{inputs.env-proper-name}}-ecs-cluster-${{inputs.environment}} + ECS_TASK_DEFINITION_TEMPLATE: ${{ inputs.env-proper-name }}-${{inputs.service-name}} + CONTAINER_NAME: ${{inputs.service-name}} + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Get short sha + id: shortsha + run: echo "sha_short=$(git describe --always --abbrev=40 --dirty)" >> $GITHUB_OUTPUT + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Retrieve Task Definition contents from template + id: get-taskdef + run: | + aws ecs describe-task-definition \ + --task-definition $ECS_TASK_DEFINITION_TEMPLATE \ + --query taskDefinition >> template_task_def.json + + - name: Interpolate image tag + id: write-tag + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} + run: | + echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc + with: + task-definition: template_task_def.json + container-name: ${{ env.CONTAINER_NAME }} + image: ${{ steps.write-tag.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: false diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml index 5b1b6545eb..c281434eec 100644 --- a/.github/workflows/ecrbuild-all.yml +++ b/.github/workflows/ecrbuild-all.yml @@ -23,3 +23,13 @@ jobs: secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + + build-jobs: + uses: ./.github/workflows/ecrbuild-template.yml + with: + target: jobs + package-name: jobs + package-src: jobs + secrets: + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml index 271247b564..00d4aa5440 100644 --- a/.github/workflows/ecrbuild-template.yml +++ b/.github/workflows/ecrbuild-template.yml @@ -5,6 +5,10 @@ name: aws ecr build template on: workflow_call: inputs: + target: + required: true + default: "main" + type: string package-name: required: true type: string @@ -61,6 +65,7 @@ jobs: --platform linux/amd64 \ --build-arg PACKAGE=$PACKAGE \ --build-arg PACKAGE_DIR=$PACKAGE_DIR \ + --target ${{ inputs.target }} \ -t $ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG \ . docker push $ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG diff --git a/Dockerfile b/Dockerfile index 6a7d0a467c..e40203a5f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ ARG PNPM_VERSION=8.14.3 ARG PACKAGE_DIR=core ARG PACKAGE=core +ARG PORT=3000 ################################################################################ # Use node image for base image for all stages. @@ -64,8 +65,11 @@ RUN ./bin/render-build.sh ${PACKAGE} ################################################################################ # Create a new stage to run the application with minimal runtime dependencies # where the necessary files are copied from the build stage. -FROM base as final -ARG PACKAGE_DIR PACKAGE +FROM base as shared +ARG PACKAGE_DIR PACKAGE PORT + +# needed so that the CMD can use this var +ENV PACKAGE=$PACKAGE # Use production node environment by default. ENV NODE_ENV production @@ -86,11 +90,18 @@ COPY ./${PACKAGE_DIR}/package.json \ COPY --from=deps /usr/src/app/node_modules ./node_modules COPY --from=build /usr/src/app/${PACKAGE_DIR}/node_modules ./${PACKAGE_DIR}/node_modules -COPY --from=build /usr/src/app/${PACKAGE_DIR}/.next ./${PACKAGE_DIR}/.next - # Expose the port that the application listens on. -EXPOSE 3000 - +EXPOSE $PORT # Run the application. CMD pnpm --filter ${PACKAGE} start + +# add an optional target to use for the jobs package only +FROM shared AS jobs +ARG PACKAGE_DIR +COPY --from=build /usr/src/app/${PACKAGE_DIR}/index.ts ./${PACKAGE_DIR}/index.ts + +# But most packages are built in this standard next.js way. +FROM shared AS main +ARG PACKAGE_DIR +COPY --from=build /usr/src/app/${PACKAGE_DIR}/.next ./${PACKAGE_DIR}/.next From a08c2636c24ca5fbac34666fa838cb13b8c43695 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Mon, 12 Feb 2024 10:00:38 -0800 Subject: [PATCH 14/16] remove required where there is default --- .github/workflows/ecrbuild-template.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml index 00d4aa5440..de2e67765a 100644 --- a/.github/workflows/ecrbuild-template.yml +++ b/.github/workflows/ecrbuild-template.yml @@ -6,7 +6,6 @@ on: workflow_call: inputs: target: - required: true default: "main" type: string package-name: From 102045a89a05be9a72235f9a6558e131551a7ec8 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 13 Feb 2024 10:46:18 -0800 Subject: [PATCH 15/16] update builds to work with monorepo docker --- .github/workflows/ecrbuild-all.yml | 6 +-- .github/workflows/ecrbuild-template.yml | 8 --- Dockerfile | 71 +++++-------------------- jobs/package.json | 4 ++ 4 files changed, 18 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml index c281434eec..bac3fe9d77 100644 --- a/.github/workflows/ecrbuild-all.yml +++ b/.github/workflows/ecrbuild-all.yml @@ -1,6 +1,6 @@ # Based on https://docs.github.com/en/actions/deployment/deploying-to-your-cloud-provider/deploying-to-amazon-elastic-container-service -name: aws ecs deploy +name: docker build to ECR on: push: @@ -10,7 +10,6 @@ jobs: uses: ./.github/workflows/ecrbuild-template.yml with: package-name: core - package-src: core secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -19,7 +18,6 @@ jobs: uses: ./.github/workflows/ecrbuild-template.yml with: package-name: integration-submissions - package-src: integrations/submissions secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -27,9 +25,7 @@ jobs: build-jobs: uses: ./.github/workflows/ecrbuild-template.yml with: - target: jobs package-name: jobs - package-src: jobs secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml index de2e67765a..1ef1fcaac0 100644 --- a/.github/workflows/ecrbuild-template.yml +++ b/.github/workflows/ecrbuild-template.yml @@ -5,15 +5,9 @@ name: aws ecr build template on: workflow_call: inputs: - target: - default: "main" - type: string package-name: required: true type: string - package-src: - required: true - type: string secrets: AWS_ACCESS_KEY_ID: required: true @@ -54,7 +48,6 @@ jobs: env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} IMAGE_TAG: ${{ steps.shortsha.outputs.sha_short }} - PACKAGE_DIR: ${{ inputs.package-src }} PACKAGE: ${{ inputs.package-name }} run: | # Build a docker container and @@ -64,7 +57,6 @@ jobs: --platform linux/amd64 \ --build-arg PACKAGE=$PACKAGE \ --build-arg PACKAGE_DIR=$PACKAGE_DIR \ - --target ${{ inputs.target }} \ -t $ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG \ . docker push $ECR_REGISTRY/$ECR_REPOSITORY_PREFIX-$PACKAGE:$IMAGE_TAG diff --git a/Dockerfile b/Dockerfile index e40203a5f0..2da8da2fa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ ARG NODE_VERSION=20.6.0 ARG PNPM_VERSION=8.14.3 -ARG PACKAGE_DIR=core ARG PACKAGE=core ARG PORT=3000 @@ -25,36 +24,10 @@ WORKDIR /usr/src/app RUN --mount=type=cache,target=/root/.npm \ npm install -g pnpm@${PNPM_VERSION} -################################################################################ -# Create a stage for installing production dependecies. -FROM base as deps -ARG PACKAGE_DIR - -# Download dependencies as a separate step to take advantage of Docker's caching. -# Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds. -# Leverage bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them -# into this layer. -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ - --mount=type=bind,source=${PACKAGE_DIR}/package.json,target=${PACKAGE_DIR}/package.json \ - --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ - --mount=type=cache,target=/root/.local/share/pnpm/store \ - --mount=type=cache,target=/pnpm/store \ - pnpm install --prod --frozen-lockfile - ################################################################################ # Create a stage for building the application. -FROM deps as build -ARG PACKAGE_DIR PACKAGE - -# Download additional development dependencies before building, as some projects require -# "devDependencies" to be installed to build. If you don't need this, remove this step. -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=${PACKAGE_DIR}/package.json,target=${PACKAGE_DIR}/package.json \ - --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ - --mount=type=cache,target=/root/.local/share/pnpm/store \ - --mount=type=cache,target=/pnpm/store \ - pnpm install --frozen-lockfile +FROM base as monorepo +ARG PACKAGE # Copy the rest of the source files into the image. COPY . . @@ -62,46 +35,28 @@ COPY . . # Run the build script. RUN ./bin/render-build.sh ${PACKAGE} +RUN pnpm --filter ${PACKAGE} --prod deploy /tmp/app + ################################################################################ # Create a new stage to run the application with minimal runtime dependencies # where the necessary files are copied from the build stage. -FROM base as shared -ARG PACKAGE_DIR PACKAGE PORT +FROM base AS app +ARG PORT -# needed so that the CMD can use this var -ENV PACKAGE=$PACKAGE +# # needed so that the CMD can use this var +# ENV PACKAGE=$PACKAGE # Use production node environment by default. ENV NODE_ENV production -# Run the application as a non-root user. -USER node - # Copy package.json so that package manager commands can be used. -COPY package.json \ - pnpm-workspace.yaml \ - . +COPY --from=monorepo /tmp/app \ + ./ -COPY ./${PACKAGE_DIR}/package.json \ - ./${PACKAGE_DIR}/ - -# Copy the production dependencies from the deps stage and also -# the built application from the build stage into the image. - -COPY --from=deps /usr/src/app/node_modules ./node_modules -COPY --from=build /usr/src/app/${PACKAGE_DIR}/node_modules ./${PACKAGE_DIR}/node_modules +# Run the application as a non-root user. +USER node # Expose the port that the application listens on. EXPOSE $PORT # Run the application. -CMD pnpm --filter ${PACKAGE} start - -# add an optional target to use for the jobs package only -FROM shared AS jobs -ARG PACKAGE_DIR -COPY --from=build /usr/src/app/${PACKAGE_DIR}/index.ts ./${PACKAGE_DIR}/index.ts - -# But most packages are built in this standard next.js way. -FROM shared AS main -ARG PACKAGE_DIR -COPY --from=build /usr/src/app/${PACKAGE_DIR}/.next ./${PACKAGE_DIR}/.next +CMD pnpm start diff --git a/jobs/package.json b/jobs/package.json index e683f20d6f..af91b2e8d3 100644 --- a/jobs/package.json +++ b/jobs/package.json @@ -6,8 +6,12 @@ "start": "tsx index.ts", "dev": "dotenv -e .env.local tsx index.ts" }, + "files": [ + "index.ts" + ], "dependencies": { "@pubpub/sdk": "workspace:*", + "contracts": "workspace:*", "graphile-worker": "^0.15.0", "tsx": "^3.13.0" }, From 92c45c7584b289e04efede3013891e6e67b460b2 Mon Sep 17 00:00:00 2001 From: "eve n.u" Date: Tue, 13 Feb 2024 11:34:42 -0800 Subject: [PATCH 16/16] update ECR only on active PR branches and main --- .github/workflows/ecrbuild-all.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml index bac3fe9d77..412cf65e17 100644 --- a/.github/workflows/ecrbuild-all.yml +++ b/.github/workflows/ecrbuild-all.yml @@ -4,6 +4,12 @@ name: docker build to ECR on: push: + branches: + - main + pull_request: + types: + - opened + - synchronize jobs: build-core: