PIPE: 더 나은 개발자 경험을 제공하기 위한 CI/CD

LINE DEVELOPER DAY 2021에서 이승 님이 발표하신 PIPE: 더 나은 개발자 경험을 위한 CI/CD 세션 내용을 옮긴 글입니다.

안녕하세요. LINE Plus Productivity Engineering 팀의 이승이라고 합니다. 이번 글에서는 저희 팀에서 쿠버네티스 기반으로 만든 PIPE라는 솔루션을 소개하겠습니다. PIPE는 CI/CD에 런타임을 통합한 일종의 에코 시스템으로, 개발자의 생산성을 향상시키고 좀 더 좋은 개발자 경험을 제공하는 것을 목표로 개발한 솔루션입니다. 

 

이번 글은 PIPE가 어떤 솔루션인지 알아보고, 저희가 서비스 수준을 향상시키기 위해서 어떤 노력을 하고 있는지 말씀드린 뒤, 마지막으로 앞으로 계획하고 있는 로드맵을 설명드리는 순서로 진행하겠습니다. 

 

PIPE란 무엇일까요?

PIPE는 워크플로(Workflow)와 런타임(Runtime), 두 개의 컴포넌트로 구성돼 있습니다. 각 컴포넌트를 간략하게 소개한 뒤 이어지는 단락에서 하나씩 자세히 살펴보겠습니다. 

 

먼저 워크플로는 Tekton Pipelines와 GitHub Events를 통합한 모델입니다. 여러 기술 중에서 Tekton이라는 기술을 선택한 이유는, 기술을 선정하기 위해 세웠던 두 가지 기준에 가장 부합했기 때문입니다. 첫 번째 기준은 ‘어떻게 하면 CI/CD를 처음부터 끝까지 완전하게 자동화할 수 있겠느냐’, 두 번째 기준은 ‘어떻게 파이프라인을 선언적으로 정의할 수 있겠느냐’였습니다. 선정 작업을 진행하면서 Tekton 외에도 Argo CD와 같은 여러 솔루션을 리뷰했는데요. 향후 직접 운영하면서 개선하고 패치하는 것을 고려했을 때 Tekton이 가장 적합하다고 판단했습니다. Tekton으로 좀 더 빠르고 재현 가능한 빌드와 테스트 환경을 제공할 수 있을 것이라고 생각했습니다. 

Tekton은 기본적으로 빌드와 테스트 기반입니다. 따라서 이를 실행하기 위한 런타임이 필요합니다. 이에 PIPE 런타임이라는 직접 운영하는 관리형 쿠버네티스 플랫폼을 같이 제공하고 있습니다. 이 플랫폼은 Tekton 워크로드를 실행하기 위한 것만은 아닙니다. 컨테이너화한 사용자의 워크로드도 같이 호스팅할 수 있는 플랫폼입니다. PIPE 런타임을 통해서 LINE 개발자의 외부 의존성을 줄이고 LINE 내부에서만 사용하는 여러 가지 워크로드 유형이나 사용 방법을 최대한 추상화해서 사용하기 편리하게 제공해 드리고자 합니다.  

 

PIPE를 왜 만들었을까요?

이 시점에서 왜 PIPE를 만들었는지 배경을 설명해야 할 것 같습니다. 배경을 두 가지로 나눠 살펴보겠습니다.

 

오픈 플랫폼 제공

이미 LINE에서는 여러 가지 다양한 CI/CD 솔루션을 사용하고 있습니다. 많은 분들이 알고 계시는 친숙한 Jenkins부터 CircleCI, Drone CI, Argo CD 등 여러 솔루션이 현재 사내에서 운영되고 있습니다. 그렇다면 왜 또 다른 솔루션을 만들었을까요. 그 이유는 이와 같은 솔루션들이 통합돼 있지 않았고, 개발자들에게 열려 있지 않았기 때문입니다. 저희가 생각했던, 해결하고자 했던 문제는 바로 이 부분이었습니다. 개발자들이 PIPE 스택을 전부 학습하고 이해한 뒤 사용하는 게 아니라, PIPE 스택에서 제공하는 여러 컴포넌트 중에서 개발자 본인의 취향에 맞는 것, 본인에게 필요한 것들만 취사선택해서 사용할 수 있게 제공해 드리고자 했습니다. 

 

또한 사용자들이 PIPE 런타임에 접근하는 측면도 고려했습니다. 이미 사내에는 자체 개발해서 사용하는 쿠버네티스 솔루션이 있었는데요. 이 솔루션이 쿠버네티스의 기본 사용 방법을 그 형태 그대로 제공하는 네이티브 형태가 아니었습니다. kubectl이 아닌 자체 CLI를 제공하고 있었으며, OpenStack과 조금 밀접하게 연결돼 있었습니다. 물론 OpenStack의 리소스를 좀 더 자유롭게 사용할 수 있다는 장점도 있었습니다만, 사실상 표준 CLI인 kubectl와 표준처럼 사용하고 있는 여러 쿠버네티스 IDE를 그대로 사용할 수 있는 환경을 제공해 드리고 싶었습니다.

 

관리형 컨테이너 런타임 제공

 

PIPE에서는 ‘Namespace as a Service’ 모델로 컨테이너 런타임을 제공합니다. 즉 개발자 여러분들에게 전용 쿠버네티스를 드리지 않습니다. 개발자 여러분들이 웹사이트에 들어가서 생성 버튼을 누르면 전용 네임스페이스가 생성되는 형태이며, 이때 따로 결재나 승인이 필요하지 않습니다. 또한 쿠버네티스가 작동하기 위해서는 기본적으로 컨트롤 플레인이나 etcd 등 보일러 플레이트 같은 노드들이 필요한데요. PIPE에서는 이런 노드를 직접 관리하며 부하를 최소화한 쿠버네티스를 제공하면서, 개발자 여러분들에게는 전용 네임스페이스에 대한 관리자 권한을 부여해서 자원 사용을 최적화하고 좀 더 빠른 서비스를 제공하고 있습니다.  

이와 같은 방식으로 제공하면 부가적인 장점도 있습니다. 만약 쿠버네티스 여러 개를 프로젝트 단위로 나눠드리면 개발자 입장에서는 쿠버네티스 클러스터를 주기적으로 업그레이드하는 게 상당히 어렵고 불편해지는데요. PIPE에서는 지정된 컨테이너 런타임 몇 가지만 운영하기 때문에 업데이트와 업그레이드를 하기 위한 관리 비용을 최소화할 수 있습니다. 

이와 같이 관리 비용을 최소화해서 개발자 여러분들이 중간에 개입하지 않고도 자동 업데이트로 항상 최신 상태의 쿠버네티스를 사용할 수 있게 제공하고자 했습니다. 이를 위해 앞서 말씀드렸듯 관리자 권한을 네임스페이스 단위로 개발자 여러분들에게 제공하고 있으며, 쿠버네티스의 RBAC(role-based access control)와 더불어 Kyverno라는 정책 엔진을 통해 권한 관리를 수행하고 있습니다.  

 

PIPE 워크플로

앞서 말씀드렸듯이 PIPE 워크플로는 Tekton Pipelines과 GitHub Events를 합친 모델입니다. 

 

Tekton Pipelines은 쿠버네티스의 규칙이나 문법을 그대로 따르는 쿠버네티스 네이티브한 CI/CD 프레임워크로, Tekton Pipelines만이 아니라 CLI나 카탈로그와 같은 여러 부가 서비스를 함께 제공하고 있습니다. Tekton은 CD Foundation의 일원으로 Jenkins에 대한 의존성이 없는 거의 유일한 솔루션에 가깝습니다. 현재 CD Foundation에 올라와 있는 다른 솔루션들은 대부분 Jenkins를 내장해서 사용하든가 외부 Jenkins를 호출해서 사용하는 방식인데요. Tekton은 그런 의존성이 없기 때문에 좀 더 빠르고 유연하게 서비스를 제공할 수 있습니다. 

두 번째로 GitHub Events입니다. PIPE에서 제공하는 CI/CD 스택은 GitHub이라는 컴포넌트를 빼고서는 이야기할 수 없습니다. 대부분의 개발이 GitHub 기반으로 진행되고 있기 때문에 GitHub Events를 처리하기 위한 애너테이션(annotation)을 설정했습니다. 또한 GitHub Actions의 이벤트 트리거 문법을 그대로 가져왔는데요. 개발자 여러분들이 GitHub Actions를 이미 많이 경험해 보셨을 것이라고 생각한 게 첫 번째 이유이고, 두 번째는 GitHub Events를 핸들링하기 위해서 자체 DSL을 만들기보다는 사실상 표준이라고 할 수 있는 GitHub Action을 그대로 사용하는 게 좀 더 편리할 것이라고 생각했기 때문입니다.

이와 같이 현재 워크플로에서는 Tekton 내부의 애너테이션에다가 GitHub Action의 문법을 제공하고 있습니다. 따라서 어떤 이벤트가 전달됐을 때 그 이벤트를 어떻게 처리할 것인지에 대해서 Tekton Pipeline의 애너테이션 필드에 정리하는 식으로 사용할 수 있습니다.  

 

워크플로 작동 방식

다음은 워크플로가 작동하는 방식을 나타낸 다이어그램입니다. 

 

