두개의 인스턴스(=노드)가 있고 각 인스턴스에는 서비스를 구현하기 위한 리소스들이 똑같이 배포되어 있다. 즉, 이중화 구현을 한것이고, 만약 인스턴스가 포함된 AZ OR 서브넷에 장애가 발생해도 로드밸런싱을 통해 다른 AZ OR 서브넷에 있는 인스턴스에 트래픽이 전달되기 때문에 클라이언트는 다운타임없이 서비스를 이용할 수 있다.
쉽게말해, 백엔드 파드와 프론트 파드가 있고, 두개의 인스턴스에 각각 한 세트로 배치되어서 각 인스턴스가 동일한 서비스를 가능하게 해야한다. 하지만 백엔드 파드 두개가 모두 하나의 인스턴스에 스케줄링된다면, 다른쪽 인스턴스는 서비스를 배포할 수 없다.
vi back-deployment.yaml
spec:
. . .
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: tier
operator: In
values:
- backend
topologyKey: "kubernetes.io/hostname"
vi front-deployment.yaml
spec:
. . .
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: tier
operator: In
values:
- frontend
topologyKey: "kubernetes.io/hostname"
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: tier
operator: In
values:
- backend
topologyKey: "kubernetes.io/hostname"
# 네임스페이스 생성 및 레포 생성
kubectl create namespace jenkins
helm repo add jenkinsci https://charts.jenkins.io
helm repo update
# 작업할 디렉터리 생성
mkdir jenkins_with_eks
cd jenkins_with_eks
# 파일 작성, 계정 및 플러그인 자동 구성
vi jenkins-values.yaml
controller:
tag: "lts-jdk11"
serviceType: LoadBalancer
installPlugins:
- kubernetes
- workflow-aggregator
- git
- configuration-as-code
- pipeline-stage-view
adminPassword: "패스워드 입력"
persistence:
storageClass: "efs-sc" # EFS 스토리지 사용
# 설치 진행
helm install jenkins jenkinsci/jenkins -n jenkins -f jenkins-values.yaml
# 젠킨스 배포 확인
kubectl get all -n jenkins
yaml파일에 스토리지클래스 항목에 gp2 스토리지클래스를 지정했다.
클러스터 배포과정에서 OIDC를 이용해서 EBS CSI Driver를 사용할 수 있는 EBS CSI Controller를 가진 서비스 계정을 생성했었다. 따라서 스토리지 클래스에 EBS타입인 gp2를 사용해도 EBS를 사용할 수 있는 권한이 있는 서비스 계정이 있기 때문에 문제없다고 생각했다.
하지만 eks 설치과정에서 문제가 있었는지 확인해보니 EBS-CSI-Controller가 존재하지 않았다. 따라서 gp2를 사용하지 않고 그냥 현재 이용중인 EFS스토리지를 이용해도 되겠다고 생각하여 이전에 생성한 EFS스토리지 클래스를 지정했다.
Jenkins를 배포하고나서 하루가 지난뒤 갑자기 접속이 안되는 현상이 발생했다. Jenkins를 띄우는 파드의 로그를 확인해보니 볼륨 마운트에 문제가 발생했다.
내 생각으로는 원래 gp2볼륨을 스토리지로 사용했는데 ebs-csi-driver가 설치되있지 않아서 그냥 EFS를 스토리지로 사용해서 발생한 문제 같았다. 왜냐하면 로그에서 Mount지점이 공유되있어서 문제가 발생했다는 것을 확인했기 때문이다.
따라서 Jenkins의 스토리지를 다시 gp2볼륨으로 설정할 것이다.
vi jenkins-values.yaml
. . .
persistence:
storageClass: "gp2" # EFS -> gp2로 수정
. . .
gp2스토리지 클래스를 사용하려면 ebs-csi-driver가 있어야 하는데 현재 없기 때문에 애드온으로 추가하여야 한다.
#서비스계정의 role-arn확인
eksctl get iamserviceaccount --cluster finalproject --name ebs-csi-controller-sa
#확인한 role-arn을 사용, 자신의 클러스터 이름 사용하여 애드온 생성
eksctl create addon --name aws-ebs-csi-driver --cluster finalproject --service-account-role-arn < role-arn > --force
#생성한 애드온 확인
eksctl get addon --cluster finalproject
배포과정에서 OIDC를 통해 생성된 IAM서비스 계정인 ebs-csi-controller-sa에는 IAM 롤이 붙어있고 IAM롤에는 IAM정책을 통해 ebs-csi-controller에 대한 권한을 가지고 있다. 따라서 해당 IAM서비스 계정은 ebs-csi-controller를 통해 ebs-csi-driver를 설치할 수 있다.
helm install jenkins jenkinsci/jenkins -n jenkins -f jenkins-values.yaml
위의 과정으로 ebs-csi-driver가 설치되었다면, 기존에 생성되어있던 gp2스토리지 클래스를 다시 이용할 수 있게 되기 때문에 jenkins-values.yaml파일을 helm으로 다시 설치해보면 jenkins가 정상적으로 배포되는 것을 확인가능하다.
AWS콘솔에 가보면 기존에 EFS에서 Jenkins에 마운트되어있는 볼륨의 엑세스포인트가 하나 사라지고, gp2볼륨이 새로 생성되어있는 것을 확인
gp3의 최고 성능은 gp2 볼륨의 최대 처리량보다 4배 빠르지만 gp3 볼륨은 범용 SSD(gp2) 볼륨보다 GiB당 20% 저렴하다. gp3 볼륨은 한 자릿수 밀리초의 대기 시간과 99.8% ~ 99.9%의 볼륨 내구성을 0.2% 이하의 연간 고장률(AFR)로 제공한다.
gp3가 gp2보다 효율도 좋고 가격도 저렴하기때문에 Jenkins에 마운트한 gp2볼륨을 gp3볼륨으로 교체하려 한다.
1 gp3 스토리지 클래스 생성
기존의 ebs-csi-driver로 설치한 스토리지 클래스는 gp2이기때문에 ebs-csi-driver를 이용하여 새로 gp3스토리지 클래스를 설치한다.
vi gp3.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
provisioner: ebs.csi.aws.com # ebs-csi-driver이용
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
parameters:
csi.storage.k8s.io/fstype: ext4
type: gp3 # gp3로 스토리지 클래스 생성
kubectl create -f gp3.yaml
2 Jenkins-values.yaml 파일 수정
vi jenkins-values.yaml
. . .
persistence:
storageClass: "gp3" # gp2 -> gp3로 교체
. . .
# helm으로 Jenkins 설치
helm install jenkins jenkinsci/jenkins -n jenkins -f jenkins-values.yaml
AWS콘솔에 새로운 로드밸런서가 추가되었고 해당 로드밸런서의 DNS Name:8080을 브라우저에 입력하면 Jenkins에 접속할 수 있다.
AWS 콘솔에서 Jenkins용 파드에 마운트된 gp3볼륨이 생성되어있는 것을 확인
Jenkins에 접속하여 Jenkins관리에 들어가보니 아래와 같은 에러 메세지가 뜨는 것을 확인했다.
쿠버네티스에서 사용하는 서비스 리소스 (Cluster IP 타입, NodePort타입 , LoadBalancer 타입)가 프록시 역할을 하는 것을 이미 알고 있다.
즉, 아래의 설명처럼 외부에서 클라이언트가 서버에 요청(ex.파드)을 보낼때 다이렉트로 서버에 도달하는 것이 아니라 중간에 프록시 역할을 하는 서비스가 존재하는 것이다.
따라서 우리가 지금까지 쿠버네티스 리소스들을 외부에 노출시키기 위해서 사용했던 서비스들이 프록시 역할을 하기때문에 리버스 프록시의 구조를 사용하고 있었던 것이다.
예를들어 우리가 생성한 파드를 외부에 노출시키기 위해 로드밸런서 타입의 서비스 리소스를 사용했다고 하자.
클라이언트는 외부용 IP를 통해 요청을 보낸다. 이 요청에 대해서 바로 파드에 접근하는 것이 아니라, 포트포워딩을 통해 노드에 뚫린 NodePort에 들어온다. 이때 ClusterIP가 내부에서 서비스를 찾아준다. 바로 이 서비스가 프록시 역할을 하는 서비스이고, 자신과 느슨하게 연결된 파드들의 IP정보를 저장하고 있는 엔드포인트 정보를 보고 포트포워딩과 로드밸런싱을 통해 파드에 연결한다.
= 이러한 구조가 리버스 프록시 구조이다.
또한 프록시 역할을 하는 서비스가 존재하는 리버스 프록시 구조이기 때문에 마지막에 파드에 대해 로드밸런싱이 가능한 것이다.
이러한 리버스 프록시 구조는 클라이언트가 서버에 다이렉트로 접근할 수 없기 때문에 보안에도 좋다.
본격적인 파이프라이닝 작업에 앞서 왜 Jenkins를 선택했는지 간단하게 설명할 것이다.
대표적인 CI/CD 툴로는 Jenkins와 GitHub Action이 있다. 하지만 요즘 개발자들 사이에서 GitHub Action이 유행이다. 그렇다면 왜 GitHub Action이 유행할까?
GitHub Action
Jenkins
위의 차이점을 봤을때 뭔가 한눈에 보면 Jenkins가 더 안좋아 보인다. 또한 우리가 하는 프로젝트는 규모가 작은 프로젝트이기 때문에 GitHub Action이 더 적합하다. 하지만 그럼에도 선택한 이유는 아래와 같다.
쿠버네티스 전문가 양성과정에서 배운 CI/CD툴이 Jenkins이기 때문에 실무와 비슷한 프로젝트에서 배운것을 한번 써보고싶다.
GitHub Action은 설정 프로세스가 간단하고, 따로 서버를 배포할 일도 없으며, 인프라에 대한 개념이 없어도 배포를 할 수 있다. 그렇기에 개발자에게도 인기가 많은 것 같다. 하지만 Jenkins는 인프라에 대한 이해도가 있어야 하며, 설정이 복잡한 만큼 커스터마이징도 다양하다. 따라서 Jenkins는 개발자보다는 좀 더 인프라엔지니어, Devops엔지니어등...에게 특화된 툴이라고 생각한다.
즉 GitHub Action은 개발자 포함 누구나 다룰 수 있는 툴이지만, Jenkins는 인프라 관련 엔지니어에게 특화되어 있기에 나같은 인프라 엔지니어를 희망하는 사람들에게는 Jenkins를 다룰줄 아는 것은 차별점이라고 생각한다.
인프라 구축을 하면서 느낀점은 주로 AWS EKS, EC2, RDS 등을 적극 활용하여 클러스터를 배포하고 인프라를 구축하였다. 그러다보니 비용이 만만치않게 청구되는 것을 확인할 수 있었다. 불과 2주만에 200달러 이상이 찍히고, 1년 예상비용이 3만8천달러가 측정되어있었다. 심지어 대규모 프로젝트도 아닌 소규모 프로젝트인데 말이다. 이것이 기업같은 대규모 인프라라면 상상을 초월하는 금액일 것이다. 따라서 비용을 고려하지 않고 단지 고가용성을 위해 인프라를 구축한다면 아무리 장애대응이 신속해도, 효율적인 인프라가 아닐 것이다.
이번 프로젝트에서 비용이 생각보다 많이 든 이유는 서버가 실행되지 않는 시간에도 인스턴스와 클러스터가 실행되고 있었고, 그 외에도 같은 인스턴스인데도 비용이 저렴한 스팟인스턴스와 같은 AWS에서 제공하는 다양한 서비스를 적극 활용하지 못했다. 지금은 처음 AWS를 사용하여 프로젝트를 진행하다보니 AWS의 정석이자 대표적인 서비스들만 이용하여 인프라를 구축하였지만 추후 프로젝트에서는 고가용성과 비용을 모두 고려하여 인프라를 설계해보도록 해야겠다.