Level 3: CI/CDパイプラインを構築¶
目的・ゴール: コンテナ化したアプリケーションのCICDを実現する¶
アプリケーションをコンテナ化したら、常にリリース可能な状態、自動でデプロイメントを出来る仕組みをつくるのが迅速な開発をするために必要になります。
そのためのCI/CDパイプラインを作成するのがこのレベルの目標です。
本ラボでは Level1, Level2 で行ったオペレーションをベースにCI/CDパイプラインを構築します。
Gitにソースがコミットされたら自動でテスト・ビルドを実現するためのツール(Jenkins)をkubernetes上へデプロイ、及び外部公開をします。 そして、Jenkinsがデプロイできたら実際にアプリケーションの変更を行い自動でデプロイするところまでを目指します。
流れ¶
- Jenkins をインストールする
- Jenkins 内部でジョブを定義する。
- あるアクションをトリガーにビルド、テストを自動実行する。
- 自動で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つ存在します。
- Helm Chartでデプロイする方法 (手軽にインストールしたい人向け)
- Level1,2と同じようにyamlファイルを作成し、デプロイする方法(仕組みをより深く知りたい人向け)
- 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チャートのインストール・Jenkinsのカスタマイズ¶
今回はJenkinsを導入するにあたり環境に併せてカスタマイズを行います。 Helmは以下のURLに様々なものが公開されています。パラメータを与えることである程度カスタマイズし使用することができます。 Helm chartと同等のディレクトリにvalues.yamlというファイルが存在し、これを環境に併せて変更することでカスタマイズしデプロイできます。
今回のJenkinsのデプロイでは2つの公開方法が選択できます。
1つ目が、今回の環境では Service
の type
を LoadBalancer
としてしてデプロイすると 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ファイルに追記済みです。
# 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ログイン後、クレデンシャルを事前に作成します。
jenkins 導入済みのネームスペースにサービスアカウントを作成します。
kubectl create clusterrolebinding jenkins --clusterrole cluster-admin --serviceaccount=jenkins:default
Configurationから「Kubernetes設定」を選択します。
ここでは必要となるパラメータを設定していきます。
- 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機能が活用できます。
- https://jenkins.io/doc/book/pipeline/
- https://github.com/jenkinsci/kubernetes-plugin/blob/master/README.md
ここではテンプレートを準備しました、上記の様なパイプラインを実装してみましょう。 Jenkins ではパイプラインを構築するために2つの記述方法があります。
- Declarative pipeline syntax https://jenkins.io/doc/book/pipeline/#declarative-pipeline-fundamentals
- Scripted pipeline syntax https://jenkins.io/doc/book/pipeline/#scripted-pipeline-fundamentals
それぞれの違いついてはこちら。
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'
}
}
}
}
}
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を作成します。
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 は終了です。