Migrate Elastic Beanstalk from Docker to ECS
- aliases
- No value
- tags
- aws elastic-beanstalk ecs docker
- description
- Elastic Beanstalk Docker AL2 환경을 ECS running on AL2023 환경으로 옮기며 겪은 실패와 안전한 전환 절차
- type
- blog
- links
- AWS Elastic Beanstalk https://app.clickup.com/25555288/v/dc/rbwar-46998
- status
- No value
- project
- false
- area
- false
- resource
- true
- title
- Migrate Elastic Beanstalk from Docker to ECS
- created
- 2026-05-07T09:04:07
- updated
- 2026-05-12T09:55:51
README
2026년 6월 30일부터 Elastic Beanstalk의 Amazon Linux 2 기반 플랫폼 브랜치가 retired 된다는 AWS Health 알림을 받았다. 기존 환경은 계속 실행할 수 있지만, 보안 업데이트와 플랫폼 업데이트를 받을 수 없다.
우리의 기존 환경은 대략 이런 상태였다.
{
"PlatformArn": "arn:aws:elasticbeanstalk:<region>::platform/Docker running on 64bit Amazon Linux 2/<version>",
"SolutionStackName": "64bit Amazon Linux 2 <version> running Docker",
"Status": "Ready",
"Health": "Green"
}
겉으로는 단순히 AL2 Docker 환경을 AL2023 Docker 환경으로 올리면 될 것 같았다. 하지만 실제로는 Node/Nest 애플리케이션의 빌드 방식, Docker 이미지 아키텍처, Dockerrun.aws.json 버전, EB CLI artifact 설정, CloudWatch 로그 경로까지 한꺼번에 바뀌었다.
이 글은 같은 상황에 처한 팀이 시간을 덜 잃도록, 우리가 밟은 실패와 최종 절차를 정리한 기록이다. 계정 ID, repository 이름, EB application/environment 이름, GitHub secret/variable 이름은 모두 예시로 redact 했다.
결론
기존 EB Docker AL2 환경을 in-place로 ECS managed Docker AL2023 환경으로 바꾸려고 하지 않는 편이 안전하다.
권장 경로는 다음과 같다.
- 기존 환경을 clone해서 새 ECS managed Docker 환경을 만든다.
- 새 환경에 ECR 이미지를 가리키는
Dockerrun.aws.jsonv2 bundle을 배포한다. - 새 환경 URL로 API를 검증한다.
- EB
Swap environment URLs또는 DNS/CNAME 전환으로 트래픽을 새 환경으로 보낸다. - 기존 환경은 즉시 삭제하지 않고 rollback target으로 보존한다.
즉, 최종 트래픽은 새 *-ecs 환경으로 넘기고, 기존 Docker 환경을 억지로 같은 이름의 ECS 환경으로 바꾸는 작업은 별도 일정으로 분리한다.
왜 기존 방식이 실패했나
기존 GitHub Actions는 source bundle 전체를 zip으로 묶어 EB에 올렸다. EB 인스턴스는 그 안의 Dockerfile로 직접 이미지를 빌드했다.
GitHub Actions
-> source bundle zip
-> Elastic Beanstalk instance
-> docker build
-> container run
문제는 Nest build가 EB 인스턴스의 제한된 메모리에서 실패했다는 점이다.
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
ERROR: process "/bin/sh -c pnpm run build" did not complete successfully
처음에는 Node heap을 늘리는 방법을 떠올릴 수 있다. 하지만 이번 마이그레이션의 목표는 메모리 정책을 바꾸는 것이 아니라 AL2023/ECS로 배포 방식을 전환하는 것이었다. 그래서 빌드를 EB 인스턴스 밖으로 빼기로 했다.
최종 구조는 이렇다.
GitHub Actions or local machine
-> docker build --platform linux/amd64
-> docker push to ECR
-> create Dockerrun.aws.json v2 zip
-> Elastic Beanstalk ECS managed Docker
-> ECS task pulls image from ECR
ECS managed Docker에서 source bundle은 무엇인가
ECS managed Docker 환경에서도 EB 배포는 zip 파일을 올린다. 다만 zip 안에 소스코드와 Dockerfile을 넣는 것이 아니다.
zip에는 보통 이런 것만 들어간다.
deploy.zip
├── Dockerrun.aws.json
├── .platform/
│ ├── hooks/...
│ └── nginx/...
└── infra/
├── amazon-cloudwatch-agent.dev.json
└── amazon-cloudwatch-agent.prod.json
핵심은 root의 Dockerrun.aws.json이다.
{
"AWSEBDockerrunVersion": 2,
"containerDefinitions": [
{
"name": "my-api",
"image": "<aws-account-id>.dkr.ecr.<region>.amazonaws.com/<ecr-repository>:<image-tag>",
"essential": true,
"memoryReservation": 512,
"portMappings": [
{
"hostPort": 80,
"containerPort": 8000
}
],
"environment": [
{
"name": "NODE_ENV",
"value": "production"
},
{
"name": "PORT",
"value": "8000"
}
]
}
]
}
AWSEBDockerrunVersion: 2는 ECS managed Docker용이다. 일반 Docker 환경에 이 파일을 올리면 실패한다.
수동 배포 절차
로컬에서 먼저 ECR에 이미지를 push한다.
export ECR_ACCOUNT="<aws-account-id>.dkr.ecr.<region>.amazonaws.com"
export ECR_REPOSITORY="<ecr-repository>"
export IMAGE_TAG="local-test"
export IMAGE_URI="${ECR_ACCOUNT}/${ECR_REPOSITORY}:${IMAGE_TAG}"
aws ecr get-login-password --region <region> \
| docker login --username AWS --password-stdin "$ECR_ACCOUNT"
docker build --platform linux/amd64 -f infra/Dockerfile.prod -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
중요한 점은 --platform linux/amd64다. Apple Silicon Mac에서 그냥 build하면 arm64 이미지만 push될 수 있다. EB의 EC2/ECS task가 amd64라면 다음 에러가 난다.
CannotPullContainerError: no matching manifest for linux/amd64 in the manifest list entries
그 다음 Dockerrun.aws.json을 포함한 EB deploy artifact를 만든다.
IMAGE_URI="$IMAGE_URI" bash scripts/build-eb-ecs-deploy-package.sh
unzip -l deploy/eb-ecs-deploy.zip
zip 안에 Dockerfile, src, node_modules, .github, .git이 들어가면 안 된다. ECS managed Docker에서는 EB 인스턴스가 이미지를 빌드하는 것이 아니라 ECR에서 이미지를 pull한다.
로컬 EB CLI는 artifact 경로를 명시해야 한다. .elasticbeanstalk/config.yml은 로컬 설정 파일이므로 git에 올리지 않는다.
deploy:
artifact: deploy/eb-ecs-deploy.zip
그리고 배포한다.
eb use <api-prod-ecs-env>
eb deploy <api-prod-ecs-env> --staged
--staged를 붙이지 않으면 EB CLI가 git archive를 만들면서 우리가 만든 artifact를 쓰지 않을 수 있다. 이 경우 EB에서 task definition을 못 찾는다.
No ecs task definition (or empty definition file) found in environment
GitHub Actions 자동 배포
GitHub Actions에서는 EB application/environment 이름은 secret이 아니라 variable로 관리하는 편이 낫다. AWS access key, secret key, ECR registry는 secret에 남긴다.
예시:
env:
IMAGE_URI: ${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY }}:${{ needs.SetApplicationVersion.outputs.versionLabel }}
steps:
- name: ECR Login
run: aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}
- name: Build image
run: docker build --platform linux/amd64 -f infra/Dockerfile.prod -t "$IMAGE_URI" .
- name: Push image
run: docker push "$IMAGE_URI"
- name: Generate deployment package
run: IMAGE_URI="$IMAGE_URI" bash scripts/build-eb-ecs-deploy-package.sh
- name: Deploy to EB
uses: einaregilsson/beanstalk-deploy@v21
with:
aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
application_name: ${{ vars.EB_APPLICATION_NAME }}
environment_name: ${{ vars.EB_PROD_ENVIRONMENT_NAME }}
region: '<region>'
version_label: ${{ needs.SetApplicationVersion.outputs.versionLabel }}
version_description: ${{ needs.SetApplicationVersion.outputs.commitMessage }}
deployment_package: deploy/eb-ecs-deploy.zip
wait_for_deployment: true
공개 글에서는 변수명을 일반화했지만, 핵심은 application_name, environment_name을 vars.*에서 읽고, credential은 secrets.*에 둔다는 점이다.
트래픽 전환 전략
처음에는 기존 환경 이름을 그대로 유지하고 싶었다. 예를 들어 기존 환경이 <api-prod>라면, 최종적으로도 <api-prod>가 ECS 플랫폼이 되길 바랐다.
하지만 기존 Docker 환경에서 ECS Dockerrun.aws.json v2를 배포하면 실패하고, 반대로 ECS 환경에 기존 Docker bundle을 배포해도 실패한다. 플랫폼과 application version bundle 형식이 서로 맞지 않기 때문이다.
따라서 안전한 전환은 다음 순서다.
- 기존 prod 환경을 clone해서
<api-prod-ecs>를 만든다. - 플랫폼은
ECS running on 64bit Amazon Linux 2023으로 설정한다. <api-prod-ecs>에 ECR image 기반 artifact를 배포한다.- 새 환경 URL로 Postman/API smoke test를 수행한다.
- EB
Swap environment URLs또는 DNS/CNAME 변경으로 기존 사용자-facing URL을<api-prod-ecs>에 연결한다. - GitHub Actions의 prod environment variable도
<api-prod-ecs>를 바라보게 둔다. - 기존
<api-prod>는 rollback target으로 보존한다.
여기서 중요한 판단은 “prod 이름으로 다시 되돌리지 않는다”는 것이다. EB environment name보다 실제 사용자-facing URL과 배포 target이 더 중요하다. 기존 environment name에 집착하면 불필요한 terminate/recreate/swap 작업이 늘어난다.
우리가 만난 에러들
Dockerrun.aws.json unsupported version
'Dockerrun.aws.json' in your source bundle specifies an unsupported version.
Elastic Beanstalk only supports version 1 for non compose app and version 3 for compose app.
이건 거의 항상 잘못된 플랫폼에 배포했다는 뜻이다.
- 일반 Docker EB 환경: version 1 또는 3
- ECS managed Docker EB 환경: version 2
Dockerrun.aws.json 문법 문제가 아니라, 배포 대상 환경의 platform branch를 먼저 확인해야 한다.
aws elasticbeanstalk describe-environments --region <region> --environment-names <environment-name> --query 'Environments[0].{Name:EnvironmentName,PlatformArn:PlatformArn,SolutionStackName:SolutionStackName,Status:Status,Health:Health}'
No ecs task definition found
No ecs task definition (or empty definition file) found in environment
대부분 EB CLI가 deploy/eb-ecs-deploy.zip이 아니라 git archive를 올린 경우다. .elasticbeanstalk/config.yml의 deploy.artifact와 eb deploy --staged를 확인한다.
no matching manifest for linux/amd64
CannotPullContainerError: no matching manifest for linux/amd64 in the manifest list entries
Apple Silicon Mac에서 arm64 이미지를 push했을 가능성이 크다. docker build --platform linux/amd64로 다시 빌드하고 같은 tag를 덮어쓴다.
zsh에서 image URI가 깨지는 문제
zsh에서는 아래처럼 쓰면 :local-test가 변수 modifier처럼 해석될 수 있다.
export IMAGE_URI="$ECR_ACCOUNT/$ECR_REPOSITORY:local-test"
반드시 braces를 쓴다.
export IMAGE_URI="${ECR_ACCOUNT}/${ECR_REPOSITORY}:local-test"
CloudWatch에서 API 로그가 안 보이는 문제
ECS managed EB에서는 stdout/stderr 로그가 다음처럼 생긴다.
/var/log/containers/my-api-<container-id>-stdouterr.log
반면 /var/log/containers/my-api/는 container 안에서 /var/log/app 같은 mount directory에 파일을 쓸 때 쓰인다. Nest logger와 morgan이 stdout에 쓰고 있다면 CloudWatch Agent는 sibling log file을 봐야 한다.
{
"file_path": "/var/log/containers/my-api-*-stdouterr.log",
"log_group_name": "/aws/elasticbeanstalk/cloudwatch/<service>/<env>",
"log_stream_name": "{instance_id}/my-api-stdouterr"
}
체크리스트
마이그레이션 전:
배포 전:
트래픽 전환 후:
마무리
Elastic Beanstalk Docker에서 ECS managed Docker로 옮기는 일은 단순한 platform upgrade가 아니었다. 배포 artifact의 의미가 바뀌고, 빌드 위치가 바뀌고, 로그 파일 경로까지 바뀐다.
가장 큰 교훈은 이것이다.
기존 환경을 억지로 in-place upgrade 하지 말고, 새 ECS 환경을 만들고 검증한 뒤 트래픽을 swap하자.
이 방식은 조금 돌아가는 것처럼 보이지만, 실패했을 때 기존 환경으로 되돌아갈 수 있는 길을 남겨준다.