Welcome! Everything is fine.

🛠️ GitHub Actions 기반 CD 실전 트러블슈팅: EC2, ECR, ElastiCache 본문

카테고리 없음

🛠️ GitHub Actions 기반 CD 실전 트러블슈팅: EC2, ECR, ElastiCache

개발곰발 2025. 3. 31.
728x90

 

처음으로 자동 배포를 설정하면서 겪은 트러블슈팅을 기록하고자 한다.✍🏻

우선 CI/CD 환경은 다음과 같다.

  • EC2: Ubuntu 22.04, Docker 설치
  • ECR: Spring Boot 앱 이미지를 빌드하여 push
  • RDS: MySQL DB 사용
  • 배포 방식: GitHub Actions를 통한 CI/CD 자동화

EC2 배포 시 오류 발생

command not found 에러

docker: command not found 에러는 EC2 서버에 Docker가 설치되어 있지 않아서 발생한 오류다. GitHub Actions에서 EC2로 SSH 접속은 성공했지만, EC2 안에 docker 명령어 자체가 없어서 전부 실패한 상황이다.

 

EC2에 SSH 접속 후 아래 명령어 실행해 Docker를 설치한다.

sudo apt update
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker

 

위와 비슷하게 aws: command not found 에러도 발생했다. 이 에러는 EC2 서버에 awscli가 아예 설치되어 있지 않아서 발생한 에러로 역시 awscli를 한 번 설치하면 해결된다.

 

EC2에 SSH 접속 후 아래 명령어 실행한다. apt install awscli 명령어를 통해 설치를 하려 했지만, Ubuntu 24.04부터는 AWS에서 제공하는 공식 zip으로 설치해야 한다고 한다.

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

 

만약 unzip도 설치되어있지 않다면 먼저 설치해준다.

sudo apt update
sudo apt install -y unzip

 

설치 후, 다음 명령어로 정상적으로 설치되었는지 확인할 수 있다.

aws --version

 

🤔 GitHub Actions에서는 왜 aws 명령어가 되는데?
GitHub Actions에서 aws 명령어가 동작하는 이유는, GitHub의 리눅스 러너에 AWS CLI가 기본적으로 설치되어 있기 때문이다. 반면 EC2는 직접 설정하는 가상 서버이기 때문에, 필요한 도구는 모두 수동으로 설치해주어야 한다.

AWS IAM 권한 연결 문제

이제 aws 명령어 실행은 잘 되지만, Unable to locate credentials. You can configure credentials by running "aws configure". 이라는 에러가 발생했다. 이 에러는 EC2에서 AWS 인증정보(Access Key, Secret Key)가 아예 없는 상태여서 발생한 것이다.

 

자세한 원인을 살펴보니...

  • EC2에서 aws ecr get-login-password를 쓰려면 AWS IAM 권한이 있어야 한다.
  • EC2 안에 ~/.aws/credentials 파일이 없어서 발생했다.
  • 즉, EC2가 ECR에 접근할 수 있는 권한이 없다는 뜻이다.

따라서 EC2에 IAM Role을 연결해 해결했다. EC2 인스턴스 세부정보를 보니, IAM Role이 비어있는 것을 확인할 수 있다.

 

따라서 IAM 역할을 EC2로 해서 새로 만든 후, AmazonEC2ContainerRegistryFullAccess 권한을 허용했다. ECR은 Private Registry라서 IAM 권한이 없으면 pull에 실패한다.

 

다음과 같이 설정해야 EC2가 ECR에서 pull/push 포함 풀 권한을 가진다.

docker pull 실패

배포 테스트에서 다음과 같은 에러가 발생했다.

 

에러 메시지를 분석해보니 두 가지 문제가 있었다.

 

1. docker login 관련 에러

Your authorization token has expired.

 

