Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> การเขียนโปรแกรม BASH

การปรับใช้ Helm โดยอัตโนมัติด้วย Bash

แอปพลิเคชันบางตัวของเราโฮสต์อยู่ในคลัสเตอร์ Kubernetes และเราใช้ GitLab Continuous Integration (CI) เพื่อทำให้การปรับใช้โดยอัตโนมัติและ Helm 2 เพื่อปรับใช้แอปพลิเคชันของเรา แผนภูมิ Helm ช่วยให้สามารถจัดเก็บเทมเพลตของไฟล์ YAML ของออบเจ็กต์ Kubernetes พร้อมตัวแปรที่สามารถตั้งค่าโดยทางโปรแกรมจากอาร์กิวเมนต์บรรทัดคำสั่งที่ส่งผ่านเมื่อใช้แผนภูมิในระหว่างการปรับใช้ ซึ่งช่วยให้เราสามารถจัดเก็บข้อมูลลับที่สำคัญในตัวแปรสภาพแวดล้อมที่มีการป้องกันด้วย GitLab หรือใน Hashicorp Vault และใช้งานได้ภายในงานการปรับใช้ CI

งานการปรับใช้ของเราใช้สคริปต์ทุบตีเพื่อเรียกใช้กระบวนการปรับใช้ สคริปต์ทุบตีนี้นำเสนอคุณลักษณะหลายอย่างที่เป็นประโยชน์ต่อการใช้งานภายในสภาพแวดล้อม CI/CD:

  1. อำนวยความสะดวกในการใช้งานนอกสภาพแวดล้อม CI/CD GitLab CI และระบบ CI อื่นๆ เก็บขั้นตอนงานเป็นบรรทัดของเชลล์โค้ดที่เรียกใช้งานได้ในส่วน "สคริปต์" ของไฟล์ข้อความ CI (เช่น .gitlab-ci.yml) แม้ว่าสิ่งนี้จะมีประโยชน์เพื่อให้แน่ใจว่าขั้นตอนปฏิบัติการพื้นฐานสามารถจัดเก็บได้โดยไม่ต้องพึ่งพาภายนอก แต่จะป้องกันไม่ให้นักพัฒนาใช้รหัสเดียวกันในการทดสอบหรือสถานการณ์การปรับใช้ด้วยตนเอง นอกจากนี้ คุณลักษณะขั้นสูงจำนวนมากของระบบ Bash ยังไม่สามารถใช้งานได้ง่ายในส่วนสคริปต์เหล่านี้
  2. อำนวยความสะดวกในการทดสอบหน่วยของกระบวนการปรับใช้ที่สำคัญ ไม่มีระบบ CI ใดที่สามารถทดสอบได้ว่าตรรกะการปรับใช้ทำงานตามที่คาดไว้หรือไม่ สคริปต์ทุบตีที่สร้างขึ้นอย่างระมัดระวังสามารถทดสอบหน่วยกับ BATS ได้
  3. ช่วยอำนวยความสะดวกในการนำฟังก์ชันแต่ละรายการมาใช้ซ้ำภายในสคริปต์ ส่วนสุดท้ายใช้ประโยคป้องกัน ถ้า [[ "${BASH_SOURCE[0]}" =="${0}" ]] ซึ่งป้องกัน run_main ฟังก์ชั่นจากการถูกเรียกเมื่อไม่ได้ดำเนินการสคริปต์ ซึ่งช่วยให้สคริปต์มีแหล่งที่มา ซึ่งทำให้ผู้ใช้สามารถใช้ฟังก์ชันที่มีประโยชน์มากมายภายในสคริปต์ได้ นี่เป็นสิ่งสำคัญสำหรับการทดสอบ BATS ที่เหมาะสม
  4. ใช้ตัวแปรสภาพแวดล้อมเพื่อปกป้องข้อมูลที่ละเอียดอ่อนและทำให้สคริปต์ใช้ซ้ำได้ในหลายโครงการและสภาพแวดล้อมแอปพลิเคชันของโครงการ GitLab CI ทำให้ตัวแปรสภาพแวดล้อมเหล่านี้จำนวนมากพร้อมใช้งานเมื่อเรียกใช้โดยนักวิ่ง GitLab CI ต้องตั้งค่าเหล่านี้ด้วยตนเองก่อนที่จะใช้สคริปต์ภายนอก GitLab CI

