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バージョンアップ