需要用到两个插件:kubernetes-plugin,credential-plugin
https://github.com/jenkinsci/kubernetes-plugin
https://github.com/jenkinsci/credentials-plugin
Jenkins通过kubectl管理远程k8s
准备一个安装k8s的环境或者单独安装kubectl客户端的机器。可以执行kubectl命令。
- 登录到远程k8s上,将/opt/kubernetes/ssl/下面的三个文件ca.crt,kubernetes.crt和kubernetes.key 拷贝到kubectl客户端所在机器。
-
修改kubectl(操作机)上的~/.kube/config 文件,添加远程k8s的相关信息,如下图:
3. 需要说明的是:在合并config相关配置的时候,需要注意context和user的相关配置,一定要对应。current-context 要改成远程的k8s的cluster name。这样执行kubectl get pods 就可以获取远程k8s的信息了.
Jenkins触发远程k8s
要完成这个目标,需要以下几步:
- k8s集群
- jenkins server
- 动态的pod templete
- 相关的认证
scenario 1: Jenkins 在k8s内部,作为k8s的一个服务
进入jenkins->全局配置->add cloud,如图所示:
在jenkins url中填写当前k8s中jenkins service的url,其他选项默认就行。点击test connection,如果显示success,则表明可以互相通信。集群内部的好处就是无需单独配置认证。但是在启动动态slave的时候用的资源也是集群里的。
scenario 2: Jenkins 在k8s外部
那么在配置jenkins url这一项的时候,直接填写对应的jenkins server地址。
scenario 3: Jenkins和需要控制的k8s不在同一个集群
这里面写两种方式(1.页面配置。2.脚本实现)
首先确认的是,无论页面配置还是脚本,由于跨集群,我们都需要考虑认证的问题。
* 页面方式
- 配置cloud(k8s)相关信息
出了前面提到的jenkins url的配置,此时由于k8s是远程的,需要配置远程k8s的相关信息。
kubernetes name: 随便命名下,后面pod templete中会用到。
kubernetes url: 远程k8s的url
kubernetes server certificate key:远程k8s中config中ca.crt中的内容。
Kubernetes Namespace:填写namespace,这个namespace一定要在k8s中存在
Credentials:下面会提到,需要添加相关认证信息
jenkins url:填写jenkins服务的地址
jenkins tunnel:指定slave连接master的端口url。此处不要加http前缀
这里面不得不提到的就是kubernetes server certificate key,如果这个没有配,会遇到如下错误:
- 配置credential
进入jenkins credential配置页面,增加certificate。如图:
图中需要上传pkcs12的证书。我们通过如下命令将远程的k8s的认证信息生成cert.pfx,并输入生成证书的certPassword(整个password我们会在页面add credential的时候用到)
openssl pkcs12 -passout pass:${certPassword} -export -out cert.pfx -inkey kubernetes.key -in kubernetes.crt -certfile ca.crt
一旦我们上传成功保存后,可以在全局配置ks8的地方,credentials的时候选择刚刚添加的credential.
如果我们不配置这个credential的话,就会遇到如下的错误:
- 页面配置pod templete
在kubernetes里面add pod templete。如图所示配置,根据提示配置image,Container的模板名称为jnlp。当Container的模板名称为jnlp的时候,jenkins-slave才会使用下面配置的docker镜像来启动pod,如果不为jnlp,则会使用默认的镜像jenkins/jnlp-slave:alpine。当使用自定义的docker镜像来启动jenkins slave pod的时候,下面的command to run(默认值是 sh -c)和arguments to pass to the command(默认值是cat)两个值需要清空。否则会出现jenkins slave jnlp连接不上master的情况,尝试100次连接之后销毁pod,然后再创建一个pod继续尝试连接,无限循环。
其中也要配置volumns,为了能docker in docker使用,需要配置以下配置。
- 测试调试
启动jenkins job, 运行job即可。
总结:页面配置操作相对来说比较简单,但是也有局限性,k8s必须已经存在,那么我们如果想动态的创建k8s,并且动态的增减slave,这种方式就不合适了。下面介绍脚本方式。
* 脚本方式
同样的四个部分:
- 配置cloud(k8s)相关信息
- 配置credential
- 编写pod templete
- 编写pipeline,调试
为了能够达到脚本话,我们需要研究插件的源码,然后根据源码中的类,进行相关配置,这也是我觉得相对来说比较难的地方。
def createCloud(String name, String serverUrl, String namespace, String jenkinsUrl, String jenkinsTunnel, String credentialsId, String serverCertificate) {
def jenkins = Jenkins.get()
def existingNames = jenkins.clouds.stream()
.map {
c -> c.name
}
.collect(Collectors.toList())
KubernetesCloud source = new KubernetesCloud(name)
source.setServerUrl(serverUrl)
source.setNamespace(namespace)
source.setJenkinsUrl(jenkinsUrl)
source.setJenkinsTunnel(jenkinsTunnel)
source.setCredentialsId(credentialsId)
def kc = new KubernetesCloud(name, source)
kc.setServerCertificate(serverCertificate)
if (existingNames.contains(name)) {
def oldCloud = jenkins.clouds.getByName(name)
jenkins.clouds.replace(oldCloud, kc)
println("replace cloud ${name}")
} else {
jenkins.clouds.add(kc)
println("create cloud ${name}")
}
}
有增加就会有删除,我们需要保证环境的干净,怎么进来的就怎么出去(“童子军规则”)。
def removeCloudConfig(cloudName) {
def jenkins = Jenkins.get()
def oldCloud = jenkins.clouds.getByName(cloudName)
oldCloud.setServerCertificate(null)
jenkins.clouds.remove(oldCloud)
println("remove cloud ${cloudName}")
}
创建credential,这里主要介绍增加certificate。其他类型的可以自己参考插件,自己改写。
CertificateCredentialsImpl credentialGenerate(fileUrl, credentialId, password) {
InputStream is = new URL(fileUrl).openStream()
byte[] bys = IOUtils.toByteArray(is)
SecretBytes uploadedKeystore = SecretBytes.fromBytes(bys)
CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(uploadedKeystore)
CertificateCredentialsImpl credentials = new CertificateCredentialsImpl(null, credentialId, null, password, storeSource)
SystemCredentialsProvider.getInstance().getCredentials().add(credentials)
SystemCredentialsProvider.getInstance().save()
println('credential added successfully!')
return credentials
}
删除credential方法:
def removeCredential(CertificateCredentialsImpl credentials) {
List allCredentials = SystemCredentialsProvider.getInstance().getCredentials()
List temp = new ArrayList()
for (Credentials c : allCredentials) {
if (c instanceof CertificateCredentialsImpl) {
if (c.equals(credentials)) {
temp.add(c)
}
}
}
SystemCredentialsProvider.getInstance().getCredentials().removeAll(temp)
SystemCredentialsProvider.getInstance().save()
println('remove credential successfully!')
}
增减方法都有了,下面就是要查找他们所需要的值。我们需要拿到生成的k8s的config中的ca.crt,通过一下方式赋值给cloud配置。
serverCertificate = sh returnStdout: true, script: """
sudo cat ca.crt
"""
还有credential的值,credentialId是jenkins为了识别系统中的credential而自动生成的唯一Id,我们可以用UUID来生成。那么它的内容,需要通过前文提到的shell命令
openssl pkcs12 -passout pass:${certPassword} -export -out cert.pfx -inkey kubernetes.key -in kubernetes.crt -certfile ca.crt
的方式拿到,由于是加密证书文件,直接将他传入File,然后读取bytes,会有乱码产生。这边采用的是ftp中间过度一下,然后stream的方式读取。
InputStream is = new URL(fileUrl).openStream()
万事具备,我们开始构造pipeline。
timestamps{
node('test'){
stage("download tool"){
//获取页面输入的机器名,下载安装工具
}
stage("install k8s"){
//安装k8s
}
stage("config remote cloud and certificate"){
//编写获取k8s的config,生成cert等
}
}
CertificateCredentialsImpl certificateCredentials = K8sUtil.credentialGenerate(fileUrl, credentialsId, certPassword)
K8sUtil.createCloud(cloudName, serverUrl, namespace, jenkinsUrl, jenkinsTunnel, credentialsId, serverCertificate)
podTemplate(cloud: cloudName, name: podName, label: label, yaml: """
kind: Pod
metadata:
name: ${podName}
spec:
containers:
- name: jnlp
image: ************
args: []
tty: true
volumes:
- name: dockersock
hostPath:
path: /var/run/docker.sock
""") {
node(label) {
stage('run remote dynamic slave') {
container('jnlp') {
sh """
echo 'success'
sleep 10
"""
}
}
}
}
stage("remove cloud && credential") {
K8sUtil.removeCloudConfig(cloudName)
K8sUtil.removeCredential(certificateCredentials)
}
}
上文中的K8sUtil,是把相关操作方法放置在sharedLibrary中。
通过这种方式,我们只需要在pipeline中传入需要部署k8s的机器(master和node),就可以达到动态到k8s中启动slave,执行相关任务,最终也会清理相关配置,对于用户无感知,就跟本地启动slave一样。
可能有读者会问,插件里的代码我们如何进行实例定制开发呢?我研究了下,主要可以从它的单元测试入手,去分析它每个类之间的关系,然后有针对性的去实例化类。
总结不易,感觉有用就点个赞吧_