Github Actions, Docker, ECR push 자동화
Nest.js로 작성한 백엔드 서비스를 배포하면서 겪은 트러블슈팅입니다.
예전에 개인프로젝트를 배포할 때 ec2인스턴스에 직접 원격접속해서 git pull로 소스를 다운로드하여 build, start 했던 경험이 있습니다.
소스부터 배포까지 해본 경험은 처음이라 ec2가 무엇이고 소스가 어떻게 세상에 공개되는지 공부하는 용도로는 적합했지만
실제 코드를 수정하고 배포하는 과정은 너무 힘들었던 경험이 있습니다.
그래서 이번에는 이 과정을 최대한 줄이고 배포를 쉽게 할 수 있는 환경을 만들어보고자 했습니다.
첫 번째는 일단 ec2관리요소가 적어지도록 dockerizing을 해야겠다는 생각을 했습니다.
일단 도커 이미지 만드는 것부터 해야 했어요. 저는 postgres를 앱과 함께 다중컨테이너로 구성해야 했습니다.
1. 도커이미지 만들기
이렇게 Dockerfile을 루트디렉터리에 넣어주고 docker build를 해서 도커이미지가 생성되는 것을 확인했습니다.
근데 앱만 실행되고 db는 실행되지 않아서 에러가 발생했습니다.
2. db까지 실행되는 다중컨테이너 만들기
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- '8000:8000'
environment:
DB_USER: ${DB_USERNAME}
DB_PWD: ${DB_PASSWORD}
DB_TYPE: postgres
DB_HOST: postgres
DB_PORT: ${DB_PORT}
DB_NAME: ${DB_DATABASE}
env_file:
- .env
depends_on:
- postgres
postgres:
image: postgres:15
restart: always
volumes:
- ./postgres-data:/var/lib/postgresql/data
ports:
- '5432:5432'
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE}
docker-compose.yaml파일을 만들고 docker-compose up --build 명령어를 통해 실행을 확인했습니다.
그리고 앱이 실행되는 8000번 포트 localhost:8000으로 접근해서 서버가 잘 작동되는 것을 확인했습니다.
여기까지 확인하니 이제 ec2에 도커이미지만 올리면 docker-compose up으로 서버를 실행할 수 있겠다는 생각이 들었습니다.
근데 ec2에 도커이미지를 어떻게 전달하죠?
3. ECR 사용하기
git처럼 ec2에서도 pull 받을 수 있는 연결고리가 필요합니다.
그런 역할을 하는 aws 기능이 바로 ECR(Elastic Container Registry)입니다. 도커이미지를 관리하는 레포지토리를 생성할 수 있습니다.
도커 프라이빗 레지스트리를 사용하면 같은 역할로 사용할 수 있지만 무료사용자는 1개만 올릴수 있다고 합니다. 저는 나중에 프론트엔드도 배포해야 하고 회사에서도 ECR을 사용하기 때문에 이참에 공부할겸 해서 ECR을 사용하기로 했습니다.
그럼 ecr을 사용하되 비용을 아낄 생각을 해야겠습니다.
4. 도커이미지 용량 줄이기
ecr의 비용청구 기준을 알아보니 도커 이미지의 용량에 따라 청구가 되고, 다른 리전끼리 통신하는 경우에도 추가비용이 있다고 합니다. 저는 ecr도 한국에 만들 거고 ec2도 한국에 만들거라 추가비용은 없을 거고 도커 이미지 크기를 좀 줄여주면 되겠네요.
검색을 해보니 Dockerfile 멀티스테이지 설정을 통해 이미지 사이즈를 줄일 수 있다고 합니다.
기존에는 도커이미지 용량이 910mb 정도 되었습니다.
Dockerfile을 이렇게 수정했더니 730mb 정도로 줄었습니다. 만족스럽진 않네요! 더 구글링을 해보겠습니다.
이렇게 수정했더니 350mb로 줄어들었습니다.
만족스러워요. 왜 도커이미지 용량이 줄어든 건지는 chatgpt에게 물어보면 잘 알려줍니다.
이 포스팅에서는 자세한 dockerfile 내용은 생략하겠습니다.
5. 진짜 ECR에 올려보자
이제 용량이 적은 도커이미지도 만들었으니 진짜 ECR에 올려보겠습니다.
자동화는 일단 접고 수동으로라도 올리는 게 우선입니다.
aws cli를 설치하고 IAM인증으로 로그인하고 도커로그인도 하겠습니다
aws ecr get-login-password --region <리전> | docker login --username AWS --password-stdin <aws account id>.dkr.ecr.<리전>.amazonaws.com
에러가 발생합니다.
AccessDeniedException - ecr:GetAuthorizationToken action
액세스가 거부되었다네요.
메시지를 읽어보면 IAM에 권한이 없답니다! 여기서 한두 시간 삽질을 했습니다.
이 부분은 좀 자세히 적겠습니다.
구글링을 해보니 IAM 사용자의 권한정책을 설정해줘야 한답니다.
정책을 하나 직접 만들겠습니다.
IAM에 들어가서 정책 탭에 들어와서 정책 생성을 누릅니다.
그럼 이런 화면에 들어오는데, JSON을 눌러서 정책편집기에 필요한 정책을 넣어줍니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:DescribeImages",
"ecr:DescribeRepositories",
"ecr:GetDownloadUrlForLayer",
"ecr:InitiateLayerUpload",
"ecr:ListImages",
"ecr:PutImage",
"ecr:UploadLayerPart",
"ecr:GetAuthorizationToken"
],
"Resource": "*"
}
]
}
로그인하려고 했을 때
ecr:GetAuthorizationToken action
이 필요하다고 했으니까 ecr:GetAuthorizationToken만 넣었었습니다. 근데 결국 ecr에 이미지 푸시하고 그런 작업들을 하려면 ecr에 대한 전체적인 권한을 넣어주면 나중에 두 번 작업할 일이 없을 것 같아서 전부 넣었습니다. 이름을 지정하고 저장해 주세요.
정책을 만드셨으면 이제 사용자에 들어가서 내 IAM사용자를 눌러서 들어갑니다. 그리고 권한정책 탭에서 방금 만들었던 정책을 추가해 주세요. 저는 ecr_policy라고 되어있는데 저는 이름을 저렇게 저장했습니다.
아마 대부분은 이렇게 하시면 로그인이 되실 겁니다.
다시 로컬 프로젝트루트로 돌아와서 아래 커맨드를 입력해 주세요
aws ecr get-login-password --region <리전> | docker login --username AWS --password-stdin <aws account id>.dkr.ecr.<리전>.amazonaws.com
Login Success였나..? 잘 기억은 안 나지만 긍정의 메시지가 뜹니다..ㅎㅎ
저의 경우에는 여전히 똑같은 에러가 발생했는데
언제 설정했는지 모를 권한경계가 설정되어 있었습니다.
권한경계를 설정해 두면 s3만 접근됩니다. 혹시 이 부분이 설정되어 있다면 지워주세요.
아니면 권한경계를 ecr로 한정하고 싶다면 AmazonEC2ContainerRegistryFullAccess로 설정했더니 잘 됐습니다.
이제 로컬 터미널에서 aws cli로 ecr과 docker에 로그인이 되었습니다.
그럼 docker build 하고 ecr에 push만 해주면 됩니다.
docker build . -t <aws account id>.dkr.ecr.<region>.amazonaws.com/<ecr repository name>:<tag>
<> 이 부분에는 본인의 정보를 넣어주세요.
빌드가 완료되면 이제 드디어 push입니다.
docker push <aws account id>.dkr.ecr.<region>.amazonaws.com/<repo name>:<tag>
푸시가 완료되면 aws ecr에 가서 진짜 올라갔는지 확인해 보시면 됩니다.
latest 태그가 올라간 것을 확인할 수 있습니다.
6. ec2에서 pull 받아보자
아직 여전히 배포 자동화는 염두에 두지 말고 풀 수동으로라도 배포가 되는지 확인하는 단계입니다.
ec2에 원격접속해 주세요. 그리고 aws cli, docker, docker compose전부 다운로드하여주세요.
로컬에서 했던 것과 마찬가지로
aws cli로 ecr과 docker에 로그인해야 합니다.
aws ecr get-login-password --region <리전> | docker login --username AWS --password-stdin <aws account id>.dkr.ecr.<리전>.amazonaws.com
근데 당연히 에러가 나겠죠?
aws configure부터 해야 됩니다.
IAM access_key_id와 secret_access_key, region까지 모두 입력해 주세요.
그리고 다시 로그인하면 될 겁니다.
이제 드디어 ecr에서 도커이미지를 pull 받아보겠습니다.
docker pull <aws account id>.dkr.ecr.<region>.amazonaws.com/<repo name>:<tag>
7. ec2에서 도커이미지를 실행해 보자
도커이미지가 받아졌으면 docker compose up을 해줘야 하는데...?
docker-compose.yaml 파일이 있어야겠죠?
원격 터미널에서 nano docker-compose.yaml이라고 입력해 주면 원격 터미널에서 직접 파일을 만들 수 있습니다.
그리고 로컬에 있는 docker-compose.yaml을 그대로 입력하면 되는데 약간 차이가 있습니다.
바로 app의 image 이름을 직접 지정해줘야 합니다.
로컬에서 바로 실행할 때와 달리 어떤 이미지를 빌드할 건지 명시적으로 입력해줘야 합니다.
같은 디렉터리에서. env파일도 만들어서 환경변수도 제공해 주세요.
그럼 이제 docker-compose up을 했을 때 잘 되는 분들도 있고 안 되는 분들도 있을 수 있습니다.
m1, m2 맥북을 쓰시는 분들은 에러가 날 수 있습니다.
exec format error
가 발생할 수 있습니다. 이것은 도커가 실행하려는 바이너리가 현재 실행 중인(우분투) 시스템의 아키텍처와 호환되지 않는 경우에 생길 수 있다고 합니다. 예를 들어 m1맥북에서 도커이미지를 빌드하면 arm아키텍처용으로 빌드되는데, 이것을 그대로 우분투서버에 올리면 호환되지 않아서 에러가 발생한다는 거겠죠?
로컬에서 도커 이미지를 빌드할 때 아키텍처를 우분투용 amd64으로 빌드하면 됩니다.
docker build . --platform linux/amd64 -t <aws account id>.dkr.ecr.<region>.amazonaws.com/<ecr repository name>:<tag>
buildx를 사용하는 방법도 있는데 차이는 없었습니다.
docker buildx build --platform linux/amd64 -t <aws account id>.dkr.ecr.<region>.amazonaws.com/<repo name>:<tag> . --push
둘 다 사용해도 되더라고요.
이렇게 amd64로 빌드한 이미지를 다시 ecr로 올리고 ec2에서 pull 받아서 docker-compose up 해보면 실행이 됩니다.
저는 백엔드를 8000번으로 열어주기 때문에 ec2 인바운드 규칙도 수정해줘야 했습니다.
이렇게 해서 Full 수동으로 배포를 완료했습니다.
8. Github action으로 자동화
풀 수동으로 배포를 하고 나니 특정 브랜치에 push가 됐을 때 도커이미지를 만들고 ecr에 푸시하게 만드는 것까지는 github action으로 자동화할 수 있겠다는 생각이 들었습니다.
만약 이것까지 성공한다면 소스코드를 main브랜치에 push 해놓고 ec2에 가서 pull & docker-compose up만 해주면 되겠죠?
프로젝트 소스 루트에 .github / workflows / main.yml 파일을 만들어줍니다.
main.yml 파일은 다음과 같이 작성했습니다.
name: Docker Build and Push to Amazon ECR
on:
push:
branches: ['main']
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- 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: ${{secrets.AWS_REGION}}
- name: Login to Amazon ECR
id: login-pf-aws-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push the tagged docker image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-pf-aws-ecr.outputs.registry }}
ECR_REPOSITORY: ${{secrets.AWS_ECR_REPO}}
IMAGE_TAG: latest
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
메인브랜치에 push가 되면 실행되는 스크립트입니다.
runs on은 이 스크립트가 실행되는 가상환경을 말합니다. 우분투에서 실행되도록 했습니다.
앗., 그러면 m1맥북에서 도커이미지 빌드할 때 추가했던 --platform linux/amd64는 필요가 없겠군요.
빌드를 우분투에서 하면 우분투 실행환경과 똑같을 테니 말이죠.
순서대로 보면, 로컬에서 하던 것과 동일한 작업들이 들어있습니다.
aws credentials 단계에서 IAM access key로 로그인 환경변수를 설정해 주고
Login to Amazon ECR단계에서는 실제로 로그인을 합니다. 그리고
Build and push단계에서 도커이미지를 빌드하고 푸시합니다.
run에 보면 로컬에서 했던 그 명령어가 그대로 들어가 있습니다. --platform linux/amd64는 제거했어요.
이제 이 파일이 github에 푸시되면 main.yml파일을 째려보고 있다가 스크립트가 실행될 겁니다.
제가 지정했던 steps의 name들이 보이네요.
그리고 로컬에서 보이던 log들이 github actions 터미널에서 보입니다. 이 과정이 끝나면 ecr에 들어가서 직접 확인해 보시면
새 도커이미지가 들어온 것을 볼 수 있습니다.
다음에는 ec2에서 직접 도커이미지를 pull 받고 docker-compose up을 실행하는 과정까지 자동화해 보겠습니다.