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は共有で準備しています。

ここでは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つ目が今までのレベルと同様に ServicetypeNodePort として公開する方法です。これは今まで通りの疎通確認が可能です。

2つ目が Ingress を使った公開です。IngressをJenkinsのHelmチャートを使ってデプロイするためには「Master.Ingress.Annotations」、「Master.ServiceType」を変更しデプロイします。 また、このvalues.yamlでは永続化ストレージが定義されていないため、Level2で作成したStorageClassを使用し動的にプロビジョニングをするように変更しましょう。

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

どちらの方法の場合も、以下のvalues.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>
  Cpu: "200m"
  Memory: "256Mi"
  # 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"
  # 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
  ContainerPort: 8080
  # Enable Kubernetes Liveness and Readiness Probes
  HealthProbes: true
  HealthProbesTimeout: 60
  SlaveListenerPort: 50000
  # 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.1
    - workflow-aggregator:2.5
    - workflow-job:2.15
    - credentials-binding:1.13
    - git:3.6.4
  # 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: {}

  Ingress:
    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
  Cpu: "200m"
  Memory: "256Mi"
  # You may want to change this to true while testing a new image
  AlwaysPullImage: false
  # 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>
  ## 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: "-"

  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 v1beta1 or v1alpha1)
  apiVersion: v1beta1
  # Cluster role reference
  roleRef: cluster-admin

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

$ helm --namespace jenkins --name jenkins -f ./jenkins-values.yaml install stable/jenkins --debug
    LAST DEPLOYED: Tue Apr 24 12:47:12 2018
    NAMESPACE: jenkins
    STATUS: DEPLOYED

    RESOURCES:
    ==> v1/Secret
    NAME     TYPE    DATA  AGE
    jenkins  Opaque  2     8m

    ==> v1/ConfigMap
    NAME           DATA  AGE
    jenkins        3     8m
    jenkins-tests  1     8m

    ==> v1/PersistentVolumeClaim
    NAME     STATUS  VOLUME                 CAPACITY  ACCESS MODES  STORAGECLASS  AGE
    jenkins  Bound   jenkins-jenkins-2c478  8Gi       RWO           ontap-gold    8m

    ==> v1/ServiceAccount
    NAME     SECRETS  AGE
    jenkins  1        8m

    ==> v1/Service
    NAME           TYPE       CLUSTER-IP   EXTERNAL-IP  PORT(S)         AGE
    jenkins-agent  ClusterIP  10.98.21.68  <none>       50000/TCP       8m
    jenkins        NodePort   10.96.24.25  <none>       8080:31050/TCP  8m

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

    ==> v1beta1/Ingress
    NAME     HOSTS                              ADDRESS  PORTS  AGE
    jenkins  jenkins.user21.web.service.consul  80       8m

    ==> v1/Pod(related)
    NAME                      READY  STATUS   RESTARTS  AGE
    jenkins-578686f98d-6pbx9  1/1    Running  0         8m

    ==> v1beta1/ClusterRoleBinding
    NAME                  AGE
    jenkins-role-binding  8m


    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. Visit http://jenkins.user21.web.service.consul

    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
    Configure the Kubernetes plugin in Jenkins to use the following Service Account name jenkins using the following steps:
      Create a Jenkins credential of type Kubernetes service account with service account name jenkins
      Under configure Jenkins -- Update the credentials config in the cloud section to use the service account credential you created in the step above.

「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を試行錯誤しながら設定していくことになると思います。 一度デプロイメントしたHelmチャートは以下のコマンドで削除することができます。

$ helm del --purge チャート名

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

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

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

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

注意
Helm chart を使ってインストールした場合は自動でIngressが導入されています。
そのため、以下の手順はHelmで実施した人は不要です。

Ingressの導入についてはこちらに Ingressを導入する まとめました。

ServiceをDNSへ登録する

HelmでデプロイしたJenkinsにはIngress経由でアクセスします。 そのためホスト名を使用してアクセスします。

注釈

なぜそのような仕組みになっているかを知りたい方はJenkinsのHelmチャートをご確認ください。 https://github.com/kubernetes/charts/tree/master/stable/jenkins

今回は名前解決にConsulを使います。

登録用JSONは以下の通りです、TagsとNameでdnsに問い合わせる名前が決まります。 今回はドメインを service.consul を使用します。

このラボでは命名規則を定義します。

  • ID, Tags: アプリケーション識別子.環境番号
  • Name: web固定
  • Address: 各環境のマスタのIP

アプリケーションにアクセスする際に jenkins.user10.web.service.consul というFQDNでアクセスしたい場合は以下のjsonファイルを作成します。 ファイル名はwebservice.jsonとします。ポート番号はアプリケーションで使用しているものに変更してください。

{

  "ID": "jenkins.user10",
  "Name": "web",
  "Tags": [ "jenkins.user10" ],
  "Address": "192.168.XX.10",
  "Port": 80
}

ファイルを作成したら以下のコマンドで登録します。

$ curl -i -s --request PUT --data @webservice.json http://infra1:8500/v1/agent/service/register

HTTP/1.1 200 OK
Date: Wed, 11 Apr 2018 05:31:37 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

登録が完了したら名前解決ができるか確認します。

$ nslookup jenkins.user10.web.service.consul

Jenkinsの設定をする

Gitリポジトリに変更があったら自動でテストを実行するジョブを定義します。 このテストは任意で作成してください。

ここでやりたいことは該当リポジトリにコミットがあり、リリースタグが付与された場合に自動でビルド・デプロイをする流れを作成することです。 そのためにはまずJenkinsでGitリポジトリに操作があった場合の動作を定義します。

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

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

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

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

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

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

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

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

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

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

Helm ChartでCI/CD

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

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

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

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

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の使い方、アプリケーションを外部に公開するためのkubernetesオブジェクトのIngressも併せて使えるようになりました。

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