Level 3: CI/CDパイプラインを構築

目的・ゴール: コンテナ化したアプリケーションのCICDを実現する

アプリケーションをコンテナ化したら、常にリリース可能な状態、自動でデプロイメントを出来る仕組みをつくるのが迅速な開発をするために必要になります。

そのためのCI/CDパイプラインを作成するのがこのレベルの目標です。

../_images/cicd_pipeline.png

本ラボでは Level1, Level2 で行ったオペレーションをベースにCI/CDパイプラインを構築します。

Gitにソースがコミットされたら自動でテスト・ビルドを実現するためのツール(Jenkins)をkubernetes上へデプロイ、及び外部公開をします。 そして、Jenkinsがデプロイできたら実際にアプリケーションの変更を行い自動でデプロイするところまでを目指します。

流れ

  1. Jenkins をインストールする
  2. Jenkins 内部でジョブを定義する。
  3. あるアクションをトリガーにビルド、テストを自動実行する。
  4. 自動でk8sクラスタにデプロイメントできるようにする。

CI/CDパイプラインの定義

このラボでのCI/CDパイプラインの定義は以下を想定しています。

  • アプリケーションビルド
  • コンテナイメージのビルド
  • レジストリへコンテナイメージのpush
  • テスト実行
  • k8sへアプリケーションデプロイ

GitはGitLabを共有で準備していますが、使いなれているサービス(GitHub等)があればそちらを使って頂いても構いません。 まずは、Jenkinsをkubernetes上にデプロイしてみましょう。

Git自体も併せてデプロイしてみたいということであればGitLabをデプロイすることをおすすめします。 GitLabを使えばコンテナのCI/CDパイプライン、構成管理、イメージレジストリを兼ねて使用することができます。

Jenkinsのデプロイ方法について

CI/CDパイプラインを実現するためのツールとしてJenkinsが非常に有名であることは周知の事実です。 このラボではJenkinsを使用しCI/CDを実現します。

まずは、各自Jenkinsをデプロイします。

方法としては3つ存在します。

  1. Helm Chartでデプロイする方法 (手軽にインストールしたい人向け)
  2. Level1,2と同じようにyamlファイルを作成し、デプロイする方法(仕組みをより深く知りたい人向け)
  3. Kubernetes用にCI/CDを提供するJenkins Xをデプロイする方法(新しい物を使いたい人向け)

今回は最初のHelmでデプロイするバージョンを記載しました。 好みのもの、挑戦したい内容に沿って選択してください。

オリジナルでyamlファイルを作成する場合は以下のサイトが参考になります。

Helmを使ってJenkinsをデプロイ

Helmの初期化

Helmを使用する事前の設定をします。 Helmの初期化、RBACの設定を実施します。

$ helm init
$ kubectl create clusterrolebinding add-on-cluster-admin --clusterrole=cluster-admin --serviceaccount=kube-system:default

Helmの基本

基本的なHelmの使い方は以下の通りです。

$ helm install stable/helm-chart名

Helmチャートのインストール・Jenkinsのカスタマイズ

今回はJenkinsを導入するにあたり環境に併せてカスタマイズを行います。 Helmは以下のURLに様々なものが公開されています。パラメータを与えることである程度カスタマイズし使用することができます。 Helm chartと同等のディレクトリにvalues.yamlというファイルが存在し、これを環境に併せて変更することでカスタマイズしデプロイできます。

今回のJenkinsのデプロイでは2つの公開方法が選択できます。

1つ目が、今回の環境では ServicetypeLoadBalancer としてしてデプロイすると external-ipが付与される環境となっています。(MetalLBをデプロイ済みです。)

2つ目が Ingress を使った公開です。IngressをJenkinsのHelmチャートを使ってデプロイするためには「Master.Ingress.Annotations」、「Master.ServiceType」を変更しデプロイします。

簡易的にデプロイをためしてみたい方は1つ目の LoadBalancer を使ったやり方を実施、新しい概念であるIngressを使った方法を実施したい方は2つ目を選択しましょう。

どちらの方法の場合も、以下のvalues.yamlをカスタマイズしてデプロイします。 このレベルではJenkinsをデプロイするのが目的ではなくCI/CDパイプラインを作成するのが目的であるため、デプロイ用のyamlファイルを準備しました。

また、このvalues.yamlでは永続化ストレージが定義されていないため、Level2で作成したStorageClassを使用し動的にプロビジョニングをするように変更しましょう。

StorageClassには環境に作成したStorageClassを設定します。このサンプルでは "ontap-gold"を設定してあります。

また、Kubernetes上でCI/CDパイプラインを作成するため Kubernetes-plugin もyamlファイルに追記済みです。

Helm設定用のvalues.yaml
# Default values for jenkins.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

## Overrides for generated resource names
# See templates/_helpers.tpl
# nameOverride:
# fullnameOverride:

Master:
  Name: jenkins-master
  Image: "jenkins/jenkins"
  ImageTag: "lts"
  ImagePullPolicy: "Always"
  # ImagePullSecret: jenkins
  Component: "jenkins-master"
  UseSecurity: true
  AdminUser: admin
  # AdminPassword: <defaults to random>
  resources:
    requests:
      cpu: "50m"
      memory: "256Mi"
    limits:
      cpu: "2000m"
      memory: "2048Mi"
  # Environment variables that get added to the init container (useful for e.g. http_proxy)
  # InitContainerEnv:
  #   - name: http_proxy
  #     value: "http://192.168.64.1:3128"
  # ContainerEnv:
  #   - name: http_proxy
  #     value: "http://192.168.64.1:3128"
  # Set min/max heap here if needed with:
  # JavaOpts: "-Xms512m -Xmx512m"
  # JenkinsOpts: ""
  # JenkinsUriPrefix: "/jenkins"
  # Enable pod security context (must be `true` if RunAsUser or FsGroup are set)
  UsePodSecurityContext: true
  # Set RunAsUser to 1000 to let Jenkins run as non-root user 'jenkins' which exists in 'jenkins/jenkins' docker image.
  # When setting RunAsUser to a different value than 0 also set FsGroup to the same value:
  # RunAsUser: <defaults to 0>
  # FsGroup: <will be omitted in deployment if RunAsUser is 0>
  ServicePort: 8080
  # For minikube, set this to NodePort, elsewhere use LoadBalancer
  # Use ClusterIP if your setup includes ingress controller
  ServiceType: LoadBalancer
  # Master Service annotations
  ServiceAnnotations: {}
  #   service.beta.kubernetes.io/aws-load-balancer-backend-protocol: https
  # Used to create Ingress record (should used with ServiceType: ClusterIP)
  # HostName: jenkins.cluster.local
  # NodePort: <to set explicitly, choose port between 30000-32767
  # Enable Kubernetes Liveness and Readiness Probes
  # ~ 2 minutes to allow Jenkins to restart when upgrading plugins. Set ReadinessTimeout to be shorter than LivenessTimeout.
  HealthProbes: true
  HealthProbesLivenessTimeout: 90
  HealthProbesReadinessTimeout: 60
  HealthProbeLivenessFailureThreshold: 12
  SlaveListenerPort: 50000
  DisabledAgentProtocols:
  - JNLP-connect
  - JNLP2-connect
  CSRF:
    DefaultCrumbIssuer:
      Enabled: true
      ProxyCompatability: true
  CLI: false
  # Kubernetes service type for the JNLP slave service
  # SETTING THIS TO "LoadBalancer" IS A HUGE SECURITY RISK: https://github.com/kubernetes/charts/issues/1341
  SlaveListenerServiceType: ClusterIP
  SlaveListenerServiceAnnotations: {}
  LoadBalancerSourceRanges:
  - 0.0.0.0/0
  # Optionally assign a known public LB IP
  # LoadBalancerIP: 1.2.3.4
  # Optionally configure a JMX port
  # requires additional JavaOpts, ie
  # JavaOpts: >
  #   -Dcom.sun.management.jmxremote.port=4000
  #   -Dcom.sun.management.jmxremote.authenticate=false
  #   -Dcom.sun.management.jmxremote.ssl=false
  # JMXPort: 4000
  # List of plugins to be install during Jenkins master start
  InstallPlugins:
  - kubernetes:1.12.3
  - workflow-job:2.24
  - workflow-aggregator:2.5
  - credentials-binding:1.16
  - git:3.9.1
  - blueocean:1.8.2
  - ghprb:1.40.0

  # Used to approve a list of groovy functions in pipelines used the script-security plugin. Can be viewed under /scriptApproval
  # ScriptApproval:
  #   - "method groovy.json.JsonSlurperClassic parseText java.lang.String"
  #   - "new groovy.json.JsonSlurperClassic"
  # List of groovy init scripts to be executed during Jenkins master start
  InitScripts:
  #  - |
  #    print 'adding global pipeline libraries, register properties, bootstrap jobs...'
  # Kubernetes secret that contains a 'credentials.xml' for Jenkins
  # CredentialsXmlSecret: jenkins-credentials
  # Kubernetes secret that contains files to be put in the Jenkins 'secrets' directory,
  # useful to manage encryption keys used for credentials.xml for instance (such as
  # master.key and hudson.util.Secret)
  # SecretsFilesSecret: jenkins-secrets
  # Jenkins XML job configs to provision
  # Jobs: |-
  #   test: |-
  #     <<xml here>>
  CustomConfigMap: false
  # Node labels and tolerations for pod assignment
  # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
  # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#taints-and-tolerations-beta-feature
  NodeSelector: {}
  Tolerations: {}
  PodAnnotations: {}

  Ingress:
    ApiVersion: extensions/v1beta1
    Annotations:
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"

    TLS:
    # - secretName: jenkins.cluster.local
    #   hosts:
    #     - jenkins.cluster.local

