쿠버네티스 실무 예제로 CI/CD 파이프라인 완성하기

소프트웨어 개발의 속도가 비즈니스의 성패를 좌우하는 시대입니다. 아이디어가 코드로 구현되고, 그 코드가 사용자에게 전달되기까지의 시간을 얼마나 단축시킬 수 있는가. 이것이 바로 현대 개발팀이 마주한 가장 큰 숙제일 것입니다. 풀스택 개발자로서 저 역시 프론트엔드부터 백엔드, 그리고 인프라까지 전 영역을 아우르며 이 '속도'의 문제를 해결하기 위해 수많은 고민과 시행착오를 거쳤습니다. 그리고 그 여정의 중심에는 언제나 DevOps, 그리고 도커(Docker)쿠버네티스(Kubernetes)가 있었습니다.

이 글은 단순히 도커와 쿠버네티스의 명령어를 나열하는 기초 가이드가 아닙니다. 실제 현업에서 어떻게 이 강력한 도구들이 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인 안에서 유기적으로 결합되어 아이디어를 현실로 만드는 과정을 자동화하는지, 그 구체적인 '실무 예제'를 통해 보여드리고자 합니다. 코드 한 줄을 커밋하는 순간부터 빌드, 테스트, 컨테이너 이미지 생성, 그리고 쿠버네티스 클러스터에 배포되기까지의 전 과정을 함께 구축하며 DevOps의 진정한 가치를 체감하게 될 것입니다.

이 글을 통해 얻을 수 있는 것들:
  • DevOps 문화에서 도커와 쿠버네티스가 왜 필수적인지 근본적으로 이해하게 됩니다.
  • 단순한 샘플 애플리케이션을 사용하여 처음부터 끝까지 완전한 CI/CD 파이프라인을 설계하고 구축하는 경험을 합니다.
  • 실무에서 바로 적용 가능한 도커 이미지 최적화 방법을 배우고, 효율적인 컨테이너 관리의 첫걸음을 뗍니다.
  • 대표적인 CI/CD 도구인 GitHub Actions와 Jenkins를 활용하여 쿠버네티스와 연동하는 두 가지 시나리오를 모두 경험하고 비교 분석할 수 있습니다.
  • 성공적인 파이프라인 구축을 넘어, 모니터링, 로깅, 배포 전략 등 운영 관점의 고려사항까지 폭넓은 시야를 갖추게 됩니다.

이 글은 이제 막 DevOps의 세계에 발을 들인 주니어 개발자부터, 자동화된 배포 파이프라인 구축에 어려움을 겪고 있는 시니어 개발자까지 모두를 위한 실용적인 나침반이 될 것입니다. 저의 경험을 바탕으로, 이론과 현실의 간극을 메우는 가장 현실적인 여정을 지금부터 시작하겠습니다.

1. 왜 우리는 도커와 쿠버네티스에 열광하는가?

CI/CD 파이프라인 구축이라는 본론에 들어가기 전에, 우리는 왜 이 기술 스택을 선택해야만 하는지에 대한 근본적인 질문에 답해야 합니다. "남들이 다 쓰니까"라는 이유는 엔지니어에게 결코 좋은 동기가 될 수 없습니다. 모든 기술 선택에는 명확한 '이유'가 있어야 하며, 그 이유를 이해할 때 비로소 기술을 제대로 활용할 수 있습니다.

DevOps: 문화, 그리고 속도의 철학

DevOps는 단순히 특정 도구나 기술을 지칭하는 용어가 아닙니다. 개발(Development)과 운영(Operations)을 통합하여 소프트웨어 개발 및 운영의 전체 생명 주기를 개선하려는 문화적 철학이자 접근 방식입니다. 과거에는 개발팀이 코드를 작성해 운영팀에 넘기면, 운영팀이 이를 배포하고 관리하는 거대한 벽이 존재했습니다. 이로 인해 배포는 느리고 위험한 작업으로 여겨졌고, 장애가 발생하면 서로를 탓하기 바빴습니다.

DevOps는 이 벽을 허물고, '당신이 만든 코드는 당신이 책임지고 운영한다(You Build It, You Run It)'는 원칙 아래 개발과 운영이 하나의 팀처럼 협업하는 것을 목표로 합니다. 이를 통해 더 빠르고, 더 안정적이며, 더 예측 가능한 배포를 가능하게 만드는 것이 핵심입니다. 그리고 이 DevOps 문화를 기술적으로 구현하기 위한 가장 이상적인 도구가 바로 컨테이너 기술과 그 오케스트레이션입니다.

도커(Docker): "제 컴퓨터에서는 잘 됐는데요?"의 종말

개발자라면 누구나 한 번쯤 "제 컴퓨터에서는 잘 됐는데요?"라는 말을 해보거나 들어본 경험이 있을 것입니다. 개발 환경, 테스트 환경, 운영 환경이 모두 다르기 때문에 발생하는 이 고질적인 문제는 엄청난 시간 낭비를 유발합니다. 도커는 바로 이 문제를 해결하기 위해 등장했습니다.

도커는 애플리케이션과 그 실행에 필요한 모든 종속성(라이브러리, 시스템 도구, 코드, 런타임 등)을 '컨테이너'라는 격리된 공간에 패키징합니다. 이 컨테이너는 마치 화물선 위의 규격화된 컨테이너 박스처럼, 어떤 환경(개발자의 노트북, 테스트 서버, 클라우드)에서든 동일하게 동작하는 것을 보장합니다. 더 이상 환경 차이로 인한 문제를 걱정할 필요가 없어진 것입니다.

도커 컨테이너는 가상 머신(VM)과 자주 비교되지만, 훨씬 가볍고 효율적입니다. VM은 게스트 운영체제(OS) 전체를 포함하지만, 컨테이너는 호스트 OS의 커널을 공유하고 애플리케이션에 필요한 바이너리 및 라이브러리만 격리하기 때문입니다. 이는 더 빠른 시작 속도와 더 적은 리소스 사용으로 이어집니다.

CI/CD 파이프라인에서 도커는 '빌드의 결과물' 즉, 최종 산출물(Artifact)의 역할을 합니다. 소스 코드가 빌드되고 테스트를 통과하면, 실행 가능한 도커 이미지로 만들어져 이미지 레지스트리(Docker Hub, GCR, ECR 등)에 저장됩니다. 이 이미지만 있으면 언제 어디서든 동일한 환경의 애플리케이션을 실행할 수 있습니다.

쿠버네티스(Kubernetes): 컨테이너 군단을 지휘하는 오케스트라

도커를 통해 애플리케이션을 컨테이너화하는 데 성공했다면, 다음 질문에 봉착하게 됩니다. "수십, 수백 개의 컨테이너를 어떻게 관리하고 운영해야 할까?" 하나의 컨테이너가 죽으면 누가 다시 실행시켜 줄까요? 트래픽이 몰릴 때 컨테이너 수를 자동으로 늘리거나 줄일 수는 없을까요? 여러 서버에 컨테이너를 효율적으로 분산 배치할 방법은 무엇일까요?

