SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

開発現場からお届け! Wantedlyのプロダクトを育て、支える技術

オペレーションとルールを統一化し、Kubernetesを使った変化に強いインフラを構築

開発現場からお届け! Wantedlyのプロダクトを育て、支える技術 第2回

Kubernetesのマニフェストの解説

Namespace

 クラスタ内に1つの名前空間を作ります。

 リポジトリごとにNamespaceを最低限切るようにすると、他のアプリケーションとバッティングすることなく、明確にリソースを区切るので事故も起こりにくいです。

apiVersion: v1
kind: Namespace
metadata:
  name: myapp # myapp という名前の namespace を定義

ConfigMap

 nginx.confなどの設定ファイルは、ConfigMapを使ってそのファイルごとにKuberntes上に保存し、コンテナ起動時にマウントするようにすると、柔軟に設定が変更できます。

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
  namespace: myapp
data:
  nginx.conf: |
    user nginx nginx;
    worker_processes 1;

    error_log /dev/stdout notice;
    pid        /var/run/nginx.pid;

    events {
        worker_connections  1024;
    }

    http {
      default_type  application/octet-stream;

      log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for"';

      access_log  /var/log/nginx/access.log  main; # dockerの公式nginxは、この path に log を出力すると stdout に表示する用になっているため

      sendfile        on;
      #tcp_nopush     on;

      keepalive_timeout  65;

      #gzip  on;

      upstream app {
        server unix:///app/tmp/sockets/puma.sock fail_timeout=0; # unix domain socket を使って通信
      }

      server {
        listen 80;
        server_name example.com;
        root /app/public; # 静的ファイルを /app/public で配信し、それ以外は、Rails サーバーに問い合わせ
        try_files $uri/index.html $uri @app;

        location @app {
            proxy_pass http://app;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
        }

        error_page 500 502 503 504 /500.html;
        client_max_body_size 4G;
        keepalive_timeout 10;
      }
    }