먼저 사용자가 GitHub에 푸시나 릴리스와 같은 액션을 전달합니다. 이때 PIPE는 웹훅(webhook)으로 동작하며, 웹훅은 GitHub 앱으로 구동됩니다. GitHub 사용자가 PIPE를 설치해 놓았다면 GitHub에서 웹훅을 통해 이벤트를 PIPE로 그대로 전달합니다. PIPE에서는 전달된 이벤트를 파악해서 개발자의 리포지터리에 해당 이벤트에 맞는 Tekton 파이프라인이 있는지 찾아봅니다. 만약 그 이벤트에 맞는 파이프라인이 있다면 그 파이프라인을 템플릿 삼아 필요한 파라미터들을 추가해서 PipelineRun이라는 인스턴스를 만듭니다. 파이프라인은 클래스, PipelineRun은 인스턴스와 같은 역할이라고 보시면 됩니다. 여기서 파라미터는 어떤 이벤트가 들어왔는지, SHA256 태그는 무엇인지, 누가 이 이벤트를 만들었는지와 같은 부가 정보입니다. 생성된 결과물을 컨테이너 런타임에서 구동한 뒤 그 결과를 사용자 GitHub의 커밋 상태(commit status)로 보여줍니다. 

 

결과 화면

아래 화면이 PIPE 워크플로의 결과 화면입니다.

 

PIPE 워크플로가 구동되면 로그 URL과 현재 개발자가 정의해 놓은 단계들이 표시되고, 어느 단계가 성공했고 어느 단계가 실패했는지 실시간으로 업데이트됩니다. 따라서 개발자 여러분들은 PIPE 웹사이트가 아니라 GitHub 사이트에서 자신의 워크플로가 현재 어떻게 구동되고 있는지 직접 확인할 수 있습니다. 물론 런타임도 같이 제공하기 때문에 런타임에 들어가서 직접 확인하는 것도 가능합니다. 

 

로그

아래는 워크플로 로그입니다.  

 

PIPE 워크플로는 Tekton Pipelines이기 때문에 tkn CLI를 통해서도 확인할 수 있는데요. 조금 불편할 수 있기 때문에 별도 웹사이트에서 로그를 볼 수 있는 기능을 제공하고 있습니다. 

 

스캐폴더(scaffolder)

아래는 스캐폴더라는 기능입니다. Tekton Pipelines는 처음 만들 때 작성해야 하는 YAML 파일이 상당히 길기 때문에 하나하나 다 코딩하기에는 조금 귀찮을 수 있습니다. 이에 스캐폴더라는 기능을 만들었습니다. 스캐폴더에서는 여러 가지 설정을 간편하게 진행할 수 있습니다. 아래 화면에서는 어떤 네임스페이스에서 개발이나 스테이지 혹은 프로덕션을 구동시킬 것인지, 혹은 어떤 이벤트를 어떤 Slack 채널로 받을 것인지와 같은 정보를 입력합니다. 

 

다음으로 아래 화면에서 개발자 여러분들이 실행시키고자 하는 플랫폼을 선택할 수 있습니다. Golang이나 Gradle, Node.js 등 원하는 플랫폼과 플랫폼의 버전을 선택한 다음 Scaffold 버튼을 누르면 Tekton Pipelines를 생성해서 그대로 다운로드할 수 있는 형태로 제공합니다. 

 

이 기능은 CircleCI에서 모티브를 얻었습니다. Jenkins의 Blue Ocean에도 비슷한 기능이 있는데요. 굉장히 편리하다고 생각해서 PIPE에서도 제공하기로 결정했습니다.   

 

개발자 경험을 향상시키기 위해 준비한 두 가지

PIPE를 개발하면서 어떻게 개발자분들에게 좀 더 좋은 경험을 드릴 수 있을지 많은 고민을 했는데요. 고민의 결과 두 가지를 소개하겠습니다.

 

재사용 가능한 태스크 제공

현재 PIPE에서는 재사용 가능한 태스크를 제공하고 있습니다. Tekton Pipelines나 Tekton Task에는 Tekton Hub라는, 일종의 Docker Hub와 같은 파이프라인을 운영하고 관리하기 위한 리포지터리가 있습니다. Tekton Hub에는 현재 수백 개의 파이프라인이 있고, 그중에는 저희가 보기에 개발자 여러분들이 많이 사용하실 만한 것들도 있는데요. 이와 같은 것들은 사내에서 사용하려면 추가로 설정이 필요한 경우가 있습니다. 대표적으로 Git Clone 같은 경우에는 주소를 GitHub.com에서 GitHub Enterprise의 주소로 바꿔야겠죠. 이와 같이 LINE에 사용할 수 있게 패치한 재사용 태스크들을 제공하고 있습니다. 

아래 목록은 PIPE에서 제공하는 재사용 태스크 중 일부를 가져온 것입니다.

 

