[FINDA] MSA를 위한 Kubernetes 세팅과 CI/CD Pipeline 구성, 그리고 Monitoring 시스템 구축 — 2

ShinChul Bang
finda 기술 블로그
22 min readFeb 24, 2020

목차

MSA란?

  • MSA의 정의와 목적, 그리고 핀다가 MSA를 채택한 이유

쿠버네티스(Kubernetes)

  • 쿠버네티스를 도입한 목적 & 환경 구성 방법(AWS EKS)

DevOps를 위한 환경 구성, CI/CD 파이프라인 구축

  • Jenkins (Continuous Integration)
  • Jenkins란?
  • Jenkins의 목적
  • Jenkins 환경 구성 방법 & 사용법
  • Argo CD (Continuous Deploy)
  • Argo CD란?
  • GitOps & Helm
  • Argo CD의 목적
  • Argo CD 환경 구성 방법 & 사용법

모니터링 환경 구축

  • 어플리케이션 모니터링 : Fluentd + Elasticsearch + Kibana → Slack
  • 인프라 스트럭쳐 모니터링 : Cloudwatch + Lambda → Slack
  • 배포 모니터링 : Jenkins & Argo CD Notification → Slack

지난 포스트에서 핀다가 MSA와 쿠버네티스를 도입한 이유에 대해서 포스트 했었습니다.

이번 포스트에서는 핀다에서의 DevOps 환경을 어떻게 구축했는지 작성해보겠습니다.

지난 포스트에서 말씀드렸던 바와 같이 우리 핀다에서는 마이크로 서비스 아키텍쳐를 도입함에 따라 쿠버네티스 환경을 구축했고, 이에 대해서 DevOps 환경의 필요성을 느꼈습니다.

DevOps

DevOps란?

그럼 DevOps란 무엇일까요?

DevOps란 간단하게 말씀드리면 개발(Dev)과 운영(Ops)을 따로 구분짓지않고 함께하는 것을 의미합니다.

기존에는 개발자들과 운영자들이 따로 존재했습니다. 개발자들이 어플리케이션을 열심히 개발하고 나서 빌드된 파일을 운영자들에게 전달하면 운영자들이 그것을 서버에 배포했습니다. 따라서 배포 주기가 꽤 길었습니다.

역할도 명확하게 분리되었습니다. 개발자는 어플리케이션의 개발을 담당하고 운영자는 인프라의 모니터링과 인프라 장애 대응 및 배포를 담당했습니다.

하지만 오늘날에는 운영을 위한 인프라를 클라우드 서비스로 제공해주는 업체들(AWS, Azure, Google Cloud 등)이 많이 생겨났고 이런 환경에 맞춰져 개발자들과 운영자들이 함께 좀 더 밀접하게 협업할 수 있는 환경이 마련되었습니다.

따라서 개발에서부터 테스트, 배포, 운영 및 모니터링까지 빠르게 진행할 수 있게 되었습니다. (참고로 개발에서부터 테스트, 배포, 운영 까지를 어플리케이션 수명주기라고 합니다.)

DevOps는 아래와 같은 장점을 가지고 있습니다.

  • 빠른 배포 속도를 통해 장애 대응과 시장 변화에 빠르게 대처할 수 있다.
  • 지속적 통합(Continuous Integration), 지속적 전달(Continuous Delivery), 모니터링 및 로깅을 통해 안정적인 서비스 품질을 보장할 수 있다.
  • 개발자들과 운영자들이 더욱 더 긴밀하게 협업할 수 있기 때문에 비효율을 줄일 수 있고 시간 절약이 가능하다.

그렇다면 핀다에서는 DevOps 환경을 구성하기 위해 어떤 일을 했을까요?

먼저 어플리케이션의 지속적 통합(Continuous Integration)을 위한 툴이 필요했습니다.