$ k8stail --namespace myapp
[myapp-2136627869-k2l7b][rails]  | I, [2017-09-11T11:20:27.327244 #1]  INFO -- : [8437189b-7c93-46d1-b65c-8b3de10aabda] Started GET "/" for 127.0.0.1 at 2017-09-11 11:20:27 +0900
[myapp-2136627869-k2l7b][rails]  | I, [2017-09-11T11:20:27.328345 #1]  INFO -- : [8437189b-7c93-46d1-b65c-8b3de10aabda] Processing by PagesController#index as HTML
[myapp-2136627869-k2l7b][rails]  | I, [2017-09-11T11:20:27.329422 #1]  INFO -- : [8437189b-7c93-46d1-b65c-8b3de10aabda]   Rendering pages/index.html.erb within layouts/application
[myapp-2136627869-k2l7b][rails]  | I, [2017-09-11T11:20:27.331569 #1]  INFO -- : [8437189b-7c93-46d1-b65c-8b3de10aabda]   Rendered pages/index.html.erb within layouts/application (1.9ms)
[myapp-2136627869-k2l7b][rails]  | I, [2017-09-11T11:20:27.355639 #1]  INFO -- : [8437189b-7c93-46d1-b65c-8b3de10aabda] Completed 200 OK in 27ms (Views: 26.3ms)
[myapp-2136627869-k2l7b][nginx]  | 172.17.0.1 - - [11/Sep/2017:02:20:27 +0000] "GET / HTTP/1.1" 200 635 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" "-"
[myapp-2136627869-k2l7b][nginx]  | 172.17.0.1 - - [11/Sep/2017:02:20:27 +0000] "GET /assets/application-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css HTTP/1.1" 200 0 "http://192.168.99.100:30552/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" "-"
[myapp-2136627869-k2l7b][nginx]  | 172.17.0.1 - - [11/Sep/2017:02:20:27 +0000] "GET /packs/hello_react-bb75777111599a9f7979.js HTTP/1.1" 200 317132 "http://192.168.99.100:30552/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" "-"
[myapp-2136627869-nb49l][nginx]  | 172.17.0.1 - - [11/Sep/2017:02:20:27 +0000] "GET /assets/application-73fbacba5f4d3758abe3a42ae957600060caa0a84677aed262b8ef009905a40f.js HTTP/1.1" 200 23226 "http://192.168.99.100:30552/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" "-"
[myapp-2136627869-k2l7b][nginx]  | 172.17.0.1 - - [11/Sep/2017:02:20:28 +0000] "GET /favicon.ico HTTP/1.1" 200 0 "http://192.168.99.100:30552/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" "-"

Secret

 環境変数を指定します。以下の例では一点注意点があります。data:の中身はBase64という64種類の印字可能な英数字のみを用いた方式でエンコードしているだけのデータで、誰でもデコードして中身を閲覧することができます。そのためSecretdata:の中身をGitのリポジトリに含めるべきではありません。今回はサンプルとしてわかりやすいように書いています。

apiVersion: v1
kind: Secret
metadata:
  name: dotenv
  namespace: myapp
type: Opaque
data:
  DATABASE_URL: cG9zdGdyZXM6Ly9jb2RlOnppbmVAcG9zdGdyZXM6NTQzMi9teWFwcA==
  RACK_ENV: cHJvZHVjdGlvbg==
  RAILS_ENV: cHJvZHVjdGlvbg==
  SECRET_KEY_BASE: OTJhNDUzZDk5NjdjYmFjYjAzYzI2ZDNhNDdiY2ZkMmMxYzU0ZmIwZjZmMDViNTYwMmI5MTZlYzVlZTBiZTIzZTE2YjkwYjVkZjdmZGM3ZmM0OWM0ZGE5ZWY4MzVmYjEzNWRlYWQzODM5MmNlM2NmNmU1ZjZiNjNhMzkzYzQxMGE=

HorizontalPodAutoscaler

 Podsのオートスケールを定義します。

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: myapp
  namespace: myapp
spec:
  maxReplicas: 20
  minReplicas: 1
  scaleTargetRef:
    apiVersion: extensions/v1beta1
    kind: Deployment
    name: myapp
  targetCPUUtilizationPercentage: 50 # CPU使用率を 50% を越えた段階で scale させる

Service

 外からのトラフィックを定義します。

apiVersion: v1
kind: Service
metadata:
  name: myapp
  namespace: myapp
  labels:
    name: myapp
    role: web
spec:
  ports:
  - port: 80 # 80 port にトラフィックを流す
    name: http
    protocol: TCP
  selector:
    name: myapp
    role: web
  type: LoadBalancer

Deployment

 実際にアプリケーションをどう動かすか宣言します。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    name: myapp
    role: web
  name: myapp
  namespace: myapp
spec:
  minReadySeconds: 30
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 50% # replicas のPod数を超えて作成できる最大数(default 1) 50% なので、 2 Podまで作成できる
      maxUnavailable: 0 # 更新処理中に使用できないPodの最大数(default 1) 0 にすることで、先に delete してリソースを開けて入れ替えではなく、 先に create してから入れ替えを行なうようにする。
  replicas: 4 # template 以下に定義するPodの数を定義
  template:
    metadata:
      labels:
        name: myapp
        role: web
      name: myapp
    spec:
      volumes:
        - name: nginx-conf # ConfigMap で作成した nginx.conf を mount する
          configMap:
            name: nginx-conf
        - name: public # Pod 内で共有する volume mount
          emptyDir: {}
        - name: sockets # Pod 内で共有する volume mount
          emptyDir: {}
      containers:
      - name: rails
        image: quay.io/koudaiii/myapp:latest
        imagePullPolicy: Always
        ports:
          - containerPort: 3000
        readinessProbe: # traffic を受けるタイミングを指定
          httpGet:
            path: /ping  # /ping で確認
            port: 3000
          initialDelaySeconds: 10 # 10s後にチェックする
          timeoutSeconds: 1 # 1s 以上は timeout
        lifecycle:
          postStart: # application 起動後に静的ファイルを nginx 側で配信するように共有ボリュームにコピーする
            exec:
              command:
                - "cp"
                - "-r"
                - "/app/public"
                - "/nginx/"
        resources:
          limits:
            memory: "256Mi" # 利用する memory のスペックを定義
            cpu: "256m" # 利用する cpu のスペックを定義
        env:
        - name: "RAILS_SERVE_STATIC_FILES" # 静的ファイルを local から配信する
          value: "enabled"
        - name: "RAILS_LOG_TO_STDOUT" # log を stdout で出力する
          value: "enabled"
        envFrom:
        - secretRef:
            name: dotenv # Secret で作成した環境変数を利用する
        command: ["bundle", "exec", "puma", "-C", "config/puma.rb"]
        volumeMounts:
          - mountPath: /nginx/public # 共有ボリュームに指定(静的ファイル)
            name: public
          - mountPath: /app/tmp/sockets # 共有ボリュームに指定(puma.sock)
            name: sockets
      - name: nginx
        image: nginx:1.13.3-alpine
        imagePullPolicy: Always
        ports:
          - containerPort: 80 # Deployment containerPort (Open Port)
        lifecycle:
          preStop: # nginx を graceful termitate する
            exec:
            # SIGTERM triggers a quick exit; gracefully terminate instead
              command: ["/usr/sbin/nginx","-s","quit"]
        volumeMounts:
          - mountPath: /app/public  # 共有ボリュームに保存されている静的ファイルを mount する
            name: public
            readOnly: true
          - mountPath: /etc/nginx/ # ConfigMap で作成した nginx.conf を mount する
            name: nginx-conf
            readOnly: true
          - mountPath: /app/tmp/sockets # 共有ボリュームに保存されてる puma.sock を mount する
            name: sockets

 maxSurgemaxUnavailableを指定することで、リリースされるまでのスケジューリングを制御することができます。

 maxSurgemaxUnavailableを初期値で利用しreplicasを1にした場合、このままではmaxUnavailableの初期値が1のため、Podを消した(delete)のちに生成(create)してしまい、ダウンタイムが発生するので注意が必要です。

 まとめて入れ替えるには、以下の通り設定する必要があります。用途としては、新旧のコードが混ざってはいけない場合や、オンメモリーでデータを固定したい場合(振り分けられる先でレスポンスの内容が変わってしまうのを避ける)に有効活用できます。

  • maxSurge: 100%⇒replicasの分追加で作成できる
  • maxUnavailable: 0⇒使えない数を0にする
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 100% # maximum number of Pods that can be created above the desired number of Pods
      maxUnavailable: 0 # maximum number of Pods that can be unavailable during the update process.

CronJob

 サービスを運用していくと、定期的にJobを実行する必要が出てきます。その場合はCronJobを利用します。CronJobはまだアルファ版ですが、先日v1beta1に昇格するPull Requestがマージされたため、Kubernetesの1.8系または1.9系からはDeploymentと同様にベータ版として利用できそうです。

apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  namespace:  myapp
  name: myapp-job
  labels:
    name: myapp-job
    role: job
spec:
  schedule: "*/15 * * * *"
  concurrencyPolicy: "Replace" # 前回のJob が実行中だった場合には入れ替えて再度実行する
  successfulJobsHistoryLimit: 10 # 10 回分のログとして保持
  failedJobsHistoryLimit: 5 # 5 回分のログとして保持
  suspend: false
  jobTemplate:
    metadata:
      name: myapp-job
      labels:
        job: myapp-job
        role: job
    spec:
      template:
        metadata:
          name: myapp-job
          labels:
            job: myapp-job
            role: job
        spec:
          hostname: scheduler
          containers:
          - name: job
            image: quay.io/koudaiii/myapp:latest
            imagePullPolicy: Always
            command: # Job の retry を許容しない場合に、bash コマンドとして実行して、必ず一回で終わるようできる
              - "/bin/bash"
              - "-l"
              - "-c"
              - "bundle exec rails hello || true"
            env:
             - name: "RAILS_SERVE_STATIC_FILES"
               value: "enabled"
             - name: "RAILS_LOG_TO_STDOUT"
               value: "enabled"
            envFrom:
            - secretRef:
                name: dotenv
          restartPolicy: Never
$ kubectl create -f kubernetes/jobs/myapp-job.yaml
cronjob "myapp-job" created

$ kubectl get cronjob -n myapp
NAME        SCHEDULE       SUSPEND   ACTIVE    LAST-SCHEDULE
myapp-job   */15 * * * *   False     0         <none>


$ kubectl get job -n myapp
NAME                                         DESIRED   SUCCESSFUL   AGE
db-migrate-koudaiii-2017091116251505114716   1         1            1h
myapp-job-1505118600                         1         1            22m


$ kubectl logs myapp-job-1505118600-sr7wn -n myapp
Hello, World!

次のページ
その他Production環境で利用したい保守作業

この記事は参考になりましたか?

開発現場からお届け! Wantedlyのプロダクトを育て、支える技術連載記事一覧

もっと読む

この記事の著者

坂部 広大(Wantedly, inc.)(サカベ コウダイ)

 Wantedlyの基盤改善および保守運用をしているインフラエンジニア。ツールを作ったり、Web Applicationを作ったり、社内情報システムの整備をしたりしています。 WANTEDLY TECH BOOKを書いています。 Site Twitter Facebook Github Wantedly Engineers Blog

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/10428 2017/09/29 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー