Skip to main content

Argo Events 도입기

· 18 min read
박도준

들어가며

안녕하세요, 비브로스에서 DevOps 엔지니어로 일하고 있는 박도준입니다.

최근 비브로스에 새롭게 합류하여 온보딩 기간을 거쳐 첫 업무를 맡게 되었습니다. 해당 업무는 Argo Events를 사내에 도입하는 것이였습니다. 이번 글에서는 "왜" Argo Events를 도입하는지와 "어떻게" 구성하고 사용하는지 공유드리려 합니다.

배경

저희 똑닥 서비스는 실시간 병원 접수 및 예약 서비스을 제공하고 있습니다. 현재 대부분의 유저들은 병원 접수, 예약 기능을 이용하기 위해 똑닥 서비스를 사용하기 때문에 병원 이용을 많이 하는 시간대에 서비스 트래픽이 몰리는 현상이 있습니다. 이러한 현상은 요일별 그리고 시간대별로 상이하지만 어느 정도 패턴화가 되었기 때문에 크론 작업을 통해 매일 인프라 스케일링 동작이 수행되도록 자동화해두었습니다.

하지만 위와 같이 정기적이고 패턴화된 상황이 아닌 경우도 있습니다. 😱

  1. 마케팅 이벤트
  2. 연휴 다음날

    연휴날은 대부분 병원도 휴무인 경우가 많기 때문에 연휴 다음날 아침 평소대비 트래픽이 증가하는 경향이 있습니다.

이런 경우에 이전에는 (1)마케터분들께 마케팅 이벤트 관련 정보를 미리 받거나 (2)연휴 전날 수작업으로 크론 작업을 새롭게 생성하여 등록하는 번거로운 방식으로 운영해왔습니다. 물론 크론 작업에 대한 정의를 helm chart로 관리하여 템플릿화했지만 이벤트에 따라 달라지는 설정값들을 일일이 수정하여 배포까지 해줘야했기 때문에 조금은(많이) 귀찮은 작업이였습니다. donoting.png

똑닥 서비스가 계속해서 성장하고 확장됨에 따라 이러한 이벤트가 더욱 비번하게 발생할 것으로 예상되어 해당 프로세스를 개선할 필요성이 점점 더 커지게 되었습니다.

그래서 왜 Argo Events야?

배경을 읽으셨다면 대충 'Argo Events를 통해 앞선 문제를 해결했겠구나'라는 것을 알 수 있을 것입니다.

??? : 그래서 어떻게 해결했어?!? stop.png

바로 본론으로 들어가기에 앞서 Argo Events가 무엇이고 왜 Argo Events를 선택했는지부터 간단히 공유드리겠습니다.

Argo Events란?

Argo Events의 공식 Docs에 나와있는 소개글을 번역해보면 아래와 같습니다.

Argo Events는 webhook, S3, 스케줄 등을 다양한 소스들의 이벤트에서 k8s 객체, Argo workflows, 서버리스 워크로드 등을 트리거할 수 있도록 지원하는 kubernetes용 이벤트 기반 워크플로우 자동화 프레임워크입니다.

argo-events.png

간단하게 정리하면 다양한 이벤트들을 통해 k8s 객체나 Argo Workflows 등을 트리거하여 자동화된 워크플로우를 제공하는 서비스입니다.

Argo Events Architecture

Argo Events의 구성요소와 아키텍처는 아래와 같습니다. argo-events-arch.png

위 아키텍처 그림에 잘 표현되어 있지만 구성 요소에 대해 간단하게 살펴보겠습니다.

  • Event Soucre : AWS SNS, Webhooks 등과 같은 외부 소스의 이벤트를 사용하는데 필요한 구성을 정의
  • Event Bus : EventSource와 Sensor를 연결하는 전송 계층 역할
    • NATS는 deprecated 되기 때문에 Jetstream과 Kafka 이렇게 2가지 선택지가 존재
  • Sensor : 일련의 이벤트 종속성(입력) 및 트리거(출력)를 정의
    • Sensor 당 하나 이상의 Trigger를 가질 수 있으며, 이를 통해 다양한 워크플로우를 실행할 수 있음

Argo Events 선택 이유

저희 팀에서 Argo Events를 선택한 이유는 크게 3가지입니다.

1. 이벤트 기반(Event-Driven) 아키텍처 구축

이전에는 직접 크론 작업을 직접 작성해서 등록해야했지만, Argo Events를 통해 해당 작업을 이벤트 기반으로 자동화하는 아키텍처를 구현할 수 있다고 판단했습니다.

2. 기존 인프라와의 통합

똑닥 서비스는 kubernetes상에서 운영되고 있으며, 스케일링 작업을 등록하는 크론 작업은 Argo Workflows의 CronWorkflow를 통해 수행되고 있습니다. 때문에 Argo Events를 기존 인프라 환경에 통합하는 것에 큰 문제가 없을 것으로 판단했습니다.

3. 다양한 이벤트 타입 제공

현재는 Webhook 기반의 이벤트를 바탕으로 사용하지만, Argo Events에서 다양한 이벤트 타입을 지원하기 때문에 추후 확장하여 사용하기에 용이하겠다는 판단을 했습니다.