สคริปต์ทำงานทั้งหมดที่จำเป็นในการปรับใช้แผนภูมิ Helm สำหรับแอปพลิเคชันไปยัง Kubernetes และรอการปรับใช้ให้พร้อมโดยใช้ kubectl และ Helm Helm ทำงานด้วยการติดตั้ง Tiller ในเครื่องแทนการเรียกใช้ Tiller ในคลัสเตอร์ Kubernetes Kubernetes HELM_USER และ HELM_PASSWORD ใช้เพื่อเข้าสู่ระบบ Kubernetes CLUSTER_SERVER และ PROJECT_NAMESPACE . เริ่ม Tiller แล้ว Helm ถูกเตรียมใช้งานในโหมดไคลเอนต์เท่านั้น และ repo ของมันได้รับการอัปเดต เทมเพลตถูกผูกไว้กับ Helm เพื่อให้แน่ใจว่าไม่มีข้อผิดพลาดทางไวยากรณ์เกิดขึ้นโดยไม่ได้ตั้งใจ จากนั้นเทมเพลตจะถูกปรับใช้ในโหมดการประกาศ โดยใช้ helm upgrade --install . Helm รอให้การปรับใช้พร้อมใช้งานโดยใช้ --รอแฟล็ก .

สคริปต์ช่วยให้แน่ใจว่ามีการตั้งค่าตัวแปรเทมเพลตบางตัวในระหว่างการปรับใช้ และอนุญาตให้ระบุตัวแปรเฉพาะโปรเจ็กต์ใน GitLab CI PROJECT_SPECIFIC_DEPLOY_ARGS ตัวแปรสภาพแวดล้อม ตัวแปรสภาพแวดล้อมทั้งหมดที่จำเป็นในการปรับใช้จะได้รับการตรวจสอบในช่วงต้นของการเรียกใช้สคริปต์ และสคริปต์จะออกจากการทำงานด้วยสถานะการออกที่ไม่ใช่ศูนย์ หากมีหายไป

สคริปต์นี้ถูกใช้ในโครงการที่โฮสต์ GitLab CI หลายโครงการ ช่วยให้เรามุ่งเน้นไปที่โค้ดของเรามากกว่าตรรกะในการทำให้ใช้งานได้ในแต่ละโปรเจ็กต์

สคริปต์

#!/bin/bash

# MIT License
#
# Copyright (c) 2019 Darin London
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

log_level_for()
{
  case "${1}" in
    "error")
      echo 1
      ;;

    "warn")
      echo 2
      ;;

    "debug")
      echo 3
      ;;

    "info")
      echo 4
      ;;
    *)
      echo -1
      ;;
  esac
}

current_log_level()
{
  log_level_for "${LOG_LEVEL}"
}

error()
{
  [ $(log_level_for "error") -le $(current_log_level) ] &&  echo "${1}" >&2
}

warn()
{
  [ $(log_level_for "warn") -le $(current_log_level) ] &&  echo "${1}" >&2
}

debug()
{
  [ $(log_level_for "debug") -le $(current_log_level) ] &&  echo "${1}" >&2
}

info()
{
  [ $(log_level_for "info") -le $(current_log_level) ] &&  echo "${1}" >&2
}

check_required_environment() {
  local required_env="${1}"

  for reqvar in $required_env
  do
    if [ -z "${!reqvar}" ]
    then
      error "missing ENVIRONMENT ${reqvar}!"
      return 1
    fi
  done
}

check_default_environment() {
  local required_env="${1}"

  for varpair in $required_env
  do
    local manual_environment=$(echo "${varpair}" | cut -d':' -f1)
    local default_if_not_set=$(echo "${varpair}" | cut -d':' -f2)
    if [ -z "${!manual_environment}" ] && [ -z "${!default_if_not_set}" ]
    then
      error "missing default ENVIRONMENT, set ${manual_environment} or ${default_if_not_set}!"
      return 1
    fi
  done
}