이 메시지는 ECR 인증 토큰이 만료되었음을 의미한다. AWS ECR은 보안을 위해 로그인 토큰이 12시간 동안만 유효하도록 설계되어 있다. 나는 EC2 인스턴스에 이미 AmazonEC2ContainerRegistryFullAccess IAM 역할을 부여했기 때문에, 별도로 로그인하지 않아도 될 줄 알았다. 하지만 실제로는 그렇지 않았다.

 

✔️ 정리하자면...
EC2가 ECR에 접근할 권한(IAM Role)은 있지만, Docker는 AWS IAM을 인식하지 못하기 때문에 별도의 로그인 절차가 필요하다.

따라서 AWS CLI를 통해 인증 토큰을 받아서 Docker에 전달해줘야 한다. 다음 명령어를 Deploy on EC2 단계에 추가하자 docker pull이 정상적으로 작동했다.

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URI

 

✔️ IAM 권한과 Docker 인증은 별개!

  • IAM Role은 “리소스를 사용할 수 있는가”를 판단하는 권한이다.
  • 하지만 Docker는 AWS의 권한 체계를 모름 → AWS CLI로 토큰을 받아서 docker login으로 명시적으로 인증해줘야 한다.

또한, ECR 인증 토큰은 단기(12시간 유효) 토큰이기 때문에, 주기적으로 재로그인하는 로직이 필요할 수 있다.

⚙️ 운영 환경에서는 ECS, Fargate, EKS 등의 컨테이너 오케스트레이션 서비스를 사용하는 경우가 많다. 이런 경우에는 해당 서비스가 ECR 인증을 자동으로 처리해주기 때문에 별도의 docker login이 필요 없다.

 

2. 포트 충돌 에러
에러 메시지 중 다음과 같은 내용이 있었다.

Bind for 0.0.0.0:8080 failed: port is already allocated.

 

이는 EC2 내에서 포트 8080을 이미 점유하고 있는 컨테이너가 실행 중이라는 뜻이다. 대부분 이전에 실행된 컨테이너가 종료되지 않아서 발생하는 문제다.

 

따라서 docker run을 하기 전에 기존 컨테이너를 정리해주는 명령어를 추가했다.

docker ps -q --filter ancestor=$ECR_URI | xargs -r docker stop
docker ps -aq --filter ancestor=$ECR_URI | xargs -r docker rm

 

💡$ECR_URI는 ECR 이미지 주소다. 하지만 이전과 다른 태그가 붙은 이미지거나 이름이 바뀌었다면 해당 필터에 안 잡힐 수 있다.

더 안전하게 하려면 컨테이너 이름을 명시해서 정리하는 것도 고려할 수 있다.

docker stop my-app-container || true
docker rm my-app-container || true

정상적으로 포트가 비워졌는지 확인하려면 다음 명령어를 사용한다.

sudo lsof -i :8080

health check 실패

배포는 성공적으로 완료되었지만, health check는 계속해서 실패하는 문제가 발생했다. 따라서 여러가지 가능성을 체크해보며 문제를 해결했다.

1단계: Docker 컨테이너 상태 확인

다음 명령어들을 통해 실행 상태 및 로그를 확인할 수 있다. 이 단계에서 컨테이너가 제대로 실행되지 않았거나, 실행되자마자 꺼지는 경우엔 로그에서 에러 메시지를 찾아야 한다.

docker ps                   # 현재 실행 중인 컨테이너 목록
docker ps -a                # 실행 이력이 있는 모든 컨테이너
docker logs <컨테이너 ID>  # 죽은 컨테이너의 로그까지 확인

2단계: Spring Actuator 설정 확인

컨테이너가 정상 작동하고 있는 것처럼 보이는데도 health check가 실패했다면,build.gradle에 Spring Actuator 의존성이 제대로 추가되었는지 확인한다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'

 

또한 배포 시 사용되는 application-prod.yml에 다음 설정이 포함되어 있는지 확인한다.

management:
  endpoints:
    web:
      exposure:
        include: health,info

3단계: Spring Profile 활성화 확인

