생성형 AI로 일적으로나 생활적으로 도움을 많이 받고 있는 요즘, 업무 차원에서 개발 속도를 어떻게 하면 더욱 향상시킬까 하는 고민이 많은데요.
하지만 때로는 우리가 선택한 개발 및 배포 환경이 개발 속도를 방해하곤 합니다. 저 역시 최근에 그런 경험을 했습니다.
1. 기존 배포 방식의 단점
이전에 저는 docker + GitHub Actions + ECS + ECR 조합으로 배포 파이프라인을 구축했습니다. 이 방식은 코드를 푸시하는 순간 자동으로 배포가 이루어지고 github에서 종합적으로 관리할 수 있다는 장점이 있었죠. 하지만 실제로 develop 브랜치에 푸시되고 github action으로 docker 이미지가 빌드되고, ECS의 Task가 생성될 때까지, 즉, 서버에 변경사항이 반영되기까지 무려 5분이나 걸렸습니다.
처음에는 '자동화됐으니 괜찮아'라고 생각했지만, 점점 이 긴 대기 시간(코드를 변경하고 서버에 반영되는 시간)이 답답하게 느껴졌습니다. 특히 릴리즈 환경이 아니라 소규모로 개발하는 경우에는 이 5분의 대기 시간이 개발 속도에 상당한 영향을 미쳤습니다. 작은 변경사항 하나 를 커밋하고 테스트하려고 해도 커피 한 잔 마시고 올 시간이 있었으니까요.
물론 여러 명이 협업할 때는 이런 자동화된 파이프라인이 분명한 장점이 있습니다. 하지만 개인 프로젝트나 빠른 개발이 필요한 상황에서는 오히려 족쇄가 되는 느낌이었습니다.
2. 새로운 배포 방식: ssh config와 shell script의 활용
이러한 문제를 해결하기 위해 저는 좀 더 단순하면서도 빠른 배포 방식을 고안했습니다. 바로 ssh config와 shell script를 활용한 방법입니다.
새로운 방식의 흐름은 다음과 같습니다:
1. 로컬 환경에서 Docker 이미지를 빌드합니다. (I love M2 Pro 맥북❤️)
2. 빌드된 이미지를 Docker hub에 푸시합니다.
3. ssh를 통해 AWS EC2 서버에 원격으로 접속합니다.
4. 서버에서 최신 Docker 이미지를 풀(pull)받습니다.
5. 풀받은 이미지로 컨테이너를 실행합니다.
이 모든 과정을 shell script로 자동화했더니, 놀랍게도 전체 배포 시간이 28초 정도로 줄어들었습니다! 기존 방식의 약 5분(300초)에서 1/10 수준으로 단축된 것이죠.
28초 ㅁㅊㄷ..!
3. 결론
개발 환경과 배포 방식은 항상 트레이드오프가 있습니다. 팀 규모, 프로젝트의 성격, 개발 단계 등에 따라 최적의 방식이 달라질 수 있습니다. 이번 경험을 통해 저는 상황에 맞는 유연한 접근이 중요하다는 것을 깨달았습니다. 때로는 '최신' 기술 스택보다 단순하지만 효율적인 방식이 더 나을 수 있다는 것도 알게 되었고요.
여러분도 현재 사용 중인 개발 및 배포 프로세스를 한 번 점검해보는 것은 어떨까요? 어쩌면 작은 변화로 큰 효율을 얻을 수 있을지도 모릅니다.
인프라에 대해 공부해보다가 Go + Docker + ECS + Fargate + ECR + Github Action으로 CI/CD를 구성했다는 블로그 글을 보고, 따라해 보았다.
2. Dockerfile 만들기
Dockerfile은 Docker 이미지를 생성하기 위한 설정 파일입니다. Go + gin backend를 위한 Dockerfile을 만들어 보겠습니다.
FROM golang:alpine AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
WORKDIR /build
COPY go.mod go.sum main.go ./
RUN go mod download
RUN go build -o main .
WORKDIR /dist
RUN cp /build/main .
FROM scratch
COPY --from=builder /dist/main .
ENTRYPOINT ["/main"]
3. ECS 만들기 (Fargate vs EC2)
Amazon ECS(Elastic Container Service)는 컨테이너화된 애플리케이션을 쉽게 실행하고 관리할 수 있는 완전 관리형 컨테이너 오케스트레이션 서비스입니다. ECS를 사용할 때 두 가지 주요 실행 모드가 있습니다 (Fargate, EC2). 생성할 때 두 개 중 하나를 선택할 수 있습니다. 저는 Fargate를 선택했습니다.
Fargate
서버리스 옵션으로, 인프라 관리 없이 컨테이너를 실행할 수 있습니다. 확장성이 뛰어나고 관리가 쉽습니다.
EC2
사용자가 직접 EC2 인스턴스를 관리하며, 더 많은 제어와 커스터마이징이 가능합니다.
예전에는 EC2 + Code Deploy 조합으로 배포 인프라 구성을 하곤 했는데, 이번에는 Fargate를 시도해보고자 합니다.
Fargate는 인프라 관리 부담을 줄이고 싶은 경우에 적합하며, EC2는 더 세밀한 제어가 필요한 경우에 선택한다고 합니다.
클러스터 (Cluster) 개념
ECS의 최상위 리소스 그룹입니다. 여러 개의 EC2 인스턴스 또는 Fargate 작업을 논리적으로 그룹화합니다. (저는 Fargate를 사용했습니다.) 한 클러스터 내에서 여러 서비스와 태스크를 실행할 수 있습니다.
태스크 (Task) 개념
애플리케이션의 최소 배포 단위입니다. 하나 이상의 컨테이너로 구성됩니다. 태스크 정의(Task Definition)를 통해 실행할 컨테이너, 사용할 리소스 등을 정의합니다.
서비스 (Service)
지정된 수의 태스크를 클러스터에서 동시에 실행하고 관리합니다. 태스크가 중지되면 자동으로 새 태스크를 시작하여 지정된 개수를 유지합니다. (저는 1개를 지정해서 최대 1개만 실행됩니다.) 로드 밸런서와 연결하여 트래픽을 분산할 수 있다고 합니다. (요금 주의)
컨테이너 (Container)
Docker 이미지를 기반으로 실행되는 격리된 실행 환경입니다. (ECR에 push하고 태그로 관리됩니다) go + gin 으로 구성된 프로젝트를 Docker로 빌드했습니다. 태스크 내에서 하나 이상의 컨테이너가 함께 실행될 수 있습니다.
4. 인바운드 규칙 편집
8080 포트 열기 (인바운드 규칙 편집)
ECS 태스크가 실행되는 Fargate 태스크에 연결된 보안 그룹의 인바운드 규칙을 편집해야 합니다. ECS -> 클러스터 -> 서비스 -> 보안 그룹으로 이동합니다. 해당 보안 그룹을 선택하고 "인바운드 규칙 편집"을 클릭합니다.
유형: 사용자 지정 TCP 포트 범위: 8080 소스: 필요에 따라 특정 IP 범위 또는 모든 트래픽(0.0.0.0/0)을 선택
저는 8080포트를 열었으므로, 8080포트를 외부에서 접근 가능하도록 개방했습니다.
이렇게 설정하면 외부에서 8080 포트를 통해 ECS에서 실행 중인 go backend app에 접근할 수 있게 됩니다. 단, 보안을 위해 가능한 한 접근을 제한적으로 설정하는 것이 좋습니다. 또한, 태스크 정의에서도 컨테이너 포트 매핑을 설정해야 합니다. 태스크 정의의 컨테이너 설정에서 8080 포트를 호스트 포트와 매핑해야 합니다.
4. ECR 만들기 (Docker 이미지 저장소)
Amazon ECR(Elastic Container Registry)은 Docker 컨테이너 이미지를 안전하게 저장, 관리 및 배포할 수 있는 완전 관리형 Docker 컨테이너 레지스트리입니다. ECR을 사용하면 프라이빗 레포지토리를 생성하여 Docker 이미지를 저장하고, ECS와 쉽게 통합할 수 있습니다.
5. Github Action (develop branch에 머지 시 워크플로우 실행)
develop 브랜치에 머지가 되었을 때 (이벤트 발생), Docker 이미지가 빌드되고 ECR에 push(업로드)된 다음에, 해당 도커 이미지를 ECS에서 실행되게 할 것입니다. (8080포트로 접근 가능하게)
GitHub Actions를 사용하면 이러한 CI/CD 파이프라인을 구축할 수 있습니다. develop 브랜치에 머지될 때 자동으로 빌드, 테스트, 배포 과정을 실행하도록 설정할 수 있습니다.
aws.yml 코드
name: Deploy to Amazon ECS
on:
push:
branches: [ "develop" ]
env:
AWS_REGION: ap-northeast-2 # set this to your preferred AWS region, e.g. us-west-1
ECR_REPOSITORY: fye-backend-repository # set this to your Amazon ECR repository name
ECS_SERVICE: fye-backend-service # set this to your Amazon ECS service name
ECS_CLUSTER: fye-cluster # set this to your Amazon ECS cluster name
ECS_TASK_DEFINITION: ./fye-task.json # set this to the path to your Amazon ECS task definition
# file, e.g. .aws/task-definition.json
CONTAINER_NAME: fye-backend-container # set this to the name of the container in the
# containerDefinitions section of your task definition
permissions:
contents: read
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@v1
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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
docker build -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
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
상단에 있는 env: 부분에서 지역과 ecs, ecr 관련 정보들을 넣습니다. json은 태스크 탭에서 다운로드할 수 있습니다. 그리고 iam을 생성해서 github secrets에 access key와 secret key를 등록하여 github action이 aws의 다양한 서비스를 조작할 수 있게 설정합니다. 저는 iam에 아래의 6개 권한을 추가했습니다. 그리고 .csv로 추출하여 github의 secret에 키를 등록했습니다.
6개 권한 추가인프라 구성 성공!, 수많은 시도들..
6. 마무리 및 소감
No Docker + Code Deploy로 했을 때는 1~2분이면 서버 배포가 끝났는데, Docker + ECS로 해보니 생각보다 오래 걸려서 당황했다..
Docker Build 하는데만 48s, Deploy ECS Task Definition 하는 데만 4m 16s가 걸렸다. 총 5분..
다수가 협업할 때는 장점이 충분히 있을 것 같지만, 1인이나 소규모로 할 때는 No Docker + Code Deploy를 사용하는 게 속도면에서 좋을 것 같다.