k8sのデータ転送基盤にスクレイピング機能を追加した話

[Selenium/Appium Advent Calendar 2019](https://qiita.com/advent-calendar/2019/selenium_and_appium) 16日目（？）になります！（遅くなりましてごめんなさいごめんなさい）

# 背景

弊社のプロダクトではデータ転送の基盤をk8s（EKS）で構築しております。
データ転送は k8sのjob で行っており、データ転送の処理は Embulk を使用しております。

今回はデータ転送にスクレイピングの機能を追加した時の話をさせていただきます。

# サイドカーパターン

気持ちとして、データ転送のメインコンテナをSeleniumのDriverを載せることで大きくしたくないと考えておりました。そのためSelenimuによるスクレイピング基盤をサイドカーパターンで構成しようと考えました。

サイドカーについて詳しくはBrendan Burns, David Oppenheimerらの論文([Design patterns for container-based distributed systems](https://www.usenix.org/system/files/conference/hotcloud16/hotcloud16_burns.pdf))を読むなり書籍を読むなりしていただくとして、概要を説明しますとサイドカーとはポッドの中でメインコンテナを補助する様なコンテナを持つ構成のことをいいます。

下記の例ではWeb Serverはメインコンテナに乗せてログの保存をサイドカーにさせるように構成しています。

<img width="326" alt="sidecar.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215051/6412e155-4e12-e805-0f99-9fbae59a7d2a.png">


今回でいうとスクレイピングとデータ転送処理はメインコンテナに持たせて、Seleniumの実行環境は別コンテナで起動するようなサイドカーの構造を作成しました。

# スクレイピングからデータ転送の方針

弊社ではデータ転送にはEmbulkを使用しています。Embulkにはローカルファイルの転送をするプラグインがあるので以下の方針でスクレイピングしたデータを転送するようにしました。

1. スクレイピングを実行し取得したデータをJSONデータにする
2. 生成したJSONデータをEmbulkで転送する

Seleniumの実行環境としてはPod上にSelenium Gridを構成するようにしています。具体的には1つのPodにSelenium HubコンテナとNodeコンテナを立ち上げています。

![pod.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215051/29ee92da-0b74-2793-075a-c90cb6ead13e.png)


# 実際に起動させる

以下は実際に起動しているPodのdescribeしたものです。
1つのPodでSelenium HubとNodeがサイドカーとして共存していることがわかります。

```
Name:               sample-job-215127-m26g7
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               ip-10-80-88-198.***/10.80.88.198
Start Time:         Wed, 18 Dec 2019 01:49:03 +0000
Labels:             controller-uid=9187b356-2138-11ea-a061-0e36cb33bc04
                    job-name=sample-job-215127
                    prometheus.io/path=metrics
                    prometheus.io/port=9010
                    prometheus.io/scrape=true
Annotations:        cluster-autoscaler.kubernetes.io/safe-to-evict: false
Status:             Running
IP:                 10.80.92.39
Controlled By:      Job/sample-job-215127
Containers:
  worker:
    Container ID:  docker://4547f0a3b43fbf14c4850e26d71e9db89799f263125d679724e5c1a09289e4d6
    Image:         ***/worker.beta.sample.io:5c0f855874849b1d29ee5f1fe7c720ca835d60c3
    Image ID:      docker-pullable://***/worker.beta.sample.io@sha256:9a1771dff18778d1995359dd1eb056d13a2c0f1b7b23a3c03198771ee978024c
    Port:          9010/TCP
    Host Port:     0/TCP
    Args:
      sample:run[215127,false]
    State:          Running
      Started:      Wed, 18 Dec 2019 01:49:04 +0000
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     2
      memory:  2Gi
    Requests:
      cpu:     2
      memory:  2Gi
    Environment Variables from:
      sample-config  ConfigMap  Optional: false
      sample-secret  Secret     Optional: false
    Environment:     <none>
    Mounts:
      /tmp from tmp-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-4nw9k (ro)
  selenium:
    Container ID:   docker://44c1d93596ff64ba63d783801ec4200d6f6fc6445305473904f5ce1e96fd686e
    Image:          selenium/hub:3.141.59
    Image ID:       docker-pullable://selenium/hub@sha256:6f6bdd8d5ce5cd8d7be42ded88bc3dbbdf7a60ec32c908eb26dd31b37f69589a
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 18 Dec 2019 01:49:04 +0000
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     500m
      memory:  2000Mi
    Requests:
      cpu:     500m
      memory:  2000Mi
    Environment Variables from:
      sample-config  ConfigMap  Optional: false
      sample-secret  Secret     Optional: false
    Environment:     <none>
    Mounts:
      /tmp from tmp-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-4nw9k (ro)
  chrome:
    Container ID:   docker://cd880ec332d2af37efd91535ebdbad0e128295aa58f6255d6c093668306438cf
    Image:          selenium/node-chrome:3.141.59
    Image ID:       docker-pullable://selenium/node-chrome@sha256:5e37ffdeae0864dd7f0df63adafcc9428766d79976a645853bc1e20d7487ff4f
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 18 Dec 2019 01:49:04 +0000
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     500m
      memory:  2000Mi
    Requests:
      cpu:     500m
      memory:  2000Mi
    Environment Variables from:
      sample-config  ConfigMap  Optional: false
      sample-secret  Secret     Optional: false
    Environment:     <none>
    Mounts:
      /tmp from tmp-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-4nw9k (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  tmp-volume:
    Type:    EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium:
  default-token-4nw9k:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-4nw9k
    Optional:    false
QoS Class:       Guaranteed
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From                                                      Message
  ----    ------     ----  ----                                                      -------
  Normal  Scheduled  18s   default-scheduler                                         Successfully assigned default/sample-job-215127-m26g7 to ip-10-80-88-198.***
  Normal  Pulled     17s   kubelet, ip-10-80-88-198.a***  Container image "***/worker.beta.sample.io:5c0f855874849b1d29ee5f1fe7c720ca835d60c3" already present on machine
  Normal  Created    17s   kubelet, ip-10-80-88-198.a***  Created container
  Normal  Started    17s   kubelet, ip-10-80-88-198.a***  Started container
  Normal  Pulled     17s   kubelet, ip-10-80-88-198.a***  Container image "selenium/hub:3.141.59" already present on machine
  Normal  Created    17s   kubelet, ip-10-80-88-198.a***  Created container
  Normal  Started    17s   kubelet, ip-10-80-88-198.a***  Started container
  Normal  Pulled     17s   kubelet, ip-10-80-88-198.a***  Container image "selenium/node-chrome:3.141.59" already present on machine
  Normal  Created    17s   kubelet, ip-10-80-88-198.a***  Created container
  Normal  Started    17s   kubelet, ip-10-80-88-198.a***  Started container
```

このように構成することでメインのコンテナを肥大化させること無くSelenimuの実行環境を構築することができました。転送処理が完了したらPodごと削除してしまえばSeleniumの環境が不要に残ることもないので管理も非常に楽です。

ただし、サイドカー構成のPodにてメインコンテナが終了した際のPodの停止処理について、現状はメインコンテナの停止してもサイドカーは停止されません。

[こちら](https://stackoverflow.com/questions/38600622/sidecar-containers-in-kubernetes-pods/38628708)で議論されているのサイドカー機能がリリースされるまでは停止処理を実装する必要があります。

# まとめ

今回はk8s上のデータ転送基盤にSeleniumによるスクレイピング機能を組み込んだ話をしました。比較的簡単かつ管理も楽にスクレイピング基盤を構築できるので、今後いろんな形で活用していきたいと思います。