이러한 컨테이너 관리의 복잡성을 해결해주는 도구가 바로 쿠버네티스입니다. 구글이 내부적으로 사용하던 보그(Borg) 시스템을 기반으로 오픈소스로 공개한 쿠버네티스는 이제 컨테이너 오케스트레이션의 사실상 표준(De facto standard)으로 자리 잡았습니다. 쿠버네티스는 여러 대의 서버(노드)를 하나의 거대한 클러스터로 묶고, 개발자가 원하는 상태(e.g., "A 애플리케이션 컨테이너 3개를 항상 실행시켜줘")를 선언적으로 정의하면, 현재 상태를 지속적으로 모니터링하며 원하는 상태를 유지시켜 줍니다.

쿠버네티스가 제공하는 핵심 기능은 다음과 같습니다.

  • 자동화된 롤아웃과 롤백: 새로운 버전의 애플리케이션을 무중단으로 배포하거나, 문제가 생겼을 때 이전 버전으로 쉽게 되돌릴 수 있습니다.
  • 서비스 디스커버리와 로드 밸런싱: 여러 개의 컨테이너에 자동으로 트래픽을 분산하고, 고유한 DNS 이름을 통해 컨테이너를 쉽게 찾을 수 있도록 합니다.
  • 자가 치유(Self-healing): 실행 중인 컨테이너가 응답하지 않으면 자동으로 재시작하거나 교체하여 서비스의 안정성을 높입니다.
  • 자동화된 스케일링(Auto-scaling): CPU 사용량과 같은 지표에 따라 컨테이너의 수를 자동으로 조절하여 리소스를 효율적으로 사용합니다.
  • 시크릿 및 구성 관리: 비밀번호나 API 키와 같은 민감한 정보를 코드와 분리하여 안전하게 관리하고 배포할 수 있습니다.

결론적으로, 도커가 애플리케이션을 표준화된 단위로 '포장'하는 역할을 한다면, 쿠버네티스는 이 포장된 상품들을 대규모로 '운송하고 관리'하는 역할을 합니다. 이 둘의 조합은 현대적인 DevOps와 CI/CD 파이프라인을 구축하는 데 있어 가장 강력하고 검증된 기반을 제공합니다.

2. 파이프라인 설계: 우리가 만들 CI/CD 여정의 지도

본격적으로 파이프라인을 구축하기 전에, 우리가 만들 결과물의 전체적인 청사진을 그려보는 것이 중요합니다. 목표 지점을 명확히 해야 길을 잃지 않고, 각 단계가 전체 흐름에서 어떤 의미를 갖는지 이해할 수 있습니다. 우리가 구축할 CI/CD 파이프라인은 다음과 같은 흐름으로 동작합니다.

CI/CD Pipeline with Docker and Kubernetes
도커와 쿠버네티스를 활용한 CI/CD 파이프라인의 전체적인 흐름
  1. 1단계 (Code): 개발자가 로컬 환경에서 코드를 작성하고, 기능 개발이 완료되면 원격 Git 저장소(이 예제에서는 GitHub)의 특정 브랜치(e.g., `main` 또는 `develop`)에 코드를 푸시(Push)합니다. 이 푸시 이벤트가 전체 자동화 파이프라인의 시작을 알리는 트리거(Trigger)가 됩니다.
  2. 2단계 (Build): CI 서버(GitHub Actions 또는 Jenkins)가 코드 변경을 감지하고, 미리 정의된 파이프라인 스크립트를 실행합니다. 이 단계에서는 소스 코드를 가져와 필요한 종속성을 설치하고, 컴파일이나 트랜스파일링 같은 빌드 작업을 수행합니다.
  3. 3단계 (Test): 빌드가 성공적으로 완료되면, 자동화된 테스트(단위 테스트, 통합 테스트 등)를 실행하여 코드의 품질과 안정성을 검증합니다. 테스트에 실패하면 파이프라인은 즉시 중단되고 개발자에게 실패를 알립니다. 이는 버그가 있는 코드가 배포되는 것을 방지하는 중요한 안전장치입니다.
  4. 4. 단계 (Package & Publish): 모든 테스트를 통과한 코드는 이제 배포 가능한 형태로 패키징됩니다. 우리의 파이프라인에서는 `Dockerfile`을 사용하여 애플리케이션을 도커 이미지로 빌드합니다. 생성된 이미지는 버전 태그(e.g., Git 커밋 해시)와 함께 중앙 이미지 레지스트리(Docker Hub, AWS ECR, Google GCR 등)에 푸시(Publish)되어 저장됩니다.
  5. 5단계 (Deploy): 이미지 레지스트리에 새로운 버전의 이미지가 푸시되면, 파이프라인은 마지막 단계인 배포를 시작합니다. CI 서버는 `kubectl`과 같은 도구를 사용하여 쿠버네티스 클러스터에 접속하고, 새로운 도커 이미지를 사용하도록 배포 설정을 업데이트합니다. 쿠버네티스는 정의된 롤링 업데이트 전략에 따라 기존 버전의 컨테이너를 새로운 버전으로 점진적으로 교체하여 무중단 배포를 수행합니다.
  6. 6단계 (Operate & Monitor): 배포가 완료되면, 쿠버네티스는 애플리케이션 컨테이너들이 정상적으로 동작하는지 지속적으로 모니터링합니다. 만약 컨테이너에 문제가 발생하면 자가 치유 기능으로 자동 복구합니다. 운영팀은 프로메테우스(Prometheus), 그라파나(Grafana) 같은 도구를 통해 애플리케이션의 상태와 성능을 실시간으로 관찰합니다.

사용할 도구들

이 여정을 위해 우리는 다음과 같은 도구들을 사용할 것입니다. 대부분 무료로 시작할 수 있거나 로컬 환경에 쉽게 구축할 수 있는 것들입니다.

  • 샘플 애플리케이션: 간단한 "Hello, World!"를 응답하는 Node.js Express 웹 서버. 어떤 언어나 프레임워크든 컨테이너화할 수 있다는 것을 보여주기 위함입니다.
  • 버전 관리 시스템: GitHub. 소스 코드를 관리하고 파이프라인의 트리거 역할을 합니다.
  • 컨테이너 기술: 도커(Docker). 애플리케이션을 컨테이너 이미지로 빌드합니다.
  • 이미지 레지스트리: Docker Hub. 빌드된 도커 이미지를 저장하고 공유하는 공간입니다.
  • CI/CD 도구: 두 가지 시나리오를 모두 다룹니다.
    • GitHub Actions: GitHub에 내장된 CI/CD 서비스로, 간단하고 편리한 설정이 장점입니다.
    • Jenkins: 풍부한 플러그인 생태계와 높은 자유도를 가진 전통적인 CI/CD 강자입니다.
  • 컨테이너 오케스트레이션: 쿠버네티스(Kubernetes). 이 글에서는 로컬 개발 환경에 쉽게 쿠버네티스 클러스터를 구축할 수 있는 Minikube를 사용할 것입니다. Minikube로 만든 파이프라인은 클라우드 환경의 실제 쿠버네티스 클러스터(GKE, EKS, AKS 등)에도 거의 동일하게 적용할 수 있습니다.