**지속적 통합(Continuous Integration)**은 자동화된 빌드 및 테스트가 수행된 후, 개발자가 코드 변경 사항을 중앙 리포지토리에 정기적으로 병합하는 데브옵스 소프트웨어 개발 방식입니다. 지속적 통합은 모든 개발을 완료한 뒤에 소프트웨어의 질적 향상과 소프트웨어를 배포하는 데 걸리는 시간과 비용을 줄이는데 초점이 맞추어져 있습니다.

핀다에서는 지속적 통합을 위해 Jenkins라는 CI 툴을 도입했습니다.

Jenkins(Continuous Integration Tool)

Jenkins는 무엇일까요?

Jenkins란, 소프트웨어 개발 시 지속적 통합 서비스를 제공하는 오픈소스 툴입니다.

Jenkins는 거의 모든 언어의 조합과 소스코드 리포지토리에 대하여 빌드, 테스트, 배포까지의 파이프라인을 제공해줍니다. 보통 원격 리포지토리에 Jenkins의 Webhook을 걸어서 특정 브랜치가 Push 될 때 Jenkins가 그 원격리포지토리의 소스코드를 바탕으로 미리 정의해놓은 빌드, 테스트, 배포의 절차를 실행하는 방식으로 동작합니다.

그렇다면 Jenkins는 어떻게 구성하는 것일까요?

간단하게 함께 구성 해봅시다.

일단 Jenkins 프로세스가 돌아갈 서버 환경이 필요합니다. 핀다에서는 AWS 클라우드 서비스를 사용하기 때문에 AWS에서 제공해주는 서버 인스턴스인 EC2를 사용했습니다. 온프레미스 환경에서도 외부 네트워크로 통신이 가능하다면 테스트에 문제는 없습니다.

간단하게 테스트하기 위해 아래와 같은 사양의 서버 인스턴스를 사용하겠습니다.

  • OS : Ubuntu 18.04 LTS
  • Core : 2 CPU
  • Memory : 2 GiB RAM

서버 인스턴스를 생성했다면 JDK를 설치해줍니다.

Jenkins는 JVM에서 돌아가는 어플리케이션이기 때문에 JRE가 꼭 필요합니다.

하지만 JDK를 설치하는 이유는 만약 Jenkins가 빌드해야 할 원격 리포지토리의 코드가 Java 언어로 되어있을 경우를 대비해서 입니다.(핀다에서는 대부분 Spring boot를 이용하여 개발합니다.) 만약 빌드할 어플리케이션이 Java로 되어있지 않다면 굳이 JDK를 설치할 필요는 없습니다. JRE만으로 충분합니다.

JDK를 설치할 경우 : Oracle에서 JDK를 로컬 PC에 설치한 후 scp 명령어 혹은 filezilla를 통해 /opt 디렉터리에 업로드하여 설치합니다. 예시에서는 jdk-8u221-linux-x64.tar.gz 파일을 예제로 진행했습니다.

# 압축 해제
$ sudo tar -zxvf /opt/jdk-8u221-linux-x64.tar.gz
# 모든 유저가 java 커맨드를 사용할 수 있도록 symlink 생성
$ sudo ln -s /opt/jdk1.8.0_221/bin/java /usr/bin/java
$ which java
/usr/bin/java
# java version 확인
$ java -version
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
# 모든 유저가 javac 커맨드를 사용할 수 있도록 symlink 생성
$ sudo ln -s /opt/jdk1.8.0_221/bin/javac /usr/bin/javac
$ which javac
/usr/bin/javac
# javac version 확인
$ javac -version
javac 1.8.0_221
# jdk 압축 파일 삭제
$ sudo rm -rf /opt/jdk-8u221-linux-x64.tar.gz

JRE를 설치할 경우 : 아래 커맨드 예제를 따릅니다.

# default로 설정된 jre 설치
$ sudo apt-get install default-jre
# 특정 버젼의 jre 설치. 1.8 버젼 예시
$ sudo apt-get install openjdk-8-jre

다음은 빌드 환경을 위한 설치입니다.

