배경
현재 개인 프로젝트 shareticon의 백엔드 서버는 EC2 인스턴스에서 도커를 통해 실행되고 있으며, Prometheus와 Grafana를 사용하여 서버 내부 지표들을 모니터링하고 있습니다. 그렇기에 서버 헬스에 문제가 생기면 이를 통한 이메일 알림으로 최대한 장애를 빠르게 파악할 수 있겠다고 생각하고 있었습니다.
그러던 중, 지인에게 shareticon 서비스를 소개해 드릴 기회가 있어 서비스 배포 주소로 접속했는데.. 정상적인 서비스 화면 대신 전혀 예상하지 못한 에러 페이지를 마주치게 되었습니다 😰
이유 조사
제가 장애를 확인한 시점은 서버가 내려간 지 무려 7시간이나 지난 뒤였습니다.
애플리케이션 내부의 메트릭만을 수집하게 되는 Prometheus의 특성 상 인스턴스 자체에 문제가 생긴 상황은 체크하지 못했기 때문에, 아무런 이메일 알림도 없었고, Grafana의 대시보드에도 뭔가 관련된 특이사항을 찾아볼 수 없었습니다.
즉, 제가 느끼기에는 아무런 징조 없이 갑작스럽게 서버가 다운되어 버린 것이죠.
문제 해결을 위해 ssh로 서버에 접속해보려 했으나 타임아웃이 발생하였고, AWS 콘솔에서 확인 결과 인스턴스 상태 검사가 1/2 상태로 정상 통과하지 못하는 상태였습니다.

우선적으로 인스턴스 재부팅을 시도했으나 문제가 해결되지 않아, 결국 인스턴스 중지 후 다시 시작하는 방식으로 재시작하고 나서야 상태 검사가 통과되며 ssh 접속이 가능해졌습니다. 이후 백엔드 서버 역시 수동으로 도커 이미지를 실행시켜 복구했습니다.
이렇게 서버는 무사히 다시 복구되었지만, 시스템 로그 기록을 통해 어떤 문제였는지를 찾아보려 시도했음에도 아쉽게도 명확한 이유를 발견하지 못했습니다. 다만 인스턴스의 재부팅 동작과 인스턴스 중지-시작 동작의 차이를 근거로 생각해 볼 부분이 있었습니다.
💡 인스턴스 재부팅 vs 인스턴스 중지
- 실제 호스트 컴퓨터의 차이
- 인스턴스 재부팅은 OS 재부팅과 동일한 개념으로, 인스턴스는 여전히 동일한 호스트 컴퓨터에 존재한다. 반면 인스턴스 중지는 일반적으로 새로운 호스트 컴퓨터로 이동된다.
- 인스턴스 스토어 볼륨 초기화 차이 (루트 볼륨은 유지)
- 재부팅 시에는 인스턴스 스토어 볼륨의 데이터가 유지되는 반면, 중지 시에는 데이터가 지워진다.
=> OS 수준이 아닌 실제 물리 서버인 호스트 컴퓨터 수준에서 뭔가 문제가 발생했을 가능성 혹은 저장공간 역할을 하는 볼륨과의 연결에서 문제가 발생했을 가능성이 있지 않을까?
문제는 다시 반복될 것이다
여기까지만 보면 일단 당장의 문제를 해결했기 때문에 안도의 한숨을 쉬며 넘어갈 수도 있었겠으나, 앞으로 비슷한 상황이 발생하지 않으리라는 보장이 없다는 것을 우리는 알고 있습니다.
그럴 때마다 매번 이렇게 수동으로 처리하기에는 어려움이 있기 때문에 개발자 대신 자동화된 시스템이 이것을 감지하고 대신 빠르게 대응해 준다면 좋을 것이라는 생각이 들었습니다.
정리해 보자면 지금처럼 인스턴스에 무언가 장애가 발생한다면 다음과 같은 절차가 자동으로 작동하기를 기대하며 다음 작업을 진행했습니다.
- 개발자에게 인스턴스 상태 검사가 실패했음을 알리는 이메일 발송
- 문제가 발생한 인스턴스를 자동으로 중지 후 새로 시작하기
- 새롭게 시작된 인스턴스에 백엔드 서버 실행하기
CloudWatch 활용하기
이번 장애 대응을 계기로 인스턴스 자체가 죽어버리는 경우에는 Prometheus와 Grafana 모니터링이 효과적이지 않음을 깨달았기 때문에, 인스턴스 모니터링을 위해 AWS CloudWatch를 활용해보고자 했습니다.
1. 이메일 발송을 위한 경보(Alarm) 작업
CloudWatch에서는 경보 기능을 통해 원하는 지표가 특정 조건을 달성했을 때, 각종 작업을 진행하도록 설정할 수 있는데요. 프리 티어에서는 최대 10개의 경보 지표까지 무료로 사용 가능하다고 합니다.
콘솔의 설정 메뉴가 쉽고 친절하게 설명되어 있기 때문에 설정하기 크게 어렵지 않았습니다. 가장 첫 번째 단계로, 프로젝트 상황에 맞게 StatusCheckFailed라는 지표를 통해 상태 체크 통과 여부를 확인하도록 정의했습니다.

