Skip to content

Start the service on AWS - #328

Merged
oskarbarcz merged 1 commit into
mainfrom
start-services
Nov 10, 2026
Merged

oskarbarcz merged 1 commit into
mainfrom
start-services

Conversation

@oskarbarcz

Copy link
Copy Markdown
Member

Why

Phase 4 of the move to AWS: start the API on ECS. Merge only in step 6 of the cutover window, after the restore into RDS is verified and the real secrets are on the production environment. It's a draft so it can't be merged by accident; mark it ready then.

What changes

  • desired_count 0 → 1. The service starts one task. Its entrypoint runs prisma migrate deploy, which finds nothing to apply on the restored database, and the deploy waits until the task is healthy.
  • app_secrets_version 1 → 2. Every SecureString, the tunnel token included, is rewritten from the production secrets, replacing the placeholders.
  • The variable's description drops "0 until the cutover".
  • Version: 3.80.2.

Don't merge early

  • Before the restore: the task would migrate the empty RDS, and the restore would then collide with that schema.
  • Before the real secrets are set: version 2 would store the placeholders, and replacing them takes another version.
  • While DigitalOcean still runs the scheduler and the Discord gateway: the jobs and the bot would run twice.

Clear MIGRATION_INGRESS_CIDR just before merging, so this deploy also closes RDS.

Verification

  • tofu validate and tofu fmt pass.
  • The dump and restore were rehearsed into a local PostgreSQL 16: 53 tables and 37,196 rows matched, and Prisma found all 102 migrations applied.

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

OpenTofu plan for infra

Commit c144e65 · Plan: 2 to add, 13 to change, 1 to destroy.

Show plan
 OpenTofu used the selected providers to generate the following execution
 plan. Resource actions are indicated with the following symbols:
+  + create
!  ~ update in-place (current -> planned)
!-/+ destroy and then create replacement
 
 OpenTofu will perform the following actions:
 
   # aws_cloudwatch_metric_alarm.app_cpu will be updated in-place
!  ~ resource "aws_cloudwatch_metric_alarm" "app_cpu" {
         id                        = "flight-tracker-api-cpu"
         tags                      = {}
!      ~ treat_missing_data        = "notBreaching" -> "breaching"
         # (19 unchanged attributes hidden)
     }
 
   # aws_cloudwatch_metric_alarm.app_memory will be updated in-place
!  ~ resource "aws_cloudwatch_metric_alarm" "app_memory" {
         id                        = "flight-tracker-api-memory"
         tags                      = {}
!      ~ treat_missing_data        = "notBreaching" -> "breaching"
         # (19 unchanged attributes hidden)
     }
 
   # aws_db_instance.postgres will be updated in-place
!  ~ resource "aws_db_instance" "postgres" {
         id                                    = "db-HEGLJAHXJHYWNBP7AGUD44CF6A"
!      ~ password_wo                           = (write-only attribute)
!      ~ publicly_accessible                   = false -> true
         tags                                  = {
             "Name" = "mypreflight"
         }
         # (60 unchanged attributes hidden)
     }
 
   # aws_ecs_service.app will be updated in-place
!  ~ resource "aws_ecs_service" "app" {
!      ~ desired_count                      = 0 -> 1
         id                                 = "arn:aws:ecs:eu-central-1:805830022748:service/mypreflight/flight-tracker-api"
         name                               = "flight-tracker-api"
         tags                               = {}
!      ~ task_definition                    = "arn:aws:ecs:eu-central-1:805830022748:task-definition/flight-tracker-api:4" -> (known after apply)
!      ~ triggers                           = {
!          ~ "app_secrets_version" = "1" -> "2"
             # (1 unchanged element hidden)
         }
!      ~ wait_for_steady_state              = false -> true
         # (16 unchanged attributes hidden)
 
         # (4 unchanged blocks hidden)
     }
 
   # aws_ecs_task_definition.app must be replaced
