data engineering

customizing spark helm chart

qkqhxla1 2021. 10. 25. 10:56

이번에 쓸 글은 spark자체를 커스터마이징 한다기보다 삽질과, bitnami spark helm chart를 구성하는 것들을 살펴보는데 중점을 둔 글입니다.

전에 쓴글 중에 https://qkqhxla1.tistory.com/1164?category=698045 에서 스파크에서도 airflow와 python 세팅이 동일해야 하는 이유를 적었었다.(spark의 python udf 사용 불가능)
최근에 회사에서 이슈가 있어서 k8s 서버를 통째로 옮겨가야 할 일이 생겼다. 랜쳐도 새로 설치했고, 내부의 App들을 옮기는 중인데 spark를 옮기던 도중 지금이 spark에 airflow와 같은 python 버전을 적용해야 할 적기라고 생각해서 세팅한다.

우선 내부의 파이썬 버전을 바꾸기 위해서는 기본적으로 spark에서 사용하는 파이썬 버전을 설치시 변경하거나, 추가적으로 파이썬을 설치한 후에 경로를 바꿔줘야 한다. 나는 spark 3.0.1을 사용하기에 일단 소스코드를 받았다.
https://github.com/bitnami/bitnami-docker-spark/releases/tag/3.0.1-debian-10-r129

들어가서 어디부터 살펴봐야 할지 모르겠으니 Dockerfile을 살펴봤다. 파이썬에 관한 부분이 명확히 보인다.

..............................
RUN . /opt/bitnami/scripts/libcomponent.sh && component_unpack "python" "3.6.12-10" --checksum cc3e62e05463929529214f65cb7922794c758a3db5ca29bf5c9a59fe66e95b80
..............................
ENV BITNAMI_APP_NAME="spark" \
    BITNAMI_IMAGE_VERSION="3.0.1-debian-10-r129" \
    JAVA_HOME="/opt/bitnami/java" \
    LD_LIBRARY_PATH="/opt/bitnami/python/lib/:/opt/bitnami/spark/venv/lib/python3.6/site-packages/numpy.libs/:$LD_LIBRARY_PATH" \
..............................

확인했다. 그런데 libcomponent.sh라는 스크립트에서 component_unpack을 사용해서 어떻게 작동을 하는것같다. 
libcomponent.sh 를 살펴보자.

#!/bin/bash
#
# Library for managing Bitnami components

# Constants
CACHE_ROOT="/tmp/bitnami/pkg/cache"
DOWNLOAD_URL="https://downloads.bitnami.com/files/stacksmith"

# Functions

########################
# Download and unpack a Bitnami package
# Globals:
#   OS_NAME
#   OS_ARCH
#   OS_FLAVOUR
# Arguments:
#   $1 - component's name
#   $2 - component's version
# Returns:
#   None
#########################
component_unpack() {
    local name="${1:?name is required}"
    local version="${2:?version is required}"
    local base_name="${name}-${version}-${OS_NAME}-${OS_ARCH}-${OS_FLAVOUR}"
    local package_sha256=""
    local directory="/opt/bitnami"
    
    # Validate arguments
    shift 2
    while [ "$#" -gt 0 ]; do
        case "$1" in
            -c|--checksum)
                shift
                package_sha256="${1:?missing package checksum}"
                ;;
            *)
                echo "Invalid command line flag $1" >&2
                return 1
                ;;
        esac
        shift
    done

    echo "Downloading $base_name package"
    if [ -f "${CACHE_ROOT}/${base_name}.tar.gz" ]; then
        echo "${CACHE_ROOT}/${base_name}.tar.gz already exists, skipping download."
        cp "${CACHE_ROOT}/${base_name}.tar.gz" .
        rm "${CACHE_ROOT}/${base_name}.tar.gz"
        if [ -f "${CACHE_ROOT}/${base_name}.tar.gz.sha256" ]; then
            echo "Using the local sha256 from ${CACHE_ROOT}/${base_name}.tar.gz.sha256"
            package_sha256="$(< "${CACHE_ROOT}/${base_name}.tar.gz.sha256")"
            rm "${CACHE_ROOT}/${base_name}.tar.gz.sha256"
        fi
    else
	      curl --remote-name --silent "${DOWNLOAD_URL}/${base_name}.tar.gz"
    fi
    if [ -n "$package_sha256" ]; then
        echo "Verifying package integrity"
        echo "$package_sha256  ${base_name}.tar.gz" | sha256sum --check -
    fi
    tar --directory "${directory}" --extract --gunzip --file "${base_name}.tar.gz" --no-same-owner --strip-components=2 "${base_name}/files/"
    rm "${base_name}.tar.gz"
}

