현재 학교에서 진행중인 프로젝트는 도커 컴포즈를 통해 컨테이너를 실행하고 있는데, 레포의 main 브랜치에 새로운 커밋이 생길 때마다 사람이 일일히 실행시키는 건 개발을 공부하는 학생으로서 도리가 아니라고 판단해 CICD 파이프라인을 구축해야겠다고 느꼈다.

사이드 프로젝트에서 빠지지 않고 등장하는 국룰 클라우드 ec2 인스턴스를 생성했다.
이전에 GCP와 Contabo 인스턴스를 사용해본 적이 있는데, GCP는 너무 비쌌고 Contabo는 프리티어가 없기 때문에 이번엔 AWS로 결정했다.
인스턴스를 생성하는 과정에서 OS로 AWS linux와 Ubuntu 중 어떤 걸 해야할지 고민하다 호환성의 측면에서 AWS linux가 낫다는 글을 보고 AWS linux로 결정했다.
그런데 알고보니 여러가지 컨테이너 전용 서비스들이 있는 것 아니겠는가? 평소에 ec2밖에 들어본적 없었기에 각자의 차이점이 궁금해졌다.
| EC2(Elastic Compute Cloud) | ECR(Elastic Container Repository) | ECS(Elastic Container Service) |
|---|---|---|
| 범용 클라우드 인스턴스 | 컨테이너 이미지 저장소 -> 도커 허브 | 컨테이너 오케스트레이션 -> 쿠버네티스 |
프로젝트가 깃허브를 통해 관리되고 있고, Github Action을 사용해 파이프라인을 구축할 생각이다.
우선 변경사항이 생기면 인스턴스에서 불러올 수 있도록 git 레포를 연동한다.
패키지를 설치하는 과정에서 당황한 부분이 있었는데, 기존 ubuntu linux에만 익숙해서 AWS linux도 당연히 패키지 메니저로 apt를 사용할 줄 알았다.
하지만 없는 명령어였고 검색해본 결과 AWS linux의 패키지 메니저로 dnf를 사용하고 있었다.
추가적으로 Docker와 Docker-componse도 설치해준다.
도커의 경우에는 dnf로 설치가 가능하고 docker-compose는 별도로 curl요청을 통해 설치해야 한다.
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
깃허브에서는 아래와 같이 민감한 정보들을 Github Action을 위한 yaml 파일에서 제외시키기 위한 secrets을 추가할 수 있게 해준다. 레포지토리 설정에서 추가할 수 있다.

이렇게 추가한 secrets은 .yml 파일에서 환경변수와 비슷하게 사용된다. 내가 저장한 것들은 깃허브 러너가 ec2 인스턴스와 ecr에 접근 가능하도록 해주는 key 들이다.
이 과정에서 ec2에 원격 접속을 위한 키 쌍을 사용해야 하는데, IAM에 들어가 사용자 그룹과 사용자를 추가해야만 해당 키 쌍을 발급 받을 수 있다.

이를 통해 .pem 형식의 개인 키를 발급 받을 수 있다.
그리고 이들을 활용해 다음과 같이 최초의 Github Action 파일을 작성했다.
name: Deploy to EC2 via ECR
on:
push:
branches: [main]
env:
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}
IMAGE_NAME: lgtm-server
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
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: Log in to Amazon ECR
run: |
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | \
docker login --username AWS --password-stdin $ECR_REGISTRY
- name: Build and push Docker image
run: |
docker build -t $IMAGE_NAME .
docker tag $IMAGE_NAME:latest $ECR_REGISTRY/$IMAGE_NAME:latest
docker push $ECR_REGISTRY/$IMAGE_NAME:latest
- name: Decode SSH private key
run: echo "${{ secrets.EC2_KEY }}" | base64 -d > private_key.pem && chmod 600 private_key.pem
- name: SSH to EC2 and deploy
run: |
ssh -o StrictHostKeyChecking=no -i private_key.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
cd /home/${USER}/server
docker-compose pull
docker-compose up -d
EOF