다음으로는 실행할 작업을 선택하는 단계인데, 기본 토픽을 선택한 다음 알림을 받길 원하는 이메일을 입력해 주면 경보 상태 트리거 시, 이메일로 알림이 전송됩니다.

2. 인스턴스 중지 - 시작 자동화를 위한 Lambda 함수 세팅
작업 구성 단계에서는 알림 말고도 여러 작업을 추가할 수 있습니다. 그중에는 EC2 작업도 포함되어 있으며 인스턴스 복구, 중지, 종료, 재부팅 이렇게 네 가지 작업을 제공합니다.

그런데 지금 필요한 작업은 인스턴스 중지 후 시작인데 아쉽게도 인스턴스 시작 작업이 따로 제공되지 않고 있습니다. 따라서 인스턴스 시작 작업을 위해 EC2 인스턴스 시작 API의 호출이 필요한데요.
작업 구성 내에서 설정할 수 있으면서, 다른 서비스를 호출할 수 있는 기능을 완벽하게 수행할 수 있는 존재가 있습니다. 바로 서버리스 컴퓨팅으로 함수를 실행시키는 AWS Lambda입니다. Lambda를 통해 EC2 인스턴스 API를 호출하는 함수를 설정해 봅시다.
Lambda에서 함수를 새로 생성하면 선택한 런타임 환경에 따라 코드를 작성할 수 있는 화면이 주어집니다. 저는 파이썬을 사용했기 때문에 파이썬 AWS SDK 라이브러리인 boto3을 이용해 타겟이 되는 인스턴스를 중지 후 재시작하는 코드를 작성해 보았습니다.
이때, 코드 작성 후에는 반드시 Deploy 버튼을 눌러 작성한 코드를 배포해야 함을 주의해 주세요.

작성한 함수는 다른 AWS 리소스에 접근할 수 있어야 하고, 또 다른 서비스로부터 호출되어야 하기 때문에 각종 접근 정책과 권한 설정이 필요합니다. 함수 구성 메뉴의 권한 항목으로 들어가면 이를 설정할 수 있습니다.
- 실행 역할
- Lambda 함수가 다른 AWS 리소스에 접근할 수 있도록 하는 IAM 역할
- 함수가 무엇을 할 수 있는지를 명시
- 기본적으로 세팅되는 역할에 CloudWatch Logs에 대한 권한이 포함되어 있어, 함수 실행 내역과 로그 등을 기본적으로 모니터링 가능
- 리소스 기반 정책
- 다른 AWS 서비스나 계정에게 이 Lambda 함수를 호출할 수 있는 권한을 주는 정책
- 누가 함수를 호출할 수 있는지를 명시
지금 상황에는 함수가 EC2 리소스에 접근할 수 있어야 하고, CloudWatch 알람으로부터 호출될 수 있어야 하죠. 따라서 실행 역할에 EC2를 중지, 실행하는 권한을 추가합니다.

리소스 기반 정책에는 앞서 생성한 CloudWatch 경보를 추가합니다.

💡 리소스 기반 정책 편집 시, 서비스 항목에 CloudWatch Logs, CloudWatch Events가 존재해서 선택에 혼동이 올 수 있는데, CloudWatch 경보에서 함수를 호출하는 경우는 보안 주체를 직접 lambda.alarms.cloudwatch.amazonaws.com로 설정해야 정상 동작함을 주의
만일 보안 주체가 정상적으로 설정되지 않는다면 CloudWatch Alarms is not authorized to perform 에러 메시지가 발생하며 호출이 실패함
이렇게 Lambda 함수의 설정을 마쳤다면, 다시 CloudWatch 경보로 돌아가 작업 구성을 마저 진행해 줍니다.