Argo Events 구성

그러면 이제 Argo Events를 어떻게 구성하여 사용하는지 공유드리겠습니다.

전체적인 구성도는 아래와 같습니다. flow.png

위 구성도의 흐름을 바탕으로 하나씩 살펴보겠습니다.

  1. 인프라 스케일링을 위해 비브로스 사용자는 똑닥 PA(Product Admin) 페이지에서 스케일링을 위한 정보들을 입력합니다.
  • 스케일링을 위한 정보로는 이벤트 담당자, 이벤트 시작/종료 시간, 이벤트 제목, 이벤트 설명 등이 포함됩니다.
  1. PA에서는 요청 정보를 바탕으로 크게 3가지 API를 호출합니다.
  • 3가지 API로는 인프라 증설 요청/인프라 축소 요청/이벤트 삭제 요청이 있습니다.
  • 배경에서 말씀드린것과 같이 똑닥서비스의 트래픽은 요일별 그리고 시간대별로 상이하기 때문에 API를 설계할때도 이를 반영합니다.
  1. Argo Events에서는 해당 API들에 대한 정의를 해두어, 요청이 들어오면 API별로 트리거 동작이 수행됩니다.
  • 해당 과정에 대해서는 아래에서 조금 더 상세하게 살펴보겠습니다.
  1. 모든 동작이 수행되면 Slack으로 해당 workflow에 대한 정보들을 전달하고, 동작이 끝난 workflow를 삭제합니다.

아래에서 Argo Events를 어떻게 구성했는지 조금 더 상세히 살펴보겠습니다.

Argo Events 설치 정보

Argo Events는 helm chart를 이용해 구성했습니다.

EventBus

apiVersion: argoproj.io/v1alpha1
kind: EventBus
metadata:
name: eventbus
namespace: argo-events
spec:
jetstream:
version: latest
replicas: 3
streamConfig: |
maxAge: 24h
auth: token
  • NATS는 deprecated되어 Jetstream 사용
  • replicas는 최소 3개가 필요하고, 3~5개로 제한하여 사용
  • auth는 클라이언트가 NATS 스트리밍 서비스에 연결할 때 사용하는 방식으로, 현재 nonetoken 방식을 지원
    • token 방식을 사용하면 client용과 server용으로 k8s secret을 각각 생성하여 EventSource와 Sensor 파드에서 client secret을 로드해 EventBus에 연결할 때 사용

EventSource

apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: eventsource
namespace: argo-events
spec:
eventBusName: eventbus
webhook:
create-infra-common-common:
endpoint: /create-infra-common-common
method: POST
port: "8888"
url: ""
delete-infra-common-night:
endpoint: /delete-infra-mon-night
method: POST
port: "9999"
url: ""
.
.
.
  • 같은 namespace에 있는 eventbus만 사용이 가능하고, 같은 namespace에 여러 개의 eventbus가 존재하는 경우 eventBusName에 지정해야함
  • Webhook 타입으로 여러 이벤트에 대한 수신을 받기 위해 위와 같이 구성

Sensor

apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: peaktime-sensor-create-infra-common-common
namespace: argo-events
spec:
template:
serviceAccountName: argo-events
eventBusName: eventbus
dependencies:
- name: eventsource
eventSourceName: eventsource
eventName: create-infra-common-common
triggers:
- template:
name: peaktime-create-infra-common-common-trigger
k8s:
operation: create
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
name: peaktime-create-infra-common-common
namespace: argo-events
spec:
schedule: "0 9 16 4 *" # 4월 16일 오전 9시
workflowSpec:
arguments:
parameters:
- name: title
- name: eventDate
entrypoint: start-batch-peaktime-create-infra-common-common
onExit: end-notification
serviceAccountName: argo-events
templates:
- name: start-batch-peaktime-create-infra-common-common
steps:
- - name: <app-service-name>-create-infra-common-common
templateRef:
template: dd-account-batch-start
- - name: <app-service-name>-create-infra-common-common
templateRef:
template: dd-app-batch-start
.
.
.

- name: end-notification
inputs:
parameters:
- name: title
value: {{workflow.parameters.title}}
- name: eventDate
value: {{workflow.parameters.eventDate}}
steps:
- - name: peaktime-slack-noti
templateRef:
name: peaktime-slack-noti
template: slack-noti
arguments:
parameters:
- name: title
value: {{inputs.parameters.title}}
- name: eventDate
value: {{inputs.parameters.eventDate}}
- name: cron-template-delete
steps:
- - name: cron-template-delete
templateRef:
template: workflow-template-cron-delete
parameters:
# 제목 (title)
- src:
dependencyName: eventsource
dataKey: body.message.title
dest: spec.workflowSpec.arguments.parameters.0.value
# 이벤트 시간 (eventDate)
## cronworkflow name 뒤에 추가되는 parm
- src:
dependencyName: eventsource
dataKey: body.message.eventDate
dest: metadata.name
operation: append

