
EC2 서버를 Git으로 길들이기
수동 배포 지옥에서 Pull-based GitOps까지
수동으로 관리되던 AWS EC2 서버를
GitHub Actions Self-hosted Runner 기반의 Pull-based GitOps 구조로 전환했습니다.
사람의 기억과 손에 의존하던 서버 설정 파일을 코드와 시스템의 책임으로 넘기는 과정에 대한 글입니다.
1. 자동화가 아니라, ‘신뢰할 수 있는 상태’가 필요하다.
프로젝트 초반, ec2 내의 nginx config와 같은 서버 설정 파일 관리는 생각보다 빠르게 혼란스러워졌습니다.
돌이켜보면 문제는 기술이 아니라 관리 방식이었습니다.
1단계 : 서로 물어보고 확인하기
- “Nginx 설정 어디 있어요?”
- “서버 들어가서 /etc/nginx 한 번 봐주세요.”
누군가는 알고 있었고, 누군가는 몰랐습니다.
설정 파일들은 서버 안에 있었고, 지식은 사람 머릿속에 있었습니다.
질문은 계속 반복됐고, 같은 대화를 하루에도 몇 번씩 했습니다.
2단계 : 노션으로 기록하며 관리하기
매번 물어봤던 걸 다시 물어보는 건 너무
“이건 문서로 남겨야 한다”는 판단 아래 노션에 정리하기 시작했습니다.
설정 파일 내용과 경로를 그대로 복사해두었습니다.
문제는 항상 그 다음이었습니다.
- 서버에서 급하게 설정을 고쳤고
- 노션 업데이트는 놓쳤고
- 어느 순간부터 문서는 더 이상 서버의 상태를 설명하지 못했습니다
노션은 빠르게 ‘죽은 문서’**가 되었고,
문서를 믿고 작업하다가 설정 충돌이나 장애가 발생하는 일도 잦아졌습니다.
3단계: Git을 도입했지만, 이중 관리가 시작됐습니다
“이력이라도 남기자”는 생각에 설정 파일을 Git에 올렸습니다.
변경 이력은 남았지만, 새로운 문제가 생겼습니다.
- Git에 커밋했지만 서버 반영을 깜빡하거나
- 서버에서만 고치고 Git push를 안 하거나
- 둘의 상태가 미묘하게 어긋나기 시작했습니다
이 시점에서 확실히 느꼈습니다.
사람의 손을 거치는 순간, 실수는 구조적으로 발생한다.
우리는 단순한 자동화가 아니라,
“서버의 현재 상태를 Git이 진실의 원천으로 보장하는 구조”,
즉 GitOps가 필요했습니다.
2. Kubernetes 없이 GitOps를 하기로 했습니다
우리 서비스는 Kubernetes 환경이 아니었습니다.
단일 EC2 인스턴스에서 운영되고 있었고,
그래서 선택한 방식은 단순했습니다.
- GitHub Actions
- Self-hosted Runner
- Pull-based 동기화
왜 Pull-based였는가
우테코 서버의 보안 그룹 제약이 가장 컸습니다.
- 외부에서 서버로 밀어 넣지 않는다
- 서버가 스스로 Git을 보고 상태를 맞춘다
서버는 Git을 기준으로 자신을 교정하는 존재가 되었습니다.
3. 구현한 GitOps의 실제 동작 방식
구조는 단순하지만, 의도는 분명했습니다.
핵심 설계
- 저장소 분리
- App Repo: 애플리케이션 코드
- Config Repo: 서버 설정 (Nginx, systemd 등)