dry_run() {
  [ ${DRY_RUN} ] && info "skipping for dry run" && return
  return 1
}

init_tiller() {
  info "initializing local tiller"
  dry_run && return

  export TILLER_NAMESPACE=$PROJECT_NAMESPACE
  export HELM_HOST=localhost:44134
  # https://rimusz.net/tillerless-helm/
  # run tiller locally instead of in the cluster
  tiller --storage=secret &
  export TILLER_PID=$!
  sleep 1
  kill -0 ${TILLER_PID}
  if [ $? -gt 0 ]
  then
    error "tiller not running!"
    return 1
  fi
}

init_helm() {
  info "initializing helm"
  dry_run && return

  helm init --client-only
  if [ $? -gt 0 ]
  then
    error "could not initialize helm"
    return 1
  fi
}

init_helm_with_tiller() {
  init_tiller || return 1
  init_helm || return 1
  info "updating helm client repository information"
  dry_run && return
  helm repo update
  if [ $? -gt 0 ]
  then
    error "could not update helm repository information"
    return 1
  fi
}

decommission_tiller() {
  if [ -n "${TILLER_PID}" ]
  then
    kill ${TILLER_PID}
    if [ $? -gt 0 ]
    then
     return
    fi
  fi
}

check_required_deploy_arg_environment() {
  [ -z "${PROJECT_SPECIFIC_DEPLOY_ARGS}" ] && return
  for reqvar in ${PROJECT_SPECIFIC_DEPLOY_ARGS}
  do
    if [ -z ${!reqvar} ]
    then
      error "missing Deployment ENVIRONMENT ${reqvar} required!"
      return 1
    fi
  done
}

project_specific_deploy_args() {
  [ -z "${PROJECT_SPECIFIC_DEPLOY_ARGS}" ] && echo "" && return

  extraArgs=''
  for deploy_arg_key in ${PROJECT_SPECIFIC_DEPLOY_ARGS}
  do
    extraArgs="${extraArgs} --set $(echo "${deploy_arg_key}" | sed 's/__/\./g' | tr '[:upper:]' '[:lower:]')=${!deploy_arg_key}"
  done

  echo "${extraArgs}"
}

check_required_cluster_login_environment() {
  check_required_environment "HELM_TOKEN HELM_USER PROJECT_NAMESPACE CLUSTER_SERVER" || return 1
}

cluster_login() {
  info "authenticating ${HELM_USER} in ${PROJECT_NAMESPACE}"
  dry_run && return

  kubectl config set-cluster ci_kube --server="${CLUSTER_SERVER}" || return 1
  kubectl config set-credentials "${HELM_USER}" --token="${HELM_TOKEN}" || return 1
  kubectl config set-context ${PROJECT_NAMESPACE}-deploy  --cluster=ci_kube --namespace=${PROJECT_NAMESPACE} --user=${HELM_USER} || return 1
  kubectl config use-context ${PROJECT_NAMESPACE}-deploy || return 1
}

lint_template() {
  info "linting template"
  dry_run && return

  helm lint ${CI_PROJECT_DIR}/helm-chart/${CI_PROJECT_NAME}
}

check_required_image_pull_environment() {
  if [ "${CI_PROJECT_VISIBILITY}" == "public" ]
  then
    check_required_environment "CI_REGISTRY CI_DEPLOY_USER CI_DEPLOY_PASSWORD" || return 1
  fi
}

image_pull_settings() {
  if [ "${CI_PROJECT_VISIBILITY}" == "public" ]
  then
    echo ""
  else
    echo "--set registry.root=${CI_REGISTRY} --set registry.secret.username=${CI_DEPLOY_USER} --set registry.secret.password=${CI_DEPLOY_PASSWORD}"
  fi
}

