kamalでRailsアプリケーションをEC2へデプロイする
kamalは, Ruby on Railsの作者として知られる,
David Heinemeier Hasson (DHH)氏によってリリースされた
WEB アプリケーションのデプロイ自動化ツールです.
めんどくさいデプロイ作業を簡単にやってくれそうなので
既存プロジェクトのRails7.1アップデートのついでに触ってみました.
復習兼,備忘録として投稿しています.
完成図
下記構成図の環境を作成し,
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
すると,最初から Dockerfile
や bin/docker-entrypoint
, .dockerignore
が生成されています.嬉しいですね.
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:
- Connect to the servers over SSH (using root by default, authenticated by your ssh key)
- Install Docker and curl on any server that might be missing it (using apt-get): root access is needed via ssh for this.
- Log into the registry both locally and remotely
- Build the image using the standard Dockerfile in the root of the application.
- Push the image to the registry.
- Pull the image from the registry onto the servers.
- Push the ENV variables from .env onto the servers.
- Ensure Traefik is running and accepting traffic on port 80.
- Ensure your app responds with 200 OK to GET /up (you must have curl installed inside your app image!).
- Start a new container with the version of the app that matches the current git version hash.
- Stop the old container running the previous version of the app.
- 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が出力するエラーではどうにも判断できない場合は,
- EC2にssh
- 実際にイメージをもとに docker runを実行
- エラーを確認
といった手順でエラー対処を行いました
EC2デプロイ完了後のエラー
kamal setup
は成功するものの, ロードバランサーでのヘルスチェックがunhealthy
になる点で結構苦戦しました.
具体的にはtraefikとpumaの疎通です.
EC2上でcurl
で試しましたがどうもポート3000にリクエストが届いてなさそうなのです.
結論, 私の環境ではこの設定で解決しました.
production.rbの編集
Rails7で
rails new
した場合,config.assume_ssl
が新しく追加されています.
デフォルトではコメントアウトされていますが, ここをtrueにします.
理由は後ほど説明します.
config.assume_ssl
をtrueにしないとtraefikとpumaで疎通ができない原因については未だ仮説段階ですが
traefikがSSL終端処理を行っており,
HTTPSでのリクエストをHTTPリクエストとしてpumaに送信するが
config.force_ssl
の設定によりHTTPSとしてリダイレクトされる
これによって無限リダイレクトループが発生してしまうというものです.
config.assume_ssl = true
を設定することで、Railsはリバースプロキシが既にSSLを終端していると仮定することができるので疎通が可能になったのでは???
終わりに
個人的に一番めんどくさい作業はデプロイだと思っているのですが,
サーバーのIPアドレスをdeploy.rbに記述するだけでほぼデプロイが終了するのはありがたいですね.GitHub Actionsでの自動デプロイなども簡単そう.
AWSでやるならECRを使うのがよさそうです.
Discussion