이제 여기까지 모든 설정이 끝났다면 생성한 경보에 대한 오버뷰를 확인할 수 있습니다. 추후 인스턴스에 문제가 발생하여 경보가 트리거 되면 이메일이 발송되고, Lambda 함수가 실행될 것입니다.
3. systemd로 새롭게 시작된 인스턴스에 백엔드 서버 실행하기
이제 마지막 남은 과정은, 인스턴스가 시작되고 리눅스 OS가 부팅될 때 자동으로 도커 컨테이너를 실행시켜 백엔드 서버를 기동하는 것입니다. 이를 위한 여러 방법이 있지만, 그 중 서비스들의 의존성과 실행 순서를 안전하게 제어할 수 있는 장점이 있는 리눅스의 systemd를 사용해 보았습니다.
💡 systemd란 리눅스의 init 역할을 하는 시스템으로, OS 부팅 과정에서 서비스들의 의존성을 처리하고 시작되는 순서를 관리함. service 파일을 통해 이러한 서비스의 동작을 설정할 수 있으며, systemctl 명령어로 과정을 제어 가능
이때 개발자가 작성하는 커스텀 service 파일은 /etc/systemd/system 내부에 생성해야만 정상적으로 인식된다
먼저 실행하고자 하는 내용의 service 파일을 작성한 후, 작성된 서비스를 등록하고 활성화하면 작업이 끝납니다. 이제 부팅 시마다 아래 파일이 실행되어 도커 컨테이너가 실행되게 될 것입니다.
# 서비스 메타데이터와 의존성 설정
[Unit]
Description= restart server with Docker Compose
After=network.target docker.service # 네트워크와 도커 실행 이후에 서비스 시작
Requires=docker.service # 도커에 의존성을 가짐
# 서비스 실행 방식 정의
[Service]
Type=oneshot # 한 번만 작동
WorkingDirectory=/home/ubuntu # docker-compose.yml 경로
ExecStart=/usr/bin/docker compose up -d # 컨테이너 실행
RemainAfterExit=yes # 서비스 명령 종료 후에도 유지
# 서비스가 실행될 부팅 레벨 설정
[Install]
WantedBy=multi-user.target # 멀티유저 레벨에서 실행
# 새로 작성된 service 파일 인식하도록 리로드
sudo systemctl daemon-reload
# 서비스 활성화
sudo systemctl enable restart_logic.service
4. 확인하기
마지막으로 AWS CloudShell을 이용해 경보를 강제 트리거하여, 전체 프로세스가 정상적으로 작동하는지 확인해 보았습니다.
aws cloudwatch set-alarm-state
--alarm-name "알림 이름"
--state-value ALARM
--state-reason "발생 사유”
그러면 아래와 같이 트리거 된 경보 상태가 표시되는 것을 볼 수 있으며, 정상적으로 메일 전송도 이루어집니다.


이후 인스턴스 역시 중지되고 시작되며 실행이 완료되었을 때 서비스에 접속하는 것도 가능하게 되어, 최종적으로 기대했던 동작이 잘 이루어지는 것을 확인할 수 있었습니다!
마무리
이렇게 경보부터 도커 컨테이너 실행까지 과정을 정리해 보았습니다. 처음에는 갑작스러운 장애로 당황했었지만.. 이를 계기로 장애 대응 절차를 구축해 볼 수 있어서 전화위복이 되지 않았나 하는 생각이 듭니다.
한 가지 아쉬운 점은 서버가 죽은 이유를 명확히 확인하지 못했다는 점. 그리고 지금 상태라면 만일 똑같은 일이 발생해도 여전히 이유는 알 수 없게 된다는 것입니다. 따라서 이 부분은 비용이 감당되는 범위 내에서 CloudWatch를 이용해 모니터링할 수 있도록 추후 학습해 볼 예정입니다.
사실 인프라 쪽은 한 번 설정해 두고 자주 들어가지 않게 되면 과정을 잊어버리기 쉽기 때문에 미래의 저를 위해 한번 쭉 정리해 보았는데요. 읽어주신 분들께도 도움이 될 수 있었다면 좋겠습니다!
'etc' 카테고리의 다른 글
| 30시간이 지나면 서버 응답이 느려지는 이유: JVM 메모리 스왑으로 인한 응답 지연 트러블슈팅 (0) | 2026.03.15 |
|---|---|
| 트랜잭션 격리 수준과 동시성 제어 이야기 (2) : SERIALIZABLE과 Deadlock (3) | 2025.01.05 |
| 트랜잭션 격리 수준과 동시성 제어 이야기 (1) : @Transactional과 synchronized (1) | 2025.01.03 |
| 왜 내가 만든 서버는 서버가 먼저 요청을 끊는 걸까? 궁금증 해결기 (0) | 2024.08.23 |
| 리눅스 scp 사용 시 Permission denied (publickey).lost connection 오류 해결기 (0) | 2024.04.13 |