이 부분은 개인마다 혹은 회사의 환경마다 모두 다를 것이기 때문에 간략하게 설명만 드리자면 Node.js를 빌드하려면 npm 을 설치하시면 되고 위에서 설명드린바와 같이 Java 프로젝트를 빌드하려면 JDK를 설치합니다. 각 빌드하시려는 언어마다 알맞게 빌드를 위한 환경을 구성하시면 됩니다.

다음은 Jenkins 설치입니다.

$ wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -$ sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'$ sudo apt update$ sudo apt install jenkins$ sudo systemctl start jenkins # jenkins 프로세스 시작
$ sudo systemctl status jenkins # jenkins 프로세스 확인

다음은 Git 설치 및 Git 환경 구성입니다.

원격 리포지토리에서 소스코드를 가져오기 위해 구성합니다.

# git 설치
$ sudo apt install git -y
# jenkins 계정으로 접속
$ sudo su - jenkins
# git 로그인 정보 설정
$ vi .netrc
# 아래 내용 입력 후 저장 :wq
machine bitbucket.org # 혹은 깃헙을 쓴다면 github.com
login <로그인할 git 계정>
password <로그인할 git 계정 비밀번호>

여기까지 모든 작업을 완료했다면 이제 Jenkins 대시보드에 접속해 봅시다.

Jenkins가 설치되어있는 서버의 Public IP:8080 으로 접속합니다.

만약 서버의 Public IP가 12.14.15.16 이라면 브라우저를 열어 http://12.14.15.16:8080 으로 접속합니다.

브라우저를 통해 Jenkins 서버의 8080포트로 접속하면 초기에 볼 수 있는 화면

초기에 대시보드로 접속하면 초기 비밀번호를 입력해달라고 합니다.

아래 커맨드를 따라서 서버 인스턴스에서 초기 비밀번호를 확인합니다.

# 초기 비밀번호 조회
$ sudo cat /var/lib/jenkins/secrets/initialAdminPassword
412bc63e05c1425c8f7a1e25bd6fc397 # <-- 이렇게 나오는 것이 초기 비밀번호 입니다.

초기 비밀번호를 확인한 후 아래 화면과 같이 초기 비밀번호를 입력한 후 Continue 버튼을 클릭합니다.

다음은 Jenkins 플러그인에 관한 설정입니다.

왼쪽 버튼은 추천되는 플러그인들을 모두 설치하는 것이고, 오른쪽 버튼은 설치하고 싶은 플로그인만 선별해서 설치할 수 있습니다.

플러그인 설정 및 설치가 모두 끝난 후에는 아래의 화면을 확인할 수 있습니다.

이 페이지에서 최초의 admin 계정을 생성합니다.

아래와 같이 admin 정보를 입력하고 진행합니다.

그 다음으로는 jenkins를 접속할 URL을 지정하는 페이지 입니다.

http://<Jenkins 서버의 public-ip>:8080으로 입력하고 진행하시면 됩니다.

자, Jenkins에 관련된 모든 설치 및 구성이 끝났습니다!

이제 Jenkins 서버 인스턴스의 public ip:8080 으로 접속하면 아래와 같은 화면을 통해 Jenkins 대시보드에 접속할 수 있습니다.

이렇게 지속적 통합(Continuous Integration)에 대한 툴을 세팅하였습니다.

핀다에서는 Jenkins를 통해서 새로운 코드를 테스트하고, 빌드한 후 빌드 파일을 Docker image로 만들어 쿠버네티스 시스템에서 새로운 코드가 돌아갈 수 있도록 파이프라인을 구성했습니다.

하지만 Jenkins에서의 역할은 오로지 빌드까지만 이였습니다. 그럼 새로운 코드의 배포는 어디서 담당할까요?

기본적으로 쿠버네티스 환경은 일반적인 서버의 환경과 달라서 외부에서 새로운 코드를 배포시키기 힘든 구조였습니다.

따라서 핀다에서는 Argo CD라는 쿠버네티스 환경에 최적화된 CD(Continuous Delivery) 툴을 도입하게 되었습니다.

Argo CD(Continuous Delivery Tool)

Argo CD는 정확히 무엇일까요?

Argo CD는 쿠버네티스를 위한 GitOps 지속적 전달 툴 입니다.

