개발을 하다 보면 코드를 작성하는 것만으로 일이 끝나지 않는다.
코드를 서버에 올려야 하고, 빌드해야 하고, 테스트해야 하고, 문제가 생기면 로그를 확인해야 한다. 서비스가 커지면 서버를 여러 대 운영해야 하고 배포 과정에서 발생하는 실수도 관리해야 한다.
DevOps는 이런 개발과 운영 사이의 문제를 다루는 영역이다.
단순히 Docker나 Kubernetes를 사용하는 기술을 의미하지 않는다. 소프트웨어를 빠르고 안정적으로 만들고 배포하고 운영하기 위한 방법과 문화, 자동화된 시스템을 함께 다루는 개념에 가깝다.
개발과 운영의 문제
전통적으로 개발과 운영은 서로 다른 역할로 분리되어 있었다.
개발팀 -> 코드 작성 -> 기능 개발 -> 운영팀에 전달
운영팀 -> 서버 설정 -> 배포 -> 모니터링 -> 장애 대응
문제는 여기서 생긴다.
개발자는 “내 컴퓨터에서는 잘 되는데?”라고 하고 운영자는 “서버에서는 안 되는데?”라고 할 수 있다.
배포도 사람이 직접 하면 실수가 발생하기 쉽다.
코드 수정 -> 빌드 -> 테스트 -> 서버 접속 -> 파일 복사 -> 설정 변경 -> 서비스 재시작
이 과정이 반복되면 배포 자체가 하나의 부담이 된다.
DevOps는 이런 개발과 운영의 단절을 줄이고 코드를 작성하는 순간부터 실제 서비스에서 실행되는 순간까지의 전체 흐름을 자동화하고 개선하는 방향을 추구한다.
DevOps의 기본 흐름
전체적인 흐름을 단순화하면 다음과 같다.
Code -> Build -> Test -> Package -> Deploy -> Monitor -> Feedback -> Code
개발자가 코드를 수정하면 자동으로 빌드하고 테스트한다.
테스트를 통과하면 배포할 수 있고, 배포된 서비스는 모니터링한다.
문제가 발견되면 다시 개발 과정으로 피드백이 들어간다.
DevOps의 핵심은 이 사이클을 짧고 안정적으로 만드는 것이다.
CI
CI(Continuous Integration)는 지속적 통합을 의미한다.
여러 개발자가 코드를 자주 통합하고, 통합할 때마다 자동으로 빌드와 테스트를 수행하는 방식이다.
Developer -> Git Push -> CI -> Build -> Test -> Result
예를 들어 Git에 코드를 Push하면 자동으로 다음과 같은 작업을 수행하도록 만들 수 있다.
소스 코드 가져오기 -> 의존성 설치 -> 빌드 -> 테스트 실행 -> 결과 확인
CI의 목적은 단순히 “자동으로 빌드한다”가 아니다.
코드가 다른 코드와 합쳐졌을 때 문제가 생겼는지를 가능한 한 빨리 발견하는 것이다.
CD
CI 다음에는 CD가 나온다.
CD는 문맥에 따라 Continuous Delivery 또는 Continuous Deployment를 의미한다.
Continuous Delivery
배포 가능한 상태까지 자동으로 만들어 놓고 실제 운영 배포는 사람이 승인할 수 있다.
Code -> Build -> Test -> Package -> Deploy Ready -> Manual Approval -> Production
Continuous Deployment
테스트와 검증을 통과하면 운영 환경까지 자동으로 배포한다.
Code -> Build -> Test -> Production Deploy
둘의 차이는 운영 배포에 사람의 승인 단계가 있느냐에 가깝다.
CI/CD Pipeline
CI/CD 작업을 자동화한 전체 과정을 Pipeline이라고 한다.
예를 들어 게임 서버라면 다음과 같이 만들 수 있다.
Git Push -> Build Server -> Compile -> Unit Test -> Integration Test -> Build Artifact -> Deploy -> Server -> Health Check
이렇게 만들어두면 개발자가 매번 서버에 접속해서 직접 빌드하고 파일을 복사할 필요가 없다.
배포 과정 자체가 코드로 정의되기 때문에 사람이 하던 작업을 일관된 방식으로 반복할 수 있다.
Git
DevOps에서 Git은 단순한 소스 코드 저장소 이상의 역할을 한다.
코드 변경 이력을 관리하고 CI/CD Pipeline의 시작점으로 사용할 수 있다.
Developer -> Git Commit -> Git Push -> CI/CD Pipeline
예를 들어 특정 브랜치에 Push되면 테스트를 실행하고, main 브랜치에 Merge되면 배포하도록 구성할 수 있다.
GitHub Actions, GitLab CI/CD, Jenkins 같은 도구를 이용해 이런 자동화 Pipeline을 구성할 수 있다.
Artifact
빌드가 끝난 결과물을 Artifact라고 한다.
언어와 프로젝트에 따라 형태는 다르다.
Source Code -> Build -> Artifact
예를 들어 다음과 같은 형태가 될 수 있다.
C# Game Server -> 실행 파일 + DLL
Java -> JAR
Web -> Build된 정적 파일
Container -> Docker Image
중요한 것은 개발자가 작성한 소스 코드와 실제 서버에서 실행되는 결과물을 구분하는 것이다.
Pipeline에서 생성된 동일한 Artifact를 여러 환경에 배포하면 환경마다 빌드 결과가 달라지는 문제도 줄일 수 있다.
환경 분리
서비스를 운영하다 보면 개발 환경과 운영 환경을 분리해야 한다.
Development -> 개발
Staging -> 운영과 비슷한 환경에서 검증
Production -> 실제 서비스
개발자가 수정한 코드를 바로 Production에 넣으면 문제가 발생했을 때 피해가 크다.
그래서 일반적으로 다음과 같은 단계를 거치게 만들 수 있다.
Code -> Development -> Test -> Staging -> Production
모든 서비스가 반드시 이 구조를 그대로 사용할 필요는 없다.
프로젝트 규모와 위험도에 따라 환경을 구성한다.
Infrastructure as Code
서버를 직접 접속해서 하나씩 설정하는 방식은 재현하기 어렵다.
어떤 서버에는 패키지가 설치되어 있고 다른 서버에는 없을 수도 있다.
이 문제를 해결하기 위해 인프라 설정 자체를 코드로 관리하는 IaC(Infrastructure as Code)를 사용할 수 있다.
Infrastructure -> Code -> Version Control -> Automated Provisioning
대표적으로 Terraform 같은 도구가 있다.
예를 들어 서버, 네트워크, 데이터베이스 등의 인프라 구성을 코드로 정의하면 동일한 환경을 반복해서 만들기 쉬워진다.
Code -> Infrastructure 생성 -> 동일한 환경 재현
인프라도 소프트웨어처럼 버전 관리할 수 있다는 것이 큰 장점이다.
Docker
환경 차이 문제를 해결하기 위해 많이 사용하는 것이 Container다.
Docker를 사용하면 애플리케이션과 실행에 필요한 환경을 하나의 이미지로 패키징할 수 있다.
Application + Runtime + Dependencies + Configuration -> Docker Image -> Container -> Application
개발 환경과 서버 환경에서 같은 이미지를 사용하면 “내 컴퓨터에서는 되는데 서버에서는 안 되는” 문제를 줄일 수 있다.
다만 Container는 가상머신과 같은 개념은 아니다.
일반적으로 컨테이너는 호스트 OS의 커널을 공유하면서 프로세스를 격리한다.
Kubernetes
컨테이너가 하나라면 Docker만으로도 충분할 수 있다.
그런데 서비스가 커져서 컨테이너가 수십, 수백 개가 된다면 문제가 생긴다.
Container 1 -> Container 2 -> Container 3 -> ... -> Container 100
어떤 컨테이너가 죽었는지 확인하고 다시 실행하고, 여러 서버에 배치하고, 트래픽을 분산시키는 작업을 직접 관리하기 어렵다.
이런 컨테이너 오케스트레이션을 위한 대표적인 시스템이 Kubernetes다.
Kubernetes는 컨테이너를 실행하는 것뿐만 아니라 배포, 스케일링, 서비스 디스커버리, 장애 대응 등의 기능을 제공한다.
다만 작은 프로젝트에 무조건 Kubernetes를 도입할 필요는 없다.
운영해야 하는 시스템보다 인프라가 더 복잡해지는 경우도 있다.
Deployment
배포는 단순히 파일을 서버에 복사하는 작업이 아니다.
실제 서비스에서는 새로운 버전으로 교체하는 과정에서 여러 문제가 발생할 수 있다.
예를 들어 다음과 같이 버전을 변경할 수 있다.
Version 1 -> Version 2
이 과정에서 서버가 잠시 중단될 수도 있다.
그래서 여러 가지 배포 전략을 사용할 수 있다.
Rolling Deployment
서버를 조금씩 새로운 버전으로 교체한다.
Server 1 -> V2
Server 2 -> V1
Server 3 -> V1
이후 다음과 같이 순차적으로 교체한다.
Server 1 -> V2
Server 2 -> V2
Server 3 -> V1
마지막으로 모든 서버를 새로운 버전으로 교체한다.
Server 1 -> V2
Server 2 -> V2
Server 3 -> V2
서비스 중단을 줄일 수 있다.
Blue-Green Deployment
운영 환경을 두 개 준비하고 트래픽을 전환한다.
Blue -> Current Version
Green -> New Version
Green을 충분히 검증한 다음 트래픽을 Green으로 전환한다.
문제가 생기면 다시 Blue로 돌릴 수 있다.
Users -> Blue
Users -> Green
Canary Deployment
새로운 버전을 일부 사용자에게만 먼저 배포한다.
Users -> 95% V1 + 5% V2
문제가 없는 것을 확인하면서 점차 V2의 비율을 높인다.
Rollback
배포가 항상 성공하는 것은 아니다.
그래서 문제가 발생했을 때 이전 버전으로 되돌릴 수 있어야 한다.
V1 -> V2 -> Error -> Rollback -> V1
여기서 중요한 것은 단순히 이전 소스 코드를 가지고 있는 것이 아니다.
실제로 배포했던 Artifact와 설정, 데이터베이스 변경까지 고려해야 한다.
특히 데이터베이스 Schema가 변경된 경우에는 애플리케이션만 이전 버전으로 되돌린다고 문제가 해결되지 않을 수 있다.
Monitoring
배포가 끝났다고 운영이 끝나는 것은 아니다.
서비스가 정상적으로 동작하는지 계속 관찰해야 한다.
대표적으로 다음과 같은 지표를 확인한다.
CPU -> Memory -> Disk -> Network -> Request Count -> Latency -> Error Rate
예를 들어 다음과 같은 현상이 발생하면 문제가 생겼다는 신호로 볼 수 있다.
Error Rate -> 갑자기 증가
Latency -> 급격히 증가
CPU -> 100% 지속
Logging
Logging은 시스템에서 발생한 이벤트를 기록하는 것이다.
Request -> Application -> Log
로그를 통해
언제
어떤 요청이 들어왔고
어떤 작업을 수행했고
어떤 오류가 발생했는지
를 확인할 수 있다.
운영 환경에서는 단순히 콘솔에 로그를 출력하는 것보다 로그를 중앙에서 수집하고 검색할 수 있도록 구성하는 경우가 많다.
로그가 없으면 장애가 발생했을 때 원인을 추적하기가 상당히 어려워진다.
Metrics와 Tracing
Monitoring을 조금 더 나누어 보면 Logs, Metrics, Traces를 함께 사용하는 경우가 많다.
Logs
무슨 일이 발생했는지 기록한다.
Request -> Application -> Log
Metrics
시스템 상태를 숫자로 측정한다.
System -> Metrics -> CPU / Memory / Request / Error Rate
Tracing
하나의 요청이 여러 서비스를 거치는 과정을 추적한다.
Client -> API Server -> Auth Service -> Game Service -> Database
특정 요청이 어디에서 오래 걸렸는지 확인할 수 있다.
이 세 가지를 통칭해서 Observability의 주요 구성 요소로 보는 경우가 많다.
장애 대응
운영에서는 장애가 발생할 수밖에 없다.
중요한 것은 장애가 발생하지 않도록 하는 것만이 아니라 발생했을 때 얼마나 빨리 발견하고 복구할 수 있는가다.
Failure -> Detection -> Alert -> Investigation -> Mitigation -> Recovery -> Postmortem
예를 들어 서버가 죽었다면 다음과 같은 과정이 필요하다.
서버 장애 -> 모니터링 감지 -> 알림 -> 원인 확인 -> 트래픽 우회 -> 서버 복구 -> 원인 분석
장애가 끝난 뒤에는 왜 문제가 발생했는지 분석하고 같은 문제가 다시 발생하지 않도록 시스템을 수정한다.
자동화
DevOps에서 반복해서 등장하는 것이 자동화다.
사람이 직접 해야 하는 작업이 많을수록 실수할 가능성이 커진다.
수동으로 작업하면 다음과 같은 과정이 반복된다.
Manual -> Build -> Test -> Deploy -> Verify
이를 자동화하면 다음과 같이 만들 수 있다.
Push -> Build -> Test -> Deploy -> Health Check
하지만 모든 것을 자동화하는 것이 목표는 아니다.
자동화 비용이 더 큰 작업까지 억지로 자동화할 필요는 없다.
반복적이고 실수하기 쉬우며 명확한 규칙으로 정의할 수 있는 작업부터 자동화하는 것이 현실적이다.
DevOps를 하나의 흐름으로 보기
지금까지의 내용을 하나로 연결하면 다음과 같은 형태가 된다.
Developer -> Git -> CI -> Build -> Test -> Artifact -> CD -> Deployment -> Infrastructure -> Production -> Monitoring -> Logs / Metrics / Traces -> Feedback -> Developer
여기서 Docker를 사용한다면 Artifact가 Docker Image가 될 수 있고, Kubernetes를 사용한다면 그 이미지를 Kubernetes 환경에 배포할 수 있다.
Terraform 같은 IaC 도구를 사용한다면 인프라 자체도 코드로 관리할 수 있다.
각 도구가 따로 존재하는 것이 아니라 하나의 배포와 운영 흐름 안에서 연결된다.
DevOps에서 중요한 관점
DevOps를 공부하면서 처음에는 Docker, Kubernetes, Jenkins, Terraform 같은 도구가 눈에 들어온다.
하지만 도구부터 공부하면 오히려 구조가 복잡해진다.
먼저 다음과 같은 흐름을 이해하는 편이 낫다.
코드 관리 -> 빌드 -> 테스트 -> 배포 -> 운영 상태 확인 -> 장애 발견 -> 복구 -> 개선
이 흐름을 자동화하고 안정적으로 만드는 과정에서 CI/CD, Container, IaC, Monitoring 같은 기술이 필요해진다.
정리
DevOps를 단순히 특정 기술 스택으로 보면
Docker
Kubernetes
Jenkins
Terraform
AWS
같은 도구들의 목록처럼 보인다.
하지만 실제로 중요한 것은 도구가 아니라 소프트웨어가 개발되어 운영되는 전체 과정이다.
Code -> Build -> Test -> Deploy -> Operate -> Monitor -> Improve
개발과 운영을 별개의 작업으로 나누기보다 하나의 지속적인 흐름으로 보고, 반복 작업은 자동화하고, 인프라는 재현 가능하게 만들고, 운영 상태는 관측 가능하게 만든다.
결국 DevOps는 “운영을 위한 도구를 많이 사용하는 것”보다 소프트웨어를 지속적으로 배포하고 안정적으로 운영할 수 있는 시스템을 만드는 것에 가깝다.