KubernetesへのデプロイにHelmを使用されている方も多いと思います。自分もつかっていますが、それとは別にChefも利用してみました。
本来はChefはインフラの自動化を行うときに非常に役立つツールですが、Chefにあるtemplateの機能がKubernetesのyaml生成やそれをデプロイするのにも使えるかと思い試してみました。
システム環境
- Chef - https://docs.chef.io/chef_install_script/
- Kubernetes - https://kubernetes.io/ja/
- Jenkins - https://www.jenkins.io/
Chef clientをJenkinsのAgentに追加
Chef clientのDockerイメージ
Chefを実行するためにはChef clientが必要です。Chef Serverなどを使わずにローカルモードでJenkinsのパイプラインから使用できるようにするため、Chef clientのDockerイメージを作ります。
FROM debian:latest
RUN apt-get update
RUN apt-get install -y --no-install-recommends wget ca-certificates git
RUN wget -qO- https://omnitruck.chef.io/install.sh|bash -s -- -v 15.8.23
ChefイメージをJenkinsのAgentのSidecarとして使用
Agent用のyamlファイルを以下のように作ります。ChefにはData bags
と呼ばれるシークレットを暗号化して保存する機能があります。CHEF_DATABAG_KEY
は暗号化・解読に使われるキーです。
metadata:
name: jenkinsagent
namespace: jenkins
spec:
containers:
- name: jnlp
image: mycompany/jenkins-agent:latest
workingDir: /jenkins
- name: chef
image: mycompany/chef:latest
env:
- name: CHEF_DATABAG_KEY
valueFrom:
secretKeyRef:
name: chef
key: databagkey
Jenkins Agent内で2つのコンテナを使用します。jnlp
はJenkinsの既定のAgentで、それに先ほど作ったChefのイメージを使用してchef
という名でコンテナを追加します。
JenkinsのパイプラインからChef clientの実行
Jenkinsのパイプラインから、先ほどのAgentを以下のようして呼び込みます。
container('chef')
と指定することで、その箇所はChefコンテナでプログラムが実行されます。encrypted_data_bag_secret
に先ほどのキーをコピーすることによってChef client実行時にData bagsからシークレットなどを解読して使用できます。
pipeline {
agent {
kubernetes {
defaultContainer 'jnlp'
yamlFile 'agent.yaml'
}
}
stages {
stage ('Run Chef') {
steps {
script{
container('chef'){
sh '''
echo $CHEF_DATABAG_KEY > /etc/chef/encrypted_data_bag_secret;
chef-client --local-mode --no-fork -c ./client.rb -j ./dna.json r=recipe[web::k8s]
'''
}
}
}
Chefでrecipeを実行してtemplateよりyamlファイル作成
ChefはRubyをベースに作られています。Chefを実行したときに何を行うかは、recipeと言われるファイルにRubyで書かれています。先ほどの例だとrecipe[web::k8s]
がそれで、web
はcookbook(レシピなどリソースの集合体のようなもの)k8s
はrecipeです。
recipeの中でtemplateをもとにyamlファイルを作ってそれをKubernetesにデプロイします。
下の例ではnamespace
、stname
、replicas
の変数にattributeから値を割り当て、templateからyamlファイルを生成し、それをでプロしています。
default['dir'] = '/tmp'
default['namespace'] = 'myapp1'
default['app1']['name'] = 'application1'
default['app1']['replicas'] = '2'
template "#{node['dir']}/statefulset.yaml" do
source "statefulset.yaml.erb"
variables(
:namespace => "#{node['namespace']}",
:stname=> "#{node['app1']['name']}",
:replicas => "#{node['app1']['replicas']}"
)
end
bash "k8s statefulset deploy" do
user "root"
code <<-EOF
kubectl apply -f "#{node['dir']}/statefulset.yaml"
EOF
end
templateは以下のような感じで、先ほどrecipeで設定した変数が動的にデプロイ時にyamlファイルに割り当てられます。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: <%=@stname%>
namespace: <%=@namespace%>
spec:
selector:
matchLabels:
app: <%=@stname%>-selector
serviceName: "app"
replicas: <%=@replicas%>
template:
metadata:
labels:
app: <%=@stname%>-selector
spec:
containers:
- name: <%=@stname%>-pod
image: mycompany/myapplication
command: ["/bin/sh", "-c"]
args:
- tail -f /dev/null
この方法を使うと、同じイメージを異なるNamespaceやサービスなどに使いたい場合に、yamlファイルをそれぞれ作ることなくrecipe内でループなどをかけることによって同じtemplateから全く違うKubenetesのリソースを作ることができます。
もちろん、イメージを変数化することもできるので、違うイメージをベースにしたStatefulSet作ることも可能です。
同じtemplateを使って違うNamespaceに名前もreplicas数も異なるStatefulsetをデプロイする例は以下の通りです。Attributeをアプリケーションごとに設定して、それをループで別々のyamlファイルを作る感じです。
applications.each do |application|
template "#{node['dir']}/statefulset-#{node[application]['id']}.yaml" do
source "statefulset.yaml.erb"
variables(
:namespace => "#{node[application]['namespace']}",
:stname=> "#{node[application]['app']['name']}",
:replicas => "#{node[application]['app']['replicas']}"
)
end
bash "k8s statefulset-#{node[application]['id']} deploy" do
user "root"
code <<-EOF
kubectl apply -f "#{node['dir']}/statefulset-#{node[application]['id']}.yaml"
EOF
end
end
実装した感想
- templateは非常に使いやすく、この記事の例のように変数がどこに収まるかなど分かりやすいので、取り掛かりやすいとは思います。また、template内でもロジックを追加することもできるので、1つのtemplateから全く異なるyamlファイルを作るといった複雑なことも可能です。
- ChefのData Bagsはシークレットを暗号化して保有することができるので、シークレットを一元管理することができます。Kubernetesのシークレットなどもこれをベースに自動化することができます。
- 生成したyamlファイルは、chefコンテナ内で生成した場合はパイプラインが終了時にすべてコンテナとともになくなりますが、保存したい場合はPVCなどを使用してマウントしたストーレジに保存することも可能です。その場合、2回目以降に実行した場合は生成されるファイルが変更された場合のみファイルの上書きがされ、同じ場合はrecipeのその個所は実行されずに次に進むため、実行時間が短くなります。
- 大きな変更をかける場合、例えばNamespace自体を変更するような場合でもyamlファイルを作り直す必要もなく
default['namespace'] = 'myapp1'
をdefault['namespace'] = 'myapp2'
とするだけでmyapp2
にすべてのリソースが作られます。 - 本来Chefは、インフラの自動化ツールなのでKubenetes以外にもVMなどを使っている場合は、両方を同時に自動化する際に1つのテクノロジーで両方を満たせるので便利かと思います。
- templateを最終的にできるyamlファイルを元に作るなど、かなり自由にいろいろな使い方ができ、変更する場合なども簡単に行えます。また、recipeで実際に実行するかどうかを決めることとができるので、例えば一部のKubernetesのリソースを触りたくない場合はrecipe側でコメントアウトするなり、削除するとによって間違って変更されることもなくなります。
- ChefもしくはRubyになれている場合は、特に新たなものを学ぶ必要なく使えるので有効かと思います。ただ現在Helmを使用していてそれでうまくいっている場合に、わざわざ変更するほどでもないかと思います。
- Chefイメージを作る必要があり、JenkinsでもSidecarのような設定が必要となりパイプラインが少し複雑になります。