위와 같이 도커를 위한 프라이빗 레포지토리를 생성해, ec2 인스턴스가 해당 레포지토리에서 이미지를 pull 할 수 있도록 만든다.
ec2에 ssh 연결을 한 후, aws ecr get-login-password 명령어를 사용하면 나와 관련된 credential을 사용해 ecr에 접근할 수 있다.
이 과정에서 docker 로그인과 관련된 경고가 발생했다.
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning.
당장 큰 문제가 되는 점은 아니엿지만 amazon-ecr-credential-helper를 설치해 암호화된 로그인 정보를 사용해 ecr에 로그인 할 수 있게 됐다.
이를 사용하지 않는다면 ~/.docker/config.json에 인증 정보가 암호화되지 않은 채로 저장된다.
인스턴스를 혼자 사용한다면 크게 문제가 되지는 않는다.
다른 것들은 정상적으로 진행이 됐는데 태스크 중 도커 이미지를 레포지토리에 push하는 곳에서 계속 오류가 발생했다.
알고보니, 이미지 레포지토리 자체를 생성할 때, 내가 설정할 이미지의 이름까지 포함한 레포지토리를 생성해야 됐다. 그러니까 레포지토리 URI가 의미하는 것이 디렉토리의 개념이 아니라, 파일의 개념이었던 것이다.
하나의 레포지토리 URI 내부에서 태그로 버전을 관리하는 방식이었다.

레포지토리 이름과 secret 값을 수정하고 나서, 정상적으로 이미지들이 레포지토리에 업로드되는 걸 확인할 수 있었다.
버전에 따른 차이인지 내 로컬 맥북에서는 docker-compose와 docker compose 모두 동일하게 작동하는 반면에, ec2 인스턴스 내부에서는 docker compose 만 작동했다.
이를 수정하기 위해 deploy.yml 파일을 다시 수정했고, 최종적으로 다음과 같은 파일이 됐다.
name: Deploy to EC2 via ECR
on:
push:
branches: [main]
env:
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}
IMAGE_NAME: core
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
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: Log in to Amazon ECR
run: |
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | \
docker login --username AWS --password-stdin $ECR_REGISTRY
- name: Build and push Docker image
run: |
docker build -t $IMAGE_NAME .
docker tag $IMAGE_NAME:latest $ECR_REGISTRY/$IMAGE_NAME:latest
docker push $ECR_REGISTRY/$IMAGE_NAME:latest
- name: Decode SSH private key
run: echo "${{ secrets.EC2_KEY }}" | base64 -d > private_key.pem && chmod 600 private_key.pem
- name: SSH to EC2 and deploy
run: |
ssh -o StrictHostKeyChecking=no -i private_key.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
cd /home/${USER}/server
docker compose pull
docker compose up -d
EOF
기존에 ec2의 사양은 t2.micro였는데 1 vcpu, 1gb ram의 사양을 가지는 인스턴스이다. 해당 인스턴스를 사용하다보니 redis와 spring 이미지가 함께 실행됐을 때 인스턴스가 버티지 못하고 뻗어버렸다.
인스턴스에 대한 지식이 많다면 프리티어 여러개와 메모리 사용량 제한을 통해 해결할 수도 있을 것 같았지만, 배포 성공이 단기적인 목표였기 때문에 눈물을 머금고 t2.medium으로 사양을 업그레이드했다.
흑흑 내 프리티어

많은 실패 끝에, deploy.yml 파일도 수정하고, 인스턴스 타입도 변경하면서 결국 성공해냈다! 브랜치에 PR이 합쳐지면 자동으로 서버에 배포가 된다!
성공하고 나서 나도 모르게 "와... 됐다"가 입 밖으로 나왔다.

하지만 내 로컬 디바이스에서 바로 요청이 보내지진 않았고, tcpdump를 통해 확인해보니 내 로컬 ip로부터 요청이 가는 걸 확인 후, 방화벽의 문제라고 판단했다.
따라서 인스턴스의 인바운드 패킷 방화벽 규칙을 추가한 후에야 서버와의 정상적인 통신이 가능해졌다.

이렇게 나의 첫 CICD 파이프라인 구축이 끝이 났다.
구축을 하는 동안 다음의 것들을 배울 수 있었다.
tcpdump, base64, ssh의 configuration 등 리눅스 시스템에 대한 것지금은 기본적인 자동 배포 파이프라인이지만, 쿠버네티스를 추가적으로 공부해 인프라 구조에 대한 이해를 더 하는 것이 목표이다.
t2.micro 인스턴스와 minikube를 사용해보고, 헬스 체크 및 무중단 배포에 대해 공부해볼 예정이다.