ECS(Fargate)でECRにpushしたRailsアプリを動かす では簡単にする為、ALB経由で直接Rails(Unicorn)にアクセスするようにしました。
テストや社内利用ならばこれでも問題ないのですが、大量のリクエストを効率的に処理したり、アクセスログを残したりするには、Webサーバー(NginxやApache)を前に置いた方が良いです。

先ずはローカルでNginxを動かす

既存のRailsアプリにDockerを導入する
RailsアプリのDockerイメージを作成して、ECRにpushする
RailsのRubyバージョンアップ(3.0.0→3.1.4)(Dockerも)

compose.yml

折角なので、環境変数でパラメータを変えられるようにして汎用化します。
置換の為に使用したenvsubstは、パラメータ指定しないと、$始まりが空で置換されるので、手間だけど明示しています。
templateは$または${}で、envsubstのパラメータは$$

  web:
    image: nginx:1.25.3-alpine # https://hub.docker.com/_/nginx
    volumes:
      - ./ecs/web/nginx/nginx.conf.template:/etc/nginx/nginx.conf.template
      - ./ecs/web/nginx/conf.d/default.conf.template:/etc/nginx/conf.d/default.conf.template
      - ./public:/usr/share/nginx/html
    command: >
      /bin/sh -c "
        envsubst '$$NGINX_WORKER_PROCESSES $$NGINX_WORKER_RLIMIT_NOFILE $$NGINX_WORKER_CONNECTIONS $$NGINX_SET_REAL_IP_FROM $$NGINX_REAL_IP_HEADER' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf &&
        envsubst '$$NGINX_PROXY_PASS' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf &&
        nginx -g 'daemon off;'"
    environment:
      NGINX_WORKER_PROCESSES: "auto" # 最大値: vCPUの数 or auto = 16と仮定して
      NGINX_WORKER_RLIMIT_NOFILE: 32768 # 最大値: 524288(cat /proc/sys/fs/file-max) / NGINX_WORKER_PROCESSES
      NGINX_WORKER_CONNECTIONS: 8192 # 最大値: NGINX_WORKER_RLIMIT_NOFILE / 4
      NGINX_SET_REAL_IP_FROM: "172.31.0.0/16" # <- ELB
      NGINX_REAL_IP_HEADER: "X-Forwarded-For" # <- ELB
      NGINX_PROXY_PASS: "http://app:3000"
    ports:
      - 80:80
    depends_on:
      - app

ecs/web/nginx/nginx.conf.template

