less is more

心のフルスタックエンジニア👨‍💻バイブスでコードを書いています🤘

Fargateにおけるpuma+Nginxのソケット通信のやり方

f:id:bluepixel:20200415190526p:plain

やること

  • pumaサーバーのアプリをFargateにデプロイする。
  • リクエストはNginxで受ける。
  • Nginxとpumaの通信はソケットを用いて行う。
  • pumaとNginxは同じFargateタスクの別コンテナとして扱う。

こちらの記事にある下図の右側の部分のイメージです。 bluepixel.hatenablog.com

f:id:bluepixel:20200405232213p:plain

今回はNATなしで、パブリックサブネットに作ります。

github.com

アプリケーション

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でも使える方法がまた異なるので、ドキュメントをよく読んで考える必要があります。

docs.aws.amazon.com

docs.docker.jp

docker -v と、Dockerfile内のVOLUMEと、docker-composeのvolumesと、ECSのDockerボリュームとバインドマウントの対応関係がとても複雑。