deployment_name() {
  if [ -n "${DEPLOYMENT_NAME}" ]
  then
    echo "${DEPLOYMENT_NAME}"
  else
    echo "${CI_ENVIRONMENT_SLUG}-${CI_PROJECT_NAME}"
  fi
}

deploy_template() {
  info "deploying $(deployment_name) from template"
  if dry_run
  then
    info "helm upgrade --force --recreate-pods --debug --set image.repository=${CI_REGISTRY_IMAGE}/${CI_PROJECT_NAME} --set image.tag=${CI_COMMIT_SHORT_SHA} --set environment=${CI_ENVIRONMENT_NAME} --set-string git_commit=${CI_COMMIT_SHORT_SHA} --set git_ref=${CI_COMMIT_REF_SLUG} --set ci_job_id=${CI_JOB_ID} $(environment_url_settings) $(image_pull_settings) $(project_specific_deploy_args) --wait --install $(deployment_name) ${CI_PROJECT_DIR}/helm-chart/${CI_PROJECT_NAME}"
  else
    helm upgrade --force --recreate-pods --debug \
    --set image.repository="${CI_REGISTRY_IMAGE}/${CI_PROJECT_NAME}" \
    --set image.tag="${CI_COMMIT_SHORT_SHA}" \
    --set environment="${CI_ENVIRONMENT_NAME}" \
    --set-string git_commit="${CI_COMMIT_SHORT_SHA}" \
    --set git_ref="${CI_COMMIT_REF_SLUG}" \
    --set ci_job_id="${CI_JOB_ID}" \
    $(image_pull_settings) \
    $(project_specific_deploy_args) \
    --wait \
    --install $(deployment_name) ${CI_PROJECT_DIR}/helm-chart/${CI_PROJECT_NAME}
  fi
}

get_pods() {
  kubectl get pods -l ci_job_id="${CI_JOB_ID}"
}

watch_deployment() {
  local watch_deployment=$(deployment_name)
  if [ -n "${WATCH_DEPLOYMENT}" ]
  then
    watch_deployment="${WATCH_DEPLOYMENT}"
  fi
  info "waiting until deployment ${watch_deployment} is ready"
  dry_run && return

  kubectl rollout status deployment/${watch_deployment} -w || return 1
  sleep 5
  get_pods || return 1
  # see what has been deployed
  kubectl describe deployment -l app=${CI_PROJECT_NAME},environment=${CI_ENVIRONMENT_NAME},git_commit=${CI_COMMIT_SHORT_SHA} || return 1
  if [ -n "${CI_ENVIRONMENT_URL}" ]
  then
    kubectl describe service -l app=${CI_PROJECT_NAME},environment=${CI_ENVIRONMENT_NAME} || return 1
    kubectl describe route -l app=${CI_PROJECT_NAME},environment=${CI_ENVIRONMENT_NAME} || return 1
  fi
}

run_main() {
  check_required_environment "CI_PROJECT_NAME CI_PROJECT_DIR CI_COMMIT_REF_SLUG CI_REGISTRY_IMAGE CI_ENVIRONMENT_NAME CI_JOB_ID CI_COMMIT_SHORT_SHA" || return 1
  check_default_environment "WATCH_DEPLOYMENT:CI_ENVIRONMENT_SLUG" || return 1
  check_required_deploy_arg_environment || return 1
  check_required_cluster_login_environment || return 1
  check_required_image_pull_environment || return 1
  cluster_login
  if [ $? -gt 0 ]
  then
    error "could not login kubectl"
    return 1
  fi

  init_helm_with_tiller
  if [ $? -gt 0 ]
  then
    error "could not initialize helm"
    return 1
  fi

  lint_template
  if [ $? -gt 0 ]
  then
    error "linting failed"
    return 1
  fi

  deploy_template
  if [ $? -gt 0 ]
  then
    error "could not deploy template"
    return 1
  fi

  watch_deployment
  if [ $? -gt 0 ]
  then
    error "could not watch deployment"
    return 1
  fi

  decommission_tiller
  info "ALL Complete!"
  return
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
  run_main
  if [ $? -gt 0 ]
  then
    exit 1
  fi
fi