- Self-hosted Runner를 EC2 내부에 설치
- SSH Inbound 없이 Outbound 연결만 사용
- Git 중심의 설정 반영
- EC2 서버 내부의 Runner가 변경 사항을 감지해 서버 설정을 자동으로 동기화 및 적용
동작 흐름
- 개발자가 Config Repo의 main 브랜치에 설정 변경을 push
- EC2 내부에서 대기 중이던 Runner가 Job을 가져옴
- Runner가 동기화 스크립트를 실행
- Git 상태와 서버 상태를 강제로 일치시킴
- 필요한 서비스를 reload (예시: nginx 설정 파일 변경 후 reload)
이 방식의 장점은 명확했습니다.
- 서버 안에도 Git 히스토리가 남았고
- 문제가 생기면 서버에서 바로 git log로 원인을 추적할 수 있었고
- “누가 언제 무엇을 바꿨는지”가 명확해졌습니다
4. 진짜 사고는 트러블슈팅에서 터졌습니다
GitOps를 적용하고 얼마 지나지 않아,
SSM 접속이 갑자기 끊기는 사고가 발생했습니다.
접속도 안 되고 명령어도 동작하지 않았습니다.
문제의 코드
sudo rsync -av $GIT_REPO_DIR/ /
왜 문제가 되었는가
당시에는 깊게 생각하지 못했습니다.
- $GIT_REPO_DIR의 소유자는 ubuntu
- rsync -a는 소유권까지 그대로 복사
- sudo로 실행되면서 /, /etc 같은 핵심 디렉터리의 owner가
root → ubuntu로 바뀌어버렸습니다
그 결과:
- root 권한이 필요한 서비스들이 접근 불가
- SSM Agent 포함 주요 시스템 컴포넌트가 정상 동작 불가
복구 과정
- 간신히 접속 경로를 확보해 chown root:root /, /etc 복구
- /etc/shadow까지 망가진 걸 확인하고 다시 복구
- 그제서야 시스템이 정상화되었습니다
5. 그래서 스크립트를 이렇게 바꿨습니다
최종 스크립트의 핵심 원칙은 세 가지였습니다.
- 필요한 디렉터리만
- 내용만 복사
- 권한은 명시적으로 선언
# 내용만 동기화, 소유권은 건드리지 않음
sudo rsync -av --no-owner --no-group $GIT_REPO_DIR/etc/ /etc/
# 사후에 명시적으로 권한 선언
sudo chown -R root:root /etc/nginx
6. 이 방식의 장점과 한계
좋았던 점
- 비용이 거의 들지 않았습니다
- SSH 포트 없이도 서버를 관리할 수 있었습니다
- Git 하나로 서버 상태를 설명할 수 있게 됐습니다
여전히 남은 한계
- 실행 중인 서버를 계속 덮어쓰는 구조
- 중간 실패 시 애매한 상태가 될 가능성
- 롤백이 느리고, 완전히 깨끗한 상태 보장은 어려움
마치며
이 방식을 온전한 GitOps라고 부르기에는 사실 애매한 지점이 있습니다.
쿠버네티스 환경도 아니고, 선언적 리소스 관리나 자동 롤백까지 갖춘 구조는 아니기 때문입니다.
그럼에도 불구하고, 지금 우리 팀과 환경에는 충분히 좋은 선택이었다고 느꼈습니다.
팀원 모두가 구조를 빠르게 이해할 수 있었고, 별도의 학습 비용 없이 바로 적용할 수 있었습니다.
설정을 바꾸기 위해 굳이 EC2에 접속할 필요도 없었고,
config repository에 변경 사항을 push하는 것만으로 서버 상태가 바로 따라오는 경험은 생각보다 꽤 편했습니다.
무거운 도구를 억지로 얹기보다는, 지금의 규모와 제약 안에서 실수를 줄이고 신뢰할 수 있는 운영 방식을 만드는 것이 목적에 더 잘 맞았습니다.
이 구조는 최종 형태라기보다는, 앞으로 더 나은 운영 방식으로 나아가기 위한 현실적인 중간 단계에 가깝습니다.
적어도 이제는 서버에 누가 들어가서 무엇을 바꿨는지를 추측하는 대신에 Git을 기준으로 현재 상태를 이야기할 수 있게 되었다는 점에서 충분히 의미 있는 변화였다고 생각합니다.
'개발' 카테고리의 다른 글
| AI 도구를 활용해 백오피스 자동화하기 (3) | 2026.01.19 |
|---|---|
| 빈번한 쓰기 요청을 어떻게 저장할 것인가 (0) | 2026.01.11 |
| JPA Auditing 환경에서 시간 로직을 테스트 가능하게 만들기 (0) | 2025.12.16 |
| 신뢰할 수 있는 API 문서를 만들기: REST Docs + Swagger UI 하이브리드 전략 (0) | 2025.11.21 |
| 개발에서의 추상화, 그리고 삶의 추상화 (2) | 2025.04.09 |
