Amazon ECS デプロイで個人的詰まった/困った箇所まとめ(Fargate, Rails API)
はじめに
今回個人開発にてAWS ECSにてRailsを起動しS3上にホスティングしたReactから通信するにあたって幾つか困った点詰まった点があったので覚書として書くこととしました。
コスト高すぎ問題
個人開発と言ってもマネタイズするとかではなく、ポートフォリオ的側面でしかなかったため月々8000円くらいランニングコストがかかるのは嫌だったので料金に関してはかなり調べたり工夫したりしました。
NATGWかVPCエンドポイントか
VPCエンドポイント
1エンドポイントにつき0.014USD/h
- 1ヶ月起動しっぱなしで10.08USD
- 今回の実装では5個必要なので約50USD
NATGateway
1ゲートウェイにつき0.062USD
- 1ヶ月起動しっぱなしで33USD
- 今回の実装では全段にALBを配置しており2サブネットのため、2個必要なので約66USD
どのような構成にするか
- 実際の業務のプロダクトの場合だとPrivateLinkを使うVPCエンドポイントを利用してよりセキュアにした方が良さそう
- NAT&Endpointだとかえって高くつく→NATだけでも対応可能
- NATオンリーで対応がコスト面で考えれば安い
RDS
- 今回DBは
MySQL
を使いました。インスタンスクラスt2.micro
,t3.micro
は無料枠があり1ヶ月あたり750インスタンス時間であれば無料でした。 - 今回のプロダクトは毎日24時間起動することないので、EventBridgeを用いて750インスタンス時間に収まるよう自動起動停止を行いました。
ECS関連
exec /usr/bin/entrypoint.sh: exec format error
のエラー
デプロイした際の- M1Mac特有のエラーのようでした。
--platform
オプションでlinux/amd64
を追加することで解決しました。
$ docker build --platform linux/amd64 . -t test_ecr:latest
<参考>
ソケット通信のエラー
今回の構成ではECSクラスターの中にRailsとNginxのコンテナをたてNginxをリバースプロキシとしRailsコンテナへリクエストを伝搬させるというものでした。
そのためにはコンテナ間でソケット通信を行う必要があり、その方法に苦戦しました。
実際に確認したエラー
open() "/usr/share/nginx/html/api/v1/health_check" failed (2: No such file or directory), client: 172.10.1.230, server: localhost, request: "GET /api/v1/health_check HTTP/1.1", host: "hoge.ap-northeast-1.elb.amazonaws.com"
puma周り
puma.rb
ソケット通信を行うためのパスの設定を最下行に追加します。
app_root = File.expand_path('..', __dir__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"
Nginx設定まわり
nginx/nginx.conf
server {
listen 80;
server_name localhost;
root /app/public;
location / {
try_files $uri @app;
}
location @app {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
client_max_body_size 100m;
keepalive_timeout 65;
}
upstream app {
server unix:///app/tmp/sockets/puma.sock;
}
nginx/dockerfile
FROM nginx:latest
RUN apt-get update && apt-get install -y curl
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80
RUN rm /etc/nginx/conf.d/default.conf
にて自動生成されるconfファイルを削除しています。理由としてはnginx.conf
にてupstream
の設定をしているにも関わらず優先順位的な問題でdefault.conf
内で設定されているローカルのパスを見にいく問題があったためです。
ボリューム周り(タスク定義,Dockerfile)
今回のエラーの原因で最も大きかったのはボリューム周りであると思っています。コンテナ間で通信する際には前述で示したunix:///app/tmp/sockets/puma.sock
のパスをコンテナ間で共有する必要がありそれにはボリュームの設定を行う必要がありました。
rails/dockerfile
VOLUME /app/tmp
の設定を追加しました。
FROM ruby:3.2.2
ARG RUBYGEMS_VERSION=3.4.6
RUN mkdir /app
WORKDIR /app
COPY ../../Gemfile /app/Gemfile
COPY ../../Gemfile.lock /app/Gemfile.lock
RUN gem update --system ${RUBYGEMS_VERSION} && \
bundle install
COPY ../../ /app
VOLUME /app/public
VOLUME /app/tmp
EXPOSE 3000
COPY ./docker/staging/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
ecs.tf
(抜粋)
volmue
の設定を追加
resource "aws_ecs_task_definition" "main" {
family = "${var.common_name}-task-def-${var.environment}"
cpu = var.cpu
memory = var.memory
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
task_role_arn = module.ecs_task_execution_role.iam_role_arn
container_definitions = templatefile("${path.module}/task_definitions.tpl.json", {
~
環境変数を格納...
~
})
# volumeの設定
volume {
name = "sockets"
}
}
task_definitions.tpl.json
各コンテナのセクションにmountPoints
を追加
[
{
"name": "rails",
"image": "${rails_ecr_uri}:${rails_tag}",
"memoryReservation": 512,
~
"mountPoints": [
{
"sourceVolume": "sockets",
"containerPath": "/app/tmp/sockets"
}
]
},
{
"name": "nginx",
"image": "${nginx_ecr_uri}:${nginx_tag}",
~
"mountPoints": [
{
"sourceVolume": "sockets",
"containerPath": "/app/tmp/sockets"
}
]
}
]
学んだこと
アプリケーションが起動しなくてもコンテナが落ちないようにする
タスク定義に以下を仕込むことでタスクが落ちないようになりデバッグがしやすくなりました
"entryPoint": ["sh", "-c", "while true; do sleep 60; done"],
ECSで実行中のコンテナを調査できるツールを使いデバッグ
ECS exec
実行中のタスクに接続しできるコマンドです。導入には以下が参考になりました。
使い方
aws ecs execute-command --cluster {ECSクラスター名} \
--task {クラスター内で実行中のarn} \
--container {コンテナ名} \
--interactive \
--command "/bin/sh"
成功すると以下挙動になります
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.
Starting session with SessionId: ecs-execute-command-oyjd334q4tyf3uflk3ievsnvua
#
デバッグで試したこと
Puma,Railsサーバーが立ち上がっているか確認
試しにヘルスチェックのパスに対しcurlしています。
curl localhost:3000/api/v1/health_check
status200
が帰ってきたらサーバーは起動しているのですが以下のようなケースがあります。その場合は以下のように再起動します。
curl: (7) Failed to connect to localhost port 3000 after 0 ms: Couldn't connect to server
Puma,Railsサーバーの手動で再起動
bundle exec puma -C config/puma.rb
Puma starting in single mode...
* Puma version: 5.6.8 (ruby 3.2.2-p53) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: staging
* PID: 55
* Listening on http://0.0.0.0:3000
* Listening on unix:///app/tmp/sockets/puma.sock
Use Ctrl-C to stop
I, [2024-07-27T10:05:11.486378 #55] INFO -- : [ef38b403-bf8d-47d9-a787-9f5d7f1f3cbb] Started GET "/api/v1/health_check" for 127.0.0.1 at 2024-07-27 10:05:11 +0000
I, [2024-07-27T10:05:11.489545 #55] INFO -- : [ef38b403-bf8d-47d9-a787-9f5d7f1f3cbb] Processing by Api::V1::HealthCheckController#index as */*
I, [2024-07-27T10:05:11.490110 #55] INFO -- : [ef38b403-bf8d-47d9-a787-9f5d7f1f3cbb] ============================================================
自分の状況ですとここで初めてopen() "/usr/share/nginx/html/api/v1/health_check" failed (2: No such file or directory)
のエラーが出てタスク内のソケットのパス(/app/tmp/sockets/puma.sock
)などを確認して設定の不備に気づくという流れでした。
終わりに
読んでくださった方ありがとうございます。
上記で挙げた以外にも細々とした設定の不備が多かったなと感じています。もし同じような実装で困っている方がいればコメントなどで聞いていただけると答えらる範囲で追記等を行いたいです。
またもっといい方法があるよなどありましたらそちらもコメントください
参考
Discussion