상세 컨텐츠

본문 제목

방치된 Spark 프로젝트에 CI/CD 파이프라인 구축기

Programming/DevOps

by 홍잭슨 2024. 2. 4. 17:30

본문

안녕하세요, 백엔드 엔지니어 잭슨입니다.

오늘은 최근 맡게 된 검색 관련 프로젝트에 CI/CD 파이프라인을 구축하여 프로젝트 관리 및 배포를 개선한 경험을 공유하고자 합니다.

프로젝트 배경

모바일 프로덕트의 홈 화면 개편 스프린트에서 홈 화면 중앙에 위치한 검색 기능의 중요도가 높아짐에 따라 검색 관련 요구사항 및 개선사항이 증가했습니다. 제가 검색 관련 태스크를 맡게 된 후 프로젝트를 파악해본 결과, 다음과 같은 문제점을 발견했습니다.

  • 코드 관리, 버전 관리, 배포 프로세스 등이 체계적으로 관리되지 않아 개발 및 배포에 어려움을 겪고 있었습니다.
    • Production에서는 AirFlow를 통해, 나머지 환경에서는 CronTab을 통해 ElasticSearch에 인덱싱을 하고 있었습니다.
  • 모든 배포 과정을 수동으로 진행하여 시간 소모가 많았고, 배포 과정에서 오류 발생 가능성이 높았습니다.
    • 모든 환경의 EC2와 EMR 인스턴스에서는 Git과 같은 버전 관리 시스템을 사용하지 않고, 수동으로 코드 통합이 진행되고 있었습니다.

CI/CD 파이프라인 도입

위의 문제점을 해결하기 위해 CI/CD 파이프라인을 도입하기로 결정했습니다. CI/CD 파이프라인은 다음과 같은 장점이 있습니다.

  • 자동화: 코드 변경 시 자동으로 테스트, 빌드, 배포를 수행하여 개발 및 배포 과정을 효율적으로 만듭니다.
  • 일관성: 모든 환경에서 동일한 프로세스를 적용하여 환경 불일치 문제를 해결합니다.
  • 신뢰성: 자동화된 테스트를 통해 배포 전에 오류를 사전에 발견하고 해결하여 배포 실패 가능성을 줄입니다.

CI/CD 파이프라인 구축

저희 프로젝트는 다음과 같은 4가지 환경으로 구성되어 있습니다.

  • Develop: 개발 진행 환경
  • RC (Release Candidate): QA 진행 환경
  • Staging: 최종 QA 진행 환경
  • Production: 운영 환경

효율적으로 CI/CD 프로세스를 관리하기 위해 여러가지 대안들을 고민해보았고 그 중 최종적으로 두가지의 대안이 떠올랐습니다.

  1. Jenkins, k8s CronJob을 통한 OpenSearch 인덱싱
  2. Jenkins, AWS CodeBuild를 통한 OpenSearch 인덱싱

기존에 CI/CD 프로세스와 가장 비슷하고 관리하기 쉬울 것 같단 판단에서 이 두가지 대안이 떠올랐으며 최종적으로 2번째 대안으로 선택하였습니다.

1번째 대안의 경우는 Apache Spark의 인덱싱 작업이 조금 무거워 Karpenter Auto Scaling으로 인해 K8S Node가 배치주기마다 새로 뜰 수 있고 그로 인한 비용문제로 인해 채택되지 못했습니다.

다음은 CI/CD 구축 과정입니다.

  • Jenkins에서 Multibranch pipeline 프로젝트를 생성하고, Github의 통합 검색 프로젝트와 연결합니다.
  • 각 환경별로 Buildspec을 AWS CodeBuild에 전달할 수 있도록 스크립트를 작성합니다.
def MERGE_RESULT=true //Variable For Merge Fail Check
def NOTIFICATION_GROUPS="@backend" //Slack Notification Group

def buildspec() { // 지정된 브랜치만 빌드 되도록
    def deploy_enables = ["develop", "rc", "staging", "production"]
  if (deploy_enables.contains(env.BRANCH_NAME))
    "ci/${env.BRANCH_NAME}/buildspec.yml"
  else
    ""
}

def channel() { // 프로덕션의 경우 다른 채널에 알림이 오도록
  if (env.BRANCH_NAME.contains("production"))
    "#product_deployment"
  else
    "#product_development"
}

def cron_expression(){ // Production의 경우 배치의 간격을 5분으로 조정
    if (env.BRANCH_NAME.contains("production"))
      "H/5 * * * *"
    else
      "H/10 * * * *"
}



pipeline {
  agent none

	triggers { cron(cron_expression()) }

  stages {
    stage('Build') {
      agent {
        label 'node-linux'
      }
      steps {
        awsCodeBuild(
          // 생략
        )
      }
    }

    stage('Git backward Merge And Push'){ // auto back merge
      agent {
        label 'node-linux'
      }
      steps{
        sshagent(credentials: ['ssh_key']) {
          script{
            // 생략
          }
        }
      }
    }
  }

  post { // slack 알림
    success {
      // 생략
    }
    failure {
      // 생략
    }
  }
}
  • 각 환경별 buildspec을 통해 Dockerfile build 및 실행되도록 파일을 작성합니다.
env:
  parameter-store: // parameter store에서 보안관련 키를 받아옵니다.
    EKS_CLUSTER_BLUEGREEN: "/develop/..."
    AWS_ACCESS_KEY_ID: "/develop/..."
    AWS_SECRET_ACCESS_KEY: "/develop/..."
  variables:
    BUILD_CI_ENV: develop
    ECR_ARN: "비밀.dkr.ecr.ap-northeast-2.amazonaws.com/search"
    AWS_ACCOUNT_ID: "비밀"
    AWS_DEFAULT_REGION: "ap-northeast-2"
    APP_FOLDER: src

phases:
  install:
    commands:
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 &
      - timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - cp ./cd/Dockerfile $APP_FOLDER/Dockerfile
      - cd $APP_FOLDER
      - docker build -t spark . --build-arg AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID
      - docker run spark
  • AWS CodeBuild 서버에서 빌드를 진행하고, buildspec 파일과 Dockerfile을 읽어서 Spark Project를 실행하고, 이를 OpenSearch 프로젝트에 인덱싱합니다.

예상치 못한 문제

정상적으로 동작하는 것을 확인한 후 다음날 확인한 결과 간헐적으로 빌드가 실패한 것을 확인할 수 있었습니다. 로그를 확인해보니 아래와 같은 로그를 확인할 수 있었습니다.

ERROR: toomanyrequests: Too Many Requests.

관련 링크 : https://subicura.com/k8s/2021/01/02/docker-hub-pull-limit/

도커 허브에서 익명의 무료사용자에게는 요청량을 제한한 것이었습니다. 이를 해결하기 위해 자체적으로 Docker Image를 만들어 AWS ECR(Elastic Container Registry)를 통해 미리 생성된 Private 이미지를 통해 빌드 되도록 수정하였습니다.

결과

CI/CD 파이프라인 도입을 통해 검색 관련 프로젝트의 개발 및 배포 효율성을 크게 향상시키고 신뢰성을 높일 수 있었습니다. 특히, 이후에 있었던 스프린트에서 개발요구사항을 빠르게 배포하고 테스트해볼 수 있어 개발 생산성에 큰 도움이 되었습니다.

오늘은 방치된 Spark 프로젝트에 CI/CD 파이프라인을 구축하여 개발 및 배포 효율성을 향상시킨 경험을 공유해보았습니다. 이 글이 CI/CD 파이프라인 도입을 고려하고 있는 분들에게 도움이 되기를 바랍니다.

관련글 더보기