위 설정들이 application-prod.yml에 포함되어 있다면, 해당 프로파일이 실제로 활성화되고 있는지도 반드시 확인해야 한다. Spring Boot 앱을 실행할 때 다음과 같이 prod 프로파일이 활성화되어야 설정이 적용된다.

-Dspring.profiles.active=prod

 

기존에는 docker run 명령어에서 직접 전달했지만, Dockerfile 내 ENTRYPOINT를 다음과 같이 수정해 고정하는 방식으로 개선했다.

ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"]

4단계: actuator 접근 권한 확인

설정이 잘 되어 있는데도 여전히 health check가 실패한다면, 보안 설정(SecurityConfig)을 점검해야 한다. EC2에 SSH 접속해 다음 명령어를 입력해 확인해봤다.

curl http://localhost:8080/actuator/health

 

해당 명령어를 실행했을 때 아래처럼 403 Forbidden 응답이 오면, actuator endpoint는 열려 있으나 접근 권한이 막혀 있다는 뜻이다.

 

또, SecurityConfig 클래스에서 actuator 경로도 허용해야 한다.

5단계: 외부 접속 테스트 및 보안 그룹 설정

EC2 내부에서는 health 응답이 잘 오는데도 외부 health check가 실패한다면, EC2 보안 그룹 설정을 점검해야 한다. AWS를 사용할 때는 특히나 보안 그룹 설정 문제인 경우가 많으니 잘 확인해두는 것이 좋다. 포트 8080이 퍼블릭으로 열려 있어야 외부에서 접근 가능하다.

EC2에서 다시 확인해보니 정상적으로 HTTP/1.1 200 OK 응답이 온 것을 볼 수 있다.

 

다음과 같이 웹 브라우저를 통해서도 볼 수 있는데, http://<EC2 퍼블릭 IPv4 주소>:8080/actuator/health 와 같이 입력해야 한다. 나는 EC2에 탄력적 IP를 할당하여 고정된 주소로 항상 접근 가능하도록 연결했다.

Redis 연결 실패 - ElastiCache 설정

배포 테스트가 정상적으로 끝났다고 생각했지만, 이후 테스트 중 Redis 연결 관련 오류가 발생했다.

 

로그를 자세히 보면 Redis 관련 오류라는 것을 알 수 있다.

Caused by: io. lettuce. core. RedisConnectionException: Unable to connect to
eightyage-cache. 2i7efi.ng. 0001.apn2. cache. amazonaws. com/<unresolved>: 6379

Redis 설정 점검

application-prod.yml에는 아래와 같이 Redis 설정을 해두었다.

data:
  redis:
    host: ${REDIS_HOST}
    port: 6379

 

그리고 REDIS_HOST 환경 변수는 GitHub Repository Secrets에 정상적으로 등록했다. 이 환경 변수에는 ElastiCache Redis 인스턴스의 엔드포인트가 들어간다.

EC2에서 직접 접속 확인

EC2에서 telnet으로 Redis에 접속을 시도해봤다.

telnet <REDIS_HOST> 6379

 

하지만 다음과 같이 연결이 되지 않았다.

원인 분석: 보안 그룹 인바운드 미설정

해당 문제는 ElastiCache Redis 인스턴스의 보안 그룹이 EC2의 접근을 허용하지 않아서 발생한 것이었다. Redis가 퍼블릭하게 열리지 않기 때문에, EC2처럼 내부 통신을 하려면 보안 그룹 간 인바운드 허용 설정이 필수다. 다음과 같이 인바운드 규칙에 EC2 인스턴스의 보안 그룹을 소스로 추가해준다.

📌 보안 그룹 간 참조를 사용하는 이유: EC2 인스턴스의 IP는 변할 수 있으므로, 고정된 IP가 아닌 EC2가 속한 보안 그룹 자체를 참조하는 방식이 유지보수에 유리하다.

Redis 연결 성공

설정 변경 후 다시 접속을 시도해보니 정상적으로 연결이 되었다.

 

Spring Boot Actuator에서도 Redis 관련 항목이 포함된 health 정보가 정상적으로 출력되었다.