🙆

kamalでRailsアプリケーションをEC2へデプロイする

2023/10/15に公開

kamalは, Ruby on Railsの作者として知られる,
David Heinemeier Hasson (DHH)氏によってリリースされた
WEB アプリケーションのデプロイ自動化ツールです.

めんどくさいデプロイ作業を簡単にやってくれそうなので
既存プロジェクトのRails7.1アップデートのついでに触ってみました.
復習兼,備忘録として投稿しています.
https://kamal-deploy.org/

完成図

下記構成図の環境を作成し,
EC2へRailsアプリケーションをデプロイします.

AWS環境の構築

今回はubuntuのイメージを利用してEC2を作成し,PostgresqlのRDSをDBとして利用します.
ここらへんはサラッと流します

  • VPC作成
  • EC2作成
    作成後ElasticIPでIPアドレスを固定にします.
    IPを固定にする理由は, kamalはIPアドレスを指定してデプロイを行うため,
    IPアドレスが変更されると確認するのが手間だからです.
  • RDS作成
    セキュリティグループにEC2のセキュリティグループを追加
  • ALB作成
    ALBのセキュリティグループをEC2のセキュリティグループに追加
  • Route53作成

Railsアプリケーションの作成

$ rails -v
Rails 7.1.0
rails new my_app --api

今回はRails7.1のapiモードで下図テーブル構成のアプリケーションを作成しました.
Rails7.1で rails new すると,最初から Dockerfilebin/docker-entrypoint , .dockerignore が生成されています.嬉しいですね.

https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/Dockerfile.tt

kamalでのデプロイ

kamalの導入

Gemfile にkamalを記述し,installします.

# Gemfile
gem 'kamal', '~> 1.0.0'
$ bundle install

kamal init コマンドを実行することで config/deploy.ymlが生成されます.
config/deploy.ymlにデプロイ設定を記述していきます.

$ kamal init
Created configuration file in config/deploy.yml
Created .env file
Created sample hooks in .kamal/hooks

config/deploy.ymlを編集することでデプロイが可能になります.

EC2インスタンスへのSSH接続準備

kamalがssh接続できるようにssh agentにEC2のpemを追加します

ssh-add /path/to/your-key.pem

EC2インスタンスにdockerとcurlのinstall

sudo apt update
sudo apt upgrade -y
sudo apt install -y docker.io curl git

EC2では作成時ubuntuというユーザーが作成されます
ubuntuユーザーがsudoを使わずにDockerコマンドを実行できるように下記コマンドを実行します

sudo usermod -a -G docker ubuntu

.envファイルの編集

kamalはdotenvを使用して,アプリケーションルートに存在する.envファイルに設定された環境変数を自動的に読み込みます.
.envファイルに以下を記述します

  • KAMAL_REGISTRY_PASSWORD
    docker imageをpushするレジストリのパスワード
    docker hubの場合tokenの利用が推奨されている
  • RAILS_MASTER_KEY
    master.keyの内容
  • DB_URL
    RDSへの接続情報
KAMAL_REGISTRY_PASSWORD=change-this
RAILS_MASTER_KEY=master.keyの値を入力
DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"

production.rbの編集

Rails7で rails newした場合, config.assume_sslが新しく追加されています.
デフォルトではコメントアウトされていますが, ここをtrueにします.
理由は後ほど説明します.

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
  config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

database.ymlの編集

今回はproduction環境でデプロイするので,
production環境で先程つくったRDSに接続させるようにします.

production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>

deploy.ymlの編集

ドキュメントとdeploy.ymlのコメントをみれば分かるようになっています

  • service
    任意の名前を入力します
  • image
    コンテナのイメージ名を入力します
  • servers
    EC2のパブリックIPアドレスを入力します
# Name of your application. Used to uniquely configure containers.
service: kamal-practice

# Name of the container image.
image: user/kamal-practice

# Deploy to these servers.
servers:
  - EC2IP
  • registry
    今回はdocker hubを使うのでdockerのuser名を入力します
  • env
    必要な環境変数をここで入力します
  • ssh
    ubuntuのイメージを利用してEC2を作成した場合,ubuntuを入力します
# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  username: user

  # Always use an access token rather than real password when possible.
  password:
    - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .env).
# Remember to run `kamal env push` after making changes!
env:
  secret:
    - RAILS_MASTER_KEY
    - DB_URL

# Use a different ssh user than root
ssh:
  user: ubuntu
生成されたdeploy.yml
# Name of your application. Used to uniquely configure containers.
service: my-app