소스를 적당히 해석해보면 component_unpack함수에서 name, version, base_name을 받는다.
함수 실행시 RUN . /opt/bitnami/scripts/libcomponent.sh && component_unpack "python" "3.6.12-10" --checksum cc3e62e05463929529214f65cb7922794c758a3db5ca29bf5c9a59fe66e95b80

로 실행하는데, name=python, version=3.6.12-10, base_name=python-3.6.12-10-linux-amd64-debian-10으로 세팅됨을 알 수 있다. base_name같은경우는 $OS_NAME같은 환경변수가 사용되는데, 다운받은 스파크 도커 이미지 bitnami/spark:3.0.1-debian-10-r139 로 들어가서 환경변수를 출력해서 확인할수 있었다.
변수가 세팅되고, 이후 package_sha256은cc3e62e05463929529214f65cb7922794c758a3db5ca29bf5c9a59fe66e95b80로 세팅될거다.

그리고 아래 파트에서 curl로 python을 받는데, 변수를 조합해서 스크립트를 만들어보면 아래와 같다.

curl --remote-name -silent "https://downloads.bitnami.com/files/stacksmith/python-3.6.12-10-linux-amd64-debian-10.tar.gz"

이 스크립트를 실행시켜보면 로컬에 python 3.6.12-10이 다운로드된다. 분석한김에 아래쪽에서 sha256sum으로 무결성 체크를 하는데 이것도 한번 조합해서 맞는지 확인해보자.

$ curl --remote-name --silent "https://downloads.bitnami.com/files/stacksmith/python-3.6.12-10-linux-amd64-debian-10.tar.gz"
$ echo "cc3e62e05463929529214f65cb7922794c758a3db5ca29bf5c9a59fe66e95b80 ./python-3.6.12-10-linux-amd64-debian-10.tar.gz" | sha256sum --check -
./python-3.6.12-10-linux-amd64-debian-10.tar.gz: OK
$ echo "cc3e62e05463929529214f65cb7922794c758a3db5ca29bf5c9a59fe66e95b81 ./python-3.6.12-10-linux-amd64-debian-10.tar.gz" | sha256sum --check -
./python-3.6.12-10-linux-amd64-debian-10.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

실행해본결과 맞으면 OK가, 하나라도 틀리면 FAILED가 나온다. 결국 component_unpack함수는 입력으로 들어온것을 다운받아서 무결성 체크를 해주는 함수이다. 구조를 이해했으니 이제 airflow에서 사용하는 python 3.8을 가져와서 호출시 3.6대신 3.8을 다운받도록 하면 된다. 아래는 내가 사용하는 airflow버전의 깃에서 받아왔다.
https://github.com/bitnami/bitnami-docker-airflow/releases/tag/2.1.4-debian-10-r10

..................................
RUN . /opt/bitnami/scripts/libcomponent.sh && component_unpack "python" "3.8.12-2" --checksum e00677f5209d9426daff2db54cf00e04c46718bbdbc0717aee07e6a727ef05ad
..................................
    LD_LIBRARY_PATH="/opt/bitnami/python/lib/:/opt/bitnami/spark/venv/lib/python3.8/site-packages/numpy.libs/:$LD_LIBRARY_PATH" \
