Laravelをskaffold使ってKubernetesで動かす

Docker for MacにKubernetesが搭載された安定版がリリースされ、少し時間が立ちました。
minikubeとのお別れは完了しましたでしょうか？今回は、Laravelで作ったアプリケーションをKubernetes上で動かせるサンプルを作成しました。

[ryosukes/laravel-k8s-sample](https://github.com/ryosukes/laravel-k8s-sample)

READMEを読めば、とりあえず動かすことは可能になるかと思うので、とりあえず動かしてみたいと思う方は試してみてください(初回はすごく時間かかるかもですが)。

僕自身が社内でLaravelを使っているため、今回はLaravelでサンプルを作成しましたが、フレームワークだったり言語を他のものにすげ替えても簡単に動作させることが可能だと思います。

この記事では、Mac上でLaravel on Kubernetesを動かし[skaffold](https://github.com/GoogleContainerTools/skaffold)を使って開発できるようにするまでの流れを解説していきたいと思います。

- 目次
  - はじめに
  - Dockerイメージ作成
  - マニフェスト作成
  - クラスタにデプロイ、 開発する
  - まとめ
 
# はじめに

必要となるLaravel, Docker for Macなどの準備を終えておきましょう

[Laravel Installation](https://laravel.com/docs/5.6)
[Docker for Mac](https://www.docker.com/docker-mac)

もしDocker for Macがインストール済で古いバージョン(Docker Community Edition 18.06.0-ceより古い)の場合は、Kubernetesが使えるよう最新版にアップデートしておきましょう。

次に、ターミナル上でKubernetesを操作するために `kubectl` というコマンドをインストールします。

https://kubernetes.io/docs/tasks/tools/install-kubectl/

`kubectl` のインストールが完了したら、Kubernetesのパッケージマネージャである `helm`をインストール。

https://github.com/helm/helm

後述する `Ingress` が使用できるよう `helm` を使用しパッケージをインストールします。

```
$ helm init
$ helm install --name my-release stable/nginx-ingress
```

最後に、Kubernetesでの開発に便利なツールである `skaffold` をインストールしておきます。

https://github.com/GoogleContainerTools/skaffold

諸々のインストールが完了したら、作業を進めていきます。

# Dockerイメージ作成

ここからコンテナの準備に入ります。まずは、Laravelを動かすためのDockerイメージを作成します。

サンプルでは、PHPとNginxが共存しているDockerイメージを作成しています。下記記事を参考にさせていただきました。

[Dockerコンテナイメージのダイエット - Laravel編](https://blog.hanhans.net/2017/12/13/multistage-php-build/)

※ このDockerファイルだと、開発時に毎回`npm install`や`composer install`が走りビルドに時間がかかります。
STG/本番リリース用に使う分にはよいですが、開発用にはそれらを省いてビルド時間が短縮できるようイメージを構築できるとローカルでの開発がより楽に進められるようになるかと思います。
(すでにnpm,yarn,composerなどがインストール済みのイメージを別で用意し、composer installなどはローカルにあるvendorをマウントするなど)

# マニフェスト作成

マニフェストと呼ばれるYAML形式の設定ファイルを作成し、それをKubernetesのクラスターに適用することで、k8s環境を構築します。

用意するマニフェストは下記。

- Application(サンプルではLaravel)
  - Deployments
  - Services
  - Ingress
  - ConfigMaps
- MySQL
  - Deployments
  - PersistentVolumes
  - PersistentVolumeClaims
  - Services
- Redis
  - Deployments
  - Services

Laravel, MySQL, Redis用で分かれていますが、それぞれ細かく見ていきます。

## Application
### Deployments

```yaml:deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 2 # Podの数
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 100% # ローリングアップデート時に許容できる超過して作られる Pod の最大数
      maxUnavailable: 0 # 最低 1 pod は ready な状態を保つ
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: sample-app
        image: sample-app/app-dev:latest # Dockerイメージを指定
        envFrom:
        - configMapRef:
            name: sample-app-config # configmapの名前を指定
        ports:
        - containerPort: 80
```

「いきなり設定ファイル出されてもわかんねーよ！」ということもあるかと思いますが、冷静に見ればそんなに難しいことは書いてありません。

Deploymentsでは、その名の通り、LaravelアプリのDockerイメージが乗っかる `Pods` と呼ばれるコンテナグループのデプロイ管理定義を行います。

Deployments及びPodsを詳しく知りたい場合は、下記記事などが参考になります。

- [Kubernetes: Deployment の仕組み](https://qiita.com/tkusumi/items/01cd18c59b742eebdc6a)
- [Kubernetes: Deployment のローリングアップデートの条件と設定](https://qiita.com/tkusumi/items/7404e422cd6b4a1c6892)

### Services, Ingress

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-app-service
spec:
  type: NodePort
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: sample-app
```

```yaml:ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sample-app-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    ingress.kubernetes.io/ssl-redirect: “true”
    ingress.kubernetes.io/rewrite-target: /
spec:
  tls:
  - hosts:
    - sample-laravel.localhost
    secretName: tls-certificate
  rules:
  - host: sample-laravel.localhost
    http:
      paths:
      - path: /
        backend:
          serviceName: sample-app-service
          servicePort: 80
```

ServiceとIngressでは、Kubernetes内でのネットワークの設定を行っています。Serviceでportや通信プロトコルを指定、IngressはAWSのALBのように、受け付けたリクエストのパスなどに応じてどのアプリケーションにリクエストを流すかの設定ができます。

また、Ingressではsslの設定を行うこともできます。サンプルではopensslで証明書を作成し、その証明書を使用しています。

```sh
# set common name 'sample-laravel.localhost'
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/nginx-selfsigned.key -out /tmp/nginx-selfsigned.crt; openssl dhparam -out /tmp/sample.pem 2048
$ kubectl create secret tls tls-certificate --key /tmp/nginx-selfsigned.key --cert /tmp/nginx-selfsigned.crt
```

Ingressではベーシック認証などもかけることができるので、ステージングでKubernetesを使用する際に参考にすることもできます。

https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx/examples/auth

なお、Services, Ingressに関して詳しく知りたい場合は下記記事が参考になるかと思います。

[KubernetesクラスタにServiceとIngressを追加し、Webアプリケーションを外部に公開](https://codezine.jp/article/detail/10523)

### ConfigMaps

```yaml:configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-app-config
data:
  # ------------------------------------------------------------------------------
  # app
  # ------------------------------------------------------------------------------

  APP_NAME: laravel
  APP_ENV: local
  APP_DEBUG: "true"
  APP_URL: https://sample-laravel.localhost
  #APP_KEY:

  # NOTE: to use stern
  LOG_CHANNEL: errorlog

  # ------------------------------------------------------------------------------
  # DB
  # ------------------------------------------------------------------------------

  DB_HOST: mysql
  DB_USERNAME: root
  DB_PASSWORD: root

  DB_CONNECTION: mysql
  DB_DATABASE: sample
  DB_DATABASE_TEST: sample_test

  REDIS_HOST: redis-master

  # ------------------------------------------------------------------------------
  # MAIL
  # ------------------------------------------------------------------------------

  MAIL_DRIVER: smtp
  MAIL_HOST: smtp.mailtrap.io
  MAIL_PORT: "2525"
  MAIL_USERNAME: "null"
  MAIL_PASSWORD: "null"
  MAIL_ENCRYPTION: "null"
```

ConfigMapではLaravelの `.env` に設定する値など環境変数を記載します。ここは `.env`をそのまま使うでも良いと思いますが、ビルドしたアプリケーションイメージに.envを含めたくないといった場合、ConfigMapを使うと良いでしょう。また、本番運用を見越して使うのであれば、DBやメールなどの秘匿情報に関しては、KubernetesのSecretを使うのが良いです。KMSなどを使えば、安全に運用できます。

[ファイルをAWS KMSで暗号化して安全にgit commitできるようにするmozilla/sopsの使い方](https://qiita.com/mumoshu/items/d5c8b93ed7898620930b)

## MySQL
続いてMySQLのPodの設定を見ていきます。

### Deployments

```yaml:mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql:5.7
        name: mysql
        resources:
          requests:
            memory: 100Mi
            cpu: 100m
          limits:
            memory: 4000Mi
        env:
          # Use secret in real usage
        - name: MYSQL_ROOT_PASSWORD
          value: root
        - name: MYSQL_DATABASE
          value: sample
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim
```

LaravelのDeploymentでは記載がありませんでしたが、cpuとmemoryの指定をしています。過去に作業していたとき、Sequel Proを使用して大きめなダンプデータを流したところフリーズしたことがありました。そのときに、cputとmemoryの割当を増やしスペックを上げたところフリーズしなくなったことがあります。この値は適宜変更して使えると良いと思います。

また、データの永続化をするためにvolumesで `persistentVolumeClaims` の設定をしています。このあたりは次で説明します。

### PersistentVolumes, PersistentVolumeClaims
データに関して、Podやクラスターが終了してもなくならないよう、データの永続化を行う必要があります。そのためにあるのが、`PersistentVolumes` および `PersistentVolumeClaims`の２つです。

```yaml:mysql-pv.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
  name: mysql-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/var/lib/mysql"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
```

サンプルではそれぞれの設定の記述が短いため一つのyamlファイルにまとめてしまっています。このマニフェストを用意しDeploymentsで設定を記述することで、`PersistentVolumes` でストレージ領域を確保、そのストレージを `PersistentVolumeClaimes` でPodに領域を紐付けるということが可能になります。

詳しくは下記記事が参考になると思います。

[Kubernetsに出てくる用語：PVCとは？](http://www.mpon.me/entry/2017/04/22/014119)

## Redis
### Deployment
```yaml:redis-deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: redis-master
  labels:
    app: redis
spec:
  selector:
    matchLabels:
      app: redis
      role: master
      tier: backend
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: master
        tier: backend
    spec:
      containers:
      - name: master
        image: redis
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
```

特筆するするところはないのですが、roleやreplicasを変更したマニフェストを作成するなどしmaster -slave のクラスタ構成を作成することも可能です。

# クラスタを起動する

さて、以上でDockerとKubernetesの準備が完了したところで、Docker for MacでDocker/Kubernetesを起動しておきます。

<img width="372" alt="領域のキャプチャ 29.png" src="https://qiita-image-store.s3.amazonaws.com/0/37036/5e1f99a8-4ba5-8295-b0f1-a148f571da02.png">

<img width="499" alt="Kubernetes 2018-11-17 15-55-51.png" src="https://qiita-image-store.s3.amazonaws.com/0/37036/a0b7ebdb-f6b3-6626-94e6-4d5db58a0914.png">

２つともグリーンのまるポチがついてrunningな状態になってればOKです。

# クラスタにデプロイ、 開発する
クラスタにデプロイするには、skaffoldを利用するのが便利です。

![skaffold.png](https://qiita-image-store.s3.amazonaws.com/0/37036/bc51661d-4c6b-7242-2773-0c5acca63c14.png)

[skaffold](https://github.com/GoogleContainerTools/skaffold)はアプリケーションコードが含まれたDockerイメージをKubernetes上にデプロイするのに便利なGoogle謹製のツールです。

skaffoldのリポジトリにskaffold用の設定サンプルファイルがあるのでそれを編集します。[ファイルはこれ](https://github.com/GoogleContainerTools/skaffold/blob/master/examples/getting-started/skaffold.yaml)

設定を変更したものが

```
apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    sha256: {}
  artifacts:
  - imageName: sample-app/app-dev # 好きなコンテナ名を指定してOK
    workspace: .
    docker:
      dockerfilePath: Dockerfile # skaffold.yamlと同じディレクトリ
  local:
    skipPush: true
deploy:
  kubectl:
    manifests:
    - kubernetes/app/* # skaffoldがmanifestファイルの変更を反映してくれる
```

になります。

この設定ファイルが用意できたら、`skaffold run` というコマンドを叩くとKubernetesのクラスター上にアプリケーションをデプロイできるようになります。

開発するときは、 ファイルの変更が発生したらすぐ自動更新したくなると思います。そこで、`skaffold dev`というコマンドを叩くとしてくれるようになります。

詳しくは下記記事が参考になるかと思います。

[Kubernetesの開発環境で困っているならskaffoldを使え](https://qiita.com/tomoyamachi/items/660bd7bb3afff8340307)

# まとめ

以上で、Kubernetes上でLaravelが動かせるようになったのではないかと思います。Kubernetesは従来のWebの開発環境ではあまり目・耳にしたことがないような単語や概念が多く、学習コストが高めです。しかし、マイクロサービス化を進めたり、安定したサービスを提供するのにとても有用なツールとなっていると思います。

今回Laravelで試しに作ってみて、フルスタックなフレームワークで開発したアプリケーションを載せようとすると、容量が大きいため開発時にKuberenetes上に反映するのに多少時間がかかるなぁと思いました。特にDockerファイルのMulti Stage Buildでnpm installやらアセットのコンパイルがビルド時のタスクとしてあると、べらぼうに時間がかかります。Dockerイメージをビルドする前後で、何かしら工夫する必要があるなと感じました。

コンテナ技術をうまく利用し、より安全にスケーラブルなアプリを作るのであれば、こういったツールの採用とともに、言語・フレームワーク・サービスの境界などの見直しが行えるとよりメリットを享受できそうです。

既存サービスでの採用は難しいところもあるかと思いますが、新規で開発するのであればKubernetesを使わない手はないなと思います。(既存サービスでも、思い切ってローカルではKubernetesを使わないというのも、一つの手かもしれません)