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)

データベースの作成

今回は、無料利用枠(安いインスタンス)を使いたいので、MySQLを選択します。
Secrets Managerを使いたい所ですが、手間が大きいので、今回は自分で管理します。
TODO: AWS Secrets Managerでマスター認証情報を管理する

https://ap-northeast-1.console.aws.amazon.com/rds/home?region=ap-northeast-1#launch-dbinstance:;isHermesCreate=true

データベースの作成
	データベース作成方法を選択
		●標準作成	※デフォルト
	エンジンのオプション
		エンジンのタイプ: ●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'."

https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters/railsapp/create-service?region=ap-northeast-1

※対象のクラスターを選択
サービス
	[作成]
サービスの作成
	環境
		(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タスクを定期的に実行するようにします。

https://ap-northeast-1.console.aws.amazon.com/ecs/v2/clusters/railsapp/scheduled-tasks?region=ap-northeast-1

新しいスケジュールされたタスクを作成
	スケジュールされたルールの名前
		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台

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です