먼저 대표적으로 컨테이너 일부를 빌드하는 태스크(build-container-image)가 있습니다. PIPE에서 제공하는 서비스는 기본적으로 컨테이너 런타임에서 실행되기 때문에 컨테이너 이미지를 빌드하는 과정이 필수입니다. 현재 컨테이너 이미지를 빌드할 수 있는 Kaniko나 BuildKit과 같은 여러 도구가 있는데요. PIPE에서는 PIPE에서 제공하는 별도 Buildkit 서비스로 연동할 수 있는 태스크를 만들어서 개발자 여러분들이 캐시를 이용해 최대한 빠르게 빌드할 수 있도록 제공하고 있습니다. Build-gradle 태스크도 마찬가지입니다. LINE에는 Nexus 리포지토리가 있는데요. 해당 Nexus 리포지토리로 퍼블리시(publish)할 수 있는 기능을 함께 제공하고 있습니다.  

정리해 보면, 업스트림에 있는 Tekton Hub에서 사용할 만한 것들을 추려 LINE 내부에서 필요한 기능들을 몇 가지 추가하고 변경하는 등의 작업을 거쳐 PIPE에서 큐레이션해서 제공하는 재사용 가능한 태스크가 있다고 보시면 될 것 같습니다.

 

스캐폴더로 세 가지 기본적인 CI/CD 유형 제공

두 번째로 앞서 소개했던 스캐폴더 기능에 대해서 말씀드리겠습니다. PIPE에서는 단순히 Tekton Pipelines 템플릿으로 YAML 파일을 생성하는 게 아니라, 개발자 여러분들에게 CI/CD 모범 사례라고 할 수 있는 최선의 방법을 좀 더 편하고 쉽게 적용할 수 있도록 제공하고자 노력했습니다. 스캐폴더는 어떻게 하면 최선의 방법으로 배포할 수 있을지 고민한 결과를 녹여낸 모델이라고 보시면 될 것 같습니다. 

 

현재 스캐폴더는 1개의 ci.yaml과 4개의 cd.yaml을 생성합니다. ci.yaml은 GitHub 푸시나 PR(Pull Request)이 들어왔을 때 구동되고, 4개의 cd.yaml은 각각에 맞는 이벤트가 들어왔을 때 호출됩니다. 간단한 샘플을 보여드리겠습니다.

먼저 ci.yaml은 기본 브랜치가 아닌 브랜치에 PR이 들어왔을 때 린트(lint)와 단위 테스트를 구동합니다. 물론 단위 테스트가 아니라 통합(intergration) 테스트 같은 것을 구동할 수도 있습니다. 

 

위와 같은 경우는 내부가 아니라 외부에서 포크된 리포지토리에서 변경이 발생해 PR이 들어왔을 때 구동되는 형태라고 보시면 될 것 같습니다. 오른쪽 구문을 보시면 tekton.dev/v1beta1에 pipeline이라는 kind가 있고, pipe.linecorp.com/event라는 애너테이션(annotation)을 구동하고 있습니다. 그 아래에서는 GitHub Actions 문법을 그대로 사용할 수 있습니다. GitHub Actions 문법을 그대로 넣으면 동일하게 구동됩니다. 현재 branches-ignore가 들어가 있기 때문에 메인 브랜치가 아닌 경우에는 ci라는 파이프라인이 구동됩니다. 

두 번째 예시를 보시겠습니다. 기본 브랜치에 변경이 발생하면 린트와 단위 테스트를 수행한 뒤 배포(deployment)를 생성해 스테이지(stage) 환경으로 배포하는 것까지 수행합니다.  

 

앞서 보여드린 예시와는 다르게 branches-ignore가 아니고 그냥 branches입니다. 즉 메인 브랜치에 변경이 발생하면 위 네 가지 작업을 수행합니다. 

PIPE에서는 메인 브랜치와 기본 브랜치에 변경이 발생하면 그때그때 빌드하고 배포까지 완료해서 스테이지 환경이 항상 기본 브랜치의 변경 내역이 반영된 최신 소스 코드로 구동되도록 서비스합니다. 이와 같은 형태에서 개발자들이 조금 더 편하고 쉽게 디버그할 수 있을 것이라고 생각해서 이를 기본 값으로 제공하고 있습니다. 

마지막으로 태그(tag)나 배포(deployment)가 생성되는 경우가 있습니다. 

 

스테이지 환경에 배포돼 있는 모델 중에서 어느 Git SHA256 태그를 배포하겠다고 설정해 놓은 경우 해당 SHA256 태그를 새로 생성하고 태그를 푸시하겠죠. 이때 push tags event가 트리거되며, 정규표현식을 지원하기 때문에 v 뒤에 *를 넣으면 cd라는 파이프라인이 구동됩니다. 이 경우에는 프로덕션으로 배포되는 모델이겠죠.

 

PIPE 런타임 