지속적 전달은 무엇일까요? 이름 그대로 지속적 전달이란, 지속적 통합을 통해 테스트되고 빌드된 코드를 지속적으로 전달하여 제품의 질적 향상을 꾀하는 것 입니다.

그럼 GitOps는 무엇일까요?

GitOps란, 쿠버네티스 특성에 맞춰 탄생된 개념입니다.

쿠버네티스는 특성상 새로운 어플리케이션의 배포나 네트워크 구성 등 쿠버네티스를 운영하기 위한 다양한 오퍼레이션들을 yaml 파일으로 작성하여 관리하고 실행합니다. 이러한 yaml 파일들을 쿠버네티스에서는 manifest 라고 합니다. 따라서 쿠버네티스 운영을 위한 manifest 파일들을 하나의 원격 리포지토리에서 관리한다는 개념입니다. 즉, 이름 그대로 Git + Ops(운영) 즉, 운영적인 부분을 Git으로 관리하겠다는 뜻입니다.

그럼 왜 GitOps가 필요한 것일까요?

일반적으로 쿠버네티스 에서는 한 종류의 어플리케이션을 배포할 때 작성해야할 manifest 파일이 기본적으로 2개 입니다.

하나는 배포를 담당하는 Deployment, 두번째는 어플리케이션의 네트워크를 담당하는 Service 입니다.

만약 여기에서 더 나아가서 어플리케이션에 대한 스토리지 설정이 필요하다면 Persistent Volume와 같은 추가적인 manifest가 필요합니다. 그리고 외부 네트워킹을 위해서는 Ingress라는 manifest도 필요합니다.

이렇듯 쿠버네티스를 통해 배포하고 운영해야 할 어플리케이션이 늘어나면 늘어날수록, 그리고 섬세하게 기능을 설계 해야 하면 할수록 작성하고 관리해야 할 manifest 들 또한 점차 늘어나게 됩니다.

따라서 이러한 수많은 manifest들을 좀 더 편하게 관리하기 위해 Git으로 관리하는 것입니다.

그럼 여기서 또 한가지 궁금증이 생깁니다. manifest 파일이 많아 진다면 줄일 수는 없을까요?

생각해보면 방법은 있습니다. 만약에 manifest 의 내용이 겹치는 부분이 발생한다면 이를 이용해서 파일을 줄일 수도 있지 않을까요?

아래는 2개의 어플리케이션에 대한 Deployment manifest의 예시 입니다.

kind: Deployment
apiVersion: apps/v1
metadata:
name: hello-app-deployment
spec:
replicas: 1
selector:
matchLabels:
app: hello-app
template:
metadata:
labels:
app: hello-app
spec:
containers:
- name: hello-app
image: python:2.7
imagePullPolicy: IfNotPresent
command: ["/bin/bash"]
args: ["-c", "echo \\"<p>Hello from $(hostname)</p>\\" > index.html; python -m SimpleHTTPServer 8080"]
ports:
- name: http
containerPort: 8080
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: world-app-deployment
spec:
replicas: 1
selector:
matchLabels:
app: world-app
template:
metadata:
labels:
app: world-app
spec:
containers:
- name: world-app
image: python:2.7
imagePullPolicy: IfNotPresent
command: ["/bin/bash"]
args: ["-c", "echo \\"<p>World from $(hostname)</p>\\" > index.html; python -m SimpleHTTPServer 8080"]
ports:
- name: http
containerPort: 8080

하나는 Hello from ${hostname}을 찍어주는 어플리케이션이고,

또 다른 하나는 World from ${hostname}을 찍어주는 어플리케이션 입니다.

두 개의 Deployment manifest를 보고 있으니 중첩되는 부분이 너무 많죠?

이 manifest 내용 중 중첩되는 부분들만 추려서 아래와 같이 수정 해보겠습니다.