# Name of the container image.
image: user/my-app

# Deploy to these servers.
servers:
  - 192.168.0.1

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  username: my-user

  # Always use an access token rather than real password when possible.
  password:
    - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .env).
# Remember to run `kamal env push` after making changes!
# env:
#   clear:
#     DB_HOST: 192.168.0.2
#   secret:
#     - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Configure builder setup.
# builder:
#   args:
#     RUBY_VERSION: 3.2.0
#   secrets:
#     - GITHUB_TOKEN
#   remote:
#     arch: amd64
#     host: ssh://app@192.168.0.1

# Use accessory services (secrets come from .env).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     port: 3306
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: redis:7.0
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

# Configure custom arguments for Traefik
# traefik:
#   args:
#     accesslog: true
#     accesslog.format: json

# Configure a custom healthcheck (default is /up on port 3000)
# healthcheck:
#   path: /healthz
#   port: 4000

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
# asset_path: /rails/public/assets

# Configure rolling deploys by setting a wait time between batches of restarts.
# boot:
#   limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
#   wait: 2

kamal setup

下記のコマンドを入力することでEC2へのデプロイが完了します

kamal setup

このコマンドがなにをしてくれているのかはドキュメントに記載されています

This will:

  1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
  2. Install Docker and curl on any server that might be missing it (using apt-get): root access is needed via ssh for this.
  3. Log into the registry both locally and remotely
  4. Build the image using the standard Dockerfile in the root of the application.
  5. Push the image to the registry.
  6. Pull the image from the registry onto the servers.
  7. Push the ENV variables from .env onto the servers.
  8. Ensure Traefik is running and accepting traffic on port 80.
  9. Ensure your app responds with 200 OK to GET /up (you must have curl installed inside your app image!).
  10. Start a new container with the version of the app that matches the current git version hash.
  11. Stop the old container running the previous version of the app.
  12. Prune unused images and stopped containers to ensure servers don’t fill up.

ロードバランサーでの振り分け

やることリストとしては下記です

  • EC2のセキュリティグループにロードバランサーのセキュリティグループを追加(忘れがち)
  • HTTPとHTTPSのリスナーを作成
  • HTTPをHTTPSにリダイレクトさせるリスナールールの作成
  • EC2をターゲットグループに割り当てる
  • SSL証明書関連の設定

Route53の設定

Route53でロードバランサーを指定したAレコードを作成します
これで設定したドメインからEC2へしっかりアクセスがいくはずです

詰まったところ

詰まった点は大別すると2点でした

  • kamal setup 中のエラー (小ハマり)
  • EC2デプロイ完了後のエラー (大ハマリ)

kamal setup 中のエラー

いろいろなエラーに遭遇しましたが,しっかりエラー文がでてくれるので
適宜Documentなどを読みつつ修正し, 大詰まりはしませんでした
kamalが出力するエラーではどうにも判断できない場合は,

  1. EC2にssh
  2. 実際にイメージをもとに docker runを実行
  3. エラーを確認
    といった手順でエラー対処を行いました

EC2デプロイ完了後のエラー

kamal setup は成功するものの, ロードバランサーでのヘルスチェックがunhealthyになる点で結構苦戦しました.
具体的にはtraefikpumaの疎通です.
EC2上でcurlで試しましたがどうもポート3000にリクエストが届いてなさそうなのです.

結論, 私の環境ではこの設定で解決しました.

production.rbの編集

Rails7で rails newした場合, config.assume_sslが新しく追加されています.
デフォルトではコメントアウトされていますが, ここをtrueにします.
理由は後ほど説明します.

config.assume_sslをtrueにしないとtraefikpumaで疎通ができない原因については未だ仮説段階ですが

traefikがSSL終端処理を行っており,
HTTPSでのリクエストをHTTPリクエストとしてpumaに送信するが
config.force_sslの設定によりHTTPSとしてリダイレクトされる
これによって無限リダイレクトループが発生してしまうというものです.

config.assume_ssl = trueを設定することで、Railsはリバースプロキシが既にSSLを終端していると仮定することができるので疎通が可能になったのでは???

終わりに

個人的に一番めんどくさい作業はデプロイだと思っているのですが,
サーバーのIPアドレスをdeploy.rbに記述するだけでほぼデプロイが終了するのはありがたいですね.GitHub Actionsでの自動デプロイなども簡単そう.
AWSでやるならECRを使うのがよさそうです.

GitHubで編集を提案

Discussion