云原生 DevOps 如何做到 Jenkins 开箱即用

前言

随着 Docker 和 K8S 的普及,云原生时代已经到来,开发工程师对应用环境的掌控力进一步加强,运维成本进一步降低。DevOps 采用容器技术更是如虎添翼,持续集成更快更灵活,部署更简单。大多数会选择使用 Jenkins 完成持续集成能力。

场景

在搭建一个云原生 DevOps 平台的时候,不管是 SaaS 化还是私有化的都需要部署前后端项目和基础设施(mysql、rabbitmq...)等。如果该 DevOps 平台要是使用了 Jenkins,则需要部署一套 Jenkins。如果使用一个个的手工部署,部署效率低下,并且出错概率大。如果使用一键部署方式,编写好一键部署脚本,可能只需要做少量的配置,一行命令,一个脚本多处运行。但是如果使用一键部署,Jenkins 初始化时是一个无任何配置的,比如账号密码、 API Token、DevOps 执行 pipeline 需要用到的插件等。难道我们每次部署 Jenkins 都需要人工去操作一遍吗?有没有什么更好的方式能做到 Jenkins 开箱即用?

分析问题

根据以上场景进行分析,搭建云原生 DevOps 平台遇到的主要问题:
• 默认无账号密码和 API Token,无法调用 Jenkins API
• 私有化部署时,如果无外网,Jenkins 插件如何安装
• 使用 kubernetes-plugin 插件,默认无 Cloud 相关配置,无法调用 K8S 创建 slave pod
解决问题
通过定制化 Jenkins 镜像可以做:
• 创建好的账号密码和 API Token,通过一键部署 DevOps 服务和 Jenkins,即可通过账号密码或 API Token 调用 Jenkins API
• 安装好需要的插件,就算在无外网环境下,也无需下载插件了
• 配置好 K8S 相关配置,可以快速使用 Jenkins kubernetes-plugin 插件

如何制作镜像

Jenkins dockerfile 参考

请参考官方提供 ‣‣‣ Jenkins dockerfile

项目目录如下:

image.png

可以根据具体情况选择不同的 dockerfile,如我选择的是 Dockerfile-alpine ,基于 Dockerfile-alpine 做定制配置。

官方 Dockerfile-alpine 文件内容如下:

FROM openjdk:8-jdk-alpine

RUN apk add --no-cache \
  bash \
  coreutils \
  curl \
  git \
  git-lfs \
  openssh-client \
  tini \
  ttf-dejavu \
  tzdata \
  unzip

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG http_port=8080
ARG agent_port=50000
ARG JENKINS_HOME=/var/jenkins_home
ARG REF=/usr/share/jenkins/ref

ENV JENKINS_HOME $JENKINS_HOME
ENV JENKINS_SLAVE_AGENT_PORT ${agent_port}
ENV REF $REF

# Jenkins is run with user `jenkins`, uid = 1000
# If you bind mount a volume from the host or a data container,
# ensure you use the same uid
RUN mkdir -p $JENKINS_HOME \
  && chown ${uid}:${gid} $JENKINS_HOME \
  && addgroup -g ${gid} ${group} \
  && adduser -h "$JENKINS_HOME" -u ${uid} -G ${group} -s /bin/bash -D ${user}

# Jenkins home directory is a volume, so configuration and build history
# can be persisted and survive image upgrades
VOLUME $JENKINS_HOME

# $REF (defaults to `/usr/share/jenkins/ref/`) contains all reference configuration we want
# to set on a fresh new installation. Use it to bundle additional plugins
# or config file with your custom jenkins Docker image.
RUN mkdir -p ${REF}/init.groovy.d

# jenkins version being bundled in this docker image
ARG JENKINS_VERSION
ENV JENKINS_VERSION ${JENKINS_VERSION:-2.60.3}

# jenkins.war checksum, download will be validated using it
ARG JENKINS_SHA=2d71b8f87c8417f9303a73d52901a59678ee6c0eefcf7325efed6035ff39372a

# Can be used to customize where jenkins.war get downloaded from
ARG JENKINS_URL=https://repo.jenkins-ci.org/public/org/jenkins-ci/main/jenkins-war/${JENKINS_VERSION}/jenkins-war-${JENKINS_VERSION}.war

