Chefを使ってKubernetesにデプロイしてみた

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イメージを作ります。

```dockerfile
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`は暗号化・解読に使われるキーです。

```agent.yaml
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からシークレットなどを解読して使用できます。

```groovy
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ファイルを生成し、それをでプロしています。

```attribute.rb
default['dir'] = '/tmp'
default['namespace'] = 'myapp1'
default['app1']['name'] = 'application1'
default['app1']['replicas'] = '2'
```
```k8s.rb
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ファイルに割り当てられます。

```eruby
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ファイルを作る感じです。

```k8s-loop.rb
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内でもロジックを追加することもできるので、１つの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のような設定が必要となりパイプラインが少し複雑になります。