sensor의 경우 포함되는 데이터가 많아 많은 부분이 생략되었으니 참고 부탁드립니다.

  • Event를 받기 위해 EventSource와 EventBus를 정의
  • Sensor는 EventSource내에 정의한 event별로 생성하여 관리
  • Sensor를 통해 CronWorkflow가 생성되는 트리거 정의
    • CronWorkflow를 사용하는 이유는 schedule을 통해 원하는 시간에 스케일링 동작이 수행되게 하기 위함
    • CronWorkflow명은 유일해야하기 때문에 사용자로부터 이벤트 시간(eventDate) 정보를 전달받아 append하는 동작으로 유일성 보장
  • CronWorkflow에서는 정의된 schedule을 event에 대해 앱서비스별로 스케일링 동작을 수행하는 workflowTemplate 호출
    • 실제 모든 코드는 helm chart로 관리하기 때문에 위와 같이 일일히 수작업으로 모든 템플릿 정보에 대해 정의하지 않음
  • 스케일링 동작을 수행하는 단계가 끝나면 순서대로 slack으로 알림 발송 후 해당 CronWorkflow를 삭제하는 동작 수행
  • parameters는 API Body에 포함되어 있는 값으로, workflow 정의 및 slack 알림 발송에 사용

위의 예시는 operation: create로 CronWorkflow를 생성하여 인프라 증설/축소 작업을 수행합니다. 이를 활용하여 operation: delete를 사용해 이미 생성된 CronWorkflow를 삭제하는 이벤트 삭제 작업을 수행합니다.

실행 결과

CronWorkflow

cronworkflow.png Webhook으로 sensor에서 트리거 동작이 수행되면 위와 같이 CronWorkflow가 생성됩니다.

Workflows

workflow.png 서비스별로 scaling 동작을 수행하는 Workflow가 실행된 후, Slack 알림 발송 Workflow가 실행됩니다.

Slack 알림 발송

slack.png 실행된 Workflow에 대한 정보들이 slack으로 알림 발송 됩니다.

TroubleShooting

작업을 진행하며 겪었던 문제와 해결방법에 대해 공유드리겠습니다.

1. Helm Chart 표현

현재 관리 편의성을 위해 argo events 관련 서비스들은 helm chart로 관리하고 있습니다. 이때 sensor나 workflow을 템플릿으로 표현하는 과정에서 구문 관련 이슈를 겪었는데 다음과 같습니다.

예를 들어 parameter에 대한 값을 사용하기 위해 아래와 같이 템플릿을 정의하면 에러가 발생합니다.

- name: end-notification
inputs:
parameters:
- name: title
value: {{workflow.parameters.title}}

⚠️ Error: function "workflow" not defined

위와 같은 에러가 발생하는 이유는 Helm의 Go 템플릿 언어와 Argo 템플릿 언어 모두 {{ }}을 사용하여 충돌이 발생하기 때문입니다.

이를 해결하기 위해선 다음과 같이 백틱(backtick)을 사용하여 백틱으로 구분된 내용을 문자 그대로 해석하도록 정의해야 합니다. 아래는 수정 후의 템플릿 정의입니다.

- name: end-notification
inputs:
parameters:
- name: title
value: {{`{{workflow.parameters.title}}`}}

2. 권한 할당

앞서 정의한 Sensor를 보면 event가 트리거되면 CronWorkflow를 생성하는 작업이 수행됩니다. 해당 작업이 수행되기 위해선 Sensor 파드에서 CronWorkflow를 생성하는 권한이 필요하기 때문에 이를 위해 CronWorkflow를 생성하는 권한을 가진 serviceaccount를 할당해줘야 합니다.

# servieaccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo-events
namespace: argo-events

# clusterrole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: argo-events
rules:
- apiGroups:
- argoproj.io
resources:
- cronworkflows
- cronworkflows/finalizers
verbs:
- create
- get
- list
- watch
- update
- patch
- delete

# clusterrolebinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-events
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: argo-events
subjects:
- kind: ServiceAccount
name: argo-events
namespace: argo-events
# sensor
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: peaktime-sensor-create-infra-common-common
namespace: argo-events
spec:
template:
serviceAccountName: argo-events
.
.
.

마치며

이전에는 인프라 증설 요청부터 템플릿 작성 그리고 배포까지 devops 엔지니어가 일일이 참여하여 수작업으로 진행했었습니다. flow2.png 하지만 Argo Events를 도입함으로써 이벤트에 대한 정보만 전달받고 그 이후 동작은 모두 자동화하여 업무 효율성을 크게 향상할 수 있었습니다. flow3.png

👇(요약 사진) click.png

무엇보다 해당 작업을 진행하며 사내 인프라 및 전체적인 업무 프로세스에 대해 더 잘 알게되었고 내부 팀원들과 조금 더 가까워질 수 있게된 좋은 경험이였습니다. 나 혼자만의 착각일수도..^^

이 경험을 바탕으로 앞으로 많은 프로젝트를 통해 더 나은 똑딱 서비스를 만들어가는 부스터가 되겠습니다. :0

또한, 서투른 첫 블로그 글이지만 해당 글이 앞으로 Argo Events를 도입하려는 분들께 도움이 되었으면 좋겠습니다.

감사합니다!🙂

참고 자료