# could use ADD but this one does not check Last-Modified header neither does it allow to control checksum
# see https://github.com/docker/docker/issues/8331
RUN curl -fsSL ${JENKINS_URL} -o /usr/share/jenkins/jenkins.war \
  && echo "${JENKINS_SHA}  /usr/share/jenkins/jenkins.war" | sha256sum -c -

ENV JENKINS_UC https://updates.jenkins.io
ENV JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental
ENV JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals
RUN chown -R ${user} "$JENKINS_HOME" "$REF"

# for main web interface:
EXPOSE ${http_port}

# will be used by attached slave agents:
EXPOSE ${agent_port}

ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY tini-shim.sh /bin/tini
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"]

# from a derived Dockerfile, can use `RUN plugins.sh active.txt` to setup $REF/plugins from a support bundle
COPY plugins.sh /usr/local/bin/plugins.sh
COPY install-plugins.sh /usr/local/bin/install-plugins.sh

从文件内容中可分析出

  • ENV JENKINS_VERSION ${JENKINS_VERSION:-2.60.3} 指定了 Jenkins 版本
  • ARG JENKINS_SHA=2d71b8f87c8417f9303a73d52901a59678ee6c0eefcf7325efed6035ff39372a 配置了 jenkins.war 的 sha256 值,用于校验下载的 Jenkins.war sha256 和配置用于校验的 sha256 是否一致

定制步骤

  • 运行 docker run -p 8080:8080 -d -v /root/jenkins_home:/var/jenkins_home jenkins/jenkins:${JENKINS_VERSION} ,将 Jenkins 通过 docker 运行起来,并且把 jenkins_home 目录挂载出来,便于我们获取到 Jenkins 数据,后面的步骤中还需使用这些数据
  • 通过 Jenkins Web 端进行操作,配置好账号密码,安装所需插件,配置所需信息等
  • 选择需要的 Jenkins 版本,通过修改 ENV JENKINS_VERSION ${JENKINS_VERSION:-2.60.3} 配置中的版本号
  • 通过 wget https://repo.jenkins-ci.org/public/org/jenkins-ci/main/jenkins-war/${JENKINS_VERSION}/jenkins-war-${JENKINS_VERSION}.war 获取到 jenkins.war,执行 sha256sum jenkins-war-${JENKINS_VERSION}.war 获取 jenkins.war sha256 值,并且填写到 ARG JENKINS_SHA 配置中
  • COPY jenkins-support /usr/local/bin/jenkins-support 前面添加 COPY ${PATH}/jenkins_home /var/jenkins_home ,将定制数据打入镜像中

如果不想每次都去远程下载 jenkins.war,可以先手动下载,再 COPY 到镜像中

如需指定时区,配置以下内容即可

RUN ln -sf /usr/share/zoneinfo/Asia/ShangHai /etc/localtime

RUN echo "Asia/Shanghai" > /etc/timezone

这样我们就完成了第一步,将需要的数据已经初始化到镜像中,只要通过这个 Jenkins 镜像部署的,都可以达到开箱即用的目的,那么我们继续说一下如何部署?不同的部署方式需要注意什么事项?

如何部署

Docker

使用 docker 或者 docker-compose 部署 Jenkins,如果需要数据持久化,只需要通过 volume 将 /var/jenkins_home 目录挂载到主机即可。

eg:

docker run -d -p 8080:8080 自定义镜像地址

K8S

通过 K8S 部署 Jenkins,如果不需要持久化,只需要编写 Deployment 进行部署即可,但是如果需要使用持久化方式,请参考 ‣‣‣ Jenkins K8S 持久化部署

非持久化

eg:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  annotations:
    app: jenkins
  labels:
    app: jenkins
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      serviceAccountName: ${serviceAccount}
      containers:
        - name: jenkins
          image: ${自定义镜像地址}
          imagePullPolicy: Always
          securityContext:
            runAsUser: 0
            privileged: true
          ports:
            - name: web
              containerPort: 8080
            - name: agent
              containerPort: 50000
          env:
            - name: "JAVA_OPTS"
              value: "
                   -Dhudson.model.LoadStatistics.clock=2000
                   -Dhudson.slaves.NodeProvisioner.recurrencePeriod=5000
                   -Dhudson.slaves.NodeProvisioner.initialDelay=0
                   -Dhudson.model.LoadStatistics.decay=0.5
                   -Dhudson.slaves.NodeProvisioner.MARGIN=50
                   -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
                   -Duser.timezone=Asia/Shanghai
                   "

