😷

Amazon ECS デプロイで個人的詰まった/困った箇所まとめ(Fargate, Rails API)

2024/08/04に公開

はじめに

今回個人開発にて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インスタンス時間に収まるよう自動起動停止を行いました。

https://zenn.dev/hrk_sgyumm23/articles/d6736fbdaebbab

ECS関連

デプロイした際のexec /usr/bin/entrypoint.sh: exec format errorのエラー

  • M1Mac特有のエラーのようでした。--platformオプションでlinux/amd64を追加することで解決しました。
$ docker build --platform linux/amd64 . -t test_ecr:latest

<参考>
https://qiita.com/OmeletteCurry19/items/fd057a7448aa3072fd1e

ソケット通信のエラー

今回の構成では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

実行中のタスクに接続しできるコマンドです。導入には以下が参考になりました。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-exec.html#ecs-exec-prerequisites

https://dev.classmethod.jp/articles/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)などを確認して設定の不備に気づくという流れでした。

終わりに

読んでくださった方ありがとうございます。
上記で挙げた以外にも細々とした設定の不備が多かったなと感じています。もし同じような実装で困っている方がいればコメントなどで聞いていただけると答えらる範囲で追記等を行いたいです。
またもっといい方法があるよなどありましたらそちらもコメントください

参考

https://qiita.com/hatsu/items/22e11e94a0a981d78efa
https://qiita.com/hatsu/items/8b30e68ba7252a749fe7#modulesfrontend
https://bluepixel.hatenablog.com/entry/2020/04/22/230721

Discussion