专栏文章 持续集成的开源方案攻略 (四) jenkins 与 k8s 集成的通信原理与配置记录

孙高飞 · 2020年02月24日 · 最后由 孙高飞 回复于 2020年02月25日 · 6710 次阅读

前言

今天的内容很偏理论, 主要讲解的内容是 jenkins 与 k8s 集成时如何进行配置,以及记录背后的原理。 为什么着重拉一篇帖子写这部分内容呢。 因为在 jenkins 与 k8s 集成的实践中,这部分内容算是比较难的。尤其新手很容易卡在跟 k8s 通信时的安全认证和权限问题上。

配置流程

  • 在插件管理中安装 kubernetes 插件
  • 在系统配置中,添加一个云
  • 在云配置中填写 kubernetes 的配置,其中包括:
    • kubernetes api server 的 ip 地址和端口号
    • kubernetes 根证书 key
    • 如果 jenkins master 部署到 k8s 中的话很简单, 按照官方文档部署即可。 如果 jenkins master 部署在 k8s 外则比较麻烦,尤其是在任何文档中都没有详细说明,只提了一句要填写 jenkins 的 service account 的 token。 其实具体的步骤是需要在 k8s 中为 jenkins 创建一个 service account, 然后为这个 service account 配置 rbac 以便让 jenkins 有权限创建相关资源。 然后 service account 下的 secret 下复制它的 token。 最后在 jenkins 中添加一个 secret text 类型的凭据,把 token 内容复制进去。 这样才能让 jenkins 与 k8s 通信。 今天也是主要讲这部分的内容以及背后的原理

k8s 安全机制详解

这里主要讲下上面第三步如何配置 kubernetes 的方法。 这里涉及到 k8s 的安全机制。 之前在研究的时候觉得这块内容非常好,所以在这里记录一下。
k8s 有很多种安全认证机制,可以说这方面它做的还是很复杂的。这里主要涉及的是基于数据证书和 service account token 的认证以及 rbac 的鉴权机制。

x509 数据证书认证

x509 认证是默认开启的认证方式, api-server 启动时会指定 k8s 的根证书 (ca) 以及 ca 的私钥,只要是通过 ca 签发的客户端 x509 证书,则可认为是可信的客户端。 这里提一下 k8s 是启用 https 进行通信的, 而且是双向加密。 也就是说不仅仅需要客户端信任服务端的根证书 (ca) 来验证服务端的身份, 也需要服务端信任客户端的数字证书来验证客户端的身份。

数据证书的原理

这里我先用一点时间来介绍一下数字证书的双向认证方式 (尽量简单易懂)。 互联网在通信时如果使用 tls 或者 ssl 协议作为安全加密方式的话, 基本认证用户信息都是采用非对称加密方式 (只有身份认证这么做, 因为非对称加密性能差) 也就是我们常见的一个私钥一个公钥。 自己留一个私钥,公钥公开给所有人。 那么从自己这里发送给别人的报文只有对应的公钥才能解密, 所以如果对方用你公开的公钥来解密,解密成功,那就能证明你是你, 如果解密不成功,那这个请求就是有风险的。 而如果对方要发消息给自己, 对方就要拿着公开的公钥加密它的小心然后发送过来, 如果你自己可以用私钥解密成功, 就说明这个报文没有经过篡改,如果解密失败那就说这个报文被人篡改过。 这就是非对称加密, 私钥和公钥都能加密数据,但是只有私钥才能解密公钥加密的数据,也只有公钥才能解密私钥加密的数据。

所以数据证书就是基于非对称加密来的。 它包含公钥、名称以及证书授权中心的数字签名。 客户端下载服务端的数字证书并选择信任这个证书。 那么就可以正常通信, 如果没有服务端的证书会怎么样。 还记得我们在浏览器中访问带有 https 的网站会蹦出来的的安全提示信息么 ----- 这是一个不受信任的网址,是否继续访问。 因为我们没有下载服务端证书并信任它, 所以我们手中没有对应的公钥能解密,所以这就是个不安全的访问 (无法确认对方的身份)。 我们可以选择继续访问, 当然这时候会有人问既然没有公钥解密那是怎么能选择继续访问的呢, 我们刚才也说了,非对称算法只用来做身份的验证,它不会加密真正的报文数据。报文加密是对称算法做的事这里不细讲了。 所以我们做接口测试的时候如果遇到 https 的请求,需要下载数据证书并用代码加载, 或者就直接用代码的方式忽略这个身份认证的风险

k8s 的数字证书认证

k8s 在部署时会自己创建一个 CA(证书颁发)并产生 CA 的私钥和数字证书。 k8s 其他服务的服务也都会生成自己的私钥并申请给 CA,让 CA 办法服务自己的证书。 所以客户端,比如我们这次将的 jenkins 要与 k8s 整合, 要填写的证书的 key 的配置。其实是 CA 的证书 (也就是跟证书) 而不是 api-server 的证书。 因为证书的机制是你信任了 CA 的证书, 那么也就顺带新人了 CA 颁发的所有其他的证书了。 那么在 k8s 中怎么查看根证书信息呢? 如果你能得到 kubeconfig 文件的话,那么它就在 kubeconfig 文件中的 cluster 信息里。 如果你不知道 kubeconfig 文件在哪, 可以使用命令 kubectl config view --raw 来查看.