!-/+ resource "aws_ecs_task_definition" "app" {
!      ~ arn                      = "arn:aws:ecs:eu-central-1:805830022748:task-definition/flight-tracker-api:4" -> (known after apply)
!      ~ arn_without_revision     = "arn:aws:ecs:eu-central-1:805830022748:task-definition/flight-tracker-api" -> (known after apply)
!      ~ container_definitions    = jsonencode(
!          ~ [
                 {
                     command                = [
                         "tunnel",
                         "--no-autoupdate",
                         "--metrics",
                         "localhost:20241",
                         "--grace-period",
                         "20s",
                         "run",
                     ]
                     dependsOn              = [
                         {
                             condition     = "HEALTHY"
                             containerName = "flight-tracker-api"
                         },
                     ]
                     environment            = []
                     essential              = true
                     healthCheck            = {
                         command     = [
                             "CMD",
                             "cloudflared",
                             "tunnel",
                             "--metrics",
                             "localhost:20241",
                             "ready",
                         ]
                         interval    = 5
                         retries     = 6
                         startPeriod = 10
                         timeout     = 2
                     }
                     image                  = "cloudflare/cloudflared@sha256:4955524aeaa379e154023d5a027c250f7b35041a2e7e950cf13f144bc9b9c5ba"
                     linuxParameters        = {
                         capabilities = {
                             add  = []
                             drop = [
                                 "ALL",
                             ]
                         }
                     }
                     logConfiguration       = {
                         logDriver = "awslogs"
                         options   = {
                             awslogs-group         = "/ecs/flight-tracker-api"
                             awslogs-region        = "eu-central-1"
                             awslogs-stream-prefix = "cloudflared"
                         }
                     }
                     mountPoints            = []
                     name                   = "cloudflared"
                     portMappings           = []
                     readonlyRootFilesystem = true
                     secrets                = [
                         {
                             name      = "TUNNEL_TOKEN"
                             valueFrom = "arn:aws:ssm:eu-central-1:805830022748:parameter/mypreflight/api/cloudflare-tunnel-token"
                         },
                     ]
                     systemControls         = []
                     volumesFrom            = []
                 },
!              ~ {
!                  ~ image                  = "ghcr.io/mypreflight/flight-tracker-api:3.80.1" -> "ghcr.io/mypreflight/flight-tracker-api:3.80.2"
                     name                   = "flight-tracker-api"
                     # (11 unchanged attributes hidden)
                 },
             ] # forces replacement
         )
!      ~ enable_fault_injection   = false -> (known after apply)
!      ~ id                       = "flight-tracker-api" -> (known after apply)
!      ~ revision                 = 4 -> (known after apply)
-      - tags                     = {} -> null
         # (11 unchanged attributes hidden)
 
-      - volume {
-          - configure_at_launch = false -> null
-          - name                = "tmp" -> null
         }
+      + volume {
+          + configure_at_launch = (known after apply)
+          + name                = "tmp"
         }
 
         # (1 unchanged block hidden)
     }
 
   # aws_security_group_rule.rds_ingress_migration[0] will be created
+  + resource "aws_security_group_rule" "rds_ingress_migration" {
+      + cidr_blocks              = [
+          + "217.119.73.215/32",
         ]
+      + description              = "PostgreSQL from the restore, temporarily"
+      + from_port                = 5432
+      + id                       = (known after apply)
+      + protocol                 = "tcp"
+      + region                   = "eu-central-1"
+      + security_group_id        = "sg-024179aab55dc2aef"
+      + security_group_rule_id   = (known after apply)
+      + self                     = false
+      + source_security_group_id = (known after apply)
+      + to_port                  = 5432
+      + type                     = "ingress"
     }
 
   # aws_ssm_parameter.cloudflare_tunnel_token will be updated in-place
!  ~ resource "aws_ssm_parameter" "cloudflare_tunnel_token" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/cloudflare-tunnel-token"
         name             = "/mypreflight/api/cloudflare-tunnel-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["ADSB_API_TOKEN"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/adsb-api-token"
         name             = "/mypreflight/api/adsb-api-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["DISCORD_APP_TOKEN"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/discord-app-token"
         name             = "/mypreflight/api/discord-app-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["DISCORD_CLIENT_SECRET"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/discord-client-secret"
         name             = "/mypreflight/api/discord-client-secret"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["DISCORD_WEBHOOK_URL"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/discord-webhook-url"
         name             = "/mypreflight/api/discord-webhook-url"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["JWT_PRIVATE_KEY"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/jwt-private-key"
         name             = "/mypreflight/api/jwt-private-key"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["JWT_PUBLIC_KEY"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/jwt-public-key"
         name             = "/mypreflight/api/jwt-public-key"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["MAILGUN_API_KEY"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/mailgun-api-key"
         name             = "/mypreflight/api/mailgun-api-key"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["SKYLINK_API_TOKEN"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/skylink-api-token"
         name             = "/mypreflight/api/skylink-api-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 1 -> 2
         # (8 unchanged attributes hidden)
     }
 
 Plan: 2 to add, 13 to change, 1 to destroy.
 
 Changes to Outputs:
!  ~ app_image        = "ghcr.io/mypreflight/flight-tracker-api:3.80.1" -> "ghcr.io/mypreflight/flight-tracker-api:3.80.2"

Workflow run

@oskarbarcz
oskarbarcz marked this pull request as ready for review November 10, 2026 11:45
@oskarbarcz oskarbarcz self-assigned this Nov 10, 2026
@oskarbarcz
oskarbarcz merged commit 236f5ae into main Nov 10, 2026
3 checks passed
@oskarbarcz
oskarbarcz deleted the start-services branch November 10, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant