はじめに
アプリケーションは Rails と Vue.js を想定して、実戦的なアプリケーション動作環境を構築するための方法を段階的に紹介していきます。
Rails と Vue を使ったアプリケーションを初めて開発する場合は Ruby on Rails, Vue.js で始めるモダン WEB アプリケーション入門 も参考にしてみて下さい。
アプリケーションの機能
- (その1) アプリケーションは冗長構成の Postgres に接続できる
- (その1) アプリケーション、DB は k8s にデプロイされる
- (その1) LE証明書を使う(定期更新できる)
- (その2) CI/CD 環境がある
- (その3) OAuth によりログイン認証できる
ブランチの運用方法と開発フローを決める
ブランチの運用方法と開発フローは以下の流れとします。
- 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 を使って勉強用クラスタを作る
- 快適な kubernetes オンプレミス環境を構築する(1. 設計編) ※シリーズ
- Rancher を利用した社内 Kubernetes 構築
※ k8s 環境へ Rails アプリケーションをデプロイする方法は kubernetesクラスタでRailsアプリを公開するチュートリアル を参照してみて下さい。
作業環境を構築する
- docker, kubectl, helm, stern をインストールする
本番環境を初期構築する
まずは本番環境を初期構築しましょう。
- stable ブランチを使ってコンテナをビルドする
- コンテナを k8s 環境にデプロイする
コンテナイメージのビルド用 Dockerfile を作成する
コンテナイメージをビルドするための Dockerfile を作成します。
少し工夫したのは、bundle install や yarn install を実行する時間を省略できるよう依存関係のあるパッケージ情報が同じであればキャッシュされたコンテナイメージを使えるようにマルチステージビルドを使いました。
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 は次のコマンドで調べることが出来ます。
$ git checkout stable
$ git log | head -1
commit <GitコミットID>
コンテナイメージをビルドするコマンドは次のとおりです。
$ 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" します。
ネームスペース名は vue-practice
にします。(例なので適宜変えて下さい)
作成が終わったらマニフェストファイルをエクスポートしておきましょう。
$ kubectl get namespaces vue-practice -o yaml --export > .kube/production/namespaces.yml
k8s に DB をデプロイする
本番DB は PostgreSQL を使います。
Helm を使って PostgreSQL を k8s にデプロイしましょう。
Helm 用の Tiller をデプロイする
helm コマンドは k8s インフラ上にデプロイされた Tiller を介して k8s のマニフェストを管理します。
helm アプリケーションはチャート(chart)単位で管理され、helm コマンドを実行するホストに chart のリポジトリを保存・更新して利用します。
まずは helm init コマンドを実行して Tiller をデプロイし、Helm Chart のリポジトリを保存します。
(既に Tiller がデプロイされている場合は、--client-only
オプションを付けます)
# Helm環境を初期化する(Tillerをデプロイし、コマンドを実行したホストにHelm Chartリポジトリを保存する)
$ helm init
# Helm環境を初期化する(Tillerはデプロイせず、、コマンドを実行したホストにHelm Chartリポジトリを保存する)
$ helm init --client-only
PostgreSQL をデプロイする
Helm アプリケーションをインストールする際、Chart の README に従って必要に応じて設定を行います。
stable/postgresql チャートの場合はここで確認できます。
# 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 にデプロイします。
$ helm install stable/postgresql --name postgresql --namespace vue-practice -f values.yml --set postgresqlPassword=XXXXXXXX
デプロイが終わると次のように結果メッセージが表示されます。
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 を初期化することにします。
$ kubectl run test -it --rm --image=ryu310/vue_practice_app:stable -- bash
deployment がデプロイされたら、DB に接続するための環境変数を export した後にデータベースの作成とマイグレーションを行います。
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 として作成します。
# 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
# 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
マニフェストファイルが作成できたらデプロイします。
$ kubectl apply -f secret/vue-practice.yml
$ kubectl apply -f vue-practice/deployment.yml
デプロイが終わったら TCP 3000 番をポートフォワードして、ブラウザで http://localhost:3000 にアクセスしてアプリケーションが DB に接続できていることを確認しましょう。
$ 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 モデルを操作できることを確認した結果を返すようにします。
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
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 をマニフェストに追加して更新します。
# 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 を作成して外部から接続できるようにします。
# 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
# 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アドレス>/) を開いて、アプリケーションの動作を確認しましょう。
$ 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 証明書をインストールして自己証明書から脱却、k8s と Route53 を使って LE 証明書を自動インストールする方法はKubernetes + Let’s Encrypt でワイルドカード証明書を自動発行できる基盤を作ってみようを参考にしてみて下さい。
テストとしてドメイン名は vue-practice.work
を購入し、DNS は AWS の Route53 を使いました。
cert-manager をデプロイする方法はマニフェストファイルを使う方法と、Helm を使う方法があります。
ここでは Helm を使うことにします。
インストールコマンドは公式を参考にしました。
Rancher を使っている場合は namespace を作成する時に、プロジェクトを指定するのを忘れないようにしましょう。
Rancher Dashboard から namespace を作成するのが簡単でよいと思います。
# 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 を作成していきます。
# 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
# Correct email and accessKeyID: XXXXX
#
# install command:
# kubectl apply -f cluster-issuer.yml
apiVersion: certmanager.k8s.io/v1alpha1
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
dns01:
providers:
- name: route53
route53:
accessKeyID: XXXXX
region: us-east-1
secretAccessKeySecretRef:
key: secret-access-key
name: route53
# install command:
# kubectl apply -f wildcard-vue-practice-work.yml
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: wildcard-vue-practice-work
namespace: vue-practice
spec:
secretName: cert-wildcard-vue-practice-work
acme:
config:
- dns01:
provider: route53
domains:
- '*.vue-practice.work'
commonName: '*.vue-practice.work'
issuerRef:
kind: ClusterIssuer
name: letsencrypt
証明書が発行されたら Ingress Resource に証明書を使うようにマニフェストを変更します。
# 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 環境の構築を進めていくことにします。