上面的 certificate-authority-data 中的内容就是根证书了, 我们只需要复制下来经过 base64 转码就可以复制到 jenkins 中了。 这样就做到了 k8s 发送给 jenkins 的认证了, 因为 jenkins 获取并信任了根证书。 但是这样是不够的,因为 k8s 要的是双向认证, 所以光是客户端认证了 k8s 的身份不够,还需要 k8s 能够认证客户端的身份。 那么在这里有两个认证方式,我们首先说数据证书的方式。也就是说套路是客户端自己也要生成一个数字证书,然后发请求给 CA, CA 再给客户端办法证书。 因为 k8s 服务端也是信任 CA 证书的,所以也就连带信任了客户端的证书。 但是这种方式比较麻烦而且无法解决权限的问题 (创建 k8s 资源的权限)。所以我们不能使用这种方式。 下面只给出这种方式步骤不会继续验证它的作用:

  • 使用 openssl 命令生成私钥
  • 使用 openssl 命令生成一个 csr 请求文件 (csr 就是要发送给 CA 用来请求颁发一个证书的请求文件)
  • 使用 kubectl 命令创建一个 k8s 的 csr(CertificateSigningRequest) 对象,对象里要把刚才生成的 csr 文件内容粘贴上去。
  • 使用 kubectl certificate approve 命令颁发证书 (这一步也就是让 CA 给客户端颁发证书了)。我们使用这个证书就可以。

代码参考:

csrName=${service}.${namespace}
tmpdir=$(mktemp -d)
echo "creating certs in tmpdir ${tmpdir} "

cat <<EOF >> ${tmpdir}/csr.conf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${service}
DNS.2 = ${service}.${namespace}
DNS.3 = ${service}.${namespace}.svc
EOF

openssl genrsa -out ${tmpdir}/server-key.pem 2048
openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf

# clean-up any previously created CSR for our service. Ignore errors if not present.
kubectl delete csr ${csrName} 2>/dev/null || true

