Skip to content

Keep the task definition stable between deploys - #327

Merged
oskarbarcz merged 1 commit into
mainfrom
stable-task-definition
Nov 10, 2026
Merged

oskarbarcz merged 1 commit into
mainfrom
stable-task-definition

Conversation

@oskarbarcz

Copy link
Copy Markdown
Member

Why

Every deploy registers a new task definition and redeploys the service, even when nothing in the task changed. AWS stores empty portMappings, volumesFrom, systemControls and capabilities.add on both containers, and empty environment and mountPoints on cloudflared. Our definitions leave them out, so every plan sees a difference and replaces the task definition.

While nothing runs, that only costs a revision. After the cutover, any apply would restart the API, even one that changes something unrelated, such as closing RDS after a migration. With a minimum healthy of 0 %, each restart is a short outage.

What changes

  • infra/ecs.tf: both containers declare those fields as empty.
  • Version: 3.80.1.

Verification

  • The two deploys on 10 Nov, which opened and closed RDS for the TLS rehearsal, each replaced the task definition (revisions 2 and 3).
  • With this change, a local tofu plan against the real state, at version 3.80.0, reports no changes.
  • This PR's plan still replaces the task definition, and so will the merge deploy, because the image tag moves to 3.80.1. Later applies that don't change the task leave it alone.

🤖 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 944606e · Plan: 1 to add, 1 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:
!  ~ update in-place (current -> planned)
!-/+ destroy and then create replacement
 
 OpenTofu will perform the following actions:
 
   # aws_ecs_service.app will be updated in-place
!  ~ resource "aws_ecs_service" "app" {
         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:3" -> (known after apply)
         # (19 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:3" -> (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.0" -> "ghcr.io/mypreflight/flight-tracker-api:3.80.1"
                     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                 = 3 -> (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)
     }
 
 Plan: 1 to add, 1 to change, 1 to destroy.
 
 Changes to Outputs:
!  ~ app_image        = "ghcr.io/mypreflight/flight-tracker-api:3.80.0" -> "ghcr.io/mypreflight/flight-tracker-api:3.80.1"

Workflow run

@oskarbarcz oskarbarcz self-assigned this Nov 10, 2026
@oskarbarcz
oskarbarcz merged commit 4b4dcfc into main Nov 10, 2026
3 checks passed
@oskarbarcz
oskarbarcz deleted the stable-task-definition branch November 10, 2026 11:04
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