DevOps 는 개발자와 운영자의 역할을 함께 수행하는 것으로 개발과 운영의 책임을 공동으로 진다. 처음 이 단어를 접한 것이 2011년 OpenStack Summit 에 참석했을 때인데 클라우드, 그 중에서 IaaS(Infrastructure as a Service)가 널리 퍼지기 시작했을 때다. DevOps 는 클라우드 기반에서 빠르게 개발하고, 배포하고, 운영하기 위해서 스타트업 회사를 중심으로 빠르게 퍼지기 시작했다.
아래는 클라우드 가상머신 기반의 DevOps 영역 중 CI/CD 에 대한 프로세스이다. 개발 언어는 Java 를 기준으로 표현하였으며, 이하 모든 설명은 Java 를 기준으로 설명한다.
문제
2015년 7월 쿠버네티스 1.0 버전이 릴리즈 되면서 DevOps 는 가상머신이 아니라 컨테이너 기반으로 점차 변화하였다. 쿠버네티스 이전의 Ops는 가상머신을 빠르게 만들고 개발된 소스 코드를 자동으로 통합 빌드하여 배포하는 영역이었다. 하지만 쿠버네티스가 나오고, 컨테이너 관리가 효율적/안정적으로 변하면서 Ops 는 소스 코드 통합 빌드, 컨테이너 이미지 만들기와 쿠버네티스에 배포, 운영하는 영역으로 바뀌었다. 즉, Ops 영역을 맡은 운영자는 컨테이너도 알아야 하고, 쿠버네티스도 알아야 한다는 의미이다.
결국 쿠버네티스 기반의 DevOps 는 소스 코드 개발, 통합 빌드, 컨테이너 이미지화, 배포의 영역 모두를 의미한다. 이를 간략하게 프로세스로 표시하면 다음과 같다.
여기서 부터 문제가 발생한다. 기존의 Dev 역할은 인프라스트럭처가 가상 머신이든 쿠버네티스이든 상관이 없지만 Ops 역할은 컨테이너와 쿠버네티스라는 새로운 기술을 알아야 하는데 해당 기술을 습득하기까지는 어느 정도의 기술 허들을 넘어야 하고 일정 기간이 지나야 한다. (클라우드 기술이 널리 퍼지기까지 기간을 생각해 보면 쉽게 이해될 것이다)
또한 배포 영역을 생각해 보면 결코 쉬운 문제가 아니다. 배포 전략에는 아래와 같은 3가지 방법이 존재한다. (크게는 4가지 이지만 가장 단순한 Recreate 배포는 생략하였다)
이미지 출처: 쿠버네티스 패턴 (책만출판사)
사족이지만 카나리아 배포를 “까나리”라고 발음하지 말자. “까나리”는 액젓이다.
해결책
개발자는 개발의 영역 즉, Dev 영역에 집중하게 하자. 어려운 Ops 영역은 시스템으로 자동으로 동작하도록 제공하자.
앞서 간단히 살펴본 개발/배포 프로세스를 다시 살펴보자.
개발자가 IDE 툴을 통해 프로그램을 개발한다.
Maven 혹은 Gradle 로 소스 코드를 빌드한다. 로컬 빌드, Jenkins 혹은 기타 다른 CI 툴을 활용한 빌드 결과물로 jar 혹은 war 파일이 생성된다. 일반적으로 스프링 부트 프로젝트는 jar 파일로 만들어지며, war 파일은 일반 스프링 프로젝트이다. 해당 결과 파일은 저장소(e.q. nexus)에 저장된다.
jar 혹은 war 파일을 로컬 빌드 혹은 기타 다른 CI 툴을 활용하여 컨테이너 이미지로 빌드한다.
빌드된 컨테이너 이미지를 이미지 저장소에 저장한다.
쿠버네티스에 배포하기 위해 deployment.yaml, service.yaml 등을 포함한 helm chart 를 만들고 이를 서버에 배포한다. 배포할 때는 배포 전략에 따라서 Rolling update, Blue-Green, Canary 로 배포한다.
1번과 2번은 개발자가 이제까지 하던 방식 그대로 개발하면 된다. 우리가 시스템으로 만들어 제공해야 할 부분은 3, 4, 5 번 영역이다.
구현 방법
해당 시스템에 대한 아키텍처를 구성하면 다음과 같다.
사용 오픈소스 S/W
Nexus
Maven 저장소로 사용되며 테스트 용도의 jar 파일을 저장하고 다운로드 할 수 있음
Keycloak
인증 서버로 활용
OIDC 접속 백엔드로 활용할 수 있음
Argo Workflow
CNCF Graduated Project
워크플로우 서버로 파이프라인을 설계하고 실행할 수 있음.
워크플로우 템플릿을 작성하면 재사용 가능함
워크플로우 실행은 컨테이너 단위로 실행됨
Harbor
CNCF Graduated Project
이미지 저장소로 활용
Gitea
Helm chart 저장소로 활용
Helm
Helm chart template 관리
Argo Rollout
배포 전략을 다양하게 지원함
지원 배포 전략: Rolling update, Blue-Green, Canary
프로세스 설명
사용자는 cli (golang)로 앱 배포를 요청한다.
API 서버 (golang) 는 해당 요청을 받아서 Argo workflow 를 호출한다.
Argo Workflow 에서 Nexus 로 부터 jar 파일을 가져온다.
Argo Workflow 에서 jar 파일을 컨테이너 이미지 파일로 빌드하고 이미지 저장소인 하버에 저장한다.
Argo Workflow 에서 이미지를 가져온다.
Argo Workflow 에서 Helm chart 템플릿을 가져온다.
Argo Workflow 에서 Helm chart 와 이미지를 조합하여 Argo Rollout 으로 배포한다.
Argo Rollout 은 초기 배포를 Blue 로 배포한다. 배포된 Blue 는 로드 발랜서와 연결된다.
$ kubectl argo rollouts completion bash | tee /home/ubuntu/.kube/kubectl-argo-rollouts > /dev/null
$ vi ~/.bash_profile
source '/home/ubuntu/.kube/completion.bash.inc'
source '/home/ubuntu/.kube/kubectl-argo-rollouts'
PATH=/home/ubuntu/bin:$PATH
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
3. 최초 App 배포 (Blue Deployment)
초기 app (blue)을 배포한다. 이전 글에서 사용된 seungkyua/nginx:blue 이미지를 배포한다. 단 replicas 를 0 으로 배포한다. 이렇게 배포하면 실제 pod 는 실행되지 않지만 pod template 은 배포된 상태가 된다. pod template 은 나중에 rollout 에서 참조하여 사용한다.
그리고 blue deployment app 에 웹접속이 가능하게 ingress 를 배포한다. ingress 를 배포하더라도 아직 웹 접속은 불가능하다. 앞에서 deployment 의 replicas 를 0 으로 생성했기 때문에 실행되고 있는 pod 가 없기 때문이다. (나중에 접속을 위해서 /etc/hosts 에 nginx-blue-green.taco-cat.xyz 를 등록해 놓자)
이제 배포가 완료되면 아래와 같이 리소스가 생성되었음을 확인할 수 있다. rollout pod 와 rollout 에서 사용하는 ReplicaSet 이 생성되어 있음을 알 수 있다.
$ kubectl argo rollouts list rollout
NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
nginx-rollout BlueGreen Healthy - - 2/2 2 2 2
$ kubectl get pods,deploy,rs
NAME READY STATUS RESTARTS AGE
pod/nginx-rollout-85c4bfb654-jmts7 1/1 Running 0 2m29s
pod/nginx-rollout-85c4bfb654-s46sm 1/1 Running 0 2m29s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx-blue-green 0/0 0 0 4m50s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-blue-green-8b4f9cddb 0 0 0 4m50s
replicaset.apps/nginx-rollout-85c4bfb654 2 2 2 2m29s
웹으로 접속하면 아래와 같은 화면을 볼 수 있다.
혹은 curl 로도 확인할 수 있다.
$ curl nginx-blue-green.taco-cat.xyz
--- output ---
<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>
Rollout dashboard 에는 아래와 같이 나온다.
4. App 업그레이드 배포 (Green Deployment)
app 을 수정하여 배포해 보자. app 은 Deployment 를 수정해서 배포하면 된다.
Deployment 가 업그레이드 되어 배포했기 때문에 Rollout 이 이를 인식하고 Green 에 해당하는 추가 Pod 와 ReplicaSet 을 생성한다. 그리고 Rollout 리소스의 상태는 Paused 가 된. (앞에서 Rollout 리소스 배포 시에 autoPromotionEnabled 는 false 로 하였기 때문이다)
$ kubectl argo rollouts list rollout
NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
nginx-rollout BlueGreen Paused - - 2/4 2 2 2
Pod 와 ReplicaSet 을 조회해 보면 아래와 같다. Blue 해당하는 ReplicaSet 1 개, Pod 2개, Green 에 해당하는 ReplicaSet 1개 Pod 2 개가 떠 있는 것을 알 수 있다.
Rollout Dashboar 는 아래와 같은 Pause 상태이다.
5. Rollout 진행 완료 (promote)
완전히 Green 으로 변경하려면 Rollout 을 promote 하여 최종 적용을 하던지 abort 하여 중단, 혹은 undo 하여 Pause 보다 이전 단계이 최초 Blue app 배포 단계로 돌아가는 방법이 있다.
storage class 에 storage type (gp2, gp3, io1 등), iops, 데이터 암호화 여부, 토폴로지 등을 넣을 수 있는데 이를 적용하면 현재 csi driver 에서 에러가 나므로 추가 확인이 필요하다. (볼륨 생성이 안된다던지, 볼륨 attach 가 안된다든지 하는 문제가 발생했는데 자세히 소스까지 찾아보지는 않았음 ㅠㅠ)
storage class 가 잘 생성되었는지 확인한다.
$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 12d
standard (default) ebs.csi.aws.com Delete WaitForFirstConsumer false 138m
kubernetes 에 서비스를 올릴 때 Service 의 Type 으로 LoadBalancer 를 선택하면 cloud 에서 자동으로 LB (external-ip ) 가 생성되어 서비스 pod 에 연결된다. 어떻게 Kubernetes 에서 설정한 값이 cloud 에 연결될까? 이는 Cloud Provider 가 있어 가능하다.
Cloud Provider 는 초기에 Kubernetes Controller 에 포함되어 있다. 하지만 지금은 External Kubernete Cloud Provider 로 Kubernetes 에서 제외되었으며, 이전 Kubernetes Controller 에 포함된 Cloud Provider 는 Legacy Cloud Provider 로 불리고 있다.
AWS Cloud Provider 의 경우에는 아직 1.23 (Kubernetes 와 같이 버전을 맞춰가고 있음) alpha 버전이라 아직은 Legacy Cloud Provider 를 사용하는 것이 안정적이다.
aws 에서는 아래의 순서대로 적용한다.
IAM Policy, Role 생성
VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성
2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성
서울 리전의 경우 VPC 를 1개 만들고, public 용도의 subnet 4개, private 용도의 subnet 4개를 만든다.
Internet Gateway 1개를 만들어서 public subnet 에 연결하고, Nat Gateway 4개를 만들어서 각각 private subnet 에 연결한다.
Routing Table 은 subnet 갯수에 맞는 8개를 만들어서 각각 연결한다. private 용 4개의 Routing table 은 0.0.0.0/0 → nat gateway 를 대상으로 설정하고, public 용 4개의 Routing table 은 0.0.0.0/0 → internet gateway 대상으로 설정한다.
VM 은 Controler Plane 3대는 각 private subnet 에 1대씩 생성하고(subnet 1개는 남는다), Node 용 4대는 각 private subnet 1대씩 생성하다.
bastion 노드로 public subnet 에 1대 생성한다.
1. bastion VM 1대
- public-subnet-a
2. Control Plane VM 3대
- private-subnet-a
- private-subnet-b
- private-subnet-c
3. Node VM 4대
- private-subnet-a
- private-subnet-b
- private-subnet-c
- private-subnet-d
4. Aws Resource 에 Tag 설정
aws cloud provider 가 리소스를 파악하기 위해서는 aws 에 적절한 값을 설정해야 한다.
4-1. VM 에 IAM Role 을 할당
VM 에서 권한을 얻기 위해서는 반드시 IAM Role 을 할당해야 한다.
Control node 에는 control-plane.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.
일반 Node 에는 nodes.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.
4-2 VM 에 Tag 설정
VM 에는 Kubernetes Cluster Name 을 Tag 로 지정한다. kubernetes.io/cluster/<cluster name> 과 같이 지정하는데 Cluster Name 은 Kubernetes 를 설치할 때 지정할 수 있다. 기본 값은 cluster.local 인데 여기서는 ahnsk 로 이름을 지정하였다.
그리고 vm 의 역할을 지정해야 하는데 Controler Node 는 control-plane 으로, Node 는 node 로 지정한다.
4-3. Subnet 에 Tag 설정
Load Balancer 를 핸들링 하기 위해서 Kubernetes Cluster 가 어느 Subnet 과 Routing Table 을 사용해야 하는지 알아야 한다.
주의해야 할 점은 public subnet 의 경우 [kubernetes.io/role/elb](http://kubernetes.io/role/elb) 이지만, private subnet 의 경우에는 [kubernetes.io/role/internal-elb](http://kubernetes.io/role/internal-elb) 로 해야 한다.
4-4. Routing Table 에 Tag 설정
5. Kubernetes Cluster 생성
Kubernetes cluster 이름은 앞에서 설명했듯이 ahnsk 로 설정한다.
kubeadm 혹은 kubespray 를 사용할 수 있으며 여기서 생성 방법을 생략한다.
aws cloud provider 를 활성화 하기 위해서는 API Server, Controller, Kubelet 에 --cloud-provider=aws 옵션을 추가해야 한다.
값을 다 채워 넣으면 apiserver 가 kube-apiserver-master-dummy 라는 이름으로 잘못 실행되므로 조심해야 한다. master-dummy 로 띄우는 방법은 aws account 가 다를 경우에만 사용하는 방법이다. 이는 아래 소스를 보면 알 수 있다.
https://github.com/kubernetes/legacy-cloud-providers/blob/707ecda639b086132369678680a1b34d4d2b5c7c/aws/aws.go#L1251
...
tagged := cfg.Global.KubernetesClusterTag != "" || cfg.Global.KubernetesClusterID != ""
if cfg.Global.VPC != "" && (cfg.Global.SubnetID != "" || cfg.Global.RoleARN != "") && tagged {
// When the master is running on a different AWS account, cloud provider or on-premise
// build up a dummy instance and use the VPC from the nodes account
klog.Info("Master is configured to run on a different AWS account, different cloud provider or on-premises")
awsCloud.selfAWSInstance = &awsInstance{
nodeName: "master-dummy",
vpcID: cfg.Global.VPC,
subnetID: cfg.Global.SubnetID,
}
awsCloud.vpcID = cfg.Global.VPC
} else {
selfAWSInstance, err := awsCloud.buildSelfAWSInstance()
if err != nil {
return nil, err
}
awsCloud.selfAWSInstance = selfAWSInstance
awsCloud.vpcID = selfAWSInstance.vpcID
}
...
RoleARN 의 값을 넣으면 안되는데 값이 없으면서도 어떻게 kubelet 이 해당 Role 로 인증을 받을 수 있을까? 이는 앞에서 설명한 VM 에 IAM Role 인 control-plane.cluster-api-provider-aws.sigs.k8s.io 이나 nodes.cluster-api-provider-aws.sigs.k8s.io 이 설정되어 있기 때문에 가능하다.
kubelet-config.yaml
kubernetes node 정보에 providerID 값이 들어가 있어야 한다. 만약 이 정보가 없다면 LoadBalancer 가 생성된다고 하더라도 인스턴스가 LoadBalancer 에 할당되지 않아 제대로 사용할 수 가 없다.
providerID 는 kubelet-config.yaml 에 추가한다. aws:///<zone-id>/<instance-id> 값으로 추가한다.
$ sudo vi /etc/kubernetes/kubelet-config.yaml
...
providerID: "aws:///ap-northeast-2d/i-0f59f059e2d64213f"
...
이미 노드가 생성된 경우에는 해당 값을 변경하고 kubelet 서비스를 다시 restart 한다고 값이 추가되지는 않는다. 그래서 patch 명령으로 동적으로 추가하는 것이 좋다.
Pod 가 새롭게 생성되면 Scheduler 를 통해서 Pod 가 실행될 노드가 결정된다. 즉 모든 Pod 는 Scheduler 에 의해서 최적의 노드가 결정되고 실행된다.
kube-scheduler 는 Control Plane 에 속하는 컴포넌트 중에 하나이며, 디폴트 scheduler 이다. 만약 특별한 스케줄링을 원한다며 커스텀 스케줄러를 만들어서 대체할 수 도 있다.
Pod 가 스케줄링 되기 위해서는 노드가 스케줄링 요구 사항에 맞아야 하는데 이 때, 요구사항에 맞는 노드를 피저블 노드(feasible node) 라고 한다.
스케줄러는 피저블 노드를 찾고 적절한 로직을 통해 노드에 점수를 부여한다. 가장 최고 점수를 갖는 노드에 Pod 가 실행된다.
스케줄링을 결정하는 데에는 하드웨어, 소프트웨어, policy contraints, affinity 와 anti-affinity spec, local data 등 여러 요구 사항들이 있다.
kube-scheduler 가 결정하는 2-step operation
Filtering
Scoring
Filtering 단계에서는 피저블 노드를 찾는 단계이다. 예를 들면, PodFitsResources 필터는 Pod 의 request 리소스를 만족하는 리소스를 갖는 후보 노드를 추려낸다. 이 단계 이후에는 적합한 노드 리스트가 도출되는데 만약 노드 리스트의 값이 비었다면 Pod 는 스케줄링 되지 않는다.
Scoring 단계에서는 필터링된 노드 리스트 중에서 Pod 가 실행될 최적의 노드를 찾는다. 스케줄러는 노드 리스트에 있는 각 노드들에 점수를 부여한다.
위와 같이 nodeSelectorTerms 에 여러 개의 matchExpressions 가 있으면 이 중 하나만 만족해도 스케줄링은 된다. nodeSelectorTerms 에 하나의 matchExpressions 가 있고, key value 가 여러 개가 있으면 matchExpressions 내의 모든 조건을 만족해야 스케줄링이 된다.
operator 로 쓸 수 있는 값은 In, NotIn, Exists, DoesNotExist, Gt, Lt 이다.
NotIn 과 DoesNotExist 는 node anti-affinity 로 사용 가능하다.
$ kubectl get pods -n kube-sample -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-node-affinity-755cf7f85d-ss9q5 1/1 Running 0 5s 10.233.113.10 k1-node02 <none> <none>
Pod affinity and anti-affinity
Pod 를 서로 다른 노드에 배치시키는 방법은 pod anti-affinity 를 통해서 구현할 수 있다.
위와 같이 pod label 에 pod anti affinity 를 설정하면 replicas 값 2 에 의해서 생성되는 2개의 pod 는 서로 다른 노드에 배치된다. topologyKey 는 kubernetes.io/hostname 값으로 세팅하고 특정 node lable 로 하고 싶으면 node affinity 를 추가하면 된다.
toleration 으로 taint 효과를 없애고 node affinity 를 활용하여 control-plane, 즉 master 노드에 pod 를 배치 시켰다. 물론 master 노드에 node-role.kubernetes.io/control-plane 이라는 label 이 존재해야 한다.
taint effect 는 NoSchedule, NoExecute, PreferNoSchedule 이 있다.
NoSchedule 은 스케줄링을 막는 것이고, NoExecute 는 실행중인 pod 를 eviction 시킨다. PreferNoSchedule 은 가능하면 스케줄링 하지 않는다는 의미이다.
operator 에는 Exists 와 Equal 이 있다. Exists 는 key 값이 존재하는 지를 판단하기 때문에 value 값이 필요 없으며, Equal 은 key 와 value 값이 지정된 값과 일치해야 한다.
$ kubectl get pods -n kube-sample -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-toleration-fb74cd98c-hkql8 1/1 Running 0 5m31s 10.233.117.4 k1-master01 <none> <none>
EKS 서비스를 이용하기 보다는 Kubernetes 를 AWS 에 직접 설치하여 활용하는 경우가 종종 있는데, 이 경우에는 세부적인 세팅들을 해줘야 할 때가 있다. 그 중에 대표적인 추가 모듈이 Pod 에서 EBS Volume 을 연결하여 사용할 수 있는 StorageClass 이다.
잘못된 Storage Class 를 사용하는 경우
Storage class 의 Provisioner 를 kubernetes.io/aws-ebs 로 사용하는 경우가 있는데, 이는 처음에는 문제가 없으나 Pod eviction (pod 가 다른 노드로 옮겨지는 경우) 에서는 문제가 발생한다.
처음 만든 Pod 에서 위의 StorageClass 를 사용하여 volume 을 ReadWriteOnce 로 attach 한 경우에는 문제없이 정상 작동한다. 하지만 pod 가 rescheduling 되면 새로 옮겨가는 노드에서 pod 는 생성되지 않는다. 이유는 volume 이 이전 노드의 Pod에 이미 사용되고 있다는 정보 때문에 새로운 노드의 Pod 에서 volume attach 가 안되기 때문이다.
즉, 해당 문제를 해결하기 위해서는 이전의 attach 정보를 삭제해 줘야 하는데 위의 storage class 로는 이를 해결할 수 없다. 그래서 다음과 같은 StroageClass 를 사용해야 한다.
kustomize 로 설치하면 ebs-csi-controller 가 HA 로 설치되고, 각 노드 마다 ebs-csi-node 가 설치된다. 이는 ceph 스토리지 용도의 CSI Driver 와도 비슷한데 volume 을 사용할 Pod 가 실행될 VM 노드에 EBS 링크를 연결하고 이를 Pod 에서 사용하기 때문에 각 노드마다 ebs-csi-node 가 설치되는 이유이다.
2. Storage class 생성
driver 를 설치했으면 이제 스토리지 클래스를 생성할 차례이다. 아래와 같이 생성한다.
annotations 의 storageclass.kubernetes.io/is-default-class":"true" 값은 스토리지 클래스를 디폴트로 지정하는 값이다. 스토리지 클래스는 여러가지를 사용할 수 있기 때문에 기본을 지정해 주는 것이고, 또한 ebs 가 생성될 AZ 리스트를 넣어줘야 한다.
3. IAM 설정
앞에서 VM 에 ebs 링크를 걸어준다고 했는데 그렇기 때문에 IAM 설정을 해야 한다. 아래와 같이 Policy 를 만들고 해당 Policy 를 control-plane.cluster-api-provider-aws.sigs.k8s.io 과 nodes.cluster-api-provider-aws.sigs.k8s.io 에 attach 해야 한다.
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
app-ebs-claim Bound pvc-4ab38fe9-7e76-4d81-85d3-0db68da9c1e0 4Gi RWO standard 6s
$ kubectl get volumeattachments
NAME ATTACHER PV NODE ATTACHED AGE
csi-60f252627e620b1e91d7d32a7d463b406a3850f3a7d7b0c535fe209f699ba3bd ebs.csi.aws.com pvc-4ab38fe9-7e76-4d81-85d3-0db68da9c1e0 ip-10-0-214-57.ap-northeast-2.compute.internal true 2m8s
또한 volumeattachments 를 보면 VM 에 해당 volume 이 attach 되어 링크가 걸려 있는 것을 알 수 있다.
외부에서 Kubernetes Cluster 위에서 돌아가는 서비스에 접근하기 위해서는 Ingress Controller 를 통해서 가능하다. aws 와 같은 클라우드에서는 ELB -> Ingress Controller -> Workload 경로로 접근가능하다.
현재 사용하고 있는 방법은 CrossPlane 으로 AWS ELB (Classic Type) 를 자동으로 생성하여 Ingress Controller 에 연결하고 있다. 그렇기 때문에 Ingress Controller 를 배포할 때 Service Type을 특정 NodePort 를 지정/오픈하여 연결점을 알아야 한다.
CrossPlane 이 AWS ELB 에 대해서 Classic Type 만 지원하기 때문에 CrossPlane 을 제거하고 Network Type 으로 변경하는 것을 고민하였다. 참고로 현재의 방법은 Ingress Controller 를 실수로 삭제해도 ELB 는 그대로 남겨져 있어야 한다는 전제로 고려한 방법이다. DNS 가 ELB 와 연동되어야 하기 때문에 ELB 의 삭제 후 재생성은 DNS 전파의 시간을 필요로 하기 때문이다.
하지만 AWS 에서 권장하는 NLB 로 넘어가는 것, NodePort 를 지정하지 않아도 된다는 것, 그리고 CrossPlane 으로 ELB를 관리하지 않아도 되기 때문에 운영 측면에서 더 낫다고 판단되어 방법을 찾아보게 되었다.
해결책
Kubernetes 프로젝트 아래 Ingress Controller 의 annotation 을 사용하면 NLB 를 자동으로 생성할 수 있다.
nodeSelector 와 podAntiAffinity 를 사용하여 가능하면 지정노드에 분포해서 Ingress Controller 를 설치할 수 있다.
Service 의 annotations 을 사용하면 ELB 를 NLB type 으로 생성하여 자동으로 Ingress Controller 에 연결시켜 준다. 한가지 주의할 점은 aws-load-balancer-proxy-protocol 을 사용하면 aws-load-balancer-target-group-attributes 의 proxy_protocol_v2 를 enabled 해야 한다. 그러나 이 annotations 은 동작하지 않는다. 왜냐하면 aws 에서는 Ingress Controller (https://github.com/kubernetes-sigs/aws-load-balancer-controller/tree/main/helm/aws-load-balancer-controller)를 kubernetes-sigs 아래에 따로 만들어서 관리하기 때문이다. 즉 더 이상의 Nginx Ingress Controller 에 대한 기능 추가가 없다고 공표했다.
그럼에도 불구하고 Nginx Ingress Controller 를 선호한다. 이유는 Nginx 의 기능, 즉 config 를 원하는 대로 세팅할 수 있기 때문이다.
nginx 를 사용할 때 가장 많이 경험하는 에러가 2가지 있다. 하나는 header 에 '_' 값이 포함될 때 에러가 나고 다른 하나는 body-size 로 인한 에러이다. 이 2가지 값은 기본적으로 허용해 주는 것이 좋다.
마지막으로 ELB, Ingress Controller 를 거치면서 real client ip 를 알고 싶어 하는 경우가 많다. nginx 의 use-proxy-protocol: "true" 로 client ip 를 알아 낼 수 있다.
X-Real-IP 와 X-Forwared-For 에 실제 client ip 값이 출력되는 것을 확인할 수 있다.
마치며
Kubernetes 위에 서비스를 올린 후 사용자 접근을 오픈하기 위해서는 LB 와 Ingress Controller 를 사용해야 한다. 직접 Kubernetes 를 설치 관리하면서 Nginx 의 기능을 사용하고 싶다면 AWS Load Balancer Controller 보다는 Nginx Ingress Controller 를 활용해야 한다. 물론 IAM Account 연동 등을 위해서 혹은 EKS Cluster 를 사용한다면 AWS Load Balancer Controller 를 사용하는 것이 건강에 좋을 것이다.
2018년에 Kubernetes 상에서 돌아가는 ML Platform 인 Kubeflow 를 처음 알게된 후로 관심을 갖고 재미삼아 하다가 2022년에는 업무와 연관되어 일을 해야하기 때문에 중점으로 살펴보고자 한다. (최근에는 ML Platform 보다는 AI Platform 이라고 말하는 사람들이 많은 듯)
Kubeflow 를 KubeCon 에서 만나다
Kubeflow 개발자들을 처음 만난 것은 2019년 KubeCon in Europe (Barcelona) 이다. 초기 Kubeflow 는 Goolge 과 Arrito 가 주축이 되어 개발하고 있었는데 마침 초기 버전이 나오면서 Kubeflow 홍보를 시작하던 때 였었다.
Kubeflow 의 주 개발자는 화면에 보이는 구글의 Senior S/W Engineer 인 Jeremy Lewi 다. 당시 저 발표는 간단한 설명후에 데모 형태로 발표가 되었는데 Kubernetes 위에 Kubeflow 를 설치한 상태에서 Jupyter Notebook 을 띄어 Model 을 개발하고, Fairing 으로 Model Training 후 Model Serving 하는 것을 시연으로 보여주었다.
저녁에 Arrito 가 주최하는 Kubeflow 개발자 / 사용자와의 저녁 만남이 있었는데 거기서 Jeremy 와 이야기를 해봤을 때는 전형적인 내성적이고 약간은 고지식한 개발자라는 느낌을 받았던 기억이(물론 이건 주관적인 생각). 시간이 좀 지나니 혼자 따로 앉아 있던데.
Kubeflow 를 왜 써야 하나?
AI Platform Developer 라면 아래의 그림은 많이 봐왔을 것이다. 정 가운데에 있는 ML Code 는 데이터 사이언티스트들이 모델을 개발하는 코드인데, 이런 개발을 도와주거나 실제 서비스로 구축하기 위해서는 코드 이외에도 많은 Tool / System 들이 필요하다는 의미를 보여준다.
ML 개발 프로세스는 Experimental Phase (실험 단계) 와 Production Phase (운영 단계) 로 나눌 수 있으며 전체 워크플로우는 아래와 같다.
실험 단계
1. 문제를 인식하고 데이터를 수집/분석
2. ML 알고리즘을 선택하여 모델을 코딩
3. 데이터를 가지고 모델 트레이닝
4. 하이퍼 파라미터 튜닝
운영 단계
1. 데이터 변환
2. 모델 트레이닝
3. 온라인/배치 예측을 위한 모델 서빙
4. 모델 성능 측정 (결과를 가지고 다시 트레이닝하거나 튜닝)
단계에 적용되는 Kubeflow 컴포넌트는 아래와 같다.
Fairing: 위성발사를 생각해보면 로켓 맨 상단에 중요 내용물인 위성을 보호하기 위해서 Fairing 으로 감싸고 있다. 모델을 안전하게 다른 시스템으로 전달하는 기능으로 python library 를 제공한다.
Pipelines: Argo workflow 기반으로 python library 를 제공하여 pipeline 을 구축할 수 있다. 예를 들면 데이터 전처리 등 반복되는 작업을 자동으로 수행할 수 있다.
Katib: 하이퍼 파라미터 튜닝을 자동으로 해주는 기능이다. 아래 그림과 같이 하이퍼 파라미터를 바꾸면서 결과를 그래프와 표로 보여준다.
KFServing: Kubeflow 에서 제공해주는 Model Serving 기능이다.
마치며
현재 Kubeflow 의 최신 버전은 v1.4 이다. 2019년 Arrito 개발자들과 만나서 논의했을 때 (당시에 v0.3 인걸로 기억한다) v1.0 으로 올리는 것이 의미있냐고 질문을 받은 적이 있다. 고객이 오픈소스의 안정성을 의심하기 때문에 기능이 조금 미흡하더라도 v1.0 으로 올리는 것이 좋겠다는 의견을 준적이 있었는데 어느덧 버전이 v1.4 까지 나왔다.(물론 그 때 이후 한참 지나서야 v1.0 이 나왔지만)
많은 기업에서 자체 AI Platform 을 만들어서 사용하는 것으로 알고 있는데, 제 개인적인 생각으로는 지금이라도 Kubeflow 로 바꾸는 것이 좋다는 생각이다. 그 이유는 이미 모두 알고 있을 것이다.