写在最前
在本章节中,我们将探索如何利用KubeSphere的DevOps功能来实现自动化的软件开发和部署流程。首先,我们需要确保KubeSphere平台已经搭建完成,并且相关的中间件服务已经部署在Kubernetes集群中。此外,我们还需要对CoreDNS进行适当的配置,以支持中间件服务的域名解析。
1. 前置要求
2. 部署流程
2.1 中间件配置
以kubernetes方式部署nacos,redis,mysql并以NodePort方式访问,按照若依本地部署方式将数据库初始化进去即可进行后续操作。其中nacos要创建新的namespace叫做uat,并且将默认的配置克隆到uat里面。
2.2 coredns配置
为了兼容dev环境的通用域名,coredns需要重写一下配置即可实现。
rewrite name app-redis1.basic.tanqidi.com redis.basic.svc.cluster.local
rewrite name app-nacos1.basic.tanqidi.com nacos.basic.svc.cluster.local
rewrite name app-mysql1.basic.tanqidi.com mysql8.basic.svc.cluster.local
# 若依后端服务域名,前端的 nginx.conf 配置中会用到该域名进行反向代理,转给k8s的集群内域名
rewrite name ruoyi-gateway.basic.tanqidi.com ruoyi-gateway.ruoyi.svc.cluster.local
2.3 创建企业空间
在构建KubeSphere环境中的DevOps实践时,企业空间的命名通常与公司的组织结构紧密相关。它可以基于公司名称、分公司或者特定的研发部门来设定。这种设计是KubeSphere提供的一种高级抽象,它并不直接属于Kubernetes的范畴,而是KubeSphere在Kubernetes之上构建的多租户管理功能的一部分。
2.4 创建项目
kubesphere中的项目就是k8s中的namespace,在此我就使用ruoyi
作为项目名
2.5 创建 DevOps 项目
2.6 配置凭证
后续会使用到gitee和aliyun镜像仓库,这里需要预先创建好凭证
gitee-secret(用户名和密码)
aliyun-harbor-secret(用户名和密码)
k8s-kubeconfig(kubeconfig)
2.7 创建流水线
因为ruoyi项目是一份代码包含多个子模块因此我们只需要创建两个流水线,分别是ruoyi-service,ruoyi-ui
2.7.1 ruoyi-service
注意看jenkinsfile中的environment部分它很关键,在这里配置harbor的仓库地址和它的namespace,与k8s集群中的namespace
拉取代码
项目编辑
镜像构建:通过
parameters
来完成,在运行的时候会choice选择一个值例如是ruoyi-system
,则cd $MODULE_NAME 进去这个目录,从而使用该目录下的Dockerfile来构建镜像。我的k8s叫做用户验收测试环境(UAT,user acceptance testing),但是Dockerfile里面还写着dev就不合适了,所以我加了一个sed语句,将dev全部替换成为uat让它指向到nacos的uat名称空间。镜像推送
部署到UAT环境:请注意观察DEPLOY_YAML这个变量,他的内容是一份部署模板yaml,关键内容都是读取
environment
的变量来动态替换生成的,最后通过它来生成文件最后应用进集群echo \'${DEPLOY_YAML}\' > deploy.yaml
pipeline {
agent {
node {
label 'maven'
}
}
stages {
stage('拉取项目') {
agent none
when {
environment name: 'BUILD_ENV', value: 'UAT_CICD'
}
steps {
container('maven') {
git(credentialsId: 'gitee-secret', branch: 'tanqidi_v3.6.4', url: 'https://gitee.com/tanqidi/RuoYi-Cloud_1.git', changelog: true, poll: false)
}
}
}
stage('项目编译') {
agent none
when {
environment name: 'BUILD_ENV', value: 'UAT_CICD'
}
steps {
container('maven') {
sh 'mvn -Dmaven.test.skip=true clean package'
}
}
}
stage('镜像构建') {
agent none
when {
environment name: 'BUILD_ENV', value: 'UAT_CICD'
}
steps {
container('maven') {
script {
def environment = params.MODULE_NAME
if (environment == "ruoyi-auth" || environment == "ruoyi-gateway") {
sh 'cd $MODULE_NAME && sed -i \'s/dev/uat/g\' ./Dockerfile && docker build -t $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION .'
} else if (environment == "ruoyi-monitor") {
sh 'cd ruoyi-visual/$MODULE_NAME && sed -i \'s/dev/uat/g\' ./Dockerfile && docker build -t $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION .'
} else {
sh 'cd ruoyi-modules/$MODULE_NAME && sed -i \'s/dev/uat/g\' ./Dockerfile && docker build -t $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION .'
}
}
}
}
}
stage('镜像推送') {
agent none
when {
environment value: 'UAT_CICD', name: 'BUILD_ENV'
}
steps {
container('maven') {
withCredentials([usernamePassword(credentialsId : 'aliyun-harbor-secret' ,passwordVariable : 'name' ,usernameVariable : 'passwd' ,)]) {
sh 'echo "$name" | docker login $REGISTRY -u "$passwd" --password-stdin'
sh 'docker push $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION'
}
}
}
}
stage('部署到UAT环境') {
agent none
steps {
container('maven') {
withCredentials([kubeconfigFile(credentialsId: "$KUBECONFIG_CREDENTIAL_ID", variable: 'KUBECONFIG')]) {
script {
sh "echo \'${DEPLOY_YAML}\' > deploy.yaml"
sh "cat deploy.yaml"
sh "envsubst < deploy.yaml | kubectl apply -f -"
}
}
}
}
}
}
environment {
KUBECONFIG_CREDENTIAL_ID = 'k8s-kubeconfig'
REGISTRY = 'registry.cn-hangzhou.aliyuncs.com'
HARBOR_NAMESPACE = 'tanqidi-temp'
PROJECT_NAMESPACE = 'ruoyi'
DEPLOY_YAML = '''
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ${MODULE_NAME}
name: ${MODULE_NAME}
namespace: ${PROJECT_NAMESPACE}
spec:
replicas: 1
selector:
matchLabels:
app: ${MODULE_NAME}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
template:
metadata:
labels:
app: ${MODULE_NAME}
appType: ruoyi-service
spec:
imagePullSecrets:
- name: harbor
containers:
- name: ${MODULE_NAME}
imagePullPolicy: Always
image: $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION
ports:
- containerPort: 8080
protocol: TCP
resources:
limits:
cpu: \'1\'
memory: 2Gi
requests:
cpu: 25m
memory: 850Mi
restartPolicy: Always
terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: ${MODULE_NAME}
namespace: ${PROJECT_NAMESPACE}
labels:
app: ${MODULE_NAME}
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: ${MODULE_NAME}
type: ClusterIP
sessionAffinity: None
'''
}
parameters {
choice(name: 'MODULE_NAME', choices: '''ruoyi-auth
ruoyi-gateway
ruoyi-system
ruoyi-job
ruoyi-monitor
ruoyi-file''', description: '模块构建')
choice(name: 'BUILD_ENV', choices: '''UAT_CICD
UAT_CD''', description: '构建环境')
string(name: 'MODULE_VERSION', defaultValue: 'v0.0Beta', description: '模块版本')
}
}
2.7.2 ruoyi-ui
流水线部署成功后访问 30889 端口即可,这是使用NodePort访问暴露的服务。
pipeline {
agent {
node {
label 'nodejs'
}
}
stages {
stage('拉取项目') {
agent none
when {
environment name: 'BUILD_ENV', value: 'UAT_CICD'
}
steps {
container('nodejs') {
git(credentialsId: 'gitee-secret', branch: 'tanqidi_v3.6.4', url: 'https://gitee.com/tanqidi/RuoYi-Cloud_1.git', changelog: true, poll: false)
}
}
}
stage('项目编译') {
agent none
when {
environment name: 'BUILD_ENV', value: 'UAT_CICD'
}
steps {
container('nodejs') {
sh 'cd $MODULE_NAME && npm install --registry=https://registry.npmmirror.com && npm run build:prod'
}
}
}
stage('镜像构建') {
agent none
when {
environment name: 'BUILD_ENV', value: 'UAT_CICD'
}
steps {
container('nodejs') {
sh 'cd $MODULE_NAME && docker build -t $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION -f Dockerfile .'
}
}
}
stage('镜像推送') {
agent none
when {
environment value: 'UAT_CICD', name: 'BUILD_ENV'
}
steps {
container('nodejs') {
withCredentials([usernamePassword(credentialsId : 'aliyun-harbor-secret' ,passwordVariable : 'name' ,usernameVariable : 'passwd' ,)]) {
sh 'echo "$name" | docker login $REGISTRY -u "$passwd" --password-stdin'
sh 'docker push $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION'
}
}
}
}
stage('部署到UAT环境') {
agent none
steps {
container('nodejs') {
withCredentials([kubeconfigFile(credentialsId: "$KUBECONFIG_CREDENTIAL_ID", variable: 'KUBECONFIG')]) {
script {
sh "echo \'${DEPLOY_YAML}\' > deploy.yaml"
sh "cat deploy.yaml"
sh "envsubst < deploy.yaml | kubectl apply -f -"
}
}
}
}
}
}
environment {
KUBECONFIG_CREDENTIAL_ID = 'k8s-kubeconfig'
REGISTRY = 'registry.cn-hangzhou.aliyuncs.com'
HARBOR_NAMESPACE = 'tanqidi-temp'
MODULE_NAME = 'ruoyi-ui'
PROJECT_NAMESPACE = 'ruoyi'
DEPLOY_YAML = '''
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ${MODULE_NAME}
name: ${MODULE_NAME}
namespace: ${PROJECT_NAMESPACE}
spec:
replicas: 1
selector:
matchLabels:
app: ${MODULE_NAME}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
template:
metadata:
labels:
app: ${MODULE_NAME}
appType: ruoyi-web
spec:
imagePullSecrets:
- name: harbor
containers:
- name: ${MODULE_NAME}
imagePullPolicy: Always
image: $REGISTRY/$HARBOR_NAMESPACE/$MODULE_NAME:$MODULE_VERSION
ports:
- containerPort: 80
protocol: TCP
resources:
limits:
cpu: \'1\'
memory: 2Gi
requests:
cpu: 25m
memory: 850Mi
restartPolicy: Always
terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: ${MODULE_NAME}
namespace: ${PROJECT_NAMESPACE}
labels:
app: ${MODULE_NAME}
spec:
ports:
- port: 80
targetPort: 80
nodePort: 30889
selector:
app: ${MODULE_NAME}
type: NodePort
sessionAffinity: None
'''
}
parameters {
choice(name: 'BUILD_ENV', choices: '''UAT_CICD
UAT_CD''', description: '构建环境')
string(name: 'MODULE_VERSION', defaultValue: 'v0.0Beta', description: '模块版本')
}
}
3. 结果演示
可以看到结果完全正确!
4. 网络策略(拓展)
在部署模板中,我们为后端和前端分别贴上了appType: ruoyi-service
和appType: ruoyi-web
的标签。这样一来,我们就能针对它们制定专门的网络策略,从而提升安全性。
4.1 ruoyi-service
ruoyi 名称空间
选中 ruoyi 名称空间中带有appType标签是 ruoyi-service 的pod容器
允许当前 namespace 中的其他pod容器访问我
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ruoyi-service
namespace: ruoyi
spec:
podSelector:
matchExpressions:
- key: appType
operator: In
values:
- ruoyi-service
policyTypes:
- Ingress
ingress:
- from:
# 允许当前 namespace 中的其他pod容器访问我
- podSelector: {}
4.2 ruoyi-web
ruoyi 名称空间
选中 ruoyi 名称空间中带有appType标签是 ruoyi-web 的pod容器
允许从ingress-nginx的namespace任意的pod访问我,允许通过nodeport来访问我
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ruoyi-web
namespace: ruoyi
spec:
podSelector:
matchExpressions:
- key: appType
operator: In
values:
- ruoyi-web
policyTypes:
- Ingress
ingress:
- from:
# 允许流量从 ingress-nginx 进来
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
# 让nodeport方式生效,因为是通过nodeport来到service,kube-proxy会进行snat转发,会丢失源ip,只能将每个节点的 tunl0 网卡ip配置进来
- from:
- ipBlock:
cidr: 10.233.127.0/32
5. 操作总结
为了简化UAT环境的配置,我们只需对CoreDNS稍作调整,即可将通用域名映射到集群内部域名,无需更改开发环境设置。同时,通过在流水线中使用sed
命令来变更Dockerfile中的dev关键词成为uat,同时需要在nacos中创建出uat的namespace将默认的配置克隆进去变更内容即可。