写在最前

在本章节中,我们将探索如何利用KubeSphere的DevOps功能来实现自动化的软件开发和部署流程。首先,我们需要确保KubeSphere平台已经搭建完成,并且相关的中间件服务已经部署在Kubernetes集群中。此外,我们还需要对CoreDNS进行适当的配置,以支持中间件服务的域名解析。

1. 前置要求

2. 部署流程

2.1 中间件配置

以kubernetes方式部署nacos,redis,mysql并以NodePort方式访问,按照若依本地部署方式将数据库初始化进去即可进行后续操作。其中nacos要创建新的namespace叫做uat,并且将默认的配置克隆到uat里面。

图片-tbtl.png

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之上构建的多租户管理功能的一部分。

image-ipsd.png

2.4 创建项目

kubesphere中的项目就是k8s中的namespace,在此我就使用ruoyi作为项目名

image-uekl.png

2.5 创建 DevOps 项目

image-pcep.png

2.6 配置凭证

后续会使用到gitee和aliyun镜像仓库,这里需要预先创建好凭证

  • gitee-secret(用户名和密码)

  • aliyun-harbor-secret(用户名和密码)

  • k8s-kubeconfig(kubeconfig)

image-mias.png

image-twzd.png

image-yhxg.png

2.7 创建流水线

因为ruoyi项目是一份代码包含多个子模块因此我们只需要创建两个流水线,分别是ruoyi-service,ruoyi-ui

image-neal.png

2.7.1 ruoyi-service

注意看jenkinsfile中的environment部分它很关键,在这里配置harbor的仓库地址和它的namespace,与k8s集群中的namespace

  1. 拉取代码

  2. 项目编辑

  3. 镜像构建:通过parameters来完成,在运行的时候会choice选择一个值例如是ruoyi-system,则cd $MODULE_NAME 进去这个目录,从而使用该目录下的Dockerfile来构建镜像。我的k8s叫做用户验收测试环境(UAT,user acceptance testing),但是Dockerfile里面还写着dev就不合适了,所以我加了一个sed语句,将dev全部替换成为uat让它指向到nacos的uat名称空间。

  4. 镜像推送

  5. 部署到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. 结果演示

可以看到结果完全正确!

图片-wecm.png

4. 网络策略(拓展)

在部署模板中,我们为后端和前端分别贴上了appType: ruoyi-serviceappType: ruoyi-web的标签。这样一来,我们就能针对它们制定专门的网络策略,从而提升安全性。

4.1 ruoyi-service

  1. ruoyi 名称空间

  2. 选中 ruoyi 名称空间中带有appType标签是 ruoyi-service 的pod容器

  3. 允许当前 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: {}

图片-ufsa.png

4.2 ruoyi-web

  1. ruoyi 名称空间

  2. 选中 ruoyi 名称空间中带有appType标签是 ruoyi-web 的pod容器

  3. 允许从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将默认的配置克隆进去变更内容即可。