Một số ứng dụng của chúng tôi được host trong cluster Kubernetes, và chúng tôi dùng GitLab Continuous Integration (CI) để tự động hóa triển khai và Helm 2 để triển khai ứng dụng. Helm chart cho phép lưu template file YAML object Kubernetes với biến có thể thiết lập lập trình từ đối số dòng lệnh truyền khi chart được dùng trong triển khai. Điều này cho phép chúng tôi lưu bí mật quan trọng trong biến môi trường được bảo vệ GitLab hoặc trong Hashicorp Vault và dùng chúng trong CI deployment job.
Deployment job của chúng tôi dùng script Bash để chạy quy trình triển khai. Script Bash này trình bày một số tính năng có giá trị cho dùng trong môi trường CI/CD:
- Nó tạo điều kiện dùng bên ngoài môi trường CI/CD. GitLab CI và các hệ thống CI khác lưu bước job như dòng mã shell thực thi trong phần "script" của file text CI (.gitlab-ci.yml, ví dụ). Mặc dù điều này hữu ích để đảm bảo các bước thực thi cơ bản có thể lưu mà không phụ thuộc bên ngoài, nó ngăn developer dùng cùng code trong test hoặc triển khai thủ công. Ngoài ra, nhiều tính năng nâng cao của hệ thống Bash không thể dễ dàng dùng trong các phần script này.
- Nó tạo điều kiện unit test các quy trình triển khai quan trọng. Không hệ thống CI nào cung cấp cách test xem logic triển khai có thực hiện như mong đợi. Script Bash xây dựng cẩn thận có thể unit test với BATS.
- Nó tạo điều kiện tái dùng các hàm riêng lẻ trong script. Phần cuối dùng mệnh đề guard, if [[ "${BASH_SOURCE[0]}" == "${0}" ]], ngăn hàm run_main được gọi khi script không được thực thi. Điều này cho phép script được source, sau đó cho phép user dùng nhiều hàm hữu ích riêng lẻ trong nó. Điều này quan trọng cho test BATS đúng.
- Nó dùng biến môi trường để bảo vệ thông tin nhạy cảm và làm script tái dùng qua nhiều dự án và môi trường ứng dụng dự án. GitLab CI làm nhiều biến môi trường này có sẵn khi chạy bởi GitLab CI runner. Chúng phải được thiết lập thủ công trước khi dùng script bên ngoài GitLab CI.
Script thực hiện tất cả tác vụ cần thiết để triển khai Helm chart cho ứng dụng đến Kubernetes và chờ triển khai sẵn sàng dùng kubectl và Helm. Helm chạy với cài đặt Tiller local thay vì chạy Tiller trong cluster Kubernetes. HELM_USER và HELM_PASSWORD Kubernetes dùng để đăng nhập vào CLUSTER_SERVER và PROJECT_NAMESPACE Kubernetes. Tiller được khởi động, Helm được khởi tạo ở chế độ client-only, và repo của nó được cập nhật. Template được lint với Helm để đảm bảo lỗi cú pháp không bị commit vô tình. Template sau đó được triển khai ở chế độ declarative, dùng helm upgrade --install. Helm chờ triển khai sẵn sàng dùng --wait flag.
Script đảm bảo một số biến template được thiết lập trong triển khai và cho phép biến đặc thù dự án được chỉ định trong biến môi trường GitLab CI PROJECT_SPECIFIC_DEPLOY_ARGS. Tất cả biến môi trường cần trong triển khai được kiểm tra sớm trong thực thi script, và script thoát với trạng thái thoát non-zero nếu thiếu bất kỳ cái nào.
Script này đã được dùng trong nhiều dự án GitLab CI-hosted. Nó đã giúp chúng tôi tập trung vào code thay vì logic triển khai trong mỗi dự án.
Script (The script)
#!/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