Fargateにおけるpuma+Nginxのソケット通信のやり方
やること
- pumaサーバーのアプリをFargateにデプロイする。
- リクエストはNginxで受ける。
- Nginxとpumaの通信はソケットを用いて行う。
- pumaとNginxは同じFargateタスクの別コンテナとして扱う。
こちらの記事にある下図の右側の部分のイメージです。 bluepixel.hatenablog.com
今回はNATなしで、パブリックサブネットに作ります。
アプリケーション
Sinatraで手早く作ります。
本質的な部分ではないので特に解説はしません。
app.rb
require "logger" require "sinatra" class App < Sinatra::Base set :server, :puma set :logging, Logger.new(STDOUT) get "/" do "Hello world" end end
config/puma.rb
app_path = File.expand_path("..", __dir__) directory app_path pidfile "#{app_path}/tmp/pids/puma.pid" state_path "#{app_path}/tmp/pids/puma.state" threads 0, 16 bind "unix://#{app_path}/tmp/sockets/puma.sock" activate_control_app
コンテナ間のソケット通信を行うので、bind
にソケットファイルの位置を指定します。アプリをusr/src/app/
に配置し、pumaのソケットをtmp/sockets/puma.sock
とします。
Dockerfile
アプリケーションコンテナはruby:2.7.1
をベースにします。
Dockerfile
FROM ruby:2.7.1 ENV LANG C.UTF-8 ENV TZ Asia/Tokyo ENV EDITOR vim RUN apt-get update RUN gem update --system RUN gem install bundler ENV APP_HOME /usr/src/app RUN mkdir -p $APP_HOME WORKDIR $APP_HOME ADD . $APP_HOME RUN mkdir -p $APP_HOME/tmp/pids RUN mkdir -p $APP_HOME/tmp/sockets VOLUME $APP_HOME/tmp ARG DEPLOYMENT RUN bundle config deployment $DEPLOYMENT RUN bundle
大事なのはここです。
VOLUME $APP_HOME/tmp
他のコンテナでマウントできるように、tmp/
をボリュームとして作成します。
Nginx
Nginxの方のDockerfileです。
nginx/Dockerfile
FROM nginx:latest # for health check RUN apt-get update && apt-get install -y curl ADD custom.conf /etc/nginx/conf.d CMD /usr/sbin/nginx -g 'daemon off;' EXPOSE 80
ECSのヘルスチェックでcurlが必要になるのでインストールしています。
custom.conf
にソケット通信の設定を書いてマウントします。
nginx/custom.conf
server { listen 80 default_server; root /usr/src/app/public; location / { try_files $uri $uri/index.html $uri.html @puma; } location @puma { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://my_app; } } upstream my_app { server unix:///usr/src/app/tmp/sockets/puma.sock; }
デプロイ
ECSにデプロイします。
諸々の設定は前述のリポジトリにCloudFormationのテンプレートがあるので参照してください。VPC含めて全部のリソースを丸ごと作っています。
重要なのはタスク定義のところだなので、そこだけ抜粋します。
ECSTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Cpu: !Ref TaskDefinitionCpu Memory: !Ref TaskDefinitionMemory RequiresCompatibilities: - FARGATE ExecutionRoleArn: !Ref ECSTaskRole TaskRoleArn: !Ref ECSTaskRole NetworkMode: awsvpc ContainerDefinitions: - Name: app Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECR}:latest MemoryReservation: !Ref AppMemory LogConfiguration: LogDriver: awslogs Options: awslogs-create-group: true awslogs-group: !Ref ECSLogsGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: !Sub ${AppName}-app Command: - bundle - exec - pumactl - start HealthCheck: Command: - CMD-SHELL - curl --unix-socket /usr/src/app/tmp/sockets/puma.sock ./ - "|| exit 1" StartPeriod: 15 Essential: true - Name: nginx Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECR}:nginx_latest MemoryReservation: !Ref NginxMemory LogConfiguration: LogDriver: awslogs Options: awslogs-create-group: true awslogs-group: !Ref ECSLogsGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: !Sub ${AppName}-nginx PortMappings: - ContainerPort: !Ref Port HealthCheck: Command: - CMD-SHELL - curl -f http://localhost/ - "|| exit 1" StartPeriod: 30 Essential: true DependsOn: - Condition: HEALTHY ContainerName: app VolumesFrom: - ReadOnly: true SourceContainer: app
- pumaを先に起動する必要があるので、
DependsOn
でappコンテナがヘルスチェックに成功してからNginxコンテナを起動するようにしています。 - pumaのヘルスチェックはcurlで行います。curl 7.40.0からUNIXソケットがサポートされているので
--unix-socket
オプションを付けます。 - appコンテナのボリュームの設定はDockerfileで行なっているため、ECSでの設定は特に不要です。
- Nginxコンテナの方でappコンテナのボリュームをマウントする設定を
VolumesFrom
で行います。
開発環境では
docker-compose を使っているのでこんな感じの設定になっています。
version: '3' services: app: environment: TZ: Asia/Tokyo build: context: ./ args: - DEPLOYMENT=false command: bundle exec pumactl start volumes: - .:/usr/src/app - bundle:/usr/local/bundle - tmp:/usr/src/app/tmp nginx: build: ./nginx/ image: nginx:latest ports: - 8080:80 volumes: - tmp:/usr/src/app/tmp depends_on: - app volumes: bundle: tmp:
まとめ
いろいろやり方はあると思いますが、これに落ちつきました。
FargateとEC2でも使える方法がまた異なるので、ドキュメントをよく読んで考える必要があります。
docker -v
と、Dockerfile内のVOLUME
と、docker-composeのvolumes
と、ECSのDockerボリュームとバインドマウントの対応関係がとても複雑。