이제 전체적인 그림이 그려졌으니, 첫 번째 단계인 애플리케이션을 컨테이너로 만드는 작업부터 차근차근 시작해 보겠습니다.

3. 1단계: 견고한 컨테이너 만들기 (feat. 도커 이미지 최적화)

모든 것은 애플리케이션을 안정적이고 효율적인 컨테이너로 만드는 것에서 시작됩니다. 이 단계에서는 간단한 Node.js 애플리케이션을 만들고, 이를 도커 이미지로 빌드하는 과정을 상세히 다룰 것입니다. 특히, 단순히 이미지를 만드는 것을 넘어 실무에서 매우 중요한 도커 이미지 최적화 방법에 대해 깊이 있게 파고들 것입니다.

간단한 Node.js 애플리케이션 준비

먼저 프로젝트 폴더를 하나 만들고, 그 안에 `app.js`와 `package.json` 파일을 생성합니다.
프로젝트 구조:


my-app/
├── app.js
└── package.json

package.json: 애플리케이션의 정보와 의존성을 정의합니다.


{
  "name": "my-app",
  "version": "1.0.0",
  "description": "Simple Node.js app for Kubernetes",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

app.js: 8080 포트로 요청이 오면 "Hello, Kubernetes!" 메시지를 응답하는 간단한 웹 서버입니다.


const express = require('express');
const app = express();
const PORT = 8080;

app.get('/', (req, res) => {
  res.send('Hello, Kubernetes! This is a new version!');
});

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

이제 터미널에서 `npm install` 명령으로 Express 프레임워크를 설치하고 `npm start`로 애플리케이션을 실행하여 로컬에서 잘 동작하는지 확인합니다.

Dockerfile 작성: 컨테이너의 설계도

이제 이 애플리케이션을 도커 이미지로 만들기 위한 설계도, 즉 `Dockerfile`을 작성할 차례입니다. `Dockerfile`은 이미지를 만들기 위한 명령어들의 집합입니다.

초기 버전의 Dockerfile (비효율적인 예시):


# 1. 베이스 이미지 선택
FROM node:18

# 2. 작업 디렉토리 설정
WORKDIR /usr/src/app

# 3. 애플리케이션 의존성 설치
COPY package*.json ./
RUN npm install

# 4. 앱 소스 코드 복사
COPY . .

# 5. 애플리케이션 실행 포트 노출
EXPOSE 8080

# 6. 컨테이너 시작 시 실행될 명령어
CMD [ "npm", "start" ]

위 `Dockerfile`은 동작은 하지만, 여러 가지 비효율적인 점을 가지고 있습니다. 빌드 속도가 느리고, 이미지 크기가 불필요하게 크며, 보안에 취약할 수 있습니다. 이제부터 실무에서 사용하는 최적화 기법들을 적용해 보겠습니다.

심층 분석: 도커 이미지 최적화 방법

CI/CD 파이프라인에서 도커 이미지를 빌드하는 시간은 전체 파이프라인의 속도에 직접적인 영향을 미칩니다. 또한, 이미지 크기는 레지스트리 저장 비용, 네트워크 전송 시간, 그리고 보안 취약점의 표면적과도 관련이 있습니다. 따라서 이미지 최적화는 선택이 아닌 필수입니다.

1. 적절한 베이스 이미지 선택 (`alpine` 또는 `slim`)

node:18과 같은 기본 태그는 데비안(Debian) 기반의 OS에 다양한 개발 도구들이 포함되어 있어 크기가 매우 큽니다. 실제 운영 환경에서는 애플리케이션 실행에 필요한 최소한의 라이브러리만 있으면 충분합니다. alpine 태그는 경량 리눅스 배포판인 Alpine Linux를 기반으로 하여 이미지 크기를 획기적으로 줄일 수 있습니다.

주의: Alpine Linux는 C 라이브러리로 musl을 사용하고, 대부분의 다른 배포판은 glibc를 사용합니다. 이 차이로 인해 특정 네이티브 모듈에서 호환성 문제가 발생할 수 있습니다. 이 경우, `slim` 태그(데비안 기반이지만 최소한의 패키지만 포함)가 좋은 대안이 될 수 있습니다.

2. 멀티 스테이지 빌드(Multi-stage builds) 활용

애플리케이션을 빌드하기 위해서는 컴파일러, 개발 라이브러리 등 많은 도구들이 필요하지만, 실제 실행(Runtime) 환경에서는 이러한 도구들이 필요 없습니다. 멀티 스테이지 빌드는 빌드 단계와 실행 단계를 분리하여 최종 이미지에는 오직 실행에 필요한 파일들만 포함시키는 기법입니다.

Node.js의 경우, `npm install` 시 `devDependencies`에 정의된 테스트나 빌드 도구들이 함께 설치됩니다. 운영 이미지에는 `dependencies`만 필요하므로, `npm install --only=production`을 사용하여 분리할 수 있습니다.

3. `.dockerignore` 파일 사용

프로젝트 폴더의 모든 파일을 도커 컨텍스트(Context)로 보내 빌드에 사용하는 것은 비효율적입니다. `node_modules`, `.git`, 로그 파일, 로컬 설정 파일 등 빌드에 불필요하거나 포함되어서는 안 될 파일들을 `.dockerignore` 파일에 명시하여 제외할 수 있습니다. 이는 빌드 속도를 향상시키고, 민감한 정보가 이미지에 포함되는 것을 방지합니다.

.dockerignore:


.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore

4. 도커 캐시 효율적으로 활용하기

도커는 `Dockerfile`의 각 명령어를 레이어(Layer)로 만들어 캐싱합니다. 파일 내용이 변경되지 않으면 다음 빌드 시 캐시된 레이어를 재사용하여 빌드 속도를 높입니다. 이 캐시를 최대한 활용하려면, 자주 변경되지 않는 내용을 Dockerfile의 위쪽에, 자주 변경되는 내용을 아래쪽에 배치해야 합니다.

위의 비효율적인 예시에서는 소스 코드(`COPY . .`)가 `npm install`보다 아래에 있습니다. `app.js` 파일 한 줄만 수정해도 `npm install` 레이어의 캐시가 깨져 매번 모든 의존성을 다시 설치하게 됩니다. `package.json` 파일이 변경될 때만 `npm install`을 실행하도록 순서를 조정해야 합니다.

최적화된 Dockerfile:


# ==================================
# 1단계: 빌드(Builder) 스테이지
# ==================================
# Alpine 기반의 Node.js 이미지를 빌더로 사용
FROM node:18-alpine AS builder

# 작업 디렉토리 설정
WORKDIR /usr/src/app

# .dockerignore에 의해 node_modules는 복사되지 않음
# 먼저 package.json과 package-lock.json을 복사하여 의존성 캐싱 활용
COPY package*.json ./

# 운영에 필요한 의존성만 설치
RUN npm install --only=production

# ==================================
# 2단계: 최종(Final) 스테이지
# ==================================
# 실행을 위한 최소한의 이미지 선택
FROM node:18-alpine

# 작업 디렉토리 설정
WORKDIR /usr/src/app

# 빌더 스테이지에서 설치된 node_modules를 복사
COPY --from=builder /usr/src/app/node_modules ./node_modules

# 소스 코드 복사 (가장 마지막에 위치하여 캐시 효율 극대화)
COPY . .

# 애플리케이션 실행 포트 노출
EXPOSE 8080

# 컨테이너 시작 시 실행될 명령어
CMD [ "npm", "start" ]
최적화 결과: 이처럼 멀티 스테이지 빌드와 캐싱 전략을 적용하면, 초기 Dockerfile 대비 이미지 크기는 수백 MB에서 수십 MB로 줄어들고, 코드 변경 시 빌드 시간은 수 분에서 수 초로 단축될 수 있습니다. 이는 CI/CD 파이프라인의 전체 실행 시간을 크게 개선하는 핵심 요소입니다.

이미지 빌드 및 로컬 테스트

이제 최적화된 `Dockerfile`을 사용하여 이미지를 빌드하고 실행해 봅시다. 터미널에서 다음 명령어를 실행합니다. `[YOUR_DOCKERHUB_ID]` 부분은 자신의 Docker Hub ID로 변경해야 합니다.


# 도커 이미지 빌드 (-t 옵션으로 이미지 이름과 태그 지정)
docker build -t [YOUR_DOCKERHUB_ID]/my-app:1.0.0 .

# 빌드된 이미지로 컨테이너 실행 (-p 옵션으로 호스트와 컨테이너 포트 매핑)
docker run -p 8080:8080 -d [YOUR_DOCKERHUB_ID]/my-app:1.0.0

# 컨테이너가 잘 실행되고 있는지 확인
docker ps

# 브라우저나 curl로 테스트
curl http://localhost:8080

"Hello, Kubernetes!" 메시지가 보인다면 성공적으로 컨테이너화가 완료된 것입니다. 마지막으로, 이 이미지를 Docker Hub에 푸시하여 나중에 쿠버네티스가 가져갈 수 있도록 합니다.


# Docker Hub에 로그인
docker login

# 이미지 푸시
docker push [YOUR_DOCKERHUB_ID]/my-app:1.0.0

이제 우리는 언제 어디서든 동일하게 동작하는, 잘 최적화된 애플리케이션 컨테이너를 갖게 되었습니다. 다음 장에서는 이 컨테이너를 지휘할 오케스트라, 쿠버네티스를 준비하겠습니다.

4. 2단계: 쿠버네티스 무대 준비와 연주법 익히기

잘 만들어진 도커 컨테이너가 배우라면, 쿠버네티스는 이 배우들이 최고의 연기를 펼칠 수 있도록 모든 것을 관리하는 무대 감독이자 오케스트라 지휘자입니다. 이번 장에서는 로컬 PC에 Minikube를 사용하여 미니 쿠버네티스 클러스터를 구축하고, 우리의 애플리케이션을 어떻게 배포하고 외부에 노출시킬지 정의하는 '연주법'(매니페스트 파일)을 작성하는 방법을 배웁니다. 이것이 바로 '쿠버네티스 실무 예제'의 핵심입니다.

로컬 쿠버네티스 환경: Minikube 설치

실제 운영 환경에서는 AWS의 EKS, Google의 GKE, MS의 AKS와 같은 관리형 쿠버네티스 서비스를 사용하지만, 개발 및 테스트 단계에서는 로컬 PC에 가볍게 클러스터를 구축할 수 있는 도구가 필요합니다. Minikube는 가상 머신이나 도커 컨테이너 내부에 단일 노드로 구성된 쿠버네티스 클러스터를 손쉽게 만들어주는 가장 대중적인 도구입니다.

설치 과정은 운영체제마다 다르므로 Minikube 공식 설치 가이드를 참조하여 진행하는 것을 권장합니다. 설치가 완료되면, 터미널에서 다음 명령으로 클러스터를 시작할 수 있습니다.


# Minikube 클러스터 시작 (드라이버로 docker를 사용하는 것을 추천)
minikube start --driver=docker

# 클러스터 상태 확인
minikube status

# kubectl이 클러스터를 제어할 수 있도록 설정되었는지 확인
kubectl cluster-info

이제 여러분의 PC에는 실제 쿠버네티스와 거의 동일하게 동작하는 작은 클러스터가 준비되었습니다.

쿠버네티스의 기본 빌딩 블록: Pod, Deployment, Service

쿠버네티스에 애플리케이션을 배포하기 전에, 가장 기본적이면서도 핵심적인 세 가지 오브젝트(Object)에 대해 이해해야 합니다.

  • Pod (파드): 쿠버네티스에서 생성하고 관리할 수 있는 가장 작은 배포 단위입니다. 파드는 하나 이상의 컨테이너 그룹으로 구성되며, 이 컨테이너들은 스토리지, 네트워크를 공유합니다. 일반적으로 하나의 파드에는 하나의 주 애플리케이션 컨테이너를 실행합니다. 파드는 언제든 죽거나 새로 생성될 수 있는 일시적인 존재(ephemeral)입니다.
  • Deployment (디플로이먼트): 파드의 생명주기를 관리하는 역할을 합니다. "내 애플리케이션 파드가 항상 3개 떠 있도록 유지해줘"와 같은 원하는 상태를 디플로이먼트에 선언하면, 쿠버네티스는 현재 상태를 지속적으로 감시하며 파드의 개수를 맞춥니다. 파드에 문제가 생겨 종료되면 디플로이먼트는 즉시 새로운 파드를 생성합니다. 애플리케이션을 업데이트할 때도 디플로이먼트를 통해 롤링 업데이트와 같은 전략을 수행할 수 있습니다.
  • Service (서비스): 파드는 일시적이며 IP 주소가 계속 바뀔 수 있기 때문에, 외부 또는 클러스터 내부의 다른 애플리케이션이 파드에 안정적으로 접근할 방법이 필요합니다. 서비스는 여러 개의 동일한 파드 그룹에 대한 고정된 진입점(endpoint)을 제공하고, 이들 파드에 대한 로드 밸런싱을 수행합니다. 마치 식당의 매니저처럼, 손님(요청)이 오면 일하고 있는 요리사(파드) 중 한 명에게 일을 분배해주는 역할을 합니다.

쿠버네티스 매니페스트(Manifest) 작성

쿠버네티스는 모든 것을 YAML 형식의 파일로 선언적으로 관리합니다. 우리는 "어떤 상태를 원한다"를 YAML 파일에 정의하여 쿠버네티스에게 전달하면, 쿠버네티스가 알아서 그 상태를 만들어줍니다. 이 YAML 파일들을 매니페스트라고 부릅니다.

프로젝트 루트에 `k8s`라는 폴더를 만들고, 그 안에 `deployment.yaml`과 `service.yaml` 파일을 작성하겠습니다.

1. deployment.yaml

이 파일은 쿠버네티스에게 'my-app' 애플리케이션을 어떻게 실행할지 알려주는 명세서입니다.


# API 버전과 오브젝트 종류를 명시
apiVersion: apps/v1
kind: Deployment
metadata:
  # 디플로이먼트의 고유한 이름
  name: my-app-deployment
spec:
  # 유지하고자 하는 파드의 개수 (복제본 수)
  replicas: 2
  # 이 디플로이먼트가 관리할 파드를 선택하는 규칙
  selector:
    matchLabels:
      app: my-app
  # 파드를 생성하기 위한 템플릿 (설계도)
  template:
    metadata:
      # 파드에 부착될 라벨. selector의 matchLabels와 일치해야 함
      labels:
        app: my-app
    spec:
      # 파드 내에서 실행될 컨테이너 목록
      containers:
      - name: my-app-container
        # 실행할 도커 이미지 (이전에 Docker Hub에 푸시한 이미지)
        image: [YOUR_DOCKERHUB_ID]/my-app:1.0.0
        # 컨테이너가 사용할 포트
        ports:
        - containerPort: 8080
주요 필드 설명:
  • apiVersion: 이 오브젝트를 생성하기 위해 사용하는 쿠버네티스 API 버전을 지정합니다.
  • kind: 생성하려는 오브젝트의 종류(Deployment, Service, Pod 등)를 지정합니다.
  • metadata.name: 이 디플로이먼트의 이름을 지정합니다. 클러스터 내에서 고유해야 합니다.
  • spec.replicas: 동일한 파드를 몇 개 실행할 것인지를 지정합니다. 2로 설정하면 쿠버네티스는 항상 `my-app` 파드 2개를 유지하려고 노력합니다.
  • spec.selector.matchLabels: 디플로이먼트가 어떤 파드를 자신의 관리 대상으로 삼을지 결정하는 라벨 선택기입니다. app: my-app 라벨이 붙은 파드를 관리하겠다는 의미입니다.
  • spec.template.metadata.labels: 이 템플릿으로 생성될 파드에 부착될 라벨입니다. 반드시 위 selector와 일치해야 디플로이먼트가 파드를 인식할 수 있습니다.
  • spec.template.spec.containers: 파드에서 실행될 컨테이너에 대한 정보를 정의합니다.
    • name: 컨테이너의 이름입니다.
    • image: 실행할 도커 이미지의 주소입니다. CI/CD 파이프라인에서는 이 부분을 새로운 이미지 버전으로 계속 업데이트하게 됩니다.
    • ports.containerPort: 애플리케이션이 컨테이너 내부에서 리스닝하는 포트 번호입니다.

2. service.yaml

이 파일은 방금 생성한 디플로이먼트의 파드들을 외부 세계에 노출시키는 방법을 정의합니다.


# API 버전과 오브젝트 종류를 명시
apiVersion: v1
kind: Service
metadata:
  # 서비스의 고유한 이름
  name: my-app-service
spec:
  # 이 서비스가 어떤 파드들로 요청을 보낼지 선택하는 규칙
  selector:
    app: my-app
  # 서비스의 타입을 지정
  # NodePort: 각 노드의 특정 포트를 통해 외부에서 서비스에 접근할 수 있게 함
  type: NodePort
  # 포트 매핑 정보
  ports:
    # 서비스 자체가 노출할 포트
  - port: 80
    # 실제 요청이 전달될 컨테이너의 포트 (deployment의 containerPort와 일치)
    targetPort: 8080
    # NodePort 타입일 경우, 각 노드에 노출될 포트 (30000-32767 사이)
    # 지정하지 않으면 쿠버네티스가 임의의 포트를 할당
    nodePort: 30007
서비스 타입(Service Type)에 대한 추가 설명:
  • ClusterIP (기본값): 클러스터 내부에서만 접근 가능한 IP를 할당합니다. 다른 파드들이 이 서비스에 접근할 때 사용합니다.
  • NodePort: 각 노드의 IP와 고정된 포트(NodePort)를 통해 서비스를 외부에 노출합니다. 주로 테스트나 개발 목적으로 사용됩니다.
  • LoadBalancer: 클라우드 제공업체(AWS, GCP, Azure 등)의 외부 로드 밸런서를 프로비저닝하여 서비스를 외부에 노출합니다. 실제 운영 환경에서 가장 일반적으로 사용되는 방식입니다.
  • ExternalName: 서비스를 외부 서비스의 DNS 이름에 매핑합니다.
Minikube 환경에서는 NodePortLoadBalancer 타입을 사용하여 쉽게 외부 접근을 테스트할 수 있습니다.

클러스터에 애플리케이션 배포 및 확인

이제 작성한 매니페스트 파일들을 `kubectl` 명령어를 사용하여 클러스터에 적용(apply)할 차례입니다.


# k8s 폴더에 있는 모든 yaml 파일을 클러스터에 적용
kubectl apply -f k8s/

# 디플로이먼트가 잘 생성되었는지 확인
kubectl get deployments

# 파드들이 생성되고 Running 상태인지 확인 (2개가 보여야 함)
kubectl get pods

# 서비스가 생성되었는지 확인
kubectl get services

모든 것이 정상적으로 실행되었다면, Minikube 환경에서 서비스에 접근해 봅시다.


# minikube가 서비스에 접근할 수 있는 URL을 알려줌
minikube service my-app-service

이 명령을 실행하면 자동으로 브라우저가 열리면서 "Hello, Kubernetes!" 메시지가 나타날 것입니다. 또는 터미널에서 `curl`을 사용하여 `(minikube ip):30007`로 직접 요청을 보내 테스트할 수도 있습니다.

축하합니다! 이제 우리는 도커 컨테이너를 쿠버네티스 클러스터 위에서 성공적으로 실행하고 외부로 노출시키는 데 성공했습니다. 하지만 지금까지의 모든 과정은 수동으로 이루어졌습니다. 진정한 DevOps의 힘은 이 모든 과정을 자동화하는 것에서 나옵니다. 다음 장에서는 GitHub Actions와 Jenkins를 사용하여 이 모든 것을 자동화하는 CI/CD 파이프라인을 구축해 보겠습니다.

5. 3단계: CI/CD 자동화 - GitHub Actions와 Jenkins 실전 연동

이제 DevOps 파이프라인의 심장부, 즉 CI/CD 자동화 부분을 구축할 차례입니다. 개발자가 코드를 푸시하는 것만으로 빌드, 테스트, 패키징, 배포의 모든 과정이 자동으로 이루어지게 만드는 과정입니다. 현대 CI/CD 환경에서 가장 많이 사용되는 두 가지 도구, GitHub ActionsJenkins를 사용하여 각각 쿠버네티스 배포를 자동화하는 방법을 모두 다루어보겠습니다. 이를 통해 각 도구의 특징과 장단점을 명확히 이해하고, 여러분의 프로젝트에 더 적합한 도구를 선택할 수 있는 안목을 기를 수 있을 것입니다.

시나리오 A: GitHub Actions으로 CI/CD 자동화하기

GitHub Actions는 GitHub 저장소에 내장된 워크플로우 자동화 도구입니다. 별도의 CI 서버를 구축할 필요 없이 YAML 파일 하나로 모든 CI/CD 로직을 정의할 수 있어, 특히 공개 프로젝트나 빠르게 시작하고 싶은 팀에게 매우 인기가 높습니다. 우리의 목표는 `main` 브랜치에 코드가 푸시될 때마다 다음 작업을 자동으로 수행하는 워크플로우를 만드는 것입니다.

  1. 소스 코드 체크아웃
  2. Docker Hub 로그인
  3. 도커 이미지 빌드 및 푸시
  4. 쿠버네티스 클러스터에 접속 (`kubectl` 설정)
  5. 새로운 이미지로 디플로이먼트 업데이트

1. GitHub 저장소에 시크릿(Secrets) 등록

워크플로우 파일에 Docker Hub 계정 정보나 쿠버네티스 접속 정보 같은 민감한 데이터를 직접 하드코딩하는 것은 매우 위험합니다. GitHub는 이를 안전하게 관리하기 위해 암호화된 시크릿 저장소를 제공합니다.

GitHub 저장소의 [Settings] > [Secrets and variables] > [Actions] 메뉴로 이동하여 다음 4개의 시크릿을 등록합니다.

  • DOCKERHUB_USERNAME: 여러분의 Docker Hub 사용자 이름
  • DOCKERHUB_TOKEN: Docker Hub의 Access Token. (비밀번호 대신 토큰 사용을 권장합니다)
  • KUBE_CONFIG_DATA: 쿠버네티스 클러스터 접속을 위한 `kubeconfig` 파일의 내용입니다. 로컬 Minikube의 경우, `cat ~/.kube/config | base64` 명령으로 생성된 base64 인코딩된 문자열을 값으로 넣습니다. (실제 클라우드 환경에서는 서비스 계정을 사용하는 것이 더 안전합니다)
  • KUBE_SERVER: `kubeconfig` 파일 내용 중 `server:` 필드에 해당하는 클러스터의 API 서버 주소입니다.

2. 워크플로우 YAML 파일 작성

프로젝트 루트에 `.github/workflows/` 디렉토리를 만들고, 그 안에 `ci-cd.yaml` 파일을 작성합니다.


# 워크플로우의 이름
name: CI/CD Pipeline to Kubernetes

# 워크플로우가 트리거될 이벤트 정의
on:
  push:
    branches: [ "main" ] # main 브랜치에 푸시될 때 실행

# 실행될 작업(job)들 정의
jobs:
  build-and-deploy:
    # 작업이 실행될 가상 환경
    runs-on: ubuntu-latest

    # 작업의 각 단계(step)들
    steps:
    # 1. 소스 코드 체크아웃
    - name: Checkout source code
      uses: actions/checkout@v3

    # 2. Docker Hub에 로그인
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    # 3. 도커 이미지 빌드 및 푸시
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        # 이미지 태그: latest와 Git 커밋 해시(짧은 버전) 두 가지로 지정
        tags: |
          ${{ secrets.DOCKERHUB_USERNAME }}/my-app:latest
          ${{ secrets.DOCKERHUB_USERNAME }}/my-app:${{ github.sha }}
        # 빌드 캐시를 활용하여 속도 개선
        cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/my-app:buildcache
        cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/my-app:buildcache,mode=max
    
    # 4. 쿠버네티스 도구(kubectl) 설치
    - name: Set up kubectl
      uses: azure/setup-kubectl@v3
      with:
        version: 'v1.28.2' # 원하는 kubectl 버전 지정

    # 5. 쿠버네티스 클러스터에 배포
    - name: Deploy to Kubernetes
      run: |
        # GitHub Secret으로부터 kubeconfig 파일 생성
        mkdir -p $HOME/.kube
        echo "${{ secrets.KUBE_CONFIG_DATA }}" | base64 --decode > $HOME/.kube/config
        
        # kubectl을 사용하여 디플로이먼트의 컨테이너 이미지를 새로 빌드한 이미지로 교체
        # --record 옵션으로 변경 이력을 기록
        kubectl set image deployment/my-app-deployment my-app-container=${{ secrets.DOCKERHUB_USERNAME }}/my-app:${{ github.sha }} --record
        
        # 롤아웃 상태 확인
        kubectl rollout status deployment/my-app-deployment

3. 실행 및 확인

이제 `app.js` 파일의 응답 메시지를 약간 수정하고, 변경 사항을 커밋한 후 `main` 브랜치에 푸시해 보세요.


// app.js
// ...
app.get('/', (req, res) => {
  res.send('Hello from GitHub Actions! The pipeline is working!');
});
// ...

GitHub 저장소의 [Actions] 탭으로 이동하면, 방금 푸시한 커밋에 대해 워크플로우가 실행되는 것을 실시간으로 확인할 수 있습니다. 모든 단계가 성공적으로 완료되면, 다시 `minikube service my-app-service` 명령을 실행하거나 브라우저를 새로고침하여 애플리케이션의 메시지가 변경되었는지 확인해 보세요. 수동 개입 없이 코드 변경만으로 배포가 완료되는 마법을 경험할 수 있습니다.

시나리오 B: Jenkins와 쿠버네티스 연동

Jenkins는 오랫동안 CI/CD 시장을 지배해 온 강력하고 유연한 자동화 서버입니다. 수천 개에 달하는 플러그인을 통해 거의 모든 도구와 연동할 수 있으며, 복잡하고 파이프라인을 자유롭게 구성할 수 있다는 장점이 있습니다. 기업 환경이나 보안이 중요한 환경에서는 여전히 Jenkins가 선호되는 경우가 많습니다.

이번에는 Jenkins를 사용하여 동일한 파이프라인을 구축해 보겠습니다. 로컬 환경에 도커를 사용하여 Jenkins를 실행하는 것을 가정합니다.


# 도커로 Jenkins 실행
docker run -d -p 8080:8080 -p 50000:50000 --name jenkins -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk11

1. Jenkins 설정 및 플러그인 설치

브라우저에서 `http://localhost:8080`으로 접속하여 Jenkins 초기 설정을 진행합니다. 초기 비밀번호는 `docker logs jenkins` 명령으로 확인할 수 있습니다. 'Install suggested plugins'를 선택하고, 관리자 계정을 생성합니다.

설정이 완료되면 [Jenkins 관리] > [플러그인 관리] > [Available] 탭에서 다음 플러그인들을 설치합니다.

  • Docker Pipeline
  • Kubernetes CLI
  • Git

2. Jenkins에 Credentials 등록

[Jenkins 관리] > [Manage Credentials] > [System] > [Global credentials]로 이동하여 GitHub Actions에서와 마찬가지로 Docker Hub와 Kubernetes 접속 정보를 등록합니다.

  • Docker Hub: 'Username with password' 타입으로 `ID`는 `DOCKERHUB_CREDENTIALS`, `Username`과 `Password`를 입력합니다.
  • Kubernetes: 'Secret file' 타입으로 `ID`는 `KUBE_CONFIG`, `File`은 로컬의 `~/.kube/config` 파일을 업로드합니다.

3. Jenkinsfile 작성

Jenkins 파이프라인은 `Jenkinsfile`이라는 스크립트 파일로 정의됩니다. 프로젝트 루트에 `Jenkinsfile`을 생성합니다.


// Declarative Pipeline
pipeline {
    // 파이프라인을 실행할 에이전트(실행 환경) 지정.
    // 'any'는 Jenkins 컨트롤러에서 사용 가능한 아무 에이전트나 사용하겠다는 의미.
    agent any

    // 환경 변수 정의
    environment {
        DOCKERHUB_CREDENTIALS_ID = 'DOCKERHUB_CREDENTIALS'
        KUBE_CONFIG_ID = 'KUBE_CONFIG'
        DOCKER_IMAGE_NAME = "your-dockerhub-id/my-app" // 실제 ID로 변경
    }

    // 파이프라인의 각 단계를 정의
    stages {
        // 1단계: 소스 코드 체크아웃
        stage('Checkout') {
            steps {
                // Git 저장소에서 코드를 가져옴
                git branch: 'main', url: 'https://github.com/your-repo/my-app.git' // 실제 저장소 주소로 변경
            }
        }

        // 2단계: 도커 이미지 빌드
        stage('Build Docker Image') {
            steps {
                script {
                    // Git 커밋 해시를 이미지 태그로 사용
                    def imageTag = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
                    // docker.build 명령으로 이미지 빌드
                    docker.build("${DOCKER_IMAGE_NAME}:${imageTag}", '.')
                }
            }
        }

        // 3단계: 도커 이미지 푸시
        stage('Push Docker Image') {
            steps {
                script {
                    def imageTag = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
                    // withRegistry 블록 안에서 Docker Hub에 로그인하고 이미지 푸시
                    docker.withRegistry('https://registry.hub.docker.com', DOCKERHUB_CREDENTIALS_ID) {
                        docker.image("${DOCKER_IMAGE_NAME}:${imageTag}").push()
                        docker.image("${DOCKER_IMAGE_NAME}:${imageTag}").push("latest")
                    }
                }
            }
        }

        // 4단계: 쿠버네티스에 배포
        stage('Deploy to Kubernetes') {
            steps {
                script {
                    def imageTag = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
                    // withKubeConfig 블록으로 Kubeconfig 파일을 안전하게 사용
                    withKubeConfig([credentialsId: KUBE_CONFIG_ID]) {
                        // kubectl 명령 실행
                        sh "kubectl set image deployment/my-app-deployment my-app-container=${DOCKER_IMAGE_NAME}:${imageTag} --record"
                        sh "kubectl rollout status deployment/my-app-deployment"
                    }
                }
            }
        }
    }
}

4. Jenkins 파이프라인 생성 및 실행

  1. Jenkins 대시보드에서 [새로운 Item]을 클릭합니다.
  2. 이름을 입력하고 [Pipeline]을 선택한 후 OK를 누릅니다.
  3. 설정 페이지의 [Pipeline] 섹션에서 `Definition`을 `Pipeline script from SCM`으로 변경합니다.
  4. `SCM`을 `Git`으로 선택하고, GitHub 저장소 URL을 입력합니다.
  5. `Script Path`가 `Jenkinsfile`로 되어 있는지 확인하고 저장합니다.

이제 Jenkins가 주기적으로 Git 저장소를 확인하며 `Jenkinsfile`을 찾아 파이프라인을 실행합니다. (또는 GitHub Webhook을 설정하여 푸시 이벤트 발생 시 즉시 빌드를 트리거할 수 있습니다.) [Build Now]를 클릭하여 파이프라인을 수동으로 실행하고, Blue Ocean과 같은 시각화 도구를 통해 각 단계가 성공적으로 완료되는지 확인합니다.

GitHub Actions vs Jenkins: 무엇을 선택할 것인가?

기준 GitHub Actions Jenkins
설정 및 관리 GitHub에 내장되어 있어 별도의 서버 구축 및 관리가 필요 없음. YAML 파일로 모든 것을 관리하여 진입 장벽이 낮음. 별도의 서버에 직접 설치하고 관리해야 함 (업데이트, 보안, 백업 등). 초기 설정 및 운영에 더 많은 노력이 필요.
통합성 GitHub 생태계(저장소, 이슈, PR 등)와 완벽하게 통합됨. Marketplace에 다양한 Action이 존재하지만 Jenkins만큼은 아님. 수천 개의 플러그인을 통해 거의 모든 도구, 클라우드, 언어와 연동 가능. 극강의 확장성을 자랑함.
유연성 및 복잡성 단순하고 직관적인 워크플로우에 최적화되어 있음. 매우 복잡한 파이프라인을 구성하기에는 제약이 있을 수 있음. 스크립트 파이프라인(Groovy)을 통해 거의 모든 종류의 복잡한 로직과 조건부 실행, 동적 파이프라인 구성이 가능.
비용 공개 저장소는 무료. 비공개 저장소는 계정 플랜에 따라 무료 제공 시간이 있으며, 초과 시 사용한 만큼 비용 지불. 소프트웨어 자체는 무료(오픈소스)지만, Jenkins를 실행할 서버 인프라 비용과 관리 인력에 대한 비용이 발생.
추천 대상 개인 프로젝트, 스타트업, 오픈소스 프로젝트, 빠르고 간단한 CI/CD를 원하는 팀. GitHub 중심의 개발 문화를 가진 팀. 대규모 엔터프라이즈, 복잡한 레거시 시스템과의 연동이 필요한 경우, 자체 인프라에 대한 완전한 통제가 필요한 조직.

결론적으로 정답은 없습니다. 프로젝트의 규모, 팀의 기술 성숙도, 기존 인프라, 보안 요구사항 등을 종합적으로 고려하여 가장 적합한 도구를 선택하는 것이 중요합니다. 이 글을 통해 두 가지 도구를 모두 경험해 보았으니, 이제 여러분의 상황에 맞는 최선의 결정을 내릴 수 있을 것입니다.

6. 파이프라인 너머: 운영과 고도화를 위한 다음 단계

축하합니다! 우리는 이제 코드 커밋부터 쿠버네티스 클러스터 배포까지 이어지는 완전 자동화된 CI/CD 파이프라인을 성공적으로 구축했습니다. 하지만 진정한 DevOps 여정은 배포에서 끝나는 것이 아니라, 운영 단계에서부터 본격적으로 시작됩니다. 안정적인 서비스를 제공하고, 변화에 더 빠르고 안전하게 대응하기 위해 파이프라인을 어떻게 고도화하고, 어떤 점들을 추가로 고려해야 할까요? 이 장에서는 '초보자를 위한 DevOps 로드맵'의 다음 이정표들을 제시합니다.

모니터링과 로깅: 내 애플리케이션은 안녕한가?

쿠버네티스가 파드를 자동으로 재시작해준다고 해서 안심할 수는 없습니다. 왜 파드가 죽었는지, 애플리케이션의 응답 시간은 얼마나 되는지, 현재 CPU와 메모리 사용량은 어떤지 파악하지 못한다면 잠재적인 대형 장애를 막을 수 없습니다.

  • 모니터링: 프로메테우스(Prometheus)는 컨테이너 환경의 모니터링에서 사실상 표준으로 사용되는 도구입니다. 클러스터의 상태, 노드의 리소스, 각 파드의 성능 지표 등 다양한 메트릭을 수집합니다. 그리고 그라파나(Grafana)를 연동하여 수집된 데이터를 시각적으로 아름답고 이해하기 쉬운 대시보드로 만들 수 있습니다. 이를 통해 서비스의 건강 상태를 한눈에 파악하고 이상 징후를 조기에 발견할 수 있습니다.
  • 로깅: 여러 파드에 분산되어 생성되는 로그를 중앙에서 수집하고 검색, 분석할 수 있는 시스템은 필수적입니다. 전통적으로 ELK 스택 (Elasticsearch, Logstash, Kibana)이 많이 사용되었지만, 최근에는 더 가벼운 로그 수집기인 FluentdFluent Bit을 사용하여 EFK 스택을 구성하는 것이 쿠버네티스 환경에서 더 인기가 있습니다. 중앙화된 로깅 시스템을 통해 특정 요청의 처리 과정을 추적하거나, 에러의 원인을 신속하게 분석할 수 있습니다.

CI/CD 파이프라인의 마지막 단계에 모니터링 시스템으로 배포 성공/실패 여부를 알리는 알림(Notification) 단계를 추가하는 것도 좋은 전략입니다. (e.g., Slack 연동)

고급 배포 전략: 장애를 두려워하지 않는 용기

우리가 구현한 롤링 업데이트(Rolling Update)는 기본적인 무중단 배포 방식이지만, 모든 트래픽을 한 번에 새로운 버전으로 전환하기 때문에 잠재적인 위험이 있습니다. 더 정교하고 안전한 배포를 위해 다음과 같은 전략들을 고려해볼 수 있습니다.

배포 전략 설명 장점 단점
블루/그린 (Blue/Green) 현재 운영 버전(Blue)과 동일한 환경에 새로운 버전(Green)을 배포합니다. 테스트가 완료되면 라우터(서비스)의 트래픽을 한 번에 Green으로 전환합니다. 빠른 롤백 가능 (트래픽을 다시 Blue로 돌리면 됨), 전환 전 충분한 테스트 시간 확보. 두 배의 리소스가 필요하여 비용이 많이 듬.
카나리 (Canary) 새로운 버전을 극소수의 사용자(e.g., 1%)에게만 먼저 공개하고, 문제가 없음을 확인하면서 점진적으로 트래픽을 늘려나가는 방식입니다. 탄광의 카나리아처럼 위험을 먼저 감지합니다. 실제 운영 트래픽으로 테스트하여 예측 불가능한 문제를 발견할 수 있음. 장애의 영향을 최소화. 파이프라인 구성이 복잡하고, 버전별 라우팅을 위한 정교한 기술(e.g., Istio, Linkerd 같은 서비스 메시)이 필요할 수 있음.

이러한 고급 배포 전략을 도입하면 배포에 대한 자신감을 높이고, 사용자의 경험을 해치지 않으면서 더 빠르고 잦은 배포를 수행할 수 있게 됩니다.

인프라 관리 자동화: Infrastructure as Code (IaC)

지금까지는 애플리케이션 배포를 자동화했지만, 쿠버네티스 클러스터 자체나 관련 클라우드 리소스(VPC, 데이터베이스 등)는 어떻게 관리할까요? 수동으로 콘솔에서 클릭하여 생성하는 것은 실수를 유발하고 재현이 어렵습니다. Infrastructure as Code (IaC)는 인프라를 코드로 정의하고 버전 관리하며, 자동화된 프로세스를 통해 프로비저닝하는 방식입니다.

  • 테라폼(Terraform): 하시코프(Hashicorp)에서 만든 IaC 도구로, 특정 클라우드에 종속되지 않고 AWS, GCP, Azure 등 다양한 제공업체의 리소스를 코드로 관리할 수 있습니다. 쿠버네티스 클러스터 생성, 네트워크 설정, 데이터베이스 인스턴스 생성 등을 모두 코드로 관리할 수 있습니다.
  • 헬름(Helm): '쿠버네티스를 위한 패키지 매니저'라고 불립니다. 우리가 작성했던 `deployment.yaml`, `service.yaml` 등 여러 매니페스트 파일들을 하나의 '차트(Chart)'라는 단위로 묶어 관리합니다. 이를 통해 애플리케이션 배포를 `helm install my-app ./charts` 와 같은 간단한 명령으로 수행할 수 있고, 버전 관리 및 재사용이 용이해집니다. CI/CD 파이프라인에서 `kubectl apply` 대신 `helm upgrade`를 사용하면 훨씬 더 체계적인 배포 관리가 가능합니다.

결론: 이제 당신도 DevOps 엔지니어입니다

이 긴 여정을 함께해 주셔서 감사합니다. 우리는 단순히 코드를 작성하는 개발자를 넘어, 그 코드가 사용자에게 전달되는 전 과정을 이해하고 자동화하는 진정한 의미의 풀스택 개발자, 혹은 DevOps 엔지니어로서의 첫발을 내디뎠습니다.

우리는 DevOps라는 철학적 배경 위에서, 애플리케이션을 도커로 견고하게 포장하는 방법을 배웠습니다. 불필요한 무게를 줄이는 도커 이미지 최적화를 통해 효율성을 높였고, 쿠버네티스라는 거대한 무대에 우리의 애플리케이션을 올리는 '실무 예제'를 직접 경험했습니다. 그리고 마침내 GitHub ActionsJenkins라는 두 명의 자동화 지휘자를 통해 코드 한 줄의 변화가 실제 서비스에 반영되기까지의 모든 과정을 자동화하는 CI/CD 파이프라인을 완성했습니다.

중요한 것은 특정 도구의 사용법을 암기하는 것이 아닙니다. 왜 이 도구들을 사용해야 하는지, 각 단계가 어떤 문제를 해결하는지, 그리고 이들이 어떻게 유기적으로 연결되어 '더 빠르고 안정적인 가치 전달'이라는 DevOps의 궁극적인 목표에 기여하는지를 이해하는 것입니다.

A Full-stack Developer

오늘 구축한 파이프라인은 여러분의 DevOps 여정의 끝이 아닌, 위대한 시작입니다. 여기에 모니터링, 로깅, 고급 배포 전략, IaC를 접목하며 여러분만의 파이프라인을 더욱 견고하고 정교하게 발전시켜 나가시길 바랍니다. 변화를 두려워하지 않고, 자동화를 통해 반복적인 작업을 컴퓨터에 맡기십시오. 그리고 우리는 더 창의적이고 가치 있는 문제 해결에 집중하면 됩니다. 그것이 바로 우리가 이 길을 걷는 이유입니다.

지금 바로 당신의 첫 파이프라인을 시작해보세요!

Post a Comment