쿠버네티스(Kubernetes)를 활용한 안정적이고 효율적인 운영 환경 구성하기

배포? 더이상 두렵지 않아요

서진형 황성구 이정민 | 2020년 09월 25일

핑퐁팀에서는 딥러닝 Model 서버를 포함한 많은 수의 서버들을 운용하고 있고, 이를 Kubernetes 상에서 효율적으로 운영하고 있습니다.

이번 글에서는 핑퐁팀이 Kubernetes를 활용하여 어떻게 안정적이고 효율적인 운영 환경을 구성했는지 소개해드리고자 합니다.

Continuous Delivery

Kubernetes 위에서 돌아가는 운영 환경을 소개하기에 앞서, 일반적으로 CD라고 부르는 Continuous Deployment보다 포괄적인 개념인 Continuous Delivery를 어떻게 구성했는지부터 소개하고자 합니다.

https://aws.amazon.com/ko/devops/continuous-delivery/

큰 규모의 업데이트를 드물게, 그리고 수동으로 진행하는 것은 많은 위험이 따릅니다. 작은 규모의 지속적이고 자동화된 배포 과정을 통해 생산성과 품질의 신뢰도를 높일 수 있습니다. (덧붙여 개발자의 수명을 늘려줍니다.)

Workflow

핑퐁 개발팀에서는 아래와 같은 워크플로우를 따릅니다.

대부분 익숙한 구성이지만, Github Actions와 관련해서는 아직 자료가 많지 않은 것 같아 아래와 같이 공유합니다.

Github Actions

name: <Workflow Name>
on:
  release:
    types: [released]

jobs:
  build:
    ...
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Get Tag Name
        ...
        run:
          echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
      - name: Dockerize
        ...
    outputs:
      VERSION: ${{ steps.get_tag_name.outputs.VERSION }}
  push:
    needs: build
    ...
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Push Image to ECR
        run:
          docker push <ECR Repository>:${{ needs.build.outputs.VERSION }}

Github Release 시에만 트리거되도록 설정할 수 있고, 각 Job의 의존관계를 설정하여 실행할 수 있습니다.

이전 Job의 결과를 가져와서 사용할 수 있고 위의 secrets.[]와 같이 민감한 정보는 Github Repository의 Secrets로 관리할 수 있습니다.

이 외에도 Github marketplace에서 편리하게 사용하는 기능을 찾을 수 있습니다.

배포 현황

루다의 업데이트 배포 현황

자동화된 배포 시스템 덕분에 핑퐁팀의 개발자는 코드에만 집중할 수 있고, 루다는 나날이 다재다능해집니다. 위 그림에서 4주 간(2020.08.20 ~ 2020.09.17) 10번의 업데이트가 있었던 것을 확인할 수 있습니다.

Kubernetes 구성

핑퐁팀의 Kubernetes Cluster는 AWS EKS 환경에서 운영합니다. 루다 등의 제품은 각각 CPU Node 위에서 돌아가는 서비스와 GPU Node 위에서 돌아가는 Model 서비스로 이루어집니다.

각 서비스의 구성요소는 다음과 같습니다.

주요 설정의 예시는 아래와 같습니다.

apiVersion: v1
kind: Service
metadata:
  name: <service>-svc
  namespace: <namespace>
spec:
  selector:
    app: <service>
  ...

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <service>-deployment
  namespace: <namespace>
spec:
  ...
  selector:
    matchLabels:
      app: <service>
  template:
    metadata:
      labels:
        app: <service>
    spec:
      containers:
      - name: <service>
        image: <ECR Image>:<Tag>
        imagePullPolicy: Always
        ...
        readinessProbe:
          ...
        livenessProbe:
          ...
      nodeSelector:
        type: <namespace>-(cpu|gpu)

---

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: <service>-hpa
  namespace: <namespace>
  labels:
    app: <service>
spec:
  ...
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: <service>-deployment

Ingress의 경우, ingress들만 관리하는 파일에서 해당 경로가 적절한 Service에 매칭되도록 설정했습니다.

Pod 사이의 Synchronize

Kubernetes의 큰 장점은 코드로 인프라를 잘 정의해주기만 하면 세부적인 관리는 알아서 해준다는 점입니다. 하지만 상황에 따라서는 Pod 안에서 Kubernetes가 관리하는 정보를 가져와야할 필요도 있습니다.

예를 들어, ReplicaSet을 구성하는 각 Pod의 상태를 동기화하기 위해 로드 밸런서를 우회하여 ReplicaSet 내 모든 WAS에 직접 API를 요청하는 경우를 들 수 있습니다.

루다의 배포 시스템에서는 bitnami/kubectl 이미지를 활용해서 프록시를 실행시킬 수 있는 사이드카 컨테이너를 Deployment에 추가하고, 이를 이용해서 ReplicaSet 내 모든 Pod의 Endpoint를 REST API를 통해 읽어와 요청을 전파합니다. (단, API Server에 접근할 수 있는 적절한 ServiceAccount 설정이 필요합니다.)