두 번째로 PIPE 런타임입니다. 아마 LINE 임직원이 아니시라면 Tekton보다 런타임에 더 관심이 가실 것 같습니다. 앞서 말씀드렸듯 PIPE에서는 쿠버네티스를 Namespace as a Service 모델로 제공해 드리고 있는데요. PIPE 런타임은 코어 클러스터(core cluster)와 런타임 클러스터(runtime cluster), 이렇게 두 가지 모델로 제공하고 있습니다.

 

코어 클러스터에서는 PIPE API와 웹사이트를 호스팅합니다. GitHub에서 들어오는 모든 이벤트는 바로 이 PIPE API로 들어가며, 추가로 OIDC(OpenID Connect) 인증도 코어 클러스터에서 진행합니다. 

런타임 클러스터는 개발자 여러분들의 빌드 요청을 수행하고 컨테이너가 구동되는 환경을 제공하는 클러스터로 지역(region)별로 배포합니다. 도쿄와 오사카, 싱가포르와 같이 여러 지역에서 배포하는데요. 인증과 API 요청은 하나의 코어 클러스터를 통해서 처리합니다.

또한 지역별로 배포하는 것 외에 대형 클러스터가 존재할 수 있습니다. 대표적으로 Kubeflow 같은 게 있는데요. 수천 개의 노드로 구성된 클러스터의 경우 기존 런타임 클러스터에서 구동하기에는 지나치게 부하가 높거나 노드 수가 많아서 관리하기 힘들 수 있기 때문에 별도 클러스터로 제공할 계획입니다.   

 

제공하는 쿠버네티스 소개 

PIPE에서 제공하는 쿠버네티스의 기본 사양을 설명하겠습니다. 

 

현재 PIPE에서 제공하는 쿠버네티스는 전부 물리 장비에서 구동하고 있습니다. 이는 경험에 따른 결과입니다. 과거에 다른 서비스에서 물리 장비와 가상 장비를 섞어서 서비스했는데요. 가상 머신에서 구동되는 노드는 대개 높은 빈도로 ‘non ready status’가 되거나 장애가 발생하는 것을 경험했습니다. 이런 경험에 기반해 PIPE에서는 가상 장비에 의존하지 않고 물리 장비만으로 서비스를 제공하고 있습니다.  

PIPE에서 배포에 사용하고 있는 툴은 kubeadm입니다. 별도로 Kubespray를 사용하지는 않았습니다. 처음에는 Kubespray로 프로비저닝하려고 했습니다만, Kubespray가 범용적인 툴이다 보니 LINE 상황에 딱 맞게 사용할 수 있는 기능이 조금 부족하다는 생각이 들었습니다. 이에 현재 자체 제작한 Ansible 스크립트를 통해서 프로비저닝하고 있는데요. 같은 상황에서 Kubespray를 사용하면 속도가 두 배 이상 느려졌습니다. 이 부분은 이후 슬라이드에서 좀 더 자세하게 설명하겠습니다.  

현재 PIPE에서 제공하는 쿠버네티스 버전은 v1.21.1입니다. 아직 서비스를 오픈한 지 얼마 안 된 시점이라서 최대한 테스트를 많이 해 본 버전으로 서비스하고 있는데요. 조만간 쿠버네티스 버전을 업그레이드할 계획입니다. 다만, 많이들 알고 계시겠지만 v1.22에서 기존에 있던 API들의 상당수가 deprecated 됐습니다. v1.21까지는 예전 버전의 API로 적용해서 구동할 수 있었지만, v1.22부터는 아예 에러가 발생하며 적용할 수 없게 됐습니다. 현재 이 문제를 어떻게 처리할지 고민하고 있습니다.

다음으로 쿠버네티스를 설치할 때 항상 선택해야 하는 것들이 있습니다. 네트워크 인터페이스와 런타임 인터페이스, 스토리지 인터페이스입니다. 그중에서 네트워크 인터페이스로는 Calico에 Typha IPAM 플러그인를 선택했고, 런타임 인터페이스로는 containerd를 선택했습니다. 마지막으로 스토리지 인터페이스입니다. 스토리지는 그때그때 추가하거나 삭제하는 게 크게 무리가 없어서 이슈가 될 만한 부분이 별로 없습니다. 현재 PIPE에서는 OpenEBS와 LINE 사내용 플랫폼인 Verda에서 제공하는 공유 파일 시스템(Shared File System), 이 두 가지를 제공하고 있습니다. 

PIPE에서 네트워크 인터페이스로 Calico에 Typha IPAM 플러그인을 제공하게 된 이유는, PIPE에서 제공하는 런타임이 확장 가능(scalabled)해야 한다는 요건 때문이었습니다. 실제로 Kubeflow 같은 경우에는 대단위 노드로 활용하는 경우가 많습니다. 그러다 보니 노드 수가 적게는 1,000개, 많으면 2,000개에서 3,000개까지 필요한 경우가 있는데요. 그와 같은 경우에 가장 큰 부하가 발생하는 부분이 네트워크 인터페이스였고, 두 번째가 스케줄러였습니다. 이때 Kubeflow 같은 머신러닝을 사용하는 경우에는 스케줄러의 부하보다는 네트워크 인터페이스의 부하가 훨씬 더 클 것이라고 예상했습니다. 