kind: Deployment
apiVersion: apps/v1
metadata:
name: {{ app-name }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ app-label }}
template:
metadata:
labels:
app: {{ app-label }}
spec:
containers:
- name: {{ app-label }}
image: python:2.7
imagePullPolicy: IfNotPresent
env: # 환경 변수 추가
- name: say
value: {{ say }}
command: ["/bin/bash"]
args: ["-c", "echo \\"<p>$(say) from $(hostname)</p>\\" > index.html; python -m SimpleHTTPServer 8080"]
ports:
- name: http
containerPort: 8080
---
# hello-app의 value를 아래와 같이 설정
app-name: hello-app-deployment
app-label: hello-app
say: Hello
---
# world-app의 value를 아래와 같이 설정
app-name: world-app-deployment
app-label: world-app
say: World

위처럼 Deployment manifest를 탬플릿 처럼 만들어서 각 {{ }} 안에 들어갈 값만 바인딩이 될 수 있다면 여러 개의 어플리케이션들 마다 개별적인 manifest를 만들지 않고도 manifest를 재활용 할 수 있지 않을까요?

이런 작업을 해주는 것이 바로 Helm 이라는 녀석입니다.

Helm은 무엇일까요?

Helm은 쿠버네티스의 패키지 매니저 입니다. 따라서 Helm은 쿠버네티스에서 패키징, 구성 및 설정, 배포에 관련된 작업들을 보다 수월하게 할 수 있도록 도와줍니다.

그러기 위해서 Helm은 Chart라는 개념을 사용하는데요, Chart란 사전에 구성된 쿠버네티스 manifest들을 말합니다. 위에서 제가 예시로 작성한 탬플릿같은 manifest가 Chart에 속합니다.

좀 더 제대로 알아볼까요?

위에서 예시로 작성한 manifest를 아래와 같은 디렉터리 구조로 작업해봅시다.

─── example-app
├── /templates
│ └── deployment.yaml
├── Chart.yaml
└── values.yaml
  • 먼저 example-app 이라는 디렉터리를 만듭니다.
  • example-app 디렉터리 내부에 templates라는 디렉터리를 만듭니다.
  • example-app/templates 디렉터리 내부에 deployment.yaml 이라는 Deployment manifest 파일을 만들고 아래의 코드를 작성합니다.
kind: Deployment 
apiVersion: apps/v1
metadata:
name: {{ .Values.app-name }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Values.app-label }}
template:
metadata:
labels:
app: {{ .Values.app-label }}
spec:
containers:
- name: {{ .Values.app-label }}
image: python:2.7
imagePullPolicy: IfNotPresent
env: # 환경 변수 추가
- name: say
value: {{ .Values.say }}
command: ["/bin/bash"]
args: ["-c", "echo \"<p>$(say) from $(hostname)</p>\" > index.html; python -m SimpleHTTPServer 8080"]
ports:
- name: http
containerPort: 8080
  • 다시 example-app 디렉터리로 들어가서 Chart.yaml 이라는 파일을 만들고 아래와 같이 코드를 작성합니다.
apiVersion: v1
appVersion: "1.0"
description: This is Example App Helm chart for Kubernetes
name: example-app
version: 0.1.0
  • 동일하게 example-app 디렉터리 내에 values.yaml 이라는 파일을 만들고 아래와 같이 코드를 작성합니다.
app-name: hello-app-deployment 
app-label: hello-app
say: Hello

자, 이렇게 구성하면 hello-app 에 대한 Chart가 만들어졌다고 보시면 됩니다.

Helm을 통해서 이 내용이 쿠버네티스에 배포되면 values.yaml에 정의한 값들이 deployment.yaml의 {{ }} 안에 알맞은 변수에 바인딩 되어서 최종적으로 쿠버네티스에 적용될 manifest가 만들어집니다.

만약 world-app을 배포하고 싶다면 어떻게 하면 될까요?

새로운 manifest를 만들 필요 없이 values.yaml의 내용만 아래 내용으로 변경하고 동일하게 배포하면 됩니다.

app-name: world-app-deployment
app-label: world-app
say: World

정리하자면 Helm이란 간단하게 설명드리면 manifest에 특정 값을 바인딩해서 새로운 manifest를 만들 수 있는 기능을 제공해주는 오픈소스 라이브러리 라고 보시면 됩니다.

