ALB -> ECS(Fargate) -> RDS, EFS, SES という構成でRailsアプリを動かしてみます。
※間にNginxや、手前にWAFも入れた方が、ベストですが、それはまた後日。
CloudFormationかTerraformを使った方が、コードで管理できるので良いのですが、今回は理解を深める事が目的なので、また後日。
EFS(Elastic File System) <- ユーザーがアップロードするファイルを永続化するのに使用
※アプリからS3にアップロードして、CDN(CloudFront)から参照する方が、コスト的には有利ですが、今回は改修不要なのでEFSを使います。
SES(Simple Email Service) <- アプリからメールを送るのに使用
RailsアプリのDockerイメージを作成して、ECRにpushする
- Relational Database Service(RSD)
- Simple Email Service(SES)
- Elastic File System(EFS)
- Elastic Container Service(ECS)
- Application Load Balancer(ALB)
- 運用メモ
Relational Database Service(RSD)
データベースの作成
今回は、無料利用枠(安いインスタンス)を使いたいので、MySQLを選択します。
Secrets Managerを使いたい所ですが、手間が大きいので、今回は自分で管理します。
TODO: AWS Secrets Managerでマスター認証情報を管理する
データベースの作成 データベース作成方法を選択 ●標準作成 ※デフォルト エンジンのオプション エンジンのタイプ: ●MySQL ※Auroraは無料利用枠(安いインスタンス)に未対応 利用可能なバージョン: 最新に変更 ※MySQL 8.0.33 -> MySQL 8.0.35 テンプレート ●本番稼働用 -> 無料利用枠 設定 DBインスタンス識別子: database-1 -> railsapp(対象に合わせて変更) マスターユーザー名: admin ■パスワードの自動生成 or マスターパスワード: ********(非表示) マスターパスワードの確認: 〃 インスタンスの設定 (無料利用枠) db.t3.micro = 18.98 USD/月 ●db.t4g.micro = 18.25 USD/月 <- テストなので (開発/テスト、本番稼働用) db.t3.medium = 91.25 USD/月 db.t4g.medium = 82.49 USD/月 ストレージ ※Auroraは除く 汎用SSD(gp2) + 20GiB = 2.76 USD/月 汎用SSD(gp3) + 20GiB = 2.76 USD/月 ●マグネティック(非推奨) + 5GiB = 0.60 USD/月 <- テストなので 追加設定 最初のデータベース名: railsapp_production(対象に合わせて変更) バックアップ: □自動バックアップを有効にします <- テストなので ログのエクスポート ■監査ログ ■エラーログ ■全般ログ ■スロークエリログ 削除保護 □削除保護の有効化 <- 本番は設定(デフォルト) [データベースの作成]
エンドポイントを確認
ステータスが利用可能になるのを待て、エンドポイントを確認。
Simple Email Service(SES)
IDの作成
メールアドレスとドメインの両方
https://ap-northeast-1.console.aws.amazon.com/ses/home?region=ap-northeast-1#/onboarding-wizard
メールアドレスとドメインのどちらか
https://ap-northeast-1.console.aws.amazon.com/ses/home?region=ap-northeast-1#/verified-identities/create
メールアドレスを追加 メールアドレス: (メアド) ※Fromのメアドと一致しないと送信できない [次へ] 送信ドメインを追加 送信ドメイン: (ドメイン) ※Fromのドメインと一致しないと送信できない [次へ] SESを確認して、使用を開始する [Get started]
認証
メールアドレスの場合
入力したメアドに下記タイトルのメールが届くのでURLをクリックすればOK。
Amazon Web Services – Email Address Verification Request in region Asia Pacific (Tokyo)
送信ドメインの場合
DNSレコードを追加する必要があります。
検証済みになるまで暫く待つ。(30分毎にチェックしているようでした)
検証済みになるとアカウントのメアドに、下記タイトルのメールが届きます。
DKIM setup SUCCESS for nightonly.com in Asia Pacific (Tokyo) region
SPF設定
上記でDNSを設定した場合はDKIMが効いていますが、迷惑メールにならないようにSPFも設定しておきます。
カスタムの MAIL FROM ドメインを使用する – Amazon Simple Email Service
"v=spf1 include:amazonses.com ~all"
SMTP認証情報作成
https://ap-northeast-1.console.aws.amazon.com/ses/home?region=ap-northeast-1#/smtp
Elastic File System(EFS)
コンテナは落ちたら保存したファイルは消えてしまうし、他のコンテナでは参照できないので、永続化したい場合に使用します。
コスト的には、S3+CDN(CloudFront)の方が安価ですが、お手軽なので。
ファイルシステムの作成
https://ap-northeast-1.console.aws.amazon.com/efs/home?region=ap-northeast-1#/file-systems
ファイルシステムの作成 名前: railsapp-uploads(対象に合わせて変更) [作成]
Elastic Container Service(ECS)
タスク定義の作成(ECS)
タスク定義でリソース(CPUやメモリ)、ログの出力先(CloudWatchのロググループ)、コマンド(Docker CMD)が決まるので、管理しやすいように下記3つを作成します。
・アプリケーションサーバー(Unicorn等) -> railsapp
・非同期処理(Delayed::JobやSidekick等) -> railsapp-job
・バッチ処理(Rakeタスク) -> railsapp-task
https://ap-northeast-1.console.aws.amazon.com/ecs/v2/create-task-definition?region=ap-northeast-1
新しいタスク定義の作成 タスク定義の設定 タスク定義ファミリー: railsapp / railsapp-job / railsapp-task(対象に合わせて変更) インフラストラクチャの要件 起動タイプ: ■AWS Fargate ※デフォルト CPU: 1vCPU -> .5vCPU <- テストなので メモリ: 3GB -> 1GB <- テストなので タスク実行ロール ecsTaskExecutionRole or 新しいロールを作成 ※デフォルト コンテナ - 1 名前: app イメージURI: xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rails-app-origin(対象に合わせて変更) ※ECRのURI -> https://ap-northeast-1.console.aws.amazon.com/ecr/repositories?region=ap-northeast-1 ポートマッピング コンテナポート: 80 ※デフォルト プロトコル: TCP ※デフォルト [削除] <- railsapp-job / railsapp-task は使わないので削除 環境変数 SECRET_KEY_BASE: (rails secretで作成) DATABASE_URL: mysql2://(RDSのユーザー名[admin]):(RDSのパスワード)@(RDSのエンドポイント)/(RDSのデータベース名[railsapp_production]) RAILS_SERVE_STATIC_FILES: 1 LOG_LEVEL: (空 = info) or debug DELIVERY_METHOD: smtp SMTP_SETTINGS: { address: '(SMTPエンドポイント[email-smtp.ap-northeast-1.amazonaws.com])', port: 587, user_name: '(SMTPユーザー名)', password: '(SMTPパスワード)', authentication: 'plain' } (独自に追加したものも追加。以降は、Unicornの設定なので、railsappのみ) WORKER_PROCESSES: (空 = 2) TIMEOUT: (空 = 60) LISTEN: 80 LISTEN_BACKLOG: (空 = 1024) ストレージ - オプション ボリューム - 1 ボリュームタイプ: バインドマウント -> EFS ボリューム名: railsapp-uploads(対象に合わせて変更) ファイルシステム ID: railsapp-uploads (fs-xxxx) コンテナマウントポイント コンテナ: app ソースボリューム: railsapp-uploads(上記のボリューム名) コンテナパス: /workdir/public/uploads(対象に合わせて変更) Docker 設定 - オプション コマンド (railsappは不要 = DockerfileのCMD) (railsapp-job)bin/delayed_job,run,-n,2 (railsapp-task)bundle,exec,rails,db:migrate,db:seed
HealthCheckは不要。Dockerのコマンドが終了したら落ちる為。
クラスターの作成
入れ物。この中に「サービス」(アプリケーションサーバー(Unicorn等)や非同期処理(Delayed::JobやSidekick等)の定義)や「スケジュールされたタスク」(バッチ処理(Rakeタスク)の定義)を作ると、必要な時に「タスク」(インスタンス)が作成されます。
https://ap-northeast-1.console.aws.amazon.com/ecs/v2/create-cluster?region=ap-northeast-1
クラスターの作成 クラスター設定 クラスター名: railsapp(対象に合わせて変更) インフラストラクチャ ■AWS Fargate (サーバーレス) ※デフォルト [作成]
CloudFormationで作られるので、不要になったら、ここから削除する。
スタック: Infra-ECS-Cluster-railsapp-xxxxxxxx
https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/
サービスの作成
アプリケーションサーバー(Unicorn等)と、非同期処理(Delayed::JobやSidekick等)を定義します。
ネットワーキングでパブリックIPをオフにすると、ECRからのイメージ取得に失敗します。イメージURIが間違っている場合も同様。
ヘルスチェックが通らなくても、デプロイに失敗します。
railsapp のデプロイ中にエラーが発生しました Resource handler returned message: "Error occurred during operation 'ECS Deployment Circuit Breaker was triggered'."
※対象のクラスターを選択 サービス [作成] サービスの作成 環境 (railsapp) コンピューティングオプション: ●キャパシティープロバイダー戦略 FARGATE ベース: 0 -> 1 ウェイト: 1 -> 0 FARGATE_SPOT ベース: 0 ウェイト: 1 ※今回はFARGATE 1台で、以降はSPOTを使うように設定してみました。 -> https://dev.classmethod.jp/articles/regrwoth-capacity-provider/ (railsapp-job) コンピューティングオプション: ●起動タイプ デプロイ設定 アプリケーションタイプ: ●サービス ※デフォルト ファミリー: railsapp / railsapp-job(対象に合わせて変更) サービス名: railsapp / railsapp-job(対象に合わせて変更) (railsapp) 必要なタスク: 1 -> 2(対象に合わせて変更) (railsapp-job) 必要なタスク: 1 ロードバランシング - オプション (railsappのみ) ロードバランサーの種類: Application Load Balancer ●新しいロードバランサーの作成 ロードバランサー名: railsapp(対象に合わせて変更) リスナー ポート: 80 -> 443 プロトコル: HTTP -> HTTPS 証明書: (選択 or 作成) ターゲットグループ ターゲットグループ名: railsapp(対象に合わせて変更) ヘルスチェックパス: / -> /_check(対象に合わせて変更) サービスのAuto Scaling - オプション (railsappのみ) ■サービスのオートスケーリングを使用 タスクの最小数: 1 -> 2(対象に合わせて変更) タスクの最大数: 1 -> 5(対象に合わせて変更) スケーリングポリシータイプ: ●ターゲットの追跡 ※デフォルト ポリシー名: railsapp-cpu(対象に合わせて変更) ECSサービスメトリクス: ECSServiceAverageCPUUtilization ターゲット値: 70 スケールアウトクールダウン期間: 300 スケールインクールダウン期間: 300 [作成]
railsapp のデプロイが進行中です。これには数分かかります。
今回は5分で完了しました。
想定通り、1つ目はFARGATE、2つ目はFARGATE_SPOTで作られました。
こちらもCloudFormationで作られるので、不要になったら、ここから削除する。
ALBとか一緒に作成したものも削除されるので、お手軽!
スタック: ECS-Console-V2-Service-railsapp-railsapp-xxxxxxxx
スタック: ECS-Console-V2-Service-railsapp-job-railsappxxxxxxxx
https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/
Tips: サービス作成に失敗する場合
origin#557 ECSで動くようにする(LBヘルスチェック、PIDディレクトリ、境変変数)、RailsAdminにDelayed::Jobを追加
Unicornが起動できない
% rails unicorn:start config/unicorn.rb:1:in `reload': undefined method `presence' for nil:NilClass (NoMethodError) config/unicorn.rb:1:in `reload': undefined method `blank?' for nil:NilClass (NoMethodError)
config/unicorn.rb ではRailsの関数は使えない。
unicorn_using = {
- worker_processes: (ENV['WORKER_PROCESSES'].presence || 2).to_i,
+ worker_processes: (ENV['WORKER_PROCESSES'] || 2).to_i,
環境変数で値を入れないと空になるので、"" || 2
で空になってエラーになる。
なので、手間だけど、先に空はnilにしてから、nil || 2
で2が取得できる。
unicorn_env = {
worker_processes: ENV['WORKER_PROCESSES'] == '' ? nil : ENV['WORKER_PROCESSES'],
unicorn_using = {
worker_processes: (unicorn_env[:worker_processes] || 2).to_i,
unicorn.pidが保存できない
CloudWatchログ
"PID_PATH: /workdir/tmp/pids/unicorn.pid(default)[Not found]" /usr/local/bundle/gems/unicorn-6.1.0/lib/unicorn/configurator.rb:104:in `block in reload': directory for pid=/workdir/tmp/pids/unicorn.pid not writable (ArgumentError) raise ArgumentError, "directory for #{var}=#{path} not writable" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{}
で複数指定の表記が効かない。多分、そのまま作られる。
なので、冗長だけど分割しました。
Dockerfile_production
- RUN mkdir -p tmp/{pids,sockets}
+ RUN mkdir -p tmp/pids
+ RUN mkdir -p tmp/sockets
ヘルスチェックが失敗する
CloudWatchログ
ERROR -- : [ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: 172.31.42.118 "GET /_check HTTP/1.1" 403 - 0.0084
Railsでドメインを制限(config.hosts)している場合、LBからのリクエストはIPでされるので、200が返らず失敗します。
特定のURLだけ許可する事が可能なので、ヘルスチェック用のURLを用意して、許可するように設定しています。
今回は、public/_checkのファイルを設置しています。
Controllerで作れば、DB接続エラー等で、LBから切り離す事も可能ですが、エラーページがALBが返すシンプルなものになる(頑張れば変えられますが)。
publicの場合はRailsの500ページが返る。ただ、Exception Notification使っていると通知が大量に来る(Sentryなら纏められる)。
一長一短あるので、状況に応じて決めるのが良さそう。
config/environments/production.rb
config.hosts << Settings.base_domain
+ # NOTE: [ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts
+ config.host_authorization = { exclude: ->(request) { request.path == '/_check' } }
config/settings/production.yml
base_domain: <%= ENV['BASE_DOMAIN'] %> # example.com
RailsAdminにDelayed::Jobを表示する
Sidekickと異なり、画面がないので、状態を確認したい場合はDBを見る必要があります。
毎回、接続するのは手間なので、RailsAdminに追加しました。
config/initializers/rails_admin.rb
+ ## == Delayed::Job ==
+ config.included_models = RailsAdmin::Config.models_pool << 'Delayed::Job'
+ config.model Delayed::Job do
+ label 'delayed_jobs'
+ navigation_label '管理'
+ end
スケジュールされたタスク(Cron)
Rakeタスクを定期的に実行するようにします。
新しいスケジュールされたタスクを作成 スケジュールされたルールの名前 railsapp_all-destroy(対象に合わせて変更) スケジュールされたルールのタイプ ●cron 式 cron(50 23 * * ? *) ※UTCなので、-9時間で設定。23=8時に実行 ターゲットID: railsapp_all-destroy(対象に合わせて変更) 起動タイプ: EC2 -> FARGATE ファミリー: railsapp-task(対象に合わせて変更) コンテナの上書き - オプション bundle,exec,rails,all:destroy[false](対象に合わせて変更) [作成]
Application Load Balancer(ALB)
作成自体は終わっていますが、このままだとアクセスできないのと、利便性を上げる為に設定を追加します。
https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#LoadBalancers:
DNS設定
対象のロードバランサーを選択して、DNS名を確認して、DNSにCNAMEレコードを追加します。
セキュリティグループの編集
サービスの作成のネットワークで設定しても良かったかもしれませんが、影響範囲が分からなかったので、手動で追加しました。
defaultがあればヘルスチェックは通ります。AllowHttpGroupだけだと通らなくなりました。
AllowHttpGroupがない場合は、先に作成してください。
※LBがIPv4のみなので、IPv6はなくてもOK
これでブラウザからアクセスできるようになりました。
リスナーとルールで、HTTPをHTTPSにリダイレクト
リスナーの追加 リスナーの設定 プロトコル: HTTP ※デフォルト ポート: 80 ※デフォルト アクションのルーティング ターゲットグループへ転送 -> URLにリダイレクト URLにリダイレクト URI部分 プロトコル: HTTPS ※デフォルト ポート: 443 ※デフォルト ステータスコード: 301 ※デフォルト [追加]
リスナールールで、IP制限
管理画面等、一般には開放したくないURIを、IPで制限します。
以前は会社や自宅のIPで制限したりしていましたが、リモートワークが普通になった今ではProxyサーバー(認証あり)のIPで制限してセキュアにしています。
ルールを追加する 名前とタグ Name: admin deny / admin allow ルール条件の定義 条件の追加 パス: /admin* [確認] 条件の追加 送信元IP: 0.0.0.0/0 / (許可したいIP)/32 [確認] [次へ] ルールアクションの定義 (admin deny) アクションのルーティング: ●固定レスポンスを返す レスポンスコード: 503 -> 403 (admin allow) アクションのルーティング: ターゲットグループへ転送 ※デフォルト ターゲットグループ: railsapp [次へ] ルールの優先度を設定する 優先度: 199 / 100 [次へ] 確認と作成 [作成]
リスナールールで、メンテナンスページ
LBから全て切り離された時に503が表示されますが、メンテナンスなのか障害なのかが分からない。
このHTMLをカスタマイズしたかったのですが、無理そう。
代替え手段は、CloudWatchで監視して、リスナールールの優先度を変える等で出来そうですが、オペミス以外ではあまりないので、今回は手動切り替え用に事前作成しておくに留める事にしました。
UIが変わって、デフォルト(最後)の優先度を設定できなくなったので、パスで切り替えます。
ルールを追加する 名前とタグ Name: メンテナンス @有効にするにはパスをワイルドカードに変更してください ルール条件の定義 条件の追加 パス: /xxx(存在しないパス) <- メンテナンス時は「*」 [確認] [次へ] ルールアクションの定義 (admin deny) アクションのルーティング: ●固定レスポンスを返す レスポンスコード: 503 ※デフォルト コンテンツタイプ: text/plain -> text/html レスポンス本文: (下記) [次へ] ルールの優先度を設定する 優先度: 1 [次へ] 確認と作成 [作成]
<html> <head> <meta name="robots" content="noindex" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>メンテナンス中</title> </head> <body> 現在、メンテナンスを行っております。<br/> 完了まで、しばらくお待ちください。<br/> </body> </html>
運用メモ
・タスク定義で新しいリビジョンを作成したらデプロイが必要
「サービス」以外に「スケジュールされたタスク」の方も
・ECRにpushしても自動でデプロイされない
「サービス」を更新で「新しいデプロイの強制」を選択。「スケジュールされたタスク」は不要
-> CodePipelineで自動化できる
アクセスがあまりない前提ですが、今回のECS+ALB+RDSの料金
87.11736 USD/30日 = 7.9632 + 36.4032 + 1.19448 + 5.46048 + 17.49600 + 18 + 0.6
料金 - AWS Fargate | AWS
料金 - Elastic Load Balancing | AWS
料金 - Amazon RDS for MySQL | AWS
Amazon Elastic Container Service APN1-Fargate-GB-Hours AWS Fargate - Memory - Asia Pacific (Tokyo) 7.9632 USD = 0.00553 USD/時 × 1GB × 2台 × 24時間 × 30日 Amazon Elastic Container Service APN1-Fargate-vCPU-Hours:perCPU AWS Fargate - vCPU - Asia Pacific (Tokyo) 36.4032 USD = 0.05056 USD/時 × 0.5 vCPU × 2台 × 24時間 × 30日 Amazon Elastic Container Service APN1-SpotUsage-Fargate-GB-Hours AWS Fargate - Memory - Asia Pacific (Tokyo) 1.19448 USD = 0.001659 USD/時 × 1GB × 1台 × 24時間 × 30日 Amazon Elastic Container Service APN1-SpotUsage-Fargate-vCPU-Hours:perCPU AWS Fargate - vCPU - Asia Pacific (Tokyo) 5.46048 USD = 0.015168 USD/時 × 0.5 vCPU × 1台 × 24時間 × 30日 Elastic Load Balancing - Application, Asia Pacific (Tokyo) $0.0243 per Application LoadBalancer-hour (or partial hour) 17.49600 USD = 0.0243 USD/時 × 1台 × 24時間 × 30日 Amazon Relational Database Service for MySQL Community Edition $0.025 per RDS db.t4g.micro Single-AZ instance hour (or partial hour) running MySQL 18 USD = 0.025 USD/時 × 1台 × 24時間 × 30日 Amazon Relational Database Service Provisioned Storage $0.12 per GB-month of provisioned magnetic storage running MySQL 0.6 USD = 0.12 USD/月 × 5GiB × 1台