같은 날 배포 방향이 두 갈래로 갈렸다. web은 내리고, 워커는 올렸다.
web은 상시 트래픽이 크지 않은 내부 도구다. Fargate 태스크를 24시간 띄워두는 비용이 아까웠다. t3.micro + Elastic IP로 바꿨다.
// deploy-web.yml — ECS update-service 대신
INSTANCE_ID=$(aws ec2 describe-instances \
--filters \
"Name=tag:Name,Values=prod-web" \
"Name=instance-state-name,Values=running" \
--query "Reservations[0].Instances[0].InstanceId" \
--output text)
처음엔 SSM RunShellScript로 systemctl restart web을 날리는 방식으로 갔다가, 몇 시간 만에 Docker Compose 방식으로 다시 갈아탔다. 이미지만 바꿔서 서비스 재시작하는 거면 systemd 유닛보다 compose가 관리하기 편했다.
ssh -i $KEY ec2-user@$PUBLIC_IP \
"cd /home/ec2-user/app && sudo docker compose pull && sudo docker compose up -d"
subtitle은 반대 방향이다. SQS 큐가 비어있을 때도 Fargate 태스크가 떠 있을 이유가 없다. Lambda SQS 트리거로 바꿨다.
| subtitle-worker | ECS Fargate | 2vCPU / 8GB | 300s |
→
| subtitle-worker | Lambda | 512MB | 120s |
Fargate에선 Long Polling 상시 실행 구조라 VisibilityTimeout을 30초마다 직접 연장하는 heartbeat 로직이 있었다. Lambda SQS 트리거는 그 자체로 폴링을 관리해주니까 이 로직 자체를 통째로 삭제했다.
ffprobe로 오디오 길이를 재던 부분도 걸렸다. Lambda 레이어에 바이너리를 올리려다가, music-metadata라는 순수 JS 라이브러리로 바꿔서 외부 바이너리 의존을 아예 없앴다.
// 이전: execSync(`${ffprobeBin} ...`)
// 이후
import { parseBuffer } from 'music-metadata';
const metadata = await parseBuffer(audioBuffer);
const durationMs = metadata.format.duration! * 1000;
render-worker는 ffmpeg 의존성이 무거워서 Lambda Layer 대신 Container Image 방식으로 갔다. aws-lambda-ric를 이미지에 심어서 컨테이너 자체를 Lambda 런타임으로 만들었다.
RUN npm install -g pnpm prisma@5 aws-lambda-ric && ...
ENTRYPOINT ["node", "/usr/local/lib/node_modules/aws-lambda-ric/bin/index.js"]
CMD ["dist/handler.handler"]
같은 날 안에 "web은 인스턴스로, 워커는 서버리스로"라는 기준이 대충 잡혔다. 상시 트래픽이 있으면 EC2, 이벤트 드리븐이면 Lambda.