📖 도식 및 고급 이론: 네트워크, 컨테이너, CI/CD, Kubernetes, 관측 가능성(Observability) 및 복구.
실제 환경에서의 DevOps 실습
러시아어 실습 자료: HTTP 서비스 실행부터 컨테이너, CI, 그리고 Kubernetes까지 다룹니다. 모든 과제는 하나의 애플리케이션을 중심으로 진행됩니다. 각 작업에는 검증 및 진단 연습이 포함되어 있습니다.
수준: 기본적인 커맨드 라인 지식이 있는 초보자. 진행 속도: 작업당 2~4시간. 여기에서 우리는 Devops에 대한 가장 완벽한 자료 및 가이드(Guide)를 링크로 모았습니다. 별도의 학습 폴더에서 순차적으로 진행하세요. 예시는 Python 3, Git, Docker와 Compose를 사용하는 Linux/macOS 환경을 기준으로 합니다. Kubernetes는 로컬 클러스터와 kubectl이 필요합니다. 실제 운영 클러스터나 실제 비밀번호(secret)는 사용하지 마세요.
학습 경로 (Маршрут)
- 서비스 및 진단 - HTTP, 프로세스 및 포트.
- Git 및 변경 사항 검증 - 브랜치(branch), 테스트, 히스토리(history).
- Docker - 이미지(image), 컨테이너(container), 로그(log) 및 제한사항.
- Compose 및 상태 - 두 개의 서비스와 영속적 데이터(persistent data).
- CI - 풀 리퀘스트(pull request) 자동 검증.
- Kubernetes - Deployment, Service, readiness 및 롤백(rollback).
- 관측 가능성 및 인시던트 - 메트릭(metric) 및 조사(investigation).
- 최종 프로젝트 - 자율적인 구성 및 운영.
진행 방법 (Как проходить)
해결책을 위한 자체 레포지토리(repository)를 만드세요. 명령어를 맹목적으로 복사하지 마세요: 실행하기 전에 어떤 파일과 리소스를 변경하는지 스스로 설명하세요. 각 과제 후에는 검증 명령어, 결과, 그리고 실패 원인을 기록하세요. 작업이 끝난 후 커밋을 하세요. 클라우드 리소스는 필요하지 않으며, 로컬 클러스터는 메모리와 프로세서를 사용합니다.
1. 서비스 및 진단
결과: 로컬 HTTP 서비스 및 해당 프로세스와 포트를 찾는 방법을 이해하는 것.
폴더를 만들고 app.py 파일을 생성하세요:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
...
하나의 터미널에서 python3 app.py를 실행하고, 다른 터미널에서는 다음을 수행하세요:
curl -i http://127.0.0.1:8080/healthz
curl -i http://127.0.0.1:8080/missing
lsof -i :8080
확인: 첫 번째 요청은 200과 JSON을 반환하고, 두 번째는 404입니다. 프로세스의 PID를 찾고 Ctrl+C로 서버를 중지하세요. 포트가 사용 중이면 소유자를 찾으세요; 알 수 없는 프로세스는 종료하지 마세요.
오류 발생: 서버를 중지하고 curl을 다시 실행하세요. 연결 오류는 HTTP 404와 어떻게 다른가요? 답변을 기록하세요. 추가로 알려지지 않은 라우트와 APP_VERSION 변수에 대한 테스트를 추가하세요.
2. Git 및 변경 사항 확인
결과: 별도의 브랜치에서 변경 사항이 발생했으며, 외부 라이브러리 없이 재현 가능한 테스트가 가능합니다.
git init
git add app.py
git commit -m 'Add HTTP service'
...
test_app.py를 생성하세요:
import json
import threading
import unittest
...
python3 -m unittest -v를 실행한 다음, 404 테스트를 추가하세요. git diff를 확인하고 변경 사항을 커밋한 후, 자신의 레포지토리에 풀 리퀘스트(pull request)를 여세요. 오류 발생: 예상 상태 코드를 201로 변경하여 테스트가 실패하는지 확인하고 수정하세요. 의도적으로 깨진 테스트는 커밋하지 마세요.
3. Docker
결과: 서비스가 비특권 사용자(unprivileged user)의 컨테이너에서 실행됩니다.
Dockerfile을 생성하세요:
FROM python:3.13-slim
WORKDIR /app
COPY app.py .
...
docker build -t devops-lab:local .
docker run --rm -d --name devops-lab -p 127.0.0.1:8080:8080 devops-lab:local
curl -i http://127.0.0.1:8080/healthz
...
확인: 응답은 200이며, 컨테이너 사용자는 65532:65532이고, stop 후 컨테이너가 삭제됩니다. 포트가 사용 중이면 자신의 학습 서비스만 중지하거나 외부 포트를 변경하세요. 오류 발생: -p 없이 실행하고, 호스트(host)에서 요청이 작동하지 않는 이유를 설명하세요. 실제 배포 시에는 기본 이미지를 digest로 고정하고 정기적으로 업데이트해야 합니다.
4. Compose 및 상태
결과: 애플리케이션과 Redis는 내부 네트워크로 통신하며, 데이터는 컨테이너 재구축을 거칩니다. 이는 별도의 스토리지 학습 검증입니다: 현재 애플리케이션은 Redis를 사용하지 않습니다.
compose.yaml 생성:
services:
app:
build: .
...
docker compose up -d --build
curl http://127.0.0.1:8080/
docker compose exec redis redis-cli SET course hello
...
확인: compose 버전으로 GET 요청 시 'hello'가 반환됩니다. docker compose down은 볼륨을 보존하고; docker compose down -v는 학습 데이터를 삭제하며 확인 후에만 실행됩니다. 오류 발생 지점: 컨테이너 내부에서 서비스 이름 redis와 localhost를 비교해 보세요. 왜 localhost가 해당 컨테이너 자체를 가리키는지 설명할 수 있나요? 비밀번호나 실제 데이터는 이 예제에 포함하지 마세요.
5. CI
결과: 모든 푸시(push)와 풀 리퀘스트(pull request)가 테스트 및 이미지 빌드를 실행합니다. 자신의 레포지토리에서 .github/workflows/ci.yml을 생성하세요:
name: CI
on: [push, pull_request]
permissions:
...
GitHub에 브랜치를 푸시하고 Actions를 열어보세요. 확인: 두 단계 모두 녹색이어야 합니다. 별도의 커밋으로 고의적으로 실패한 테스트는 검사를 빨간색으로 만들고, 수정하면 다시 녹색 상태로 돌아옵니다. 프로덕션 환경에서는 외부 액션(actions)을 전체 SHA 값으로 고정하고, 종속성을 확인하며, 풀 리퀘스트의 신뢰할 수 없는 코드에 비밀 정보를 전달하지 마세요.
6. Kubernetes
결과: 두 개의 복제본(replica)과 검증 가능한 롤백이 가능합니다. kubectl과 별도의 로컬 클러스터(예: kind)가 필요합니다. 다음 명령어는 현재 컨텍스트에만 학습 네임스페이스를 생성합니다: 먼저 kubectl config current-context를 확인하여 이것이 로컬 클러스터인지 확인하세요.
클러스터를 kind create cluster --name devops-lab 명령어로 생성한 후, 로컬 이미지를 로드합니다: kind load docker-image devops-lab:local --name devops-lab. 현재 컨텍스트를 확인하세요. k8s.yaml을 생성하세요:
apiVersion: v1
kind: Namespace
metadata:
...
kubectl apply -f k8s.yaml
kubectl -n devops-lab rollout status deployment/app
kubectl -n devops-lab get pods,service
...
다른 터미널에서 다음 명령을 실행하세요: curl http://127.0.0.1:8080/healthz. Ctrl+C를 사용하여 포트 포워딩을 중지합니다.
장애 상황 시나리오: kubectl -n devops-lab set image deployment/app app=devops-lab:missing 명령어로 잘못된 이미지를 설정합니다. 이후 kubectl -n devops-lab describe pods를 보고, 다음으로 kubectl -n devops-lab rollout undo deployment/app을 실행하고 롤아웃 상태(rollout status)가 될 때까지 기다립니다. 업데이트 실패 시 준비된 이전 레플리카들이 왜 계속 요청을 처리할 수 있는지 설명하세요. 학습용 클러스터만 삭제합니다: kind delete cluster --name devops-lab.
7. 관측 가능성 및 인시던트 (Observability & Incident)
과제: 증상(Symptom)과 원인(Cause)을 분리하는 방법을 배웁니다. Compose로 애플리케이션을 실행하고 데이터를 수집하세요:
docker compose ps
docker compose logs --tail=50 app
curl -s -o /dev/null -w 'status=%{http_code} time=%{time_total}\n' http://127.0.0.1:8080/healthz
docker compose stop app을 사용하여 앱만 중지합니다. 재확인하여 상태와 로그를 분석한 후, docker compose up -d app으로 복구하세요. 짧은 보고서를 작성하세요: 감지 시간, 증상, 영향, 원인, 조치, 복구 확인 및 예방 조치 1가지. 응답 부재가 원인의 충분한 증거는 아님을 명시하고, 이를 프로세스 상태와 연관 지어 설명해야 합니다.
추가 과제: 응답 코드별 요청 카운터와 지연 시간을 메트릭에 추가하세요. 전체 URL이나 사용자 ID를 레이블로 사용하지 마세요. 이는 높은 카디널리티(cardinality)를 생성합니다. Prometheus를 연결하고 오류 빈도 및 지연 시간 그래프를 그려, 메트릭이 실제로 수집되는지 확인하세요.
8. 최종 프로젝트 (Final Project)
자신의 리포지토리에 서비스, Dockerfile, Compose, 테스트(tests), 워크플로우(workflow) 및 매니페스트(manifests)를 모두 모으세요. 실행 방법, 검증 방법, 제약 사항, 정리 방법을 포함하는 README를 준비하세요. 다음 내용을 시연하세요:
- 클린 부팅 및 자동화 테스트;
- 의도적으로 실패한 배포 진단 및 성공적인 롤백;
- 명령을 통한 학습 데이터 저장 및 삭제;
- 인시던트 보고서 작성 및 readiness와 liveness의 차이 설명;
- Git에 비밀번호, 토큰 또는 상태 파일 없음.
자가 점검: README를 통해 다른 사람이 결과를 재현할 수 있을까요? 하나의 레플리카가 다운되면 어떻게 될까요? 단순히 명령을 실행하는 것뿐만 아니라 복구 과정을 어떻게 확인할 수 있을까요?
다음으로 나아갈 방향
스스로 확장해 볼 다음 주제들: Terraform/OpenTofu와 안전한 State 저장; Ansible과 Idempotency(멱등성); Helm; Argo CD를 사용한 GitOps; 전용 스토리지 시스템을 통한 Secret 관리; 백업 및 복구; SLO 및 Alerting. 사용하기 전에 반드시 도구의 공식 문서를 참조하고 검증된 버전을 고정하세요.
본 과정은 DevOps Core Course의 구조에서 영감을 받았습니다. 이 리포지토리의 자료는 자체적으로 작성되었습니다.
고급 이론: 시스템은 장애 상황에서 어떻게 동작하는가
이 섹션은 실습을 보완합니다. 아래 예시들은 운영 원칙과 작은 실험들을 설명하며, 추가 구성 요소들이 자동으로 설치되지는 않습니다. 코드는 격리된 학습 환경에서만 실행하세요.
1. 시간 손실 지점: DNS, TCP, TLS 또는 애플리케이션
요청에 대한 응답은 여러 단계로 구성됩니다: 이름 확인(name resolution), TCP 연결 설정, HTTPS를 위한 TLS, 첫 바이트 대기, 그리고 본문 수신. 지속적인 연결 및 DNS 캐시는 특정 요청에서 일부 단계를 제외시킬 수 있습니다.
flowchart TD
A["클라이언트"] --> B["DNS: 이름 -> 주소"]
B --> C["TCP 및 TLS"]
...
병목 지점은 어느 단계에서든 발생할 수 있습니다. 작업 1의 학습용 로컬 서버를 확인해 보세요:
curl -sS -o /dev/null \
-w 'connect=%{time_connect}s first_byte=%{time_starttransfer}s total=%{time_total}s status=%{http_code}\n' \
http://127.0.0.1:8080/healthz
curl 값은 요청 시작부터 누적된 간격이므로, 첫 바이트 대기 시간(Time To First Byte)을 단순히 애플리케이션의 순수 작동 시간으로 계산할 수 없습니다. IP로 요청하는 경우 DNS가 관여하지 않으며, HTTP는 TLS 핸드셰이크가 없습니다. 프록시, 네트워크, 큐 또한 결과에 영향을 미칩니다.
| 관찰 내용 | 가능한 원인 | 확인 방법 |
|---|
| 이름이 해석되지 않음 (Name not resolved) | DNS 서버, 레코드, 클라이언트 설정 | 필요한 환경에서 이름 해석 비교 |
| ... | | |
| 스스로 점검해 보세요: 애플리케이션이 200을 반환했지만 사용자가 지연 문제를 제기합니다. 이 응답만으로 문제가 브라우저에 있다고 결론 내리기 충분할까요? 아닙니다. 단계를 측정하고 서버 및 종속성 메트릭과 비교해야 합니다. | | |
2. 큐(Queues), 포화(Saturation) 및 리틀의 법칙(Little's Law)
안정적인 시스템에서 시스템 내 평균 요청 수는 도착률에 평균 체류 시간을 곱한 값과 근사합니다: L = λ × W. 단위는 일치해야 합니다. 예를 들어, 초당 100개의 요청이 오고 평균 시간이 0.2초인 경우, 시스템에는 평균적으로 약 20개의 요청이 있습니다. 이는 최대 처리량에 대한 공식이라기보다는 안정 상태에서의 관찰된 관계입니다.
도착 속도가 처리 속도를 장기간 초과하면 큐가 증가하여 지연 시간과 메모리 사용량을 늘립니다. 클라이언트는 요청을 반복하기 시작하고, 부하가 훨씬 더 강하게 증가할 수 있습니다. 작은 제한된 큐(limited queue), 병렬성 제한(limitations on parallelism) 및 서킷 브레이커(circuit breaker)는 서비스가 통제 불가능하게 축적되는 것을 방지합니다. 이러한 매개변수들은 측정과 클라이언트 요구사항에 따라 선택됩니다.
클라이언트의 타임아웃은 이미 완료된 작업을 취소하지 않습니다. 변경 요청을 재시도할 경우 중복(duplicate)이 발생할 수 있습니다. 부작용(side effect)이 있는 작업에는 고유성 키(idempotency key)를 사용하여 이 키를 서버 결과와 원자적으로 연결해야 합니다. 재시도는 횟수와 시간으로 제한하고, 지터(jitter)가 포함된 백오프(backoff) 전략을 추가하며 전체 요청 기간을 고려해야 합니다. 고유성은 헤더의 존재만으로는 자동으로 발생하지 않습니다.
과제: 서비스와 데이터베이스를 거치는 요청 경로를 그리세요. 큐(Queue) 위치를 식별하고, 병렬성 제한을 제안하며, 과부하 시 사용자에게 무엇이 보일지 설명하세요. 그런 다음 높은 CPU 사용률이 아닌 포화 상태임을 입증하는 메트릭들을 나열하세요.
3. 컨테이너 내 프로세스: 신호 및 올바른 종료
작업 3의 Dockerfile은 exec 형식의 CMD를 사용합니다: Python이 컨테이너의 주 프로세스가 되어 직접 신호를 받습니다. 컨테이너가 중지될 때, 환경은 일반적으로 종료 신호를 보내고, 이후 대기 기간을 거쳐 강제로 프로세스를 종료할 수 있습니다. 정확한 시간 및 신호는 설정에 따라 달라집니다.
프로덕션(production) 서비스의 경우, 새로운 요청을 받지 않고, 허용 가능한 시간 내에 현재 진행 중인 요청들을 완료하며, 리소스를 해제해야 합니다. 이를 graceful shutdown (그레이스풀 셧다운)이라고 부릅니다. 학습용 HTTP 서버는 완전한 준비 상태 해제(readiness mechanism) 및 활성 요청 관리 메커니즘을 구현하지 않았으므로, 이를 프로덕션 템플릿으로 간주해서는 안 됩니다.