持久化

eg:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins
  annotations:
    app: jenkins
  labels:
    app: jenkins
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      securityContext:
        runAsUser: 0
      serviceAccountName: ${serviceAccount}
      initContainers:
        - name: backup-data
          image: {自定义镜像地址}
          imagePullPolicy: Always
          command: [ "cp", "-r", "/var/jenkins_home", "/var/jenkins_home_bak"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
        - name: revert-data
          image: {自定义镜像地址}
          imagePullPolicy: Always
          command: [ "cp", "-r", "/var/jenkins_home_bak/jenkins_home", "/var"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
            - mountPath: /var/jenkins_home
              name: jenkins-home
        - name: copy-default-config
          image: {自定义镜像地址}
          imagePullPolicy: Always
          command: [ "sh", "/var/jenkins_config/apply_config.sh" ]
          volumeMounts:
            - mountPath: /var/jenkins_home
              name: jenkins-home
            - mountPath: /var/jenkins_config
              name: jenkins-config
            - mountPath: /usr/share/jenkins/ref/plugins/
              name: plugin-dir
            - mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
      containers:
        - name: jenkins
          image: {自定义镜像地址}
          imagePullPolicy: Always
          env:
            - name: JAVA_OPTS
              value: "-Dhudson.model.LoadStatistics.clock=2000 -Dhudson.slaves.NodeProvisioner.recurrencePeriod=5000 -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.model.LoadStatistics.decay=0.5 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 -Duser.timezone=Asia/Shanghai
"
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 50000
              name: slavelistener
          livenessProbe:
            httpGet:
              path: /login
              port: http
            initialDelaySeconds: 5
          readinessProbe:
            httpGet:
              path: /login
              port: http
            initialDelaySeconds: 5
          volumeMounts:
            - mountPath: /var/jenkins_home
              name: jenkins-home
              readOnly: false
            - mountPath: /var/jenkins_config
              name: jenkins-config
              readOnly: true
            - mountPath: /usr/share/jenkins/ref/plugins/
              name: plugin-dir
              readOnly: false
            - mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
              readOnly: false
      volumes:
      - name: jenkins-config
        configMap:
          name: jenkins-config
      - name: plugin-dir
        emptyDir: {}
      - name: secrets-dir
        emptyDir: {}
      - name: jenkins-home-bak
        emptyDir: {}
      - name: jenkins-home
        persistentVolumeClaim:
          claimName: jenkins // 使用 PVC 进行持久化
         
 ---
 # config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins-config
data:
  config.xml: |-
    
    
      
      2.222.3-alpine
      RUNNING
      10
      NORMAL
      true
      
        true
      
      
        true
        false
      
      false
      
      ${JENKINS_HOME}/workspace/${ITEM_FULLNAME}
      ${ITEM_ROOTDIR}/builds
      
      
      
      
      
        
          kubernetes
          
            
              
              default
              2147483647
              0
              
              
                NORMAL
              
              
              
                
                  jnlp
                  jenkins/jnlp-slave:4.0.1-1
                  false
                  false
                  /home/jenkins
                  
                  ${computer.jnlpmac} ${computer.name}
                  false
                  200m
                  256Mi
                  200m
                  256Mi
                  
                    
                      JENKINS_URL
                      http://jenkins:8080
                    
                  
                
              
              
              
              
              
            
          https://kubernetes.default
          false
          default
          http://jenkins:8080
          jenkins-agent:50000
          10
          5
          0
          0
        
      
      5
      0
      
        
          
          All
          false
          false
          
        
      
      All
      50000
      
      
      
      true
    
  apply_config.sh: |-
    mkdir -p /usr/share/jenkins/ref/secrets/;
    echo "false" > /usr/share/jenkins/ref/secrets/slave-to-master-security-kill-switch;
    cp -f /var/jenkins_config/config.xml /var/jenkins_home;
  plugins.txt: |-

可通过 PVC 进行持久化,也可通过 nodeSelector 选择一台主机通过 volume 进行挂载

Helm

请参考官方提供 ‣‣‣ jenkins-helm-charts

由于官方提供的 helm charts 不会保留自定义镜像中的数据,需要我们通过改造官方脚本进行自定义,在templates/jenkins-master-deployment.yamlinitContainers 中,将自定义的 config.xml 覆盖容器中的,并且执行 config.yaml 中定义的脚本。这样只能做到,自定义 config.xml 文件和在线安装插件等操作,由于挂载了 /var/jenkins_home 目录,且挂载的是一个空目录,这样会导致原镜像中的数据也置空,基于这个目的,我们进行 templates/jenkins-master-deployment.yaml 的改造。

templates/jenkins-master-deployment.yaml 文件需要新增内容如下:

...
      initContainers:
        - name: "backup-data"
          image: "{{ .Values.Master.Image }}:{{ .Values.Master.ImageTag }}"
          imagePullPolicy: "{{ .Values.Master.ImagePullPolicy }}"
          command: [ "cp", "-r", "/var/jenkins_home", "/var/jenkins_home_bak"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
        - name: "revert-data"
          image: "{{ .Values.Master.Image }}:{{ .Values.Master.ImageTag }}"
          imagePullPolicy: "{{ .Values.Master.ImagePullPolicy }}"
          command: [ "cp", "-r", "/var/jenkins_home_bak/jenkins_home", "/var"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
            - mountPath: /var/jenkins_home
              name: jenkins-home
        - name: "copy-default-config"
          image: "{{ .Values.Master.Image }}:{{ .Values.Master.ImageTag }}"
          imagePullPolicy: "{{ .Values.Master.ImagePullPolicy }}"
          command: [ "sh", "/var/jenkins_config/apply_config.sh" ]
          volumeMounts:
            - mountPath: /var/jenkins_home
              name: jenkins-home
            - mountPath: /var/jenkins_config
              name: jenkins-config
            {{- if .Values.Master.CredentialsXmlSecret }}
            - mountPath: /var/jenkins_credentials
              name: jenkins-credentials
              readOnly: true
            {{- end }}
            {{- if .Values.Master.SecretsFilesSecret }}
            - mountPath: /var/jenkins_secrets
              name: jenkins-secrets
              readOnly: true
            {{- end }}
            {{- if .Values.Master.Jobs }}
            - mountPath: /var/jenkins_jobs
              name: jenkins-jobs
              readOnly: true
            {{- end }}
            - mountPath: /usr/share/jenkins/ref/plugins/
              name: plugin-dir
            - mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
 ...

新增两个 initContainer,backup-data 用于将原镜像需要备份出来,revert-data 再将数据还原,再通过 copy-default-config 自定义 config.xml 和执行脚本

注意事项

Jenkins config.xml 中使用的 securityRealmhudson.security.LegacySecurityRealm 会导致使用 Jenkins API 请求报错,如果需要通过 Jenkins API 操作,需修改为如下:


  true
  false

但是修改成上面这种方式,会导致无法通过 values.yaml 自定义 Jenkins 账号密码,只能使用原镜像初始化的账号密码。

疑问

为什么不适用 docker commit 构建自定义镜像

对于 docker commit 命令,几乎所有的操作都围绕着可读可写层(Read-Write Layer),一次 commit 将可读可写层打包为一个全新的镜像,同时也保证镜像之间的独立性。当然,由于一个镜像同时包含镜像层文件系统内容和镜像 json 文件,因此对于一个 commit 操作,Docker Daemon 还会为镜像产生一个全新的 json 文件。

对于 Docker 容器而言,文件系统视角包含的内容有 Docker 镜像构成的内容(一个可读可写层加上多个只读层)、数据卷 VOLUME 挂载的目录内容,还有类似于 hosts、hostsname 和 resolv.conf 等挂载文件,当然还有一些如 /proc 和 /sys 等虚拟文件系统的内容。commit 操作只专注于可读可写层(Read-Write Layer),因此其他的内容都将不会出现在打包后的镜像中。举例说明,类似于 Jenkins 的数据容器,由于其自身的数据一般都持久化到数据卷 VOLUME 中,因此 Jenkins 在运行过程中产生的数据将不会在 commit 操作后被打包进入镜像。

总结

我们通过定制化 Jenkins 镜像,将业务平台需要的数据初始化进镜像中,能帮我们达到开箱即用的目的,无需人工介入,即降低人工成本,也减少人工出错的概率。

你可能感兴趣的:(云原生 DevOps 如何做到 Jenkins 开箱即用)