user  nginx;
### START ###
# worker_processes  auto;
worker_processes     ${NGINX_WORKER_PROCESSES};
worker_rlimit_nofile ${NGINX_WORKER_RLIMIT_NOFILE};
# error_log  /var/log/nginx/error.log notice;
error_log  /dev/stderr;
### END ###
pid        /var/run/nginx.pid;
events {
### START ###
#    worker_connections  1024;
    worker_connections ${NGINX_WORKER_CONNECTIONS};
    accept_mutex_delay 100ms;
    multi_accept on;
### END ###
}
http {
### START ###
    server_tokens off;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options nosniff;
    client_max_body_size 64m;
### END ###
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
### START ###
    set_real_ip_from   ${NGINX_SET_REAL_IP_FROM};
    real_ip_header     ${NGINX_REAL_IP_HEADER};
#    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
#                      '$status $body_bytes_sent "$http_referer" '
#                      '"$http_user_agent" "$http_x_forwarded_for"';
    log_format  addhost  '$remote_addr - $remote_user [$time_local] "$request" '
                         '$status $body_bytes_sent "$http_referer" '
                         '"$http_user_agent" $request_time "$host"';
#    access_log  /var/log/nginx/access.log  main;
    access_log  /dev/stdout  addhost;
### END ###
    sendfile        on;
    #tcp_nopush     on;
### START ###
    tcp_nopush      on;
    tcp_nodelay     on;
### END ###
### START ###
#    keepalive_timeout  65;
    keepalive_timeout   120;
    open_file_cache     max=100 inactive=20s;
    types_hash_max_size 2048;
### END ###
    #gzip  on;
### START ###
    gzip on;
    gzip_types text/plain text/css text/javascript application/javascript application/x-javascript application/json text/xml application/xml application/xml+rss;
### END ###
    include /etc/nginx/conf.d/*.conf;
}

ecs/web/nginx/conf.d/default.conf.template

server {
    listen       80;
### START ###
    listen       [::]:80 default_server;
#    server_name  localhost;
    server_name  _;
    root         /usr/share/nginx/html;
    add_header Strict-Transport-Security "max-age=31536000";
### END ###
    #access_log  /var/log/nginx/host.access.log  main;
    location / {
### START ###
#        root   /usr/share/nginx/html;
#        index  index.html index.htm;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Host    $host;
        proxy_set_header    X-Forwarded-Proto   $http_x_forwarded_proto;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_redirect      off;
        proxy_pass          ${NGINX_PROXY_PASS};
### END ###
    }
    #error_page  404              /404.html;
    # redirect server error pages to the static page /50x.html
    #
### START ###
#    error_page   500 502 503 504  /50x.html;
    error_page   502 503  /503.html;
#    location = /50x.html {
#        root   /usr/share/nginx/html;
#    }
    location = /503.html {
    }
    location = /_check {
    }
### END ###
    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}
    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

動作確認

% docker compose up --build web
% open http://localhost/

専用のECSタスクを用意する場合

Nginxのconfに直接環境変数をセットできないので(方法はありますが)、ENTRYPOINTでenvsubstで置換するようにします。
CMDでも出来ますが、必ず実行するものはENTRYPOINTに入れた方が良さそう。CMDは上書きして使う事も多いので。
ENTRYPOINTは必要か?

ecs/web/Dockerfile

envsubstを使うには、gettextを入れる必要があります。

# https://hub.docker.com/_/nginx
FROM nginx:1.25.3-alpine
RUN apk update && apk add --no-cache --update gettext

COPY ecs/web/nginx/nginx.conf.template /etc/nginx/
COPY ecs/web/nginx/conf.d/default.conf.template /etc/nginx/conf.d/
COPY public/_check /usr/share/nginx/html/

# Add a script to be executed every time the container starts.
COPY ecs/web/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

# Configure the main process to run when running the image
CMD ["nginx", "-g", "daemon off;"]

ecs/web/entrypoint.sh

envsubstではデフォルト値を指定できない為、bashで未設定や空だったら初期値を入れるように実装しました。空が入ったり、置換されないと起動できなくなるので。

nginx: [emerg] invalid number of arguments in "worker_processes" directive in /etc/nginx/nginx.conf:4

設定値が確認できるように標準出力して、ログに出力されるようにもしています。

#!/bin/sh
set -e

if [ "$NGINX_WORKER_PROCESSES" = '' ]; then
    export NGINX_WORKER_PROCESSES="auto" # 最大値: vCPUの数 or auto = 16と仮定して
    echo "NGINX_WORKER_PROCESSES: $NGINX_WORKER_PROCESSES(default)"
else
    echo "NGINX_WORKER_PROCESSES: $NGINX_WORKER_PROCESSES"
fi
if [ "$NGINX_WORKER_RLIMIT_NOFILE" = '' ]; then
    export NGINX_WORKER_RLIMIT_NOFILE=32768 # 最大値: 524288(cat /proc/sys/fs/file-max) / NGINX_WORKER_PROCESSES
    echo "NGINX_WORKER_RLIMIT_NOFILE: $NGINX_WORKER_RLIMIT_NOFILE(default)"
else
    echo "NGINX_WORKER_RLIMIT_NOFILE: $NGINX_WORKER_RLIMIT_NOFILE"
fi
if [ "$NGINX_WORKER_CONNECTIONS" = '' ]; then
    export NGINX_WORKER_CONNECTIONS=8192 # 最大値: NGINX_WORKER_RLIMIT_NOFILE / 4
    echo "NGINX_WORKER_CONNECTIONS: $NGINX_WORKER_CONNECTIONS(default)"
else
    echo "NGINX_WORKER_CONNECTIONS: $NGINX_WORKER_CONNECTIONS"
fi
if [ "$NGINX_SET_REAL_IP_FROM" = '' ]; then
    export NGINX_SET_REAL_IP_FROM="172.31.0.0/16" # <- ELB
    echo "NGINX_SET_REAL_IP_FROM: $NGINX_SET_REAL_IP_FROM(default)"
else
    echo "NGINX_SET_REAL_IP_FROM: $NGINX_SET_REAL_IP_FROM"
fi
if [ "$NGINX_REAL_IP_HEADER" = '' ]; then
    export NGINX_REAL_IP_HEADER="X-Forwarded-For" # <- ELB
    echo "NGINX_REAL_IP_HEADER: $NGINX_REAL_IP_HEADER(default)"
else
    echo "NGINX_REAL_IP_HEADER: $NGINX_REAL_IP_HEADER"
fi
if [ "$NGINX_PROXY_PASS" = '' ]; then
    export NGINX_PROXY_PASS="http://unicorn_sock" # <- webapp, web: LBのURL
    echo "NGINX_PROXY_PASS: $NGINX_PROXY_PASS(default)"
else
    echo "NGINX_PROXY_PASS: $NGINX_PROXY_PASS"
fi

envsubst '$$NGINX_WORKER_PROCESSES $$NGINX_WORKER_RLIMIT_NOFILE $$NGINX_WORKER_CONNECTIONS $$NGINX_SET_REAL_IP_FROM $$NGINX_REAL_IP_HEADER' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
envsubst '$$NGINX_PROXY_PASS' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

動作確認

作り方は下記と同じなので省略します。
環境変数: NGINX_PROXY_PASS = アプリサーバーのALBのURLを設定
※アプリサーバーのALBは、80で、ローカルアクセスのみに制限した方が良い。
ECS(Fargate)でECRにpushしたRailsアプリを動かす

アプリのECSタスクと同居する場合

ecs/webapp/Dockerfile

RailsアプリのDockerfileをベースに、nginxとgettextを追加しています。
NginxとUnicornは、UNIXドメインソケットで繋いでいます。
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin?path=/ecs/app/Dockerfile

# https://hub.docker.com/_/ruby
FROM ruby:3.2.2-alpine
RUN apk update && apk add --no-cache --update build-base tzdata bash python3 imagemagick graphviz ttf-freefont gcompat
# RUN apk add --no-cache --update sqlite-dev sqlite-libs
RUN apk add --no-cache --update mysql-dev mysql-client
RUN apk add --no-cache --update postgresql-dev postgresql-client
RUN apk add --no-cache --update nginx gettext

WORKDIR /workdir
ENV LANG="ja_JP.UTF-8"

ENV RAILS_ENV="production"
ENV RAILS_LOG_TO_STDOUT=1

COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'test development'
RUN bundle install --no-cache

COPY . ./
RUN mkdir -p tmp/pids
RUN mkdir -p tmp/sockets

# NOTE: 存在チェック
COPY public/robots.txt ./public/

COPY ecs/web/nginx/nginx.conf.template /etc/nginx/
COPY ecs/web/nginx/conf.d/default.conf.template /etc/nginx/conf.d/
COPY ecs/webapp/nginx/conf.d/unicorn.conf /etc/nginx/conf.d/
COPY public/_check /usr/share/nginx/html/

# Add a script to be executed every time the container starts.
COPY ecs/web/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

# Configure the main process to run when running the image
# NOTE: assets:precompileは起動時に実施。SECRET_KEY_BASEが必要な為
CMD ["bash", "-c", "nginx && bundle exec rails db:migrate db:seed assets:precompile && bundle exec unicorn -c config/unicorn.rb"]

ecs/webapp/nginx/conf.d/unicorn.conf

upstream unicorn_sock {
    server unix:/workdir/tmp/sockets/unicorn.sock;
}

動作確認

作り方は下記と同じなので省略します。
ECS(Fargate)でECRにpushしたRailsアプリを動かす

ヘルスチェックはRails側で応答するものを指定した方が良いです。Rails起動前に通ってしまう為。
/_check はNginxが応答するようにした為、/health_check で実装したのを使います。
origin ヘルスチェックURL作成、SchemaSpyバージョンアップ

今回のコミット内容

origin#459 ECS(Fargate)でアプリを動かしたい

コメントを残す

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