..................................

spark의 Dockerfile에서 3.8.12-2로 변경하고 ./bitnami-docker-spark-3.0.1-debian-10-r129/3/debian-10/prebuildfs/opt/bitnami/.bitnami_components.json 에도 3.6이 있던데 이것까지 바꾸고
build스크립트를 넣은 후 spark 도커 이미지를 재빌드했다.

#! /bin/bash
REPOSITORY="{private docker registry}/app/spark"

TAG="3.0.1-debian-10-r139"

docker build -t $REPOSITORY:$TAG .
docker build -t $REPOSITORY .

docker push $REPOSITORY:$TAG
docker push $REPOSITORY

그리고 내가 빌드한 도커 이미지를 사용하기 위해 helm spark가 사용하게 될 values.yaml을 아래와 같이 설정해주었다.(최종 버전은 아래에 있음.)

....................
image:
  registry: {private docker registry}
  repository: app/spark
  tag: 3.0.1-debian-10-r139
....................

이제 helm으로 위의 values.yaml을 참조해서 spark app을 만들면, {private docker registry}에 있는 내가 push한 이미지를 다운받아서 spark master, worker를 만드는데, 내가 설정했던 파이썬 버전이 3.8이어야 한다.

성공했다. spark의 python버전은 바꿔줬으니, 이제는 requirements.txt를 받아서 설치해야 한다. airflow의 경우는 sidecar가 있어서 그거로 git 버전을 관리한다.

이런식인데, clone-repositories는 init container로써, worker가 실행되기 전에 실행되어 git의 모든 dag들을 가져온다. 이후 sync-repositories가 설정한 시간마다(위 그림에서는 60초) 변동사항을 가져오기 위해 pull을 한다. 어떻게 60초마다 pull을 할까? 원리가 궁금했는데 그냥 쉘 스크립트로 60초마다 sleep 하면서 git pull을 한다. airflow worker는 dag folder만 참조하여 작업을 하므로 sync-repositories가 깃을 가져와서 dag를 업데이트해주면 airflow에도 자동으로 업데이트가 된다.

왜 airflow구조를 가져왔냐면, airflow의 python requirements.txt가 깃에 존재해서, clone-repositories가 처음에 git 소스 전체를 가져올때 같이 딸려오기 때문이다. clone-repositories가 requirements.txt를 가져와서 dag folder에 넣으면, 이후 airflow-worker컨테이너가 처음 올라올때 특정 위치에 requirements.txt가 존재하면 pip로 설치한다.
https://github.com/bitnami/bitnami-docker-airflow/blob/cafc8eab1efddb5efda5a00cc861ef10f35f1d49/1/debian-10/rootfs/run.sh#L14

spark도 airflow의 clone-repositories를 가져와서 동일하게 설정하면 될것같다.

대충 이런식으로 만들어보면 되지 않을까? 생각해서 만들었다. 그럼 이제 helm chart에서 initContainer관련 옵션을 찾아보자! https://artifacthub.io/packages/helm/bitnami/spark/5.1.2 ??????? 내가 사용하는 spark 3.0.2인데 container를 새로 만드는 옵션이 없다. 근데 최신 버전에는 있다(initContainers) : https://artifacthub.io/packages/helm/bitnami/spark/5.7.6
-_-..... 이왕 한김에 최신버전으로 해보기로 하고.. 기존에 있던 airflow 내부의 spark submit용 spark버전도 최신으로 바꾸고,(현재 airflow가 kubernetes executor가 아니라 celery executor를 사용중이라 spark3.0.2를 워커에 직접 설치하는방식으로 운영하고있었음.) spark 3.1.2로 이미지를 재빌드했다.

자.. 이제 spark helm에 master.initContainers과 worker.initContainers를 세팅해보자. 멀리왔는데 다시적자면initContainer를 세팅하는 이유는, spark 워커나 마스터가 올라왔을때 처음 한번 airflow git을 clone해서 requirements.txt를 가져오기 위함이다.