Agent:
  Enabled: true
  Image: jenkins/jnlp-slave
  ImageTag: 3.10-1
  # ImagePullSecret: jenkins
  Component: "jenkins-slave"
  Privileged: false
  resources:
    requests:
      cpu: "200m"
      memory: "256Mi"
    limits:
      cpu: "200m"
      memory: "256Mi"
  # You may want to change this to true while testing a new image
  AlwaysPullImage: false
  # Controls how slave pods are retained after the Jenkins build completes
  # Possible values: Always, Never, OnFailure
  PodRetention: Never
  # You can define the volumes that you want to mount for this container
  # Allowed types are: ConfigMap, EmptyDir, HostPath, Nfs, Pod, Secret
  # Configure the attributes as they appear in the corresponding Java class for that type
  # https://github.com/jenkinsci/kubernetes-plugin/tree/master/src/main/java/org/csanchez/jenkins/plugins/kubernetes/volumes
  volumes:
  # - type: Secret
  #   secretName: mysecret
  #   mountPath: /var/myapp/mysecret
  NodeSelector: {}
  # Key Value selectors. Ex:
  # jenkins-agent: v1

Persistence:
  Enabled: true
  ## A manually managed Persistent Volume and Claim
  ## Requires Persistence.Enabled: true
  ## If defined, PVC must be created manually before volume will be bound
  # ExistingClaim:

  ## jenkins data Persistent Volume Storage Class
  ## If defined, storageClassName: <storageClass>ls
  ## If set to "-", storageClassName: "", which disables dynamic provisioning
  ## If undefined (the default) or set to null, no storageClassName spec is
  ##   set, choosing the default provisioner.  (gp2 on AWS, standard on
  ##   GKE, AWS & OpenStack)
  ##
  StorageClass: "ontap-gold"

  Annotations: {}
  AccessMode: ReadWriteOnce
  Size: 8Gi
  volumes:
  #  - name: nothing
  #    emptyDir: {}
  mounts:
  #  - mountPath: /var/nothing
  #    name: nothing
  #    readOnly: true

NetworkPolicy:
  # Enable creation of NetworkPolicy resources.
  Enabled: false
  # For Kubernetes v1.4, v1.5 and v1.6, use 'extensions/v1beta1'
  # For Kubernetes v1.7, use 'networking.k8s.io/v1'
  ApiVersion: extensions/v1beta1

## Install Default RBAC roles and bindings
rbac:
  install: false
  serviceAccountName: default
  # RBAC api version (currently either v1, v1beta1, or v1alpha1)
  apiVersion: v1
  # Role reference
  roleRef: cluster-admin
  # Role kind (RoleBinding or ClusterRoleBinding)
  roleBindingKind: ClusterRoleBinding

実行イメージとしては以下の通りです。

$ helm --namespace jenkins --name jenkins -f ./jenkins-values.yaml install stable/jenkins --debug
[debug] Created tunnel using local port: '44511'

[debug] SERVER: "127.0.0.1:44511"

[debug] Original chart version: ""
[debug] Fetched stable/jenkins to /home/localadmin/.helm/cache/archive/jenkins-0.16.20.tgz

[debug] CHART PATH: /home/localadmin/.helm/cache/archive/jenkins-0.16.20.tgz

NAME:   jenkins
REVISION: 1
RELEASED: Mon Aug 27 23:54:09 2018
CHART: jenkins-0.16.20
USER-SUPPLIED VALUES:
Agent:
  AlwaysPullImage: false
  Component: jenkins-slave
  Enabled: true
  Image: jenkins/jnlp-slave
  ImageTag: 3.10-1
  NodeSelector: {}
  PodRetention: Never
  Privileged: false
  resources:
    limits:
      cpu: 200m
      memory: 256Mi
    requests:
      cpu: 200m
      memory: 256Mi
  volumes: null
Master:
  AdminUser: admin
  CLI: false
  CSRF:
    DefaultCrumbIssuer:
      Enabled: true
      ProxyCompatability: true
  Component: jenkins-master
  CustomConfigMap: false
  DisabledAgentProtocols:
  - JNLP-connect
  - JNLP2-connect
  HealthProbeLivenessFailureThreshold: 12
  HealthProbes: true
  HealthProbesLivenessTimeout: 90
  HealthProbesReadinessTimeout: 60
  Image: jenkins/jenkins
  ImagePullPolicy: Always
  ImageTag: lts
  Ingress:
    Annotations: null
    ApiVersion: extensions/v1beta1
    TLS: null
  InitScripts: null
  InstallPlugins:
  - kubernetes:1.12.3
  - workflow-job:2.24
  - workflow-aggregator:2.5
  - credentials-binding:1.16
  - git:3.9.1
  - blueocean:1.4.1
  - ghprb:1.40.0
  LoadBalancerSourceRanges:
  - 0.0.0.0/0
  Name: jenkins-master
  NodeSelector: {}
  PodAnnotations: {}
  ServiceAnnotations: {}
  ServicePort: 8080
  ServiceType: LoadBalancer
  SlaveListenerPort: 50000
  SlaveListenerServiceAnnotations: {}
  SlaveListenerServiceType: ClusterIP
  Tolerations: {}
  UsePodSecurityContext: true
  UseSecurity: true
  resources:
    limits:
      cpu: 2000m
      memory: 2048Mi
    requests:
      cpu: 50m
      memory: 256Mi
NetworkPolicy:
  ApiVersion: extensions/v1beta1
  Enabled: false
Persistence:
  AccessMode: ReadWriteOnce
  Annotations: {}
  Enabled: true
  Size: 8Gi
  StorageClass: ontap-gold
  mounts: null
  volumes: null
rbac:
  apiVersion: v1
  install: false
  roleBindingKind: ClusterRoleBinding
  roleRef: cluster-admin
  serviceAccountName: default

COMPUTED VALUES:
Agent:
  AlwaysPullImage: false
  Component: jenkins-slave
  Enabled: true
  Image: jenkins/jnlp-slave
  ImageTag: 3.10-1
  NodeSelector: {}
  PodRetention: Never
  Privileged: false
  resources:
    limits:
      cpu: 200m
      memory: 256Mi
    requests:
      cpu: 200m
      memory: 256Mi
  volumes: null
Master:
  AdminUser: admin
  CLI: false
  CSRF:
    DefaultCrumbIssuer:
      Enabled: true
      ProxyCompatability: true
  Component: jenkins-master
  CustomConfigMap: false
  DisabledAgentProtocols:
  - JNLP-connect
  - JNLP2-connect
  HealthProbeLivenessFailureThreshold: 12
  HealthProbes: true
  HealthProbesLivenessTimeout: 90
  HealthProbesReadinessTimeout: 60
  Image: jenkins/jenkins
  ImagePullPolicy: Always
  ImageTag: lts
  Ingress:
    Annotations: null
    ApiVersion: extensions/v1beta1
    TLS: null
  InitScripts: null
  InstallPlugins:
  - kubernetes:1.12.3
  - workflow-job:2.24
  - workflow-aggregator:2.5
  - credentials-binding:1.16
  - git:3.9.1
  - blueocean:1.4.1
  - ghprb:1.40.0
  LoadBalancerSourceRanges:
  - 0.0.0.0/0
  Name: jenkins-master
  NodeSelector: {}
  PodAnnotations: {}
  ServiceAnnotations: {}
  ServicePort: 8080
  ServiceType: LoadBalancer
  SlaveListenerPort: 50000
  SlaveListenerServiceAnnotations: {}
  SlaveListenerServiceType: ClusterIP
  Tolerations: {}
  UsePodSecurityContext: true
  UseSecurity: true
  resources:
    limits:
      cpu: 2000m
      memory: 2048Mi
    requests:
      cpu: 50m
      memory: 256Mi
NetworkPolicy:
  ApiVersion: extensions/v1beta1
  Enabled: false
Persistence:
  AccessMode: ReadWriteOnce
  Annotations: {}
  Enabled: true
  Size: 8Gi
  StorageClass: ontap-gold
  mounts: null
  volumes: null
rbac:
  apiVersion: v1
  install: false
  roleBindingKind: ClusterRoleBinding
  roleRef: cluster-admin
  serviceAccountName: default

HOOKS:
---
# jenkins-ui-test-1g5nb
apiVersion: v1
kind: Pod
metadata:
  name: "jenkins-ui-test-1g5nb"
  annotations:
    "helm.sh/hook": test-success
spec:
  initContainers:
    - name: "test-framework"
      image: "dduportal/bats:0.4.0"
      command:
      - "bash"
      - "-c"
      - |
        set -ex
        # copy bats to tools dir
        cp -R /usr/local/libexec/ /tools/bats/
      volumeMounts:
      - mountPath: /tools
        name: tools
  containers:
    - name: jenkins-ui-test
      image: jenkins/jenkins:lts
      command: ["/tools/bats/bats", "-t", "/tests/run.sh"]
      volumeMounts:
      - mountPath: /tests
        name: tests
        readOnly: true
      - mountPath: /tools
        name: tools
  volumes:
  - name: tests
    configMap:
      name: jenkins-tests
  - name: tools
    emptyDir: {}
  restartPolicy: Never
MANIFEST:

---
# Source: jenkins/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: jenkins
  labels:
    app: jenkins
    chart: "jenkins-0.16.20"
    release: "jenkins"
    heritage: "Tiller"
type: Opaque
data:

  jenkins-admin-password: "N3EwZWtydDAyQg=="

  jenkins-admin-user: "YWRtaW4="