# create  server cert/key CSR and  send to k8s API
cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: ${csrName}
spec:
  groups:
  - system:authenticated
  request: $(cat ${tmpdir}/server.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF

# verify CSR has been created
while true; do
    kubectl get csr ${csrName}
    if [ "$?" -eq 0 ]; then
        break
    fi
done

# approve and fetch the signed certificate
kubectl certificate approve ${csrName}
# verify certificate has been signed
for x in $(seq 10); do
    serverCert=$(kubectl get csr ${csrName} -o jsonpath='{.status.certificate}')
    if [[ ${serverCert} != '' ]]; then
        break
    fi
    sleep 1
done
if [[ ${serverCert} == '' ]]; then
    echo "ERROR: After approving csr ${csrName}, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2
    exit 1
fi
echo ${serverCert} | openssl base64 -d -A -out ${tmpdir}/server-cert.pem

Service Account Token 认证

这种认证方式是我们推荐的 jenkins 与 k8s 整合中用来给 k8s 验证 jenkins 身份的机制。

什么是 service account

service account 是 Kubernetes 唯一由自己管理的账号实体,意味着 service account 可以通过 Kubernetes 创建,每个 namespace 会有一个默认的 service account(以下简称 sa), pod 在启动的时候如果你不去指定自己创建的 sa 的话, 就会被分配到这个默认的 sa。 sa 是使用 Bearer Token 认证的,基于 JWT(JSON Web Token) 认证机制,JWT 原理和 x509 证书认证其实有点类似,都是通过 CA 根证书进行签名和校验,只是格式不一样而已, 所以认证流程参考刚才说的数字证书的方式即可。 所以在配合 jenkins 与 k8s 整合的时候, 我们需要为 jenkins 创建一个 sa。 如下:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: qa-jenkins
  namespace: default

一个很简单的配置, 我们通过 kubectl create 命令创建以后我们就在 default 名称空间看到这个 sa。 但是我们可以查看一下它的内容。

我们可以看到 k8s 自动为 sa 创建了一个 token,我们上面说 sa 的认证是基于 token 的, 所以这个 token 的内容就非常重要了。事实上, k8s 为 sa 专门创建了一个 secret 对象来保存这个 token。 如下:

可以看到这个 secret 对象里分别保存了 ca 的根证书,namespce 以及 token。 这些我们认证身份需要的信息就都有了。 而事实上这 3 个文件会被挂载在 pod 的/run/secrets/kubernetes.io/serviceaccount 这个目录下。 每个 Pod 启动的时候都会有这些文件, 而我们在使用 client-go 的时候,如果不额外指定 kubeconfig, 它会默认读取这里面的内容来初始化。

OK, 到了这里我们只需要复制 token 的内容再经过 base64 转码后, 在 jenkins 上创建一个 secret text 类型的凭据后,在 jenkins 上的云配置中设置一下即可。 至此, 我们的双向认证就解决了。这时候你在 UI 上点击测试, 就可以看到连接成功的字样。

下面我们要解决最后一个问题,那就是权限, 虽然身份得到了认证,那并不代表你就有权限做各种各样的事情。 尤其是 jenkins 与 k8s 整合后需要动态创建 pod 作为 slave 节点, 所以它需要获取创建,更新,删除,查询 k8s 各种资源的权限。 接下来就讲 RBAC

基于 RBAC 的权限管理机制

RBAC-- 基于角色的权限管理机制。 RBAC 可以作用在 service account 上, 规定这个 sa 所有用的权限。 不多说,直接看配置文件。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: qa-jenkins
  namespace: default

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: qa-jenkins
  namespace: default
rules:
- apiGroups: ["", "apps", "autoscaling", "batch"]
  resources: ["pods/exec", "services", "endpoints", "pods","secrets","configmaps","crontabs","deployments","jobs","nodes","rolebindings","clusterroles","daemonsets","replicasets","statefulsets","horizontalpodautoscalers","replicationcontrollers","cronjobs"]
  verbs: ["*"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: qa-jenkins
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: qa-jenkins
subjects:
- kind: ServiceAccount
  name: qa-jenkins
  namespace: default

在 k8s 中 Role 规定了一个角色, Role 中 rules 这个字段中规定了 apigroups(上面定了所有的 4 个 group), resources(所能操作的资源对象)和 verbs(所拥有的权限,* 代表所有权限)也就是说上面我定义的这个 Role 能基本上拥有所有对象的所有权限(我懒,不想去一个一个的筛选 jenkins 到底需要哪些权限了, 所以干脆直接给他所有权限)。 这里要注意的是 Pod/exec 也是一个资源,这个当初坑到我了, jenkins 切换 container 执行命令的时候走的是类似 kubetl exec 这个命令的方式。 它是需要在 pod/exec 这个资源上有权限的, 而我当初不知道 pod/exec 这个资源对象, 单纯的以为只要在 Pod 上有 exec 权限就可以了。 结果悲剧了, 卡了我好一会才知道这个坑。

而 RoleBinding 则负责把 Role 和一个 sa 绑定, 也就是赋予 sa 一个角色,上面我就把这个有所有权限的角色给这样的 sa 上。 而我们在 jenkins 上配置了这个 sa 的 token 作为认证。 所以 jenkins 也就有了权限了。 详细的 RBAC 内容不细展开了~~~。这篇文章就到这。 至此我们 jenkins 和 k8s 的整合也就结束了。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 4 条回复 时间 点赞

三处错别字,

信任
消息
颁发

恒温 回复

😂 😂 😂 😂

孙哥,能说说,你们后续的 CD 部分是怎么做的吗?镜像上传到私有仓库里,然后如何通过 Jenkins 部署到 K8S 中,动态生成域名填入 dnsmasq,让大家能直接访问

韩将 回复

部署的时候配置域名跟 ingress 集成起来是这样的。 首先需要到你们的 dns 中添加一个泛域名解析。 解析地址填写你 ingress controller 的地址。 比如我们做的凡是以 testenv.4pd.io 为结尾的域名全部解析成我们 ingress controller 的 ip 地址。 然后为每一个环境创建一个 ingress 规则。 比如我们曾经做的:

def create_ingress_yaml(config):
    document = """
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: %s
  annotations:
    nginx/client_max_body_size: 10240m
    nginx.org/client-max-body-size: "10240m"
    ingress.kubernetes.io/proxy-body-size: 10240m
spec:
  rules:
  - host: %s.testenv.4pd.io
    http:
      paths:
      - path: /
        backend:
          serviceName: %s
          servicePort: 8888
  - host: %s.preditor.testenv.4pd.io
    http:
      paths:
      - path: /
        backend:
          serviceName: %s
          servicePort: 8090
  - host: %s.history.testenv.4pd.io
    http:
      paths:
      - path: /
        backend:
          serviceName: %s
          servicePort: 18080
    """ % (config.pht_pod_name, config.name_prefix, config.pht_pod_name, config.name_prefix, config.pht_pod_name,
           config.name_prefix, config.pht_pod_name)
    data = yaml.load_all(document)
    with open(config.ingress_conf_path, 'w') as stream:
        yaml.dump_all(data, stream)

在部署环境的时候就把 ingress 创建好。 然后配合泛域名解析,ingress controller 会自动的帮我们转发请求到具体的环境上。 就可以达到每个环境都自动的有一个域名对应上了。

部署其实很简单, 就像我上面做的那样就好。 维护一套 yaml 模板动态的去生成部署这些模块的 k8s 配置就可以了。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册