Help us understand the problem. What is going on with this article?

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

はじめに

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

Rails と Vue を使ったアプリケーションを初めて開発する場合は Ruby on Rails, Vue.js で始めるモダン WEB アプリケーション入門 も参考にしてみて下さい。

アプリケーションの機能

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

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

  • 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 環境の構築方法は以下の記事も参考にしてみて下さい

※ 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" します。

image.png

ネームスペース名は 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コマンドを初めて使用する場合
# Helm環境を初期化する(Tillerをデプロイし、コマンドを実行したホストにHelm Chartリポジトリを保存する)
$ helm init

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

PostgreSQL をデプロイする

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

.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 にデプロイします。

$ 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 deletehelm 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 として作成します。

.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
.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

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

$ 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 モデルを操作できることを確認した結果を返すようにします。

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アドレス>/) を開いて、アプリケーションの動作を確認しましょう。

$ 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 を作成していきます。

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: 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
kube/production/cert/wildcard-vue-practice-work.yml
# 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 に証明書を使うようにマニフェストを変更します。

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)

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away