이에 그런 대단위 노드를 서비스할 수 있는 플러그인을 찾다가 전통적인 Calico를 선택했습니다. Calico는 쿠버네티스에서 테스트할 때도 사용하고 있으니 어떻게 보면 사실상 표준이라고도 할 수 있습니다. 거기에 Typha라는 플러그인을 적용해 봤는데요. 성능이 꽤 잘 나와서 현재 Calico에 Typha IPAM 플러그인으로 서비스를 제공하고 있습니다. Typha 플러그인은 Calico와 Kube API 서버 사이에 위치하는 일종의 프락시 노드라고 보시면 될 것 같습니다. 각각의 노드가 IP를 직접 Kube API 서버에서 가져오는 게 아니고 Typha라는 단계를 하나 더 거쳐서 가져옵니다. 이를 통해 Kube API 서버의 부하를 분산하고 줄일 수 있습니다. 하나의 Typha 노드는 대략 200개 정도의 Calico 노드를 서비스할 수 있다고 나와 있는데요. 그렇다면 이론적으로 쿠버네티스 노드 4,000 개까지도 서비스할 수 있다는 얘기입니다. 물론 아직 그 정도로 대용량의 쿠버네티스 클러스터 요청은 없었기 때문에 실제로 테스트해 본 건 아닙니다만, 대략 1,000개 정도의 노드로 테스트해 봤을 때 별문제는 없었습니다. 또한 PIPE에서 제공하는 노드에는 역할이 있습니다. 컨트롤 플레인과 빌드, 게이트웨이, 아무 역할도 없는 노드, 이렇게 총 네 가지 유형의 노드 역할을 제공합니다. 각 워크로드는 노드의 역할에 따라서 스케줄링됩니다.

아래는 PIPE에서 사전에 설치해 놓는 컴포넌트입니다.  

 

서비스 메시(Service Mesh)는 Istio입니다. 인그레스(ingress)는 기본 설정은 Contour인데요. ingress-nginx도 있기 때문에 인그레스 클래스를 통해서 nginx를 선택해 사용할 수도 있습니다. 로드 밸런서는 Verda에서 제공하는 로드 밸런서와 연동돼 있고, 빌드로는 Tekton과 buildkitd, 두 가지를 준비했습니다. 모니터링(observability)을 위해서는 Fluent Bit와 Prometheus, 두 가지를 준비했고, 인증으로는 Dex, 권한 관리로는 Kyverno, 스토리지로는 OpenEBS와 VSFS, 두 가지를 준비했습니다. 

이 중에서 인증 과정에 대해 설명드리겠습니다. 

 

먼저 런타임이 쿠버네티스 코어 클러스터에서 구동되고 있는 Dex를 통해서 OIDC 엔드포인트를 찾아옵니다. 이때 코어 클러스터가 먼저 준비돼 실행 중이어야 한다는 전제가 필요하겠죠. 코어 클러스터와 Dex가 실행되고 있을 때 런타임의 Kube API 서버가 OIDC 엔드포인트를 찾아옵니다. 다음으로 사용자가 ‘kubectl get pod’와 같이 리소스를 요청합니다. 요청받은 런타임은 자체적으로 ID 토큰의 유효성을 검증해서 유효하면 요청을 처리하고, 유효하지 않으면 GitHub의 OAuth2 Flow를 통해서 새로 JWT(JSON Web Token)를 발급합니다. 발급된 JWT를 갖고 Dex로 가면, Dex에서 ID 토큰이 나옵니다. 이 ID 토큰을 갖고 다시 런타임으로 가면 사용자 요청을 처리합니다. PIPE에서는 현재 Dex에서 16시간짜리 토큰을 발급해서 개발자가 처음 로그인하면 대략 하루 정도 크게 문제없이 사용할 수 있을 정도의 라이프 사이클을 제공하고 있습니다.

두 번째로 권한 관리에 대해 말씀드리겠습니다. 

 

PIPE에서는 권한 관리 플러그인으로 쿠버네티스의 기본 RBAC과 Kyverno를 제공합니다. 기본적으로 RBAC으로 권한을 관리하며, RBAC으로 부족한 경우에는 Kyverno로 한 번 더 관리합니다. Kyverno를 선택한 이유는 Kyverno가 Open Policy Agent에 비해서 좀 더 사용하기 편리하기 때문입니다.  

개발자 여러분들은 아래 보이는 PIPE 대시보드에 접속해서 kubeconfig 파일 다운로드와 네임스페이스 생성, PIPE API를 호출할 수 있는 API 키 발급, GitHub 앱 설치 등의 기능을 수행하실 수 있습니다. 

 

서비스 수준을 높이기 위한 노력

다음으로 PIPE의 서비스 수준을 어떻게 향상시킬 것인지 고민한 결과를 조금 더 자세히 말씀드리겠습니다. 

 

모니터링 체계

먼저 모니터링입니다. 일반적으로 널리 사용하는 Grafana와 Prometheus를 모두 사용하고 있습니다. 

 

Prometheus 내부에는 Alertmanager와 Kube-state-metrics, node-exporter가 있습니다. 그 외에 metrics-server와 node-problem-detector 등도 운영하고 있습니다. 

앞서 언급한 Kube-state-metrics에서는 노드가 준비 상태인지 혹은 클라이언트 인증서가 다음 주에 만료되는지 등과 같은 경고 알림을 미리 설정해 놓은 Slack 채널로 전송합니다. 이를 통해 작업 필요 여부를 쉽고 빠르게 인지할 수 있습니다. 

 

그 외에 파드 상태가 좋지 않거나 파드가 계속 CrashLoop 상태에 빠져있을 때 혹은 엔드포인트 주소가 준비 상태가 아닌 경우 등 각종 상황에 대한 경고 알림이 Slack 채널로 전송되게 설정해 놓았습니다.

 

관리 체계 확립

PIPE를 관리하기 위해 현재 세 가지 작업을 진행하고 있습니다. 

 

첫 번째로, 쿠버네티스의 가장 핵심적인 데이터베이스인 etcd를 30분마다 백업합니다. 클러스터 내부적으로 백업하는 것은 아니고, 앞서 말씀드린 LINE 사내용 플랫폼인 Verda의 오브젝트 스토리지에 백업합니다. Amazon의 S3와 비슷한 형태라고 보시면 됩니다. 

두 번째로, 사용자의 PipelineRun이 구동되고 나서도 런타임 내부에 계속 남아 있으면 쿠버네티스 클러스터, 특히 etcd 서버에 부하가 많이 발생하기 때문에 일주일을 단위로 설정해 일주일이 넘은 PipelineRun은 삭제하고 있습니다.

마지막으로, PIPE에서는 PIPE 리포지터리 이 외에도 PIPE 컨트롤 플레인이라는 리포지터리를 별도로 운영하고 있습니다. 여기서는 PIPE 클러스터를 프로비저닝하거나 노드를 증설 혹은 줄이는 등의 관리 작업을 진행합니다. 

 

처음에는 Ring 0 클러스터라는 개념을 적용해서 Rancher와 비슷하게 클러스터를 관리하는 상위 클러스터를 두는 형태였습니다. Kubefed와 비슷하다고도 볼 수 있는데요. 이와 같은 방식으로 운영해 보니 상위 클러스터에서 어떤 인증서가 만료되거나, 노드가 다운되거나, 로드 밸런서에 문제가 발생하면 하위 클러스터도 오퍼레이션할 수 없는 문제가 발생했습니다. 상위 클러스터의 문제가 하위 클러스터로 전파된 것인데요. 이를 해결하기 위해 구조를 변경했습니다. 현재 PIPE 컨트롤 플레인은 쿠버네티스가 아니라 VM에서 실행되고 있는 Jenkins로 구성돼 있고, 그 Jenkins에서 PIPE 자체에 대한 CI/CD 수정을 진행하고 있습니다. 순환 참조를 막기 위한 방법이라고 보시면 될 것 같습니다. 쿠버네티스 클러스터 자체는 직접 만든 Ansible Playbook으로 운영합니다. 마지막으로 쿠버네티스의 각 컴포넌트는 Helmfile과 kustomize로 관리합니다.

 

노드 역할 분리 

앞서 말씀드렸듯 PIPE에서는 노드의 역할을 분리해 놓았습니다. 개발자의 워크로드와 빌드 워크로드를 서로 다른 노드로 스케줄링하고 싶었기 때문입니다.

 

빌드는 작업 특성상 CPU와 메모리를 상당히 많이 사용하기 때문에 원래 실행되고 있던 개발자의 워크로드에 영향을 끼칠 수밖에 없습니다. 이를 방지하기 위해 빌드가 실행되는 노드와 사용자의 워크로드가 실행되는 노드를 Kyverno를 통한 뮤테이션 웹훅을 이용해 명시적으로 완전히 분리했습니다. 

 

향후 계획

마지막으로 향후 로드맵을 말씀드리겠습니다. 

 

고가용성 확보

현재 PIPE의 가장 큰 약점이라면 고가용성을 들 수 있습니다. 여러 클러스터가 아니라 하나의 클러스터로 운영하고 있기 때문에 만약 클러스터에 문제가 발생하면 수많은 개발자 여러분들이 피해를 볼 수 있습니다. 

 

이 문제를 해결하고자 조만간 LINE 내부에 멀티플 AZ(Availability Zone)를 설정해서 운영할 예정입니다. PIPE 자체에 대한 컨트롤 플레인과 워커 노드에 멀티플 AZ 방식을 도입할 예정인데요. 컨트롤 플레인의 etcd와 API 서버, 컨트롤 매니저, 스케줄러와 워커 노드를 여러 개의 AZ에 분산시켜서 특정 IDC에 문제가 발생하더라도 서비스를 지속할 수 있도록 개선할 예정입니다. 

 

PIPE Functions 개발

두 번째로 PIPE Functions라는 기능을 개발하고 있습니다. Azure Functions나 AWS Lambda와 비슷한 기능이라고 생각하시면 될 것 같은데요. 현재 기본 인프라는 모두 설치 완료 후 운영하고 있기 때문에 좀 더 상위 수준에서, 조금 더 사용하기 편한 플랫폼을 제공하기 위해 개발하고 있습니다.

 

먼저 개발자 여러분들이 클라우드의 이점을 그대로 활용할 수 있는 클라우드 네이티브 이벤트를 활용하고, 워크로드를 좀 더 쉽고 간단하게 구동시킬 수 있게 하려고 합니다. 또한 현재 PIPE를 사용하려면 쿠버네티스의 인프라 구조를 깊이 이해하고 있어야 하는데요. 향후 그런 이해가 뒷받침되지 않아도 사용할 수 있게 만드는 것이 목표입니다. Heroku나 Netlify, Google App Engine과 비슷한 모델이라고 생각하시면 될 것 같습니다.  

마지막으로 LINE 내부 플랫폼인 Verda에서 제공하고 있는 Verda Functions을 PIPE Functions로 대체하고자 합니다. 대체하는 과정에서 좀 더 많은 이벤트를 제공할 생각이며, 현재 Verda function에서는 Python만 지원하지만 기본적인 언어를 모두 포함해 더욱 많은 언어를 지원하려고도 합니다. 이를 통해 개발자 경험을 더욱 향상시키는 것이 저희의 목표 중 하나입니다. 

 

CPaaS: Control Planes as a Service

아직 조사 단계지만 CPaaS도 고려하고 있습니다. CPaaS는 현재 PIPE에서 제공하는 Namespace as a Service 기반의 클러스터와 독자적인 클러스터의 중간 단계 모델(vCluster)이라고 생각하시면 됩니다.  

 

개발자 여러분들이 vCluster를 설정하면 언더레이(underlay) 쿠버네티스1에서 봤을 때 이 vCluster는 하나의 배포(deployment)로 보이지만, 그 안에는 풀 스케일의 쿠버네티스가 구동되고 있는 형태입니다. 즉 Minikube 같은 형태라고 보시면 될 것 같습니다. 이와 같은 클러스터를 제공하면 개발자 여러분들이 커스텀 자원 정의나 커스텀 컨트롤러 등에 기반한 개발을 편리하게 진행하실 수 있을 것이라고 생각합니다. 그 외에도 확장성을 좀 더 확보할 수 있는 측면도 있습니다. 앞서 말씀드렸듯 아직 조사하고 있는 단계인데요. 추후 좀 더 많은 경험이 쌓이면 제공할 예정입니다. 

 

VM 기반 CRI(Container Runtime Interface)

마지막으로 VM 기반 CRI가 있습니다. 

 

현재 거의 표준으로 자리 잡은 솔루션으로 AWS의 Firecracker나 Kata Containers가 있는데요. 이런 솔루션들을 이용해 보안 관점에서 좀 더 안전한 런타임 환경이 필요한 경우 컨테이너가 아닌 VM 기반으로 런타임을 운영할 수 있는 기능을 제공할 계획입니다. 

 

마치며

이번 글에서는 개발자의 생산성을 향상시키고 좀 더 좋은 개발자 경험을 제공하는 것을 목표로 CI/CD에 런타임을 통합해 만든 PIPE라는 솔루션을 소개했습니다. 향후 계획에서 말씀드렸듯 여기서 끝이 아니라 앞으로 지속적으로 개선해 더 나은 솔루션으로 만들어 나갈 예정이니 많은 기대 바랍니다. 긴 글 읽어주셔서 감사합니다.

 


 

  1. 오버레이(overlay) 쿠버네티스는 사용자 관점에서 보이는 가상의 클러스터(virtual cluster, vCluster)이고, 언더레이 쿠버네티스는 오버레이 쿠버네티스들을 구동하기 위해 오버레이 쿠버네티스의 하부에 존재하는 쿠버네티스 클러스터입니다.