开启 agent
root@jenkins:~/learning-jenkins-cicd/07-jenkins-agents# docker-compose -f docker-compose-inbound-agent.yml up -d
pipeline {
agent { label 'docker-jnlp-agent' }
parameters {
booleanParam(name:'pushImage', defaultValue: 'true', description: 'Push Image to Harbor?')
}
tools {
maven 'Maven-3.8.8'
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
token: 'fClZ0e/kTcqL2ARh7YqxW/3ndOCZA2SqfKnRTLat',
causeString: 'Triggered on $ref',
printContributedVariables: true,
printPostContent: true
)
}
environment {
codeRepo="http://192.168.1.50/root/spring-boot-helloWorld.git"
harborServer='harbor.luohw.net'
projectName='spring-boot-helloworld'
imageUrl="${harborServer}/ikubernetes/${projectName}"
imageTag='latest'
}
stages {
stage('Source') {
steps {
git branch: 'main', credentialsId: 'gitlab-root-credential', url: "${codeRepo}"
}
}
stage('Build') {
steps {
sh 'mvn -B -DskipTests clean package'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
stage("SonarQube Analysis") {
steps {
withSonarQubeEnv('SonaQube-Server') {
sh 'mvn sonar:sonar'
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 30, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Docker Image') {
agent { label 'master' }
steps {
sh 'docker image build . -t "${imageUrl}:${imageTag}"'
// input(message: '镜像已经构建完成,是否要推送?')
}
}
stage('Push Docker Image') {
agent { label 'master' }
when {
expression { params.pushImage }
}
steps {
withCredentials([usernamePassword(credentialsId: 'harbor-user-credential', passwordVariable: 'harborUserPassword', usernameVariable: 'harborUserName')]) {
sh "docker login -u ${env.harborUserName} -p ${env.harborUserPassword} ${harborServer}"
sh "docker image push ${imageUrl}:${imageTag}"
}
}
}
}
post{
success{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=5530d220-0983-490e-ada5-a74fa66570c8'
}
failure{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=5530d220-0983-490e-ada5-a74fa66570c8'
}
}
}
192.168.1.51:50000
示例:基于Kubernetes完成构建
添加pod模版
kube-agent222
maven:3.8.6-eclipse-temurin-17-alpine
挂载路径
/root/.m2
编辑流水线
pipeline {
agent {
kubernetes {
inheritFrom 'kube-maven'
}
}
parameters {
booleanParam(name:'pushImage', defaultValue: 'true', description: 'Push Image to Harbor?')
}
tools {
maven 'Maven-3.8.8'
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
token: 'fClZ0e/kTcqL2ARh7YqxW/3ndOCZA2SqfKnRTLat',
causeString: 'Triggered on $ref',
printContributedVariables: true,
printPostContent: true
)
}
environment {
codeRepo="http://192.168.1.50/root/spring-boot-helloWorld.git"
harborServer='hub.magedu.com'
projectName='spring-boot-helloworld'
imageUrl="${harborServer}/ikubernetes/${projectName}"
imageTag='latest'
}
stages {
stage('Source') {
steps {
git branch: 'main', credentialsId: 'gitlab-root-credential', url: "${codeRepo}"
}
}
stage('Build') {
steps {
container('maven') {
sh 'mvn -B -DskipTests clean package'
}
}
}
stage('Test') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage("SonarQube Analysis") {
steps {
container('maven') {
withSonarQubeEnv('SonarQube-Server') {
sh 'mvn sonar:sonar'
}
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 30, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
post{
success{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=c79e3215-d39f-44a7-a760-fa0ab63ca46b'
}
failure{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=c79e3215-d39f-44a7-a760-fa0ab63ca46b'
}
}
}
pipeline {
agent {
kubernetes {
inheritFrom 'kube-maven'
}
}
parameters {
booleanParam(name:'pushImage', defaultValue: 'true', description: 'Push Image to Harbor?')
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
token: 'fClZ0e/kTcqL2ARh7YqxW/3ndOCZA2SqfKnRTLat',
causeString: 'Triggered on $ref',
printContributedVariables: true,
printPostContent: true
)
}
environment {
codeRepo="http://192.168.1.50/root/spring-boot-helloWorld.git"
harborServer='harbor.luohw.net'
projectName='spring-boot-helloworld'
imageUrl="${harborServer}/ikubernetes/${projectName}"
imageTag='latest'
}
stages {
stage('Source') {
steps {
git branch: 'main', credentialsId: 'gitlab-root-credential', url: "${codeRepo}"
}
}
stage('Build') {
steps {
container('maven') {
sh 'mvn -B -DskipTests clean package'
}
}
}
stage('Test') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage("SonarQube Analysis") {
steps {
container('maven') {
withSonarQubeEnv('SonaQube-Server') {
sh 'mvn sonar:sonar'
}
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 30, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
post{
success{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=c79e3215-d39f-44a7-a760-fa0ab63ca46b'
}
failure{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=c79e3215-d39f-44a7-a760-fa0ab63ca46b'
}
}
}
流水线 maven-and-docker
pipeline {
agent {
kubernetes {
inheritFrom 'maven-and-docker'
}
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
token: 'fClZ0e/kTcqL2ARh7YqxW/3ndOCZA2SqfKnRTLat',
causeString: 'Triggered on $ref',
printContributedVariables: true,
printPostContent: true
)
}
environment {
codeRepo="http://192.168.1.50/root/spring-boot-helloWorld.git"
harborServer='harbor.luohw.net'
projectName='spring-boot-helloworld'
imageUrl="${harborServer}/ikubernetes/${projectName}"
imageTag="${BUILD_ID}"
}
stages {
stage('Source') {
steps {
git branch: 'main', credentialsId: 'gitlab-root-credential', url: "${codeRepo}"
}
}
stage('Build') {
steps {
container('maven') {
sh 'mvn -B -DskipTests clean package'
}
}
}
stage('Test') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage("SonarQube Analysis") {
steps {
container('maven') {
withSonarQubeEnv('SonaQube-Server') {
sh 'mvn sonar:sonar'
}
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 30, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Image') {
steps {
container('dind') {
sh 'docker image build -t ${imageUrl}:${imageTag} .'
}
}
}
stage('Push Image') {
steps {
withCredentials([usernamePassword(credentialsId: 'harbor-user-credential', passwordVariable: 'harborUserPassword', usernameVariable: 'harborUserName')]) {
container('dind') {
sh ''' #当在一个内部需要运行多条shell命令的时候,可以三引号,不必每一步写shell
echo ${harborUserPassword} | docker login -u ${harborUserName} --password-stdin ${harborServer}
docker image push ${imageUrl}:${imageTag}
docker image tag ${imageUrl}:${imageTag} ${imageUrl}:latest
docker image push ${imageUrl}:latest
'''
}
}
}
}
}
post {
always {
mail to: '1153454651@qq.com',
subject: "Status of pipeline: ${currentBuild.fullDisplayName}",
body: "${env.BUILD_URL} has result ${currentBuild.result}"
}
}
}
每一个节点能够解析harbor.luohw.net
打完镜像是放在节点上
确保不验证证书
pipeline {
agent {
kubernetes {
inheritFrom 'maven-and-docker' #不在使用maven模板,使用maven-and-docker模板
}
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
token: 'fClZ0e/kTcqL2ARh7YqxW/3ndOCZA2SqfKnRTLat',
causeString: 'Triggered on $ref',
printContributedVariables: true,
printPostContent: true
)
}
environment {
codeRepo="http://192.168.1.50/root/spring-boot-helloWorld.git"
harborServer='harbor.luohw.net'
projectName='spring-boot-helloworld'
imageUrl="${harborServer}/ikubernetes/${projectName}"
imageTag="${BUILD_ID}"
}
stages {
stage('Source') {
steps {
git branch: 'main', credentialsId: 'gitlab-root-credential', url: "${codeRepo}"
}
}
stage('Build') {
steps {
container('maven') {
sh 'mvn -B -DskipTests clean package'
}
}
}
stage('Test') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage("SonarQube Analysis") {
steps {
container('maven') {
withSonarQubeEnv('SonaQube-Server') {
sh 'mvn sonar:sonar'
}
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 30, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Image') {
steps {
container('dind') {
sh 'docker image build -t ${imageUrl}:${imageTag} .'
}
}
}
stage('Push Image') {
steps {
withCredentials([usernamePassword(credentialsId: 'harbor-user-credential', passwordVariable: 'harborUserPassword', usernameVariable: 'harborUserName')]) {
container('dind') {
sh ''' #当在一个内部需要运行多条shell命令的时候,可以三引号,不必每一步写shell
echo ${harborUserPassword} | docker login -u ${harborUserName} --password-stdin ${harborServer}
docker image push ${imageUrl}:${imageTag}
docker image tag ${imageUrl}:${imageTag} ${imageUrl}:latest
docker image push ${imageUrl}:latest
'''
}
}
}
}
}
post {
always {
mail to: '2368756722@qq.com',
subject: "Status of pipeline: ${currentBuild.fullDisplayName}",
body: "${env.BUILD_URL} has result ${currentBuild.result}"
}
}
}
其他推送镜像方式
示例12:在Pod Agetnt中制作和推送Docker Image的另一种方法
Docker Pipline插件提供了一个名为docker的全局变量,以docker变量作为入口,可以调用该插件的所有功能。
build(image):基于当前工作目录下的Dockerfile构建Image,其name和tag则由“image”指定;
push():推送构建好的Image对象;
push('tag'):将构建好的Image对象上的tag替换为此处指定的tag,并完成push
stage('Build Image') {
steps {
container('dind') {
script {
dockerImage = docker.build("${imageUrl}:${imageTag}")
}
}
}
}
stage('Push Image') {
steps {
container('dind') {
script {
docker.withRegistry(registryUrl, registryCredential) {
dockerImage.push()
dockerImage.push('latest')
}
}
}
}
}
}
jenkins安装 kubernetes cli 插件
通过令牌认证,准备认证
root@k8s-master01:~/learning-jenkins-cicd/08-jenkins-on-kubernetes/static-token-auth-example# cp -rp auth /etc/kubernetes/
创建
root@k8s-master01:/# cat /etc/kubernetes/auth/tokens.csv
83d07d.d1d200dd0c85c694,tom,1001,“kube-users”
7dbe3f.03d7e8f69d576210,jerry,1002,“kube-admins”
07c31c.17bddf45d355d902,mageedu,1003,“kube-admins”
拷贝apiserver并添加配置
cp /etc/kubernetes/manifests/kube-apiserver.yaml /
修改后的yaml文件
cat kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 192.168.1.180:6443
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=192.168.1.180
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --token-auth-file=/etc/kubernetes/auth/token.csv
- --enable-admission-plugins=NodeRestriction
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
- --requestheader-allowed-names=front-proxy-client
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --requestheader-group-headers=X-Remote-Group
- --requestheader-username-headers=X-Remote-User
- --secure-port=6443
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
image: registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.24.17
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: 192.168.1.180
path: /livez
port: 6443
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-apiserver
readinessProbe:
failureThreshold: 3
httpGet:
host: 192.168.1.180
path: /readyz
port: 6443
scheme: HTTPS
periodSeconds: 1
timeoutSeconds: 15
resources:
requests:
cpu: 250m
startupProbe:
failureThreshold: 24
httpGet:
host: 192.168.1.180
path: /livez
port: 6443
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/ssl/certs
name: ca-certs
readOnly: true
- mountPath: /etc/ca-certificates
name: etc-ca-certificates
readOnly: true
- mountPath: /etc/pki
name: etc-pki
readOnly: true
- mountPath: /etc/kubernetes/pki
name: k8s-certs
readOnly: true
- mountPath: /usr/local/share/ca-certificates
name: usr-local-share-ca-certificates
readOnly: true
- mountPath: /usr/share/ca-certificates
name: usr-share-ca-certificates
readOnly: true
- mountPath: /etc/kubernetes/auth/
name: static-auth-token
readOnly: true
hostNetwork: true
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- hostPath:
path: /etc/ssl/certs
type: DirectoryOrCreate
name: ca-certs
- hostPath:
path: /etc/ca-certificates
type: DirectoryOrCreate
name: etc-ca-certificates
- hostPath:
path: /etc/pki
type: DirectoryOrCreate
name: etc-pki
- hostPath:
path: /etc/kubernetes/pki
type: DirectoryOrCreate
name: k8s-certs
- hostPath:
path: /usr/local/share/ca-certificates
type: DirectoryOrCreate
name: usr-local-share-ca-certificates
- hostPath:
path: /usr/share/ca-certificates
type: DirectoryOrCreate
name: usr-share-ca-certificates
- hostPath:
path: /etc/kubernetes/auth
type: DirectoryOrCreate
name: static-auth-token
status: {}
修改后替换原来的 cp /kube-apiserver.yaml /etc/kubernetes/manifests/
k8s中创建角色
root@k8s-master01:/etc/kubernetes/auth# kubectl create clusterrolebinding tom-cluster-admin --clusterrole=cluster-admin --user=tom
clusterrolebinding.rbac.authorization.k8s.io/tom-cluster-admin create
在jenkins添加tom凭证,ID为 k8s-user-tom-cluster-admin-token,后面jenkins流水线会引用
jenkins创建流水线
第二种:基于证书添加认证用户账号
root@k8s-master01:~# kubectl create sa k8s-cicd-admin -n kube-system #建了一个名为 k8s-cicd-admin 的 ServiceAccount,并将其放置在 kube-system 命名空间中。ServiceAccount 是 Kubernetes 中用于验证和授权 Pod 访问 API 的机制。
root@k8s-master01:~# kubectl create clusterrolebinding k8s-cicd-admin --clusterrole=cluster-admin --serviceaccount=kube-system:k8s-cicd-admin
#该命令创建了一个名为 k8s-cicd-admin 的 ClusterRoleBinding(集群角色绑定),将 cluster-admin 集群角色授予了 kube-system:k8s-cicd-admin ServiceAccount。ClusterRoleBinding 用于将角色绑定到特定的用户、组或 ServiceAccount 上,以授予其相应的权限。
root@k8s-master01:~# kubectl run mypod --image=ikubernetes/demoapp:v1.0 --dry-run=client -oyaml -n kube-system > mypod.ymal
创建一个测试容器获取token
root@k8s-master01:~# cat mypod.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: mypod
name: mypod
namespace: kube-system
spec:
containers:
- image: ikubernetes/demoapp:v1.0
name: mypod
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
serviceAccountName: k8s-cicd-admin
root@k8s-master01:~# kubectl apply -f mypod.yaml
pod/mypod created
root@k8s-master01:~# kubectl exec -it -n kube-system mypod -- sh
[root@mypod /]# cat /run/secrets/kubernetes.io/serviceaccount/token && echo
eyJhbGciOiJSUzI1NiIsImtpZCI6IklQaC1rY29PSDVqMlZGblpYbVpwMGhzN0V2Nm5jSFNTd3Vmc1dBQ0dTa1EifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxODM5MDE5LCJpYXQiOjE3MDAzMDMwMTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsInBvZCI6eyJuYW1lIjoibXlwb2QiLCJ1aWQiOiJhOTg3ZDlhYS05NDExLTQ2YzgtYWZhNy1hYjUyMTA5MzY4MGUifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6Ims4cy1jaWNkLWFkbWluIiwidWlkIjoiNGQzN2U4MjktOTE5ZS00MDcwLTk4YTUtMDA1YzAwZDAwMzc5In0sIndhcm5hZnRlciI6MTcwMDMwNjYyNn0sIm5iZiI6MTcwMDMwMzAxOSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOms4cy1jaWNkLWFkbWluIn0.sW75Lp3YQzWheZoz1sQOy65ADGjszJ3WzWbWsKs_4ktI7gJf27iwC7b-kE_m5sXFT4GUaVWl79A7ObEj8D8mRCl9LJDtWunD20kdSa5dcj6_miCUZwgh46bFamB5pkNlZTUElAXMHCmUciBlnDTmXrPQGn-Ssc3sSh5i9xcHngeN9Om9RRfCr5IP_4GAmnwkKLEDKL9arQlYYrIIKrUMXA070RHqcZp2BpCpoAMY0FX5dhvZIfgnCbLj4d5PPMJKQslyEKLG9BwPXfzjY6sf3TLhGSXmidADpjg_Vknut3KZIO_VOM9vFrvN6TjkafhaP8CAKGZ5gLH0dhRBH4nndQ
复制令牌添加到jenkins
k8s-cicd-admin-credentials
修改流水线中的deploy令牌
pipeline {
agent {
kubernetes {
inheritFrom 'maven-docker-kubectl'
}
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
token: 'fClZ0e/kTcqL2ARh7YqxW/3ndOCZA2SqfKnRTLat',
causeString: 'Triggered on $ref',
printContributedVariables: true,
printPostContent: true
)
}
environment {
codeRepo="http://192.168.1.50/root/spring-boot-helloWorld.git"
registry='harbor.luohw.net'
registryUrl='https://harbor.luohw.net'
registryCredential='harbor-user-credential'
projectName='spring-boot-helloworld'
imageUrl="${registry}/ikubernetes/${projectName}"
imageTag="${BUILD_ID}"
}
stages {
stage('Source') {
steps {
git branch: 'main', credentialsId: 'gitlab-root-credential', url: "${codeRepo}"
}
}
stage('Build') {
steps {
container('maven') {
sh 'mvn -B -DskipTests clean package'
}
}
}
stage('Test') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage("SonarQube Analysis") {
steps {
container('maven') {
withSonarQubeEnv('SonaQube-Server') {
sh 'mvn sonar:sonar'
}
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 30, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Image') {
steps {
container('dind') {
script {
dockerImage = docker.build("${imageUrl}:${imageTag}")
}
}
}
}
stage('Push Image') {
steps {
container('dind') {
script {
docker.withRegistry(registryUrl, registryCredential) {
dockerImage.push()
dockerImage.push('latest')
}
}
}
}
}
stage('Update-manifests') {
steps {
container('jnlp') {
sh 'sed -i "s#__IMAGE__#${imageUrl}:${imageTag}#gi" deploy/all-in-one.yaml'
}
}
}
stage('Deploy') {
steps {
container('kubectl') {
withKubeConfig([credentialsId: 'k8s-cicd-admin-credentials', serverUrl: 'https://kubernetes.default.svc']) {
sh '''
kubectl apply -f deploy/01-namespace.yaml
kubectl apply -f deploy/all-in-one.yaml -n hello
'''
}
}
}
}
}
post{
always{
qyWechatNotification failNotify: true, webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=5530d220-0983-490e-ada5-a74fa66570c8'
}
}
}
root@k8s-node01:~/.kube# kubectl get pod -n hello -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
spring-boot-helloworld-55d658d97d-jrwzv 1/1 Running 0 4m5s 10.244.2.174 k8s-node02 <none> <none>