Ruby on Rails, Vue.js によるモダン WEB アプリケーション 実践編 (その1)

# はじめに

アプリケーションは Rails と Vue.js を想定して、実戦的なアプリケーション動作環境を構築するための方法を段階的に紹介していきます。

Rails と Vue を使ったアプリケーションを初めて開発する場合は [Ruby on Rails, Vue.js で始めるモダン WEB アプリケーション入門](https://qiita.com/tatsurou313/items/4f18c0d4d231e2fb55f4) も参考にしてみて下さい。


## アプリケーションの機能

* (その1) アプリケーションは冗長構成の Postgres に接続できる
* (その1) アプリケーション、DB は k8s にデプロイされる
* (その1) LE証明書を使う(定期更新できる)
* [(その2) CI/CD 環境がある](https://qiita.com/tatsurou313/items/91afe2502acde9370db1)
* [(その3) OAuth によりログイン認証できる](https://qiita.com/tatsurou313/items/c7a3bd31bce1ffb82eec)


# ブランチの運用方法と開発フローを決める

ブランチの運用方法と開発フローは以下の流れとします。

* master ブランチは開発用の最新ブランチとする
* stable ブランチは本番用の最新ブランチとする
* 開発フロー
    * master ブランチからトピックブランチを作成する
    * 適宜 master ブランチをトピックブランチにマージして最新化する
    * トピックブランチでの開発が終わったら master ブランチへの PR を作成する
    * PR をレビューして問題なければトピックブランチを master ブランチにマージする
* ステージング環境は master ブランチのアプリケーションを動作させる
    * ステージング環境で動作を確認する
* 本番環境は stable ブランチのアプリケーションを動作させる

前提としてステージング環境は開発チームや場合によっては QA チームにが利用できる環境であるとします。

尚、上記における課題としては開発用のブランチを master ブランチにマージする**前に**動作確認できない点が挙げられます。
もし、トピックブランチ事にチーム全体で動作確認できる環境を用意できれば master ブランチが動作可能であることを動作確認により保証できます。

GitHub 等のリポジトリを設定して master, stable ブランチは直接 push できないようし、PR はビルドとテストがパスしないとマージできないようにするなどの制限をしておくとよいでしょう。


# 構成

各項目に対して使用するツールは以下のとおりです。

|項目|使用ツール|
| --- | --- |
|ソースコードリポジトリ|GitHub|
|CIツール|GitHub Actions|
|CDツール|GitHub Actions|
|イメージリポジトリ|Docker Hub|
|サーバ (以下)|Kubernetes(k8s)|

※ k8s 環境の構築方法は以下の記事も参考にしてみて下さい

* [安いクラウド環境で RancherOS / Kubernetes を使って勉強用クラスタを作る](https://qiita.com/tatsurou313/items/5e3d94ebb809f114aa53)
* [快適な kubernetes オンプレミス環境を構築する(1. 設計編)](https://qiita.com/kaishuu0123/items/0f5f4dfca637f9a00676) ※シリーズ
* [Rancher を利用した社内 Kubernetes 構築](https://tech.weseek.co.jp/entry/2019/09/09/122702)

※ k8s 環境へ Rails アプリケーションをデプロイする方法は [kubernetesクラスタでRailsアプリを公開するチュートリアル](https://qiita.com/tatsurou313/items/223dfa599ee5aaf6b2f0) を参照してみて下さい。


# 作業環境を構築する

* docker, kubectl, helm, stern をインストールする


# 本番環境を初期構築する

まずは本番環境を初期構築しましょう。

* stable ブランチを使ってコンテナをビルドする
* コンテナを k8s 環境にデプロイする

## コンテナイメージのビルド用 Dockerfile を作成する

コンテナイメージをビルドするための Dockerfile を作成します。

少し工夫したのは、bundle install や yarn install を実行する時間を省略できるよう依存関係のあるパッケージ情報が同じであればキャッシュされたコンテナイメージを使えるようにマルチステージビルドを使いました。

```dockerfile
ARG RUBY_VERSION=2.5.3
ARG BUNDLER_BASE_IMAGE=circleci/ruby:${RUBY_VERSION}-node-browsers
ARG APP_BASE_IMAGE=circleci/ruby:${RUBY_VERSION}-node


# アプリケーションが依存するパッケージをインストールしてキャッシュするためのコンテナ
FROM ${BUNDLER_BASE_IMAGE} as bundler
RUN mkdir -p /home/circleci/app
WORKDIR /home/circleci/app

# install bundle gems to /usr/local/bundle
COPY --chown=circleci:circleci Gemfile* ./
RUN bundle install --with production

# install npm packages
COPY --chown=circleci:circleci package.json yarn.lock ./
RUN yarn install


# アプリケーションコンテナ
FROM ${APP_BASE_IMAGE}
WORKDIR /home/circleci/app
EXPOSE 3000

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES 1

COPY --from=bundler --chown=circleci:circleci /usr/local/bundle /usr/local/bundle
COPY --from=bundler --chown=circleci:circleci /home/circleci/app/node_modules /home/circleci/app/node_modules
COPY --chown=circleci:circleci . /home/circleci/app

CMD export SECRET_KEY_BASE=$(rails secret); rails webpacker:compile assets:precompile && rails server -b 0.0.0.0
```

出来上がったら docker build でビルドが出来ること、docker run で Rails が起動することを確認しましょう。
確認が出来たらイメージリポジトリに docker push します。

タグは Git コミット ID と stable タグをつけることにしましょう。
それぞれのタグは次の目的で使うことにします。

* Git コミット ID タグ
    * 本番環境でアプリケーションを動作させるとき
    * これは、イメージを固定する方が望ましいためです
* stable タグ
    * マニフェストファイルにデフォルトで指定するとき
    * 動作確認でアプリケーションを動作させるとき
    * stable ブランチの最新であることが分かるためです

Git コミット ID は次のコマンドで調べることが出来ます。

```bash
$ git checkout stable
$ git log | head -1
commit ＜GitコミットID＞
```

コンテナイメージをビルドするコマンドは次のとおりです。

```bash
$ docker build . -t ＜YOUR_DOCKER_ACCOUNT＞/vue_pactice_app:＜GitコミットID＞ -t ＜YOUR_DOCKER_ACCOUNT＞/vue_practice_app:stable
$ docker run -it --rm -p 3000:3000 vue_pactice_app
...
Ctrl + C
$ docker login
$ docker push ＜YOUR_DOCKER_ACCOUNT＞/vue_pactice_app:＜GitコミットID＞
$ docker push ＜YOUR_DOCKER_ACCOUNT＞/vue_pactice_app:stable
```


## k8s のネームスペースを作成する

アプリケーション毎に k8s のネームスペースを作成し、関連するマニフェストはそのネームスペースに追加していくことにします。

ネームスペースを追加するために Rancher の Dashboard から Project/Namespaces タブを選択して、"Add Namespace" ボタンを押して、ネームスペース名を入力して "Create" します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206493/bbd64ba3-4e37-e567-2052-4162f0220791.png)

ネームスペース名は `vue-practice` にします。(例なので適宜変えて下さい)

作成が終わったらマニフェストファイルをエクスポートしておきましょう。

```bash
$ kubectl get namespaces vue-practice -o yaml --export > .kube/production/namespaces.yml
```

## k8s に DB をデプロイする

本番DB は PostgreSQL を使います。
Helm を使って PostgreSQL を k8s にデプロイしましょう。

### (Helm v2) Helm 用の Tiller をデプロイする

helm コマンドは k8s インフラ上にデプロイされた Tiller を介して k8s のマニフェストを管理します。([Helm v3 では Tiller は使わなくなりました](https://helm.sh/docs/faq/#removal-of-tiller))

helm アプリケーションはチャート(chart)単位で管理され、helm コマンドを実行するホストに chart のリポジトリを保存・更新して利用します。

まずは helm init コマンドを実行して Tiller をデプロイし、Helm Chart のリポジトリを保存します。
(既に Tiller がデプロイされている場合は、`--client-only` オプションを付けます)

```bash:helmコマンドを初めて使用する場合
# Helm環境を初期化する(Tillerをデプロイし、コマンドを実行したホストにHelm Chartリポジトリを保存する)
$ helm init

# Helm環境を初期化する(Tillerはデプロイせず、、コマンドを実行したホストにHelm Chartリポジトリを保存する)
$ helm init --client-only
```

###  (Helm v3) Helm のリポジトリを追加する

[Helm v3 では Tiller は使わなくなりました](https://helm.sh/docs/faq/#removal-of-tiller)。
helm init コマンドも存在しません。

stable リポジトリがあることだけ確認し、なければ追加しましょう。

```bash
$ helm repo list
NAME    URL
stable  https://kubernetes-charts.storage.googleapis.com

# 以下は stable リポジトリが無い場合に実施する([参考](https://github.com/helm/charts#how-do-i-enable-the-stable-repository-for-helm-3))
$ helm repo add stable https://kubernetes-charts.storage.googleapis.com
```

### PostgreSQL をデプロイする

Helm アプリケーションをインストールする際、Chart の README に従って必要に応じて設定を行います。
stable/postgresql チャートの場合は[ここ](https://hub.helm.sh/charts/stable/postgresql)で確認できます。

```yaml:.kube/production/helm/postgresql/values.yml
# Correct password XXXXXX of "install command"
# 
# install command:
#   helm install stable/postgresql --name postgresql --namespace vue-practice -f values.yml --set postgresqlPassword=XXXXXXXX
global:
  postgresql:
    postgresqlUsername: vue_practice
image:
  tag: 11.5.0-r44
```

設定をファイルに保存したら helm install コマンドを使って PostgreSQL を k8s にデプロイします。

```bash
# Helm v2
$ helm install stable/postgresql --name postgresql --namespace vue-practice -f values.yml --set postgresqlPassword=XXXXXXXX

# Helm v3
$ helm install postgresql stable/postgresql --namespace vue-practice -f values.yml --set postgresqlPassword=XXXXXXXX
```

デプロイが終わると次のように結果メッセージが表示されます。

```text
NAME:   postgresql
LAST DEPLOYED: Sun Sep 22 06:11:26 2019
NAMESPACE: vue-practice
STATUS: DEPLOYED

RESOURCES:
==> v1/Pod(related)
NAME                     READY  STATUS    RESTARTS  AGE
postgresql-postgresql-0  0/1    Init:0/1  0         3s

==> v1/Secret
NAME        TYPE    DATA  AGE
postgresql  Opaque  1     3s

==> v1/Service
NAME                 TYPE       CLUSTER-IP     EXTERNAL-IP  PORT(S)   AGE
postgresql           ClusterIP  10.43.210.140  <none>       5432/TCP  3s
postgresql-headless  ClusterIP  None           <none>       5432/TCP  3s

==> v1beta2/StatefulSet
NAME                   READY  AGE
postgresql-postgresql  0/1    3s


NOTES:
** Please be patient while the chart is being deployed **

PostgreSQL can be accessed via port 5432 on the following DNS name from within your cluster:

    postgresql.vue-practice.svc.cluster.local - Read/Write connection
To get the password for "vue_practice" run:

    export POSTGRES_PASSWORD=$(kubectl get secret --namespace vue-practice postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)

To connect to your database run the following command:

    kubectl run postgresql-client --rm --tty -i --restart='Never' --namespace vue-practice --image docker.io/bitnami/postgresql:11.5.0-r44 --env="PGPASSWORD=$POSTGRES_PASSWORD" --command -- psql --host postgresql -U postgres -p 5432



To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace vue-practice svc/postgresql 5432:5432 &
    PGPASSWORD="$POSTGRES_PASSWORD" psql --host 127.0.0.1 -U postgres -p 5432
```

アンインストールする場合や、設定を変更する場合は `helm delete` や `helm upgrade` を使います。


## DBを初期化する

アプリケーションが DB を使えるよう初期化を行います。

いきなり完成形のマニフェストを作成しようとすると大変なので、先に作成したコンテナを使って一時的な作業用の deployment をデプロイして DB を初期化することにします。

```bash
$ kubectl run test -it --rm --image=ryu310/vue_practice_app:stable -- bash
```

deployment がデプロイされたら、DB に接続するための環境変数を export した後にデータベースの作成とマイグレーションを行います。

```bash
circleci@test-788747645d-nsz9p:~/app$ export VUE_PRACTICE_DATABASE_HOST=postgresql
circleci@test-788747645d-nsz9p:~/app$ export VUE_PRACTICE_DATABASE_PORT=5432
circleci@test-788747645d-nsz9p:~/app$ export VUE_PRACTICE_DATABASE_DBNAME=vue_practice_production
circleci@test-788747645d-nsz9p:~/app$ export VUE_PRACTICE_DATABASE_USERNAME=vue_practice
circleci@test-788747645d-nsz9p:~/app$ export VUE_PRACTICE_DATABASE_PASSWORD=XXXXX
circleci@test-788747645d-nsz9p:~/app$ 
circleci@test-788747645d-nsz9p:~/app$ SECRET_KEY_BASE=$(bin/rails secret) bin/rails db:create db:migrate
Created database 'vue_practice_production'
== 20190205185733 CreateEmployees: migrating ==================================
-- create_table(:employees)
   -> 0.0580s
== 20190205185733 CreateEmployees: migrated (0.0587s) =========================

== 20190205192323 CreateActiveAdminComments: migrating ========================
-- create_table(:active_admin_comments)
   -> 0.0758s
-- add_index(:active_admin_comments, [:namespace])
   -> 0.0182s
== 20190205192323 CreateActiveAdminComments: migrated (0.0957s) ===============
```

## k8s マニフェストファイルを作成する

次は k8s のマニフェストファイルを作成します。

### deployment の基本動作を作成

パスワードは Kind: secret として作成し、アプリケーションは Kind: deployment として作成します。

```yaml:.kube/production/secret/vue-practice.yml
# Correct password XXXXXX below.
# 
# install command:
#   kubectl apply -f vue-practice.yml
apiVersion: v1
data:
  VUE_PRACTICE_DATABASE_HOST: cG9zdGdyZXNxbA==
  VUE_PRACTICE_DATABASE_PORT: NTQzMg==
  VUE_PRACTICE_DATABASE_DBNAME: dnVlX3ByYWN0aWNlX3Byb2R1Y3Rpb24=
  VUE_PRACTICE_DATABASE_PASSWORD: XXXXXX
  VUE_PRACTICE_DATABASE_USERNAME: dnVlX3ByYWN0aWNl
kind: Secret
metadata:
  creationTimestamp: null
  name: vue-practice
  selfLink: /api/v1/namespaces/vue-practice/secrets/vue-practice
type: Opaque
```

```yaml:.kube/production/vue-practice/deployment.yml
# install command:
#   kubectl apply -f deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vue-practice
  namespace: vue-practice
spec:
  replicas: 2
  selector:
    matchLabels:
      app: vue_practice
  template:
    metadata:
      labels:
        app: vue-practice
    spec:
      containers:
        - name: app
          image: ryu310/vue_practice_app:stable
          envFrom:
            - secretRef:
                name: vue-practice
```

マニフェストファイルが作成できたらデプロイします。

```bash
$ kubectl apply -f secret/vue-practice.yml
$ kubectl apply -f vue-practice/deployment.yml
```

デプロイが終わったら TCP 3000 番をポートフォワードして、ブラウザで http://localhost:3000 にアクセスしてアプリケーションが DB に接続できていることを確認しましょう。

```bash
$ kubectl port-forward deployment/vue-practice 3000:3000
```


### deployment の設定を本番環境を意識して確認・追加する

次に本番環境を意識して設定を確認・追加してみます。

* deployment の Strategy は Rolling update にする
    * デフォルトで Rolling update になっています
    * 余分なリソースが必要となりますが、ダウンタイムが出ないため本番環境では Rolling update が望ましいでしょう (アプリケーションの品質によりますが)
* deployment から Secret を参照する方法は環境変数(envFrom)でもボリュームマウント(volume)でもどちらでもよい
    * アプリケーションが起動した時にしか参照しない値であり、変更したことを反映させるためにはアプリケーションを再起動しないといけないため、どちらも方法でも違いはないと思います
* ReadinessProbe を設定する
    * Pod にアクセスを振り分けられるかどうかを判断する方法を指定します
    * この結果が Pass であるとサービスからアクセスが割り振られます
    * この結果が Fail であるとサービスからアクセスが割り振られません
* LivenessProbe を設定する
    * アプリケーションが正常に "起動している" を判断する方法を指定します
    * この結果が Pass であると Pod が Ready になります
    * この結果が Fail であると Pod が再起動されます

DB を利用する Web アプリケーションの場合、ReadinessProbe はアプリケーションが DB へ接続できていることを確認できると望ましいでしょう。

ここではステータスを確認するための API を作成して DB モデルを操作できることを確認した結果を返すようにします。

```config/routes.rb
Rails.application.routes.draw do
    : <snip>
  namespace :api, {format: 'json'} do
    namespace :v1 do
        : <snip>
      get '/liveness', to: 'statuses#liveness'
      get '/readiness', to: 'statuses#readiness'
    end
  end
end
```

```app/controllers/api/v1/statuses_controller.rb
class Api::V1::StatusesController < ApiController
  def liveness
    render_status_200
  end

  def readiness
    begin
      Employee.all.size
      render_status_200
    rescue => e
      render_status_500(e)
    end
  end

  private

  def render_status_200
    render json: { api: 'success' }, status: 200
  end

  def render_status_500(exception)
    logger.warn "render 500 server error with exception: #{exception}" if exception.present?
    render json: { api: 'fail' }, status: 500
  end
end
```

ReadinessProbe, LivenessProbe をマニフェストに追加して更新します。

```.kube/production/vue-practice/deployment.yml
# install command:
#   kubectl apply -f deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vue-practice
  namespace: vue-practice
spec:
  replicas: 2
  selector:
    matchLabels:
      app: vue-practice
  template:
    metadata:
      labels:
        app: vue-practice
    spec:
      containers:
        - name: app
          image: ryu310/vue_practice_app:stable
          envFrom:
            - secretRef:
                name: vue-practice
          livenessProbe:
            httpGet:
              port: 3000
              path: /api/v1/liveness
              scheme: HTTP
            initialDelaySeconds: 600
          readinessProbe:
            httpGet:
              port: 3000
              path: /api/v1/readiness
              scheme: HTTP
```

コンテナ起動時にコンパイルをしているため initialDelaySeconds が長めです。

### 外部からの接続するための Ingress, Service を作成する

次に Ingress, Service を作成して外部から接続できるようにします。

```.kube/production/vue-practice/service.yml
# install command:
#   kubectl apply -f vue-practice.yml
apiVersion: v1
kind: Service
metadata:
  name: vue-practice
  namespace: vue-practice
spec:
  type: NodePort
  selector:
    app: vue-practice
  ports:
    - name: vue-practice
      protocol: "TCP"
      port: 80
      targetPort: 3000
```

```.kube/production/ingress/vue-practice.yml
# install command:
#   kubectl apply -f vue-practice.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: vue-practice
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: vue-practice
              servicePort: 3000
```

デプロイが終わったら Ingress に割り当てられた外部IPアドレスを確認して、ブラウザで IP アドレスの URL (http://＜IPアドレス＞/) を開いて、アプリケーションの動作を確認しましょう。

```bash
$ kubectl get ingress
NAME           HOSTS   ADDRESS         PORTS   AGE
vue-practice   *       ＜IPアドレス＞   80      5m57s
```

### Let's Encrypt(LE) 証明書を発行する

最後に証明書を発行します。
ここでは Let's Encrypt(LE) 証明書を発行することにします。

尚、LE証明書の概要については[Let's Encrypt で SSL/TLS 証明書をインストールして自己証明書から脱却](https://qiita.com/tatsurou313/items/cd3cd8fc96eabf3f6eb4)、k8s と Route53 を使って LE 証明書を自動インストールする方法は[Kubernetes + Let’s Encrypt でワイルドカード証明書を自動発行できる基盤を作ってみよう](https://tech.weseek.co.jp/entry/2019/09/12/164455)を参考にしてみて下さい。

テストとしてドメイン名は `vue-practice.work` を購入し、DNS は AWS の Route53 を使いました。

cert-manager をデプロイする方法はマニフェストファイルを使う方法と、Helm を使う方法があります。
ここでは Helm を使うことにします。

インストールコマンドは[公式](https://cert-manager.readthedocs.io/en/latest/getting-started/install/kubernetes.html#steps)を参考にしました。

Rancher を使っている場合は namespace を作成する時に、プロジェクトを指定するのを忘れないようにしましょう。
Rancher Dashboard から namespace を作成するのが簡単でよいと思います。

```bash
# Install the CustomResourceDefinition resources separately
kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml

# Create the namespace for cert-manager
# ※ Rancherを使っている場合は namespace が所属する Project を指定するようにしましょう
kubectl create namespace cert-manager

# Label the cert-manager namespace to disable resource validation
kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true

# Add the Jetstack Helm repository
helm repo add jetstack https://charts.jetstack.io

# Update your local Helm chart repository cache
helm repo update

# Install the cert-manager Helm chart
helm install \
  --name cert-manager \
  --namespace cert-manager \
  --version v0.10.0 \
  jetstack/cert-manager
```

Helm インストールコマンドが成功したら次のようなメッセージが表示されます。

```
NAME:   cert-manager
LAST DEPLOYED: Mon Sep 23 09:40:12 2019
NAMESPACE: cert-manager
STATUS: DEPLOYED

RESOURCES:
==> v1/ClusterRole
NAME                                    AGE
cert-manager-edit                       7s
cert-manager-view                       7s
cert-manager-webhook:webhook-requester  7s

==> v1/Deployment
NAME                     READY  UP-TO-DATE  AVAILABLE  AGE
cert-manager             0/1    1           0          7s
cert-manager-cainjector  0/1    1           0          7s
cert-manager-webhook     0/1    1           0          7s

==> v1/Pod(related)
NAME                                      READY  STATUS             RESTARTS  AGE
cert-manager-7c49b7766d-xnh52             0/1    ContainerCreating  0         6s
cert-manager-cainjector-57988f84f7-xndhm  0/1    ContainerCreating  0         6s
cert-manager-webhook-54b5f85648-p7d8l     0/1    ContainerCreating  0         6s

==> v1/Service
NAME                  TYPE       CLUSTER-IP     EXTERNAL-IP  PORT(S)   AGE
cert-manager          ClusterIP  10.43.222.251  <none>       9402/TCP  7s
cert-manager-webhook  ClusterIP  10.43.234.154  <none>       443/TCP   7s

==> v1/ServiceAccount
NAME                     SECRETS  AGE
cert-manager             1        8s
cert-manager-cainjector  1        8s
cert-manager-webhook     1        8s

==> v1beta1/APIService
NAME                                AGE
v1beta1.webhook.certmanager.k8s.io  7s

==> v1beta1/ClusterRole
NAME                                    AGE
cert-manager-cainjector                 8s
cert-manager-controller-certificates    7s
cert-manager-controller-challenges      7s
cert-manager-controller-clusterissuers  7s
cert-manager-controller-ingress-shim    7s
cert-manager-controller-issuers         7s
cert-manager-controller-orders          7s
cert-manager-leaderelection             7s

==> v1beta1/ClusterRoleBinding
NAME                                    AGE
cert-manager-cainjector                 7s
cert-manager-controller-certificates    7s
cert-manager-controller-challenges      7s
cert-manager-controller-clusterissuers  7s
cert-manager-controller-ingress-shim    7s
cert-manager-controller-issuers         7s
cert-manager-controller-orders          7s
cert-manager-leaderelection             7s
cert-manager-webhook:auth-delegator     7s

==> v1beta1/MutatingWebhookConfiguration
NAME                  AGE
cert-manager-webhook  6s

==> v1beta1/RoleBinding
NAME                                                AGE
cert-manager-webhook:webhook-authentication-reader  7s

==> v1beta1/ValidatingWebhookConfiguration
NAME                  AGE
cert-manager-webhook  6s


NOTES:
cert-manager has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://docs.cert-manager.io/en/latest/reference/issuers.html

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://docs.cert-manager.io/en/latest/reference/ingress-shim.html
```

事前に Route53 に DNS のレコード設定と IAM ユーザが設定できているとして、cert-manager に ClusterIssuer と Certificate を作成していきます。

```.kube/production/helm/cert-manager/secret.yml
# Correct password XXXXXX in base64 below.
#
# install command:
#   kubectl apply -f secret.yml
apiVersion: v1
data:
  secret-access-key: XXXX
kind: Secret
metadata:
  creationTimestamp: null
  name: route53
  namespace: cert-manager
  selfLink: /api/v1/namespaces/cert-manager/secrets/route53
type: Opaque
```

```.kube/production/helm/cert-manager/cluster-issuer.yml
# Correct email and accessKeyID: XXXXX
#
# install command:
#   kubectl apply -f cluster-issuer.yml
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt
  namespace: cert-manager
spec:
  acme:
    email: XXXXX
    # ステージングで成功したら本番の設定にしましょう
    # server: https://acme-v02.api.letsencrypt.org/directory
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-private-key
    solvers:
      - selector:
          dnsZones:
            - "vue-practice.work"
        dns01:
          route53:
            accessKeyID: XXXXX
            region: us-east-1
            secretAccessKeySecretRef:
              key: secret-access-key
              name: route53
```

```.kube/production/cert/wildcard-vue-practice-work.yml
# install command:
#   kubectl apply -f wildcard-vue-practice-work.yml
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: wildcard-vue-practice-work
  namespace: vue-practice
spec:
  secretName: cert-wildcard-vue-practice-work
  dnsNames:
    - '*.vue-practice.work'
  issuerRef:
    kind: ClusterIssuer
    name: letsencrypt
```

証明書が発行されたら Ingress Resource に証明書を使うようにマニフェストを変更します。

```.kube/production/ingress/vue-practice.yml
# install command:
#   kubectl apply -f vue-practice.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: vue-practice
spec:
  rules:
    - host: www.vue-practice.work
      http:
        paths:
          - backend:
              serviceName: vue-practice
              servicePort: 3000
  tls:
    - hosts:
        - www.vue-practice.work
      secretName: cert-wildcard-vue-practice-work
```

以上で、本番環境が k8s 上で作成できました。

# さいごに

これで Rails アプリケーションを本番環境で動作させるためのインフラ作りは終わりです。

次はステージング環境を構築し、本番環境とステージング環境へアプリケーションをデプロイできる CD 環境を構築していくことにします。

また、開発に必須である CI 環境の構築を進めていくことにします。

[Ruby on Rails, Vue.js によるモダン WEB アプリケーション 実践編 (その2)](https://qiita.com/tatsurou313/items/91afe2502acde9370db1)