# ***kubernetes 基于Jenkins的動態應用實部署實踐***
##
# 自我介紹
- 光榮的武警戰士
- 紅帽RHCA講師
- 知道創宇高級運維工程師
- 點融網資深運維工程師

# 胡毅 yi.hu@dianrong.com
##
# 目錄
- Kubernetes Arch
- Why install jenkins in kubernetes
- 在kubernetes中使用Jenkins部署一個應用
## kubernetes Arch
- kubespray
- k8s-cluser :v1.10.4
- docker: docker-ce-17.03.2.ce-1.el7.centos
- nginx-ingress :
- coresdns
- kube-proxy(IPVS)

## Cluster HA-mode

### kubernets 集群管理策略(一)
- Taints and Tolerations
master 所有主機添加Taints 規則
kubectl taint nodes dl-demo-k8s-master-01 node.kubernetes.io=unschedulable:NoSchedule
kubectl taint nodes dl-demo-k8s-master-02 node.kubernetes.io=unschedulable:NoSchedule
kubectl taint nodes dl-demo-k8s-master-03 node.kubernetes.io=unschedulable:NoSchedule
設置效果
Name: dl-demo-k8s-master-01
Roles: ingress,master,node
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/hostname=dl-demo-k8s-master-01
node-role.kubernetes.io/ingress=true
node-role.kubernetes.io/master=true
node-role.kubernetes.io/node=true
Annotations: alpha.kubernetes.io/provided-node-ip=10.34.11.12
flannel.alpha.coreos.com/backend-data={"VtepMAC":"e6:20:4c:cf:4f:2b"}
flannel.alpha.coreos.com/backend-type=vxlan
flannel.alpha.coreos.com/kube-subnet-manager=true
flannel.alpha.coreos.com/public-ip=10.34.11.12
node.alpha.kubernetes.io/ttl=0
volumes.kubernetes.io/controller-managed-attach-detach=true
CreationTimestamp: Tue, 10 Jul 2018 15:06:51 +0800
Taints: node.kubernetes.io=unschedulable:NoSchedule
master組件Deployment添加Tolerations 規則
tolerations:
- key: node.kubernetes.io
value: unschedulable
effect: NoSchedule
### kubernetes 集群管理策略(二)
- LimitRange
給一個namespace 設置全局資源限制規則
apiVersion: v1
kind: LimitRange
metadata:
name: ftc-demo-limit
namespace: ftc-demo
spec:
limits:
- max : # 在一個pod 中最高使用資源
cpu: "2"
memory: "6Gi"
min: # 在一個pod中最低使用資源
cpu: "100m"
memory: "2Gi"
type: Pod
- default: # 啟動一個Container 默認資源規則
cpu: "1"
memory: "4Gi"
defaultRequest:
cpu: "200m"
memory: "2Gi"
max: #匹配用戶手動指定Limits 規則
cpu: "1"
memory: "4Gi"
min:
cpu: "100m"
memory: "256Mi"
type: Container
限制規則說明
* Container 資源限制總量必須 <= Pod 資源限制總和
* 在一個pod 中最高使用2 核6G 內存
* 在一個pod中最低使用100m核 2G 內存
* 默認啟動一個Container服務 強分配cpu 200m,內存2G。Container 最高使用 1 cpu 4G 內存。
* 如果用戶私有指定limits 規則。最高使用1 CPU 4G內存,最低使用 cpu 100m 內存 256Mi
## Why install jenkins in kubernetes
### 傳統的 Jenkins Slave 一主多從方式會存在一些痛點?
* 主 Master 發生單點故障時,整個流程都不可用了
* 每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導致管理起來非常不方便,維護起來也是比較費勁
* 資源分配不均衡,有的 Slave 要運行的 job 出現排隊等待,而有的 Slave 處于空閑狀態
* 資源有浪費,每臺 Slave 可能是物理機或者虛擬機,當 Slave 處于空閑狀態時,也不會完全釋放掉資源。
正因為上面的這些種種痛點,我們渴望一種更高效更可靠的方式來完成這個 CI/CD 流程,而 Docker 虛擬化容器技術能很好的解決這個痛點,又特別是在 Kubernetes 集群環境下面能夠更好來解決上面的問題,下圖是基于 Kubernetes 搭建 Jenkins 集群的簡單示意圖

從圖上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式運行在 Kubernetes 集群的 Node 上,Master 運行在其中一個節點,并且將其配置數據存儲到一個 Volume 上去,Slave 運行在各個節點上,并且它不是一直處于運行狀態,它會按照需求動態的創建并自動刪除。
這種方式的工作流程大致為:當 Jenkins Master 接受到 Build 請求時,會根據配置的 Label 動態創建一個運行在 Pod 中的 Jenkins Slave 并注冊到 Master 上,當運行完 Job 后,這個 Slave 會被注銷并且這個 Pod 也會自動刪除,恢復到最初狀態。
那么我們使用這種方式帶來了哪些好處呢?
* 服務高可用,當 Jenkins Master 出現故障時,Kubernetes 會自動創建一個新的 Jenkins Master 容器,并且將 Volume 分配給新創建的容器,保證數據不丟失,從而達到集群服務高可用。
* 動態伸縮,合理使用資源,每次運行 Job 時,會自動創建一個 Jenkins Slave,Job 完成后,Slave 自動注銷并刪除容器,資源自動釋放,而且 Kubernetes 會根據每個資源的使用情況,動態分配 Slave 到空閑的節點上創建,降低出現因某節點資源利用率高,還排隊等待在該節點的情況。
* 擴展性好,當 Kubernetes 集群的資源嚴重不足而導致 Job 排隊等待時,可以很容易的添加一個 Kubernetes Node 到集群中,從而實現擴展。
# 光說不練假把式 (實驗演示一)
##
# 在Jenkins中部署一個應用
- Pipeline VS Scripte
### 部署 Kubernetes 應用
如何來部署一個原生的 Kubernetes 應用呢? 部署一個kubernetes 應用流程如下:

1. pull代碼
2. 測試registry,并登陸registry.
3. 編寫應用 Dockerfile
4. 構建打包 Docker 鏡像
5. 推送 Docker 鏡像到倉庫
6. 更改 Deployment YAML 文件中參數
7. 利用 kubectl 工具部署應用
8. 檢查應用狀態
### 第一步,Pull 代碼及全局環境變量申明
#!/bin/bash
# Filename: k8s-deploy_v0.2.sh
# Description: jenkins CI/CD 持續發布腳本
# Author: yi.hu
# Email: yi.hu@dianrong.com
# Revision: 1.0
# Date: 2018-08-10
# Note: prd
# zookeeper基礎服務,依照環境實際地址配置
init() {
local lowerEnv="$(echo ${AppEnv} | tr '[:upper:]' 'lower')"
case "${lowerEnv}" in
dev)
CFG_ADDR="10.34.11.186:4181"
DR_CFG_ZOOKEEPER_ENV_URL="10.34.11.186:4181"
;;
demo)
CFG_ADDR="10.34.11.186:4181"
DR_CFG_ZOOKEEPER_ENV_URL="10.34.11.186:4181"
;;
*)
echo "Not support AppEnv: ${AppEnv}"
exit 1
;;
esac
}
# 函數執行
init
# 初始化變量
AppId=$(echo ${AppOrg}_${AppEnv}_${AppName} |sed 's/[^a-zA-Z0-9_-]//g' | tr "[:lower:]" "[:upper:]")
CFG_LABEL=${CfgLabelBaseNode}/${AppId}
CFG_ADDR=${CFG_ADDR}
VERSION=$(echo "${GitBranch}" | sed 's@release/@@')
### 第二步,登錄harbor 倉庫
docker_login () {
docker login ${DOCKER_REGISTRY} -u${User} -p${PassWord}
}
### 第三步,編譯代碼,制作應用鏡像,上傳鏡像到harbor倉庫。
build() {
if [ "x${ACTION}" == "xDEPLOY" ] || [ "x${ACTION}" == "xPRE_DEPLOY" ]; then
echo "Test harbor registry: ${DOCKER_REGISTRY}"
curl --connect-timeout 30 -I ${DOCKER_REGISTRY}/api/projects 2>/dev/null | grep 'HTTP/1.1 200 OK' > /dev/null
echo "Check image EXIST or NOT: ${ToImage}"
ImageCheck_Harbor=$(echo ${ToImage} | sed 's/\([^/]\+\)\([^:]\+\):/\1\/api\/repositories\2\/tags\//')
Responed_Code=$(curl -u${User}:${PassWord} -so /dev/null -w '%{response_code}' ${ImageCheck_Harbor} || true)
echo ${Responed_Code}
if [ "${NoCache}" == "true" ] || [ "x${ResponedCode}" != "x200" ] ; then
if [ "x${ActionAfterBuild}" != "x" ]; then
eval ${ActionAfterBuild}
fi
echo "生成Dockerfile文件"
echo "FROM ${FromImage}" > Dockerfile
cat >> Dockerfile <<-EOF
${Dockerfile}
EOF
echo "同步上層鏡像: ${FromImage}"
docker pull ${FromImage} # 同步上層鏡像
echo "構建鏡像,并Push到倉庫: ${ToImage}"
docker build --no-cache=${NoCache} -t ${ToImage} . && docker push ${ToImage} || exit 1 # 開始構建鏡像,成功后Push到倉庫
echo "刪除鏡像: ${ToImage}"
docker rmi ${ToImage} || echo # 刪除鏡像
fi
fi
}
### 第四步,發布、預發布、停止、重啟
deploy() {
if [ "x${ACTION}" == "xSTOP" ]; then
# 停止當前實例
kubectl delete -f ${AppName}-deploy.yaml
elif [ "x${ACTION}" == "xRESTART" ]; then
kubectl delete pod -n ${NameSpace} -l app=${AppName}
elif [ "x${ACTION}" == "xDEPLOY" ]; then
kubectl apply -f ${AppName}-deploy.yaml
fi
}
### 第五步,查看pod 是否正常啟動,如果失敗則返回1,進而會詳細顯示報錯信息。
check_status() {
RETRY_COUNT=5
echo "檢查 pod 運行狀態"
while (( $RETRY_COUNT )); do
POD_STATUS=$(kubectl get pod -n ${NameSpace} -l app=${AppName} )
AVAILABLE_COUNT=$(kubectl get deploy -n ${NameSpace} -l app=${AppName} | awk '{print $(NF-1)}' | grep -v 'AVAILABLE')
if [ "X${AVAILABLE_COUNT}" != "X${Replicas}" ]; then
echo "[$(date '+%F %T')] Show pod Status , wait 30s and retry #$RETRY_COUNT "
echo "${POD_STATUS}"
let RETRY_COUNT-- || true
sleep 30
elif [ "X${AVAILABLE_COUNT}" == "X${Replicas}" ]; then
echo "Deploy Running successed"
break
else
echo "[$(date '+%F %T')] NOT expected pod status: "
echo "${POD_STATUS}"
return 1
fi
done
if [ "X${RETRY_COUNT}" == "X0" ]; then
echo "[$(date '+%F %T')] show describe pod status: "
echo -e "`kubectl describe pod -n ${NameSpace} -l app=${AppName}`"
fi
}
#主流程函數執行
docker_login
build
### 第六步, 更改 YAML 文件中參數
cat > ${WORKSPACE}/${AppName}-deploy.yaml <<- EOF
#####################################################
#
# ${ACTION} Deployment
#
#####################################################
apiVersion: apps/v1beta2 # for versions before 1.8.0 use apps/v1beta1
kind: Deployment
metadata:
name: ${AppName}
namespace: ${NameSpace}
labels:
app: ${AppName}
version: ${VERSION}
AppEnv: ${AppEnv}
spec:
replicas: ${Replicas}
selector:
matchLabels:
app: ${AppName}
template:
metadata:
labels:
app: ${AppName}
spec:
containers:
- name: ${AppName}
image: ${ToImage}
ports:
- containerPort: ${ContainerPort}
livenessProbe:
httpGet:
path: ${HealthCheckURL}
port: ${ContainerPort}
initialDelaySeconds: 90
timeoutSeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: ${HealthCheckURL}
port: ${ContainerPort}
initialDelaySeconds: 5
timeoutSeconds: 5
periodSeconds: 5
# configmap env
env:
- name: CFG_LABEL
value: ${CFG_LABEL}
- name: HTTP_SERVER
value: ${HTTP_SERVER}
- name: CFG_ADDR
value: ${CFG_ADDR}
- name: DR_CFG_ZOOKEEPER_ENV_URL
value: ${DR_CFG_ZOOKEEPER_ENV_URL}
- name: ENTRYPOINT
valueFrom:
configMapKeyRef:
name: ${ConfigMap}
key: ENTRYPOINT
- name: HTTP_TAR_FILES
valueFrom:
configMapKeyRef:
name: ${ConfigMap}
key: HTTP_TAR_FILES
- name: WITH_SGHUB_APM_AGENT
valueFrom:
configMapKeyRef:
name: ${ConfigMap}
key: WITH_SGHUB_APM_AGENT
- name: WITH_TINGYUN
valueFrom:
configMapKeyRef:
name: ${ConfigMap}
key: WITH_TINGYUN
- name: CFG_FILES
valueFrom:
configMapKeyRef:
name: ${ConfigMap}
key: CFG_FILES
# configMap volume
volumeMounts:
- name: applogs
mountPath: /volume_logs/
volumes:
- name: applogs
hostPath:
path: /opt/app_logs/${AppName}
imagePullSecrets:
- name: ${ImagePullSecrets}
---
apiVersion: v1
kind: Service
metadata:
name: ${AppName}
namespace: ${NameSpace}
labels:
app: ${AppName}
spec:
ports:
- port: ${ContainerPort}
targetPort: ${ContainerPort}
selector:
app: ${AppName}
EOF
### 第七步,創建configmap 環境變量
kubectl delete configmap ${ConfigMap} -n ${NameSpace}
kubectl create configmap ${ConfigMap} ${ConfigMapData} -n ${NameSpace}
# 執行部署
deploy
# 打印配置
cat ${WORKSPACE}/${AppName}-deploy.yaml
# 執行啟動狀態檢查
check_status
### 實驗演示(二)