이렇게 우리는 지속적 전달(Continuous Delivery)과 GitOps, 그리고 Helm에 대해 아주 간단하게 알아봤습니다.

자, Argo CD를 설명드리기 위해 너무 먼길을 돌아왔습니다..;;

그럼 다시 본론으로 돌아가 볼까요?

그렇다면 Argo CD는 GitOps 지속적 전달 툴이라고 했는데, 어떤식으로 동작하는 것일까요?

우선 Argo CD는 쿠버네티스 운영에 관련된 manifest 파일들을 관리하고 있는 원격 리포지토리를 조회합니다.(GitOps)

그래서 이 원격 리포지토리에 존재하는 manifest 파일의 내용과 실제 쿠버네티스에 적용되어 있는 manifest 파일의 내용들을 비교(Diff) 하여 어떤 부분이 변경 및 업데이트 되었는지 확인하고 내용이 변경 되었다면 그 내용을 쿠버네티스에 반영(Sync)하는 작업을 합니다.

그리고 추가적으로 새로운 manifest 반영 혹은 배포 시에 manifest 의 내용 과는 다른 값을 새로 넣어서 적용할 수 있습니다.(Helm 사용)

이러한 기능을 저희 핀다에서는 Dev, Stage, Production 스텍을 구분지어서 배포하는 기능에 사용하고 있습니다.

여기서 문제입니다. Argo CD는 Jenkins와는 달리, 따로 서버를 구성하지 않습니다. 왜 일까요?

이유는 Argo CD는 쿠버네티스에 배포 되어있는 yaml 파일의 내용을 알고 있어야 하기 때문입니다.

따라서 Argo CD는 쿠버네티스로 배포되고 관리됩니다.

Argo CD를 배포하고 구성하는 방법은 Argo CD 공식 문서에 잘 나와 있으니 참고해보시면 될 것 같습니다.

정리하자면 핀다에서는 Argo CD를 쿠버네티스에 배포하여 CD 툴로써 활용하고 있습니다.

그리고 GitOps 리포지토리를 구성하여 해당 리포지토리에서 쿠버네티스에 배포 및 운영을 하기 위한 모든 manifest들을 관리하고 있습니다.

따라서 Jenkins가 가장 우선적으로 새로운 코드를 테스트하고 빌드한 후 Docker image를 만들어내고 이를 AWS ECR(Elastic Container Repository)에 저장한 후 이에 대한 새로운 Docker image 버젼 태그 내용을 GitOps 리포지토리의 내용에 업데이트 해줍니다.

그리고 Argo CD에서는 새로 업데이트된 GitOps 리포지토리의 내용을 감지해서 현재 쿠버네티스에 배포된 manifest 내용과 새로 업데이트된 GitOps 리포지토리의 manifest 내용을 비교(Diff)하여 업데이트 된 내역을 확인하면 운영자의 조작에 따라서 혹은 자동으로 업데이트된 내용을 적용(Sync) 하여 쿠버네티스에 배포합니다.

이를 다이어그램으로 나타낸 그림은 아래와 같습니다.

쿠버네티스 환경에서의 CI/CD 파이프라인 매커니즘
쿠버네티스 환경에서의 CI/CD 파이프라인 매커니즘

이렇게 핀다에서는 쿠버네티스 환경 구성과 DevOps를 위한 CI/CD 파이프라인 구성까지 마쳤습니다.

자, 이렇게 MSA를 위한 환경구성이 모두 끝난…줄 알았는데요.. 끝나지 않았습니다..

MSA를 위한 인프라부터 DevOps 환경까지 모두 구성했지만, 서비스에서 장애 발생시 그리고 우리가 새로운 버전의 어플리케이션을 배포했을 때 모니터링을 하고 알림을 받을 수 있는 방법이 필요했습니다.

그래서 우리 핀다에서는 DevOps에 이어서, 모니터링에 대한 욕구(?)가 생기게 되었습니다.

--

--