---
# Source: jenkins/templates/config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins
data:
  config.xml: |-
    <?xml version='1.0' encoding='UTF-8'?>
    <hudson>
      <disabledAdministrativeMonitors/>
      <version>lts</version>
      <numExecutors>0</numExecutors>
      <mode>NORMAL</mode>
      <useSecurity>true</useSecurity>
      <authorizationStrategy class="hudson.security.FullControlOnceLoggedInAuthorizationStrategy">
        <denyAnonymousReadAccess>true</denyAnonymousReadAccess>
      </authorizationStrategy>
      <securityRealm class="hudson.security.LegacySecurityRealm"/>
      <disableRememberMe>false</disableRememberMe>
      <projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
      <workspaceDir>${JENKINS_HOME}/workspace/${ITEM_FULLNAME}</workspaceDir>
      <buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
      <markupFormatter class="hudson.markup.EscapedMarkupFormatter"/>
      <jdks/>
      <viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
      <myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
      <clouds>
        <org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud plugin="kubernetes@1.12.3">
          <name>kubernetes</name>
          <templates>
            <org.csanchez.jenkins.plugins.kubernetes.PodTemplate>
              <inheritFrom></inheritFrom>
              <name>default</name>
              <instanceCap>2147483647</instanceCap>
              <idleMinutes>0</idleMinutes>
              <label>jenkins-jenkins-slave</label>
              <nodeSelector></nodeSelector>
                <nodeUsageMode>NORMAL</nodeUsageMode>
              <volumes>
              </volumes>
              <containers>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
                  <name>jnlp</name>
                  <image>jenkins/jnlp-slave:3.10-1</image>
                  <privileged>false</privileged>
                  <alwaysPullImage>false</alwaysPullImage>
                  <workingDir>/home/jenkins</workingDir>
                  <command></command>
                  <args>${computer.jnlpmac} ${computer.name}</args>
                  <ttyEnabled>false</ttyEnabled>
                  # Resources configuration is a little hacky. This was to prevent breaking
                  # changes, and should be cleanned up in the future once everybody had
                  # enough time to migrate.
                  <resourceRequestCpu>200m</resourceRequestCpu>
                  <resourceRequestMemory>256Mi</resourceRequestMemory>
                  <resourceLimitCpu>200m</resourceLimitCpu>
                  <resourceLimitMemory>256Mi</resourceLimitMemory>
                  <envVars>
                    <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                      <key>JENKINS_URL</key>
                      <value>http://jenkins:8080</value>
                    </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  </envVars>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
              </containers>
              <envVars/>
              <annotations/>
              <imagePullSecrets/>
              <nodeProperties/>
              <podRetention class="org.csanchez.jenkins.plugins.kubernetes.pod.retention.Default"/>
            </org.csanchez.jenkins.plugins.kubernetes.PodTemplate></templates>
          <serverUrl>https://kubernetes.default</serverUrl>
          <skipTlsVerify>false</skipTlsVerify>
          <namespace>jenkins</namespace>
          <jenkinsUrl>http://jenkins:8080</jenkinsUrl>
          <jenkinsTunnel>jenkins-agent:50000</jenkinsTunnel>
          <containerCap>10</containerCap>
          <retentionTimeout>5</retentionTimeout>
          <connectTimeout>0</connectTimeout>
          <readTimeout>0</readTimeout>
          <podRetention class="org.csanchez.jenkins.plugins.kubernetes.pod.retention.Never"/>
        </org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud>
      </clouds>
      <quietPeriod>5</quietPeriod>
      <scmCheckoutRetryCount>0</scmCheckoutRetryCount>
      <views>
        <hudson.model.AllView>
          <owner class="hudson" reference="../../.."/>
          <name>All</name>
          <filterExecutors>false</filterExecutors>
          <filterQueue>false</filterQueue>
          <properties class="hudson.model.View$PropertyList"/>
        </hudson.model.AllView>
      </views>
      <primaryView>All</primaryView>
      <slaveAgentPort>50000</slaveAgentPort>
      <disabledAgentProtocols>
        <string>JNLP-connect</string>
        <string>JNLP2-connect</string>
      </disabledAgentProtocols>
      <label></label>
      <crumbIssuer class="hudson.security.csrf.DefaultCrumbIssuer">
        <excludeClientIPFromCrumb>true</excludeClientIPFromCrumb>
      </crumbIssuer>
      <nodeProperties/>
      <globalNodeProperties/>
      <noUsageStatistics>true</noUsageStatistics>
    </hudson>
  jenkins.model.JenkinsLocationConfiguration.xml: |-
    <?xml version='1.1' encoding='UTF-8'?>
    <jenkins.model.JenkinsLocationConfiguration>
      <adminAddress></adminAddress>
      <jenkinsUrl>http://jenkins:8080</jenkinsUrl>
    </jenkins.model.JenkinsLocationConfiguration>
  jenkins.CLI.xml: |-
    <?xml version='1.1' encoding='UTF-8'?>
    <jenkins.CLI>
      <enabled>false</enabled>
    </jenkins.CLI>
  apply_config.sh: |-
    mkdir -p /usr/share/jenkins/ref/secrets/;
    echo "false" > /usr/share/jenkins/ref/secrets/slave-to-master-security-kill-switch;
    cp -n /var/jenkins_config/config.xml /var/jenkins_home;
    cp -n /var/jenkins_config/jenkins.CLI.xml /var/jenkins_home;
    cp -n /var/jenkins_config/jenkins.model.JenkinsLocationConfiguration.xml /var/jenkins_home;
    # Install missing plugins
    cp /var/jenkins_config/plugins.txt /var/jenkins_home;
    rm -rf /usr/share/jenkins/ref/plugins/*.lock
    /usr/local/bin/install-plugins.sh `echo $(cat /var/jenkins_home/plugins.txt)`;
    # Copy plugins to shared volume
    cp -n /usr/share/jenkins/ref/plugins/* /var/jenkins_plugins;
  plugins.txt: |-
    kubernetes:1.12.3
    workflow-job:2.24
    workflow-aggregator:2.5
    credentials-binding:1.16
    git:3.9.1
    blueocean:1.4.1
    ghprb:1.40.0
---
# Source: jenkins/templates/test-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins-tests
data:
  run.sh: |-
    @test "Testing Jenkins UI is accessible" {
      curl --retry 48 --retry-delay 10 jenkins:8080/login
    }
---
# Source: jenkins/templates/home-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jenkins
  labels:
    app: jenkins
    chart: "jenkins-0.16.20"
    release: "jenkins"
    heritage: "Tiller"
spec:
  accessModes:
    - "ReadWriteOnce"
  resources:
    requests:
      storage: "8Gi"
  storageClassName: "ontap-gold"
---
# Source: jenkins/templates/jenkins-agent-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: jenkins-agent
  labels:
    app: jenkins
    chart: "jenkins-0.16.20"
    component: "jenkins-jenkins-master"
spec:
  ports:
    - port: 50000
      targetPort: 50000

      name: slavelistener
  selector:
    component: "jenkins-jenkins-master"
  type: ClusterIP
---
# Source: jenkins/templates/jenkins-master-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: jenkins
  labels:
    app: jenkins
    heritage: "Tiller"
    release: "jenkins"
    chart: "jenkins-0.16.20"
    component: "jenkins-jenkins-master"
spec:
  ports:
    - port: 8080
      name: http
      targetPort: 8080

  selector:
    component: "jenkins-jenkins-master"
  type: LoadBalancer

  loadBalancerSourceRanges:
    - 0.0.0.0/0
---
# Source: jenkins/templates/jenkins-master-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins
  labels:
    heritage: "Tiller"
    release: "jenkins"
    chart: "jenkins-0.16.20"
    component: "jenkins-jenkins-master"
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      component: "jenkins-jenkins-master"
  template:
    metadata:
      labels:
        app: jenkins
        heritage: "Tiller"
        release: "jenkins"
        chart: "jenkins-0.16.20"
        component: "jenkins-jenkins-master"
      annotations:
        checksum/config: f1949fdff0e0d3db7c6180357f63c007db61b13e5c107e5980a5eac982863c21
    spec:
      securityContext:
        runAsUser: 0
      serviceAccountName: "default"
      initContainers:
        - name: "copy-default-config"
          image: "jenkins/jenkins:lts"
          imagePullPolicy: "Always"
          command: [ "sh", "/var/jenkins_config/apply_config.sh" ]
          resources:
            limits:
              cpu: 2000m
              memory: 2048Mi
            requests:
              cpu: 50m
              memory: 256Mi

          volumeMounts:
            -
              mountPath: /var/jenkins_home
              name: jenkins-home
            -
              mountPath: /var/jenkins_config
              name: jenkins-config
            -
              mountPath: /var/jenkins_plugins
              name: plugin-dir
            -
              mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
      containers:
        - name: jenkins
          image: "jenkins/jenkins:lts"
          imagePullPolicy: "Always"
          args: [ "--argumentsRealm.passwd.$(ADMIN_USER)=$(ADMIN_PASSWORD)",  "--argumentsRealm.roles.$(ADMIN_USER)=admin"]
          env:
            - name: JAVA_TOOL_OPTIONS
              value: ""
            - name: JENKINS_OPTS
              value: ""
            - name: ADMIN_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: jenkins
                  key: jenkins-admin-password
            - name: ADMIN_USER
              valueFrom:
                secretKeyRef:
                  name: jenkins
                  key: jenkins-admin-user
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 50000
              name: slavelistener
          livenessProbe:
            httpGet:
              path: "/login"
              port: http
            initialDelaySeconds: 90
            timeoutSeconds: 5
            failureThreshold: 12
          readinessProbe:
            httpGet:
              path: "/login"
              port: http
            initialDelaySeconds: 60
          # Resources configuration is a little hacky. This was to prevent breaking
          # changes, and should be cleanned up in the future once everybody had
          # enough time to migrate.
          resources:

            limits:
              cpu: 2000m
              memory: 2048Mi
            requests:
              cpu: 50m
              memory: 256Mi


          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
      - name: plugin-dir
        emptyDir: {}
      - name: secrets-dir
        emptyDir: {}
      - name: jenkins-home
        persistentVolumeClaim:
          claimName: jenkins
LAST DEPLOYED: Mon Aug 27 23:54:09 2018
NAMESPACE: jenkins
STATUS: DEPLOYED

RESOURCES:
==> v1/Secret
NAME     TYPE    DATA  AGE
jenkins  Opaque  2     0s

==> v1/ConfigMap
NAME           DATA  AGE
jenkins        5     0s
jenkins-tests  1     0s

==> v1/PersistentVolumeClaim
NAME     STATUS   VOLUME      CAPACITY  ACCESS MODES  STORAGECLASS  AGE
jenkins  Pending  ontap-gold  0s

==> v1/Service
NAME           TYPE          CLUSTER-IP     EXTERNAL-IP     PORT(S)         AGE
jenkins-agent  ClusterIP     10.109.172.86  <none>          50000/TCP       0s
jenkins        LoadBalancer  10.97.161.136  192.168.10.210  8080:30376/TCP  0s

==> v1beta1/Deployment
NAME     DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
jenkins  1        1        1           0          0s

==> v1/Pod(related)
NAME                     READY  STATUS   RESTARTS  AGE
jenkins-965668c95-7tzmc  0/1    Pending  0         0s


NOTES:
1. Get your 'admin' user password by running:
  printf $(kubectl get secret --namespace jenkins jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo
2. Get the Jenkins URL to visit by running these commands in the same shell:
  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        You can watch the status of by running 'kubectl get svc --namespace jenkins -w jenkins'
  export SERVICE_IP=$(kubectl get svc --namespace jenkins jenkins --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}")
  echo http://$SERVICE_IP:8080/login

3. Login with the password from step 1 and the username: admin

For more information on running Jenkins on Kubernetes, visit:
https://cloud.google.com/solutions/jenkins-on-container-engine

「NOTES」欄に記載の通りadminパスワードを取得します。

$ printf $(kubectl get secret --namespace jenkins jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

    sShJg2gig9

以上で、Jenkinsのデプロイが完了しました。

Helmが生成するマニフェストファイル

Helmを使いvalues.yamlを定義するとどのようなマニフェストファイルが生成されるかが予測しづらいこともあります。

その場合には --dry-run--debug を付与することでデプロイメントされるYAMLファイルが出力されます。

helm --namespace jenkins --name jenkins -f ./values.yaml install stable/jenkins --dry-run --debug

values.yamlのTry & Error: インストールが上手くいかない場合は?

values.yamlを試行錯誤しながら設定していくことになると思います。 一度デプロイメントしたHelmチャートは以下のコマンドで削除することができます。

$ helm del --purge チャート名

Helm以外でJenkinsをデプロイした場合

本セクションに記載してあることはオプションです。

必要に応じて実施してください。

外部にアプリケーションを公開する方法として Ingress があります。 Helmを使ってJenkinsをインストー時にvalues.yamlで設定を行うことでIngressが作成されます。 それ以外の手法を取った場合は、kubernetesクラスタ外のネットワークからアクセスできるようにIngressを作成しアクセスする方法があります。

Ingressの導入についてはLevel4 運用編の Ingressを導入 にまとめました。

Jenkinsの設定をする

Gitリポジトリに変更があったら自動でテストを実行するpipelineを定義します。 そのためにはまずJenkinsでGitリポジトリに操作があった場合の動作の定義とKubernetesとの接続の設定をします。

定義出来る動作としては以下の単位が考えられます。 細かく設定することも可能です。運用に合わせた単位で設定します。

  • pull request 単位
  • release tag 単位
  • 定期実行

前述した項目を盛り込みCI/CDパイプラインを作成しましょう。 シンプルなパイプラインからはじめ、必要に応じてステージを追加していきましょう。

Jenkins AgentをKubernetes上で実行できるようにする

Jenkinsからkubernetes上でJenkins agentを実行する場合にはJenkins kubernetes-plugin の導入が必要です。 通常はソースコードの取得から実施することになります。gitを使う場合であればgitのjenkins-pluginが必要です。

本ガイドで準備した values.yaml を使用している場合にはすでにどちらも導入されている状態となります。

ここでは Jenkins から kubernetesへ接続できるようにする設定を提示いたします。

Jeninsログイン後、クレデンシャルを事前に作成します。

../_images/Kubernetes-Credentials.png

jenkins 導入済みのネームスペースにサービスアカウントを作成します。

kubectl create clusterrolebinding jenkins --clusterrole cluster-admin --serviceaccount=jenkins:default

Configurationから「Kubernetes設定」を選択します。

../_images/jenkins-configuration.jpg

ここでは必要となるパラメータを設定していきます。

  • kubernetes URL: マスタのIP, 192.168.XX.10
  • Kubernetes Namespace: Jenkinsをインストールしたnamespace名
  • kubernetes certificate key: /etc/kubernetes/pki/apiserver.crtの内容をペースト
  • Credentials: Secret を選択

Jenkins Pipelineの作成

  • テスト実行
  • アプリケーションビルド
  • コンテナイメージのビルド
  • レジストリへコンテナイメージのpush
  • アプリケーションデプロイ

上記のようなパイプラインを作成にはJenkins pipeline機能が活用できます。

ここではテンプレートを準備しました、上記の様なパイプラインを実装してみましょう。 Jenkins ではパイプラインを構築するために2つの記述方法があります。

それぞれの違いついてはこちら。

Jenkins pipelineのフォーマット
pipeline {
    agent {
        kubernetes {
            label 'jenkins-ci'
            defaultContainer 'jnlp'
            yamlFile 'KubernetesPod.yaml'
        }
    }

    stages {

        stage('Pre Build Check') {
            steps {
                script {
                    echo "Pre Build staget implement!"
                }
                container('busybox') {
                    sh 'echo Hello Container World in Pre Build Check'
                }
            }
        }

        stage('Build') {
            steps {
                echo "Build staget implement!"
                script {
//                    Dockerfile 内部でdockerを導入する
//                    docker.build("nodejs-build-${env.BUILD_ID}").inside() {
//                        node -v
//                    }
                }
            }
        }

        stage('Test') {
            steps {
                echo 'Test Stage implement!'
                container('kubectl') {
                    sh 'kubectl version'
                }
            }
        }

        stage('Deploy') {
            steps {
                echo 'printenv'
                container('helm') {
                    sh 'helm version'
                }
            }
        }
    }
}
Jenkins pipelineをkubernetesで動作させるコンテナのテンプレートを定義
metadata:
  labels:
    some-label: jenkins-ci
spec:
  containers:
  - name: jnlp
    env:
    - name: CONTAINER_ENV_VAR
      value: jnlp
  - name: busybox
    image: busybox
    command:
    - cat
    tty: true
    env:
    - name: CONTAINER_ENV_VAR
      value: busybox
  - name: kubectl
    image: lachlanevenson/k8s-kubectl:v1.8.8
    command:
    - cat
    tty: true
    env:
    - name: CONTAINER_ENV_VAR
      value: kubectl
  - name: helm
    image: lachlanevenson/k8s-helm:latest
    command: 
    - cat
    tty: true
    env:
    - name: CONTAINER_ENV_VAR
      value: helm

Jenkins pipeline の作成が完了したら任意のGitリポジトリにpushします。 以降のJenkins Pipelineの実行にJenkinsfileを使用します。

アプリケーションの変更を検知してデプロイメント可能にする

CI/CDのパイプラインを作成したら実際にアプリケーションの変更をトリガー(ソースコードの変更、Gitリポジトリへのpush等)としてk8sへアプリケーションをデプロイします。

ポリシーとして大きく2つに別れます、参考までに以下に記載いたします。

  • デプロイ可能な状態までにし、最後のデプロイメントは人が実施する(クリックするだけ)
  • デプロイメントまでを完全自動化する

実際にkubernetes環境へのデプロイができたかの確認とアプリケーションが稼働しているかを確認します。

今回はサンプルとしてJenkinsのBlueOcean pluginを使用してPipelineを作成します。

../_images/jenkins_blueocean.png

BlueOcean plugin を使用するとウィザード形式でPipelineを作成することができます。

各入力値については以下のURLにてどのような形式で入力されるかの記載があります。

コンテナをCI/CDする方法 Helmを使ってみる

コンテナのCI/CDではいくつか方法があります。 ここではコンテナをCI/CDするために必要な検討事項を記載するとともに

個別のアプリケーションデプロイメントからHelm Chartを使ったデプロイメントに変更します。

作成したコンテナをHelm Chartを使ってデプロイするようにします。

Helm Chartの開発ガイドは以下のURLを確認ください。

他にも以下のようなCI/CDを行いやすくする構成管理・パッケージマネジメントのツールが存在しています。

  • Kustomize
  • Draft
  • GitKube
  • Skaffold

デプロイメントのさらなる進化

CI/CDプロセスを成熟させていくと常にリリース可能な状態となっていきます。 そのような状態になると本番環境へのデプロイを迅速にし、ダウンタイムを最小化するための方法が必要になってきます。 元々存在するプラクティスや考え方となりますがコンテナ技術、kubernetesのスケジューラー機能を使うことで今までの環境とくらべて実現がしやすくなっています。

Blue/Greenデプロイメント, Canary リリースというキーワードで紹介したいと思います。

Blue/Greenデプロイメント

従来のやり方では1つの環境にデプロイし何かあれば戻すという方法をほとんどのケースで採用していたかと思いますが、さらなる進化として常に戻せる環境を準備し迅速にロールバック 新バージョン、旧バージョンをデプロイしたままルータで切り替えるようになります。

様々な企業で行き着いている運用でもあるかと思いますが、2010年にBlueGreenデプロイメントという名称で説明しています。

実現方法、切り替えのタイミングなどあり、BlueGreenの実装の決定的なものはなく、1つのプラクティスとして存在しています。

2つの環境を準備し、どこかのタイミングで切り替えを行うためDBのマイグレーションの方法などを検討する必要はでてきます。

Canary

Canary リリースは BlueGreen デプロイメントと類似したデプロイメントになります。 Blue/Green デプロイメントはすぐに古いバージョンにもどせるように仕組みを整えたものですが、Canaryリリースは新しいバージョン、旧バージョンにアクセスする比率を決めてデプロイするプラクティスです。

こちらは2つの環境ではなく、1環境に複数バージョンのアプリケーションが存在することになります。そのためDBのデータをどのように取り扱うかは検討が必要となります。

まとめ

このラボではコンテナ化したアプリケーションのCI/CDパイプラインの構築に挑戦しました。 CI/CDパイプラインを作成するためのJenkins/GitLabをインストールするために必要なHelmが使えるようになりました。

本ラボでは簡易的なパイプラインを実際に構築しました。パイプライン内の処理については個々で実装したものから発展させ様々な処理を追加することができます。

ここまでで Level3 は終了です。