Kubernetes Pod 내부에서 아래와 같이 호출하고 <ip>에 해당하는 정보를 읽어와서 원하는 Pod들에 대해 API를 호출합니다.

root@<pod_id>:/# curl http://localhost:8001/api/v1/namespaces/<namespace>/endpoints
{
  "kind": "EndpointsList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/namespaces/<namespace>/endpoints",
    "resourceVersion": "13942454"
  },
  "items": [
    {
      "metadata": {
        "namespace": "<namespace>",
        ...
      },
      "subsets": [
        {
          "addresses": [
            {
              "ip": "<ip>",
              ...
              "targetRef": {
                "kind": "Pod",
                ...
              }
            }
          ],
          ...
        }
      ]
    },
    ...
  ]
}

딥러닝 모델 서빙

핑퐁팀의 딥러닝 모델들은 tensorflow/serving으로 Serving합니다.

모델별 Config를 설정하고 tensorflow/serving 이미지를 Wrapping하는 이미지를 만들 수도 있지만, 의도치 않은 디펜던시가 생기는 문제가 발생합니다.

아래와 같이 env에 Credential을 설정하면 Model과 Config를 S3에서 받아와서 동적으로 처리할 수 있습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <service>-deployment
  namespace: <namespace>
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: <name>
        image: tensorflow/serving:<version>-gpu
        args:
          - --model_config_file=s3://<S3 Bucket>/<S3 Object>
        env:
        - name: AWS_ACCESS_KEY_ID
          valueFrom:
            secretKeyRef:
              key: AWS_ACCESS_KEY_ID
              name: aws-credential
        - name: AWS_SECRET_ACCESS_KEY
          valueFrom:
            secretKeyRef:
              key: AWS_SECRET_ACCESS_KEY
              name: aws-credential
        - name: AWS_REGION
          value: ap-northeast-2
        - name: S3_USE_HTTPS
          value: "true"
        - name: S3_VERIFY_SSL
          value: "true"
        ...
      nodeSelector:
        type: <namespace>-gpu

이를 위한 Credential을 구성하는 secret은 다음과 같이 설정할 수 있습니다.

apiVersion: v1
metadata:
  name: aws-credential
  namespace: <workspace>
data:
  AWS_ACCESS_KEY_ID: <AWS_ACCESS_KEY_ID>
  AWS_SECRET_ACCESS_KEY: <AWS_SECRET_ACCESS_KEY>
kind: Secret

Logging

Analytics를 위해 Google BigQuery로 로그를 Streaming합니다. 정상적인지 혹은 에러인지 등의 로그 유형에 따라 Sidecar 패턴으로 구성된 Log Streamer가 이를 처리하고, 필요하면 Slack 알림을 보내 개발자들이 상황에 대처하도록 처리합니다.

그 외에도 사내에 Airflow로 구성한 Log Pipeline이 있고, Batch Job으로 데이터 백업 및 정제/분석 작업을 수행합니다. 이에 관한 일련의 과정은 이후에 게시될 일상 대화 챗봇 분석을 위한 Analytics 구축기에서 다룰 예정입니다.

배포 방법

배포 방법은 매우 간단합니다. CI에서 Dockerize 한 후에 ECR에 Push가 되면 Kubernetes의 해당 yml에 이미지 태그를 수정하고 kubectl apply -f <서비스 이름>.yml로 업데이트할 수 있습니다.

배포 Policy는 Deployment에서 관리할 수 있는데, 사용하는 환경에 따라 Blue/Green 혹은 Rolling Update 등을 적절히 수행하면 됩니다.

배포 상황을 실시간으로 확인하려면 kubectl -n <namespace> rollout status deployment <서비스 deployment 이름>으로 확인할 수 있습니다.

이후 수정한 <서비스 이름>.yml 파일들은 모두 Git으로 관리합니다.

마치며

이번 글에서는 핑퐁팀이 Kubernetes를 활용하여 어떻게 안정적이고 효율적인 운영 환경을 구성했는지 소개해드렸습니다. 이로써 핑퐁팀의 개발자들은 수동으로 처리했을 때 실수가 발생할 가능성을 줄이고 비즈니스 로직을 개발하는 데에 온전히 집중할 수 있었습니다.

이 외에도, 곧 게시될 일상 대화 챗봇 분석을 위한 Analytics 구축기와 이후 블로그 글을 기대해주세요!

참고자료

핑퐁팀이 직접 전해주는
AI에 관한 소식을 받아보세요

능력있는 현업 개발자, 기획자, 디자이너가
지금 핑퐁팀에서 하고 있는 일, 세상에 벌어지고 있는 흥미로운 일들을 알려드립니다.