spark가 사용할 values.yml파일에 image이외에도 master와 worker에 airflow와 동일하게 /bitnami/python 경로로 airflow의 git을 clone해줬다. 그리고 master와 worker 둘다 airflow에서 clone-repositories용으로 사용하던 사이드 컨테이너를 그대로, command도 그대로 가져왔다.

image:
  registry: {private docker registry}
  repository: app/spark
  tag: 3.1.2-debian-10-r99
  pullPolicy: Always

master:
  extraVolumeMounts:
    - mountPath: /bitnami/python
      name: git-cloned-dag-files-my-airflow
  extraVolumes:
    - emptyDir: {}
      name: git-cloned-dag-files-my-airflow
  initContainers:
    - name: pip-install-initialize
      image: {private docker registry}/app/git:2.33.0-debian-10-r45
      volumeMounts:
      - mountPath: /dags_my-airflow
        name: git-cloned-dag-files-my-airflow
      command: ['/bin/bash', '-ec', '. /opt/bitnami/scripts/libfs.sh; [[ -f "/opt/bitnami/scripts/git/entrypoint.sh" ]] && . /opt/bitnami/scripts/git/entrypoint.sh; is_mounted_dir_empty "/dags_my-airflow" && git clone https://{my id: key}@{private github.com}/my_airflow.git --branch master /dags_my-airflow || true']

worker:
  replicaCount: 6
  extraVolumeMounts:
    - mountPath: /bitnami/python
      name: git-cloned-dag-files-my-airflow
  extraVolumes:
    - emptyDir: {}
      name: git-cloned-dag-files-my-airflow
  initContainers:
    - name: pip-install-initialize
      image: {private docker registry}/app/git:2.33.0-debian-10-r45
      volumeMounts:
      - mountPath: /dags_my-airflow
        name: git-cloned-dag-files-my-airflow
      command: ['/bin/bash', '-ec', '. /opt/bitnami/scripts/libfs.sh; [[ -f "/opt/bitnami/scripts/git/entrypoint.sh" ]] && . /opt/bitnami/scripts/git/entrypoint.sh; is_mounted_dir_empty "/dags_my-airflow" && git clone https://{my id: key}@{private github.com}/my_airflow.git --branch master /dags_my-airflow || true']

이제 이 상태로 spark를 설치하면 master와 worker의 /bitnami/python경로에 airflow의 git이 clone되어 requirements.txt가 잘 복사되어 들어옴을 확인할 수 있다.

이제 spark master, worker컨테이너가 올라올때 pip install -r /bitnami/python/requirements.txt로 설치해주면 된다. 위에서 spark와 airflow를 3.1.2로 다시 만들었다고 했는데 현재 사용하는 spark 3.1.2의 소스 버전은 다음과 같다 : 3.1.2-debian-10-r99 : https://github.com/bitnami/bitnami-docker-spark/releases/tag/3.1.2-debian-10-r99

이걸 받아다 컨테이너가 실행하는 run.sh를 찾아서 중간에 pip install 파트를 추가해준다.

.................
# Install custom python package if requirements.txt is present
if [[ -f "/bitnami/python/requirements.txt" ]]; then
    pip install -r /bitnami/python/requirements.txt
fi
.................

그리고 테스트를 해보니 권한 관련 에러가 나서 Dockerfile에도 아래 구문을 중간에 추가해준다.(https://qkqhxla1.tistory.com/1159에 썼었음.)

.........................
# uid 1001 to root group
RUN echo "spark:x:1001:0:root:/root:/bin/bash" >> /etc/passwd
# for pip install.
RUN mkdir -m775 /.local
.........................

이미지를 재빌드하고, 이제 worker와 master를 새로 만들었을때 pip가 잘 실행되었음을 확인할 수 있다.

git에서 pip흐름을 간단하게 그려보면 아래와 같다.