💻

Docker と MinIO を使った ActiveStorage 開発環境の構築

2023/12/21に公開

Ruby on Rails の ActiveStorage はデフォルトだとローカルではディスクに保存するようになっていますが、S3互換オブジェクトストレージである MinIO を使うことによって S3 を用いた場合と近い環境で開発することができます。このエントリーでは、Docker Compose と MinIO を使って、ActiveStorage の開発環境を構築する方法を紹介します。

MinIOを導入する

まず、MinIO を導入します。Dockerイメージ が用意されているので、これを使います。その場合、以下のようにします。

compose.yaml
---
services:
  rails:
    ...
    ports:
      - target: 3000
        published: 3000
    environment:
      FILE_AWS_S3_BUCKET: file
      FILE_AWS_S3_ENDPOINT: http://minio:9000
      FILE_AWS_S3_FORCE_PATH_STYLE: true
      FILE_AWS_ACCESS_KEY_ID: user
      FILE_AWS_SECRET_ACCESS_KEY: password

  minio:
    image: minio/minio:RELEASE.2023-07-11T21-29-34Z
    command: minio server /data --console-address ':9001'
    ports:
      - target: 9000
        published: 9000
      - target: 9001
        published: 9001
    volumes:
      - type: volume
        source: minio
        target: /data
    environment:
      MINIO_ROOT_USER: user
      MINIO_ROOT_PASSWORD: password

volumes:
  minio:
config/storage.yml
local:
  service: S3
  access_key_id: <%= ENV.fetch('FILE_AWS_ACCESS_KEY_ID', '') %>
  secret_access_key: <%= ENV.fetch('FILE_AWS_SECRET_ACCESS_KEY', '') %>
  region: ap-northeast-1
#<% if ENV['FILE_AWS_S3_ENDPOINT'].present? %>
  endpoint: <%= ENV.fetch('FILE_AWS_S3_ENDPOINT', '') %>
#<% end %>
  bucket: <%= ENV.fetch('FILE_AWS_S3_BUCKET') %>
  force_path_style: <%= ENV.fetch('FILE_AWS_S3_FORCE_PATH_STYLE', 'false') %>
  public: true

ただし、この場合以下の問題があります。

rails-activestorage-minio-dev-1.png

  • http://file.minio:9000/xxxx のように virtual-hosted style でアクセスできない
    • MinIO と S3 で場合分けする必要がある
  • ブラウザからアクセスするときに http://minio:9000/xxxx でアクセスしようとするが、minio の名前解決ができない
    • /etc/hosts で対処する必要がある

nip.io と network-scoped alias を使ってブラウザからも Rails コンテナからも MinIO コンテナにアクセスできるようにする

上記の問題は、nip.io と Docker の network-scoped alias を使うと解決できます。

nip.io とは

nip.io は、 <ip-address>.nip.io もしくは <sub-domain>.<ip-address>.nip.io とすることで、指定したIPアドレスに名前解決できるワイルドカードDNSサービスです。

$ dig 127.0.0.1.nip.io +short
127.0.0.1

$ dig 192.168.1.10.nip.io +short
192.168.1.10

$ dig www.127.0.0.1.nip.io +short
127.0.0.1

$ dig www.192.168.1.10.nip.io +short
192.168.1.10

network-scoped alias とは

通常、Docker Compose で起動したコンテナは、Docker内部の内蔵DNSとサービス名を使うことでほかのコンテナにアクセスすることができます。

$ docker compose run --rm rails dig minio +short
172.25.0.3

ここで、compose.yaml でサービスを定義するときに network-scoped alias を指定すると、Dockerの内蔵DNSに登録され、その名前でもコンテナにアクセスすることができます。

compose.yaml
---
services:
  rails:
    ...

  minio:
    ...
    networks:
      default:
        aliases:
          - my-minio
$ docker compose run --rm rails dig minio +short
172.25.0.3

$ docker compose run --rm rails dig my-minio +short
172.25.0.3

ブラウザからも Rails コンテナからも MinIO コンテナにアクセスできるようにする

上記を組み合わせて MinIO のドメインを minio.127.0.0.1.nip.io とし、network-scoped alias で minio.127.0.0.1.nip.io を登録すると、ブラウザからは nip.io で名前解決されたループバックアドレスと Docker の Port forwarding によって MinIO のコンテナにアクセスでき、コンテナ内部では Docker内蔵DNSによってコンテナのIPに名前解決されるため、こちらでも MinIO のコンテナにアクセスできます。また、MinIO を virtual-hosted style でアクセスできるようになります。

rails-activestorage-minio-dev-2

compose.yaml
---
services:
  rails:
    ...
    ports:
      - target: 3000
        published: 3000
    environment:
      FILE_AWS_S3_BUCKET: file
      FILE_AWS_S3_ENDPOINT: http://minio.127.0.0.1.nip.io:9000
      FILE_AWS_ACCESS_KEY_ID: user
      FILE_AWS_SECRET_ACCESS_KEY: password

  minio:
    ...
    ports:
      - target: 9000
        published: 9000
      - target: 9001
        published: 9001
    environment:
      MINIO_DOMAIN: minio.127.0.0.1.nip.io
      MINIO_ROOT_USER: user
      MINIO_ROOT_PASSWORD: password
    networks:
      default:
        aliases:
          - minio.127.0.0.1.nip.io
          - file.minio.127.0.0.1.nip.io
config/storage.yml
local:
  service: S3
  access_key_id: <%= ENV.fetch('FILE_AWS_ACCESS_KEY_ID', '') %>
  secret_access_key: <%= ENV.fetch('FILE_AWS_SECRET_ACCESS_KEY', '') %>
  region: ap-northeast-1
#<% if ENV['FILE_AWS_S3_ENDPOINT'].present? %>
  endpoint: <%= ENV.fetch('FILE_AWS_S3_ENDPOINT', '') %>
#<% end %>
  bucket: <%= ENV.fetch('FILE_AWS_S3_BUCKET') %>
  public: true

他の端末からアクセスしたときも MinIO コンテナにアクセスできるようにする

今のままだと、他の端末のブラウザからアクセスしたときに、MinIO のアドレスがループバッグアドレスとなってしまうため、うまくいきません。この場合は、nip.io のドメインのIPアドレス部分を docker compose up しているPCのプライベートIPアドレスを指定することで解決できます。

rails-activestorage-minio-dev-3

compose.yaml
---
services:
  rails:
    ...
    ports:
      - target: 3000
        published: 3000
    environment:
      FILE_AWS_S3_BUCKET: file
      FILE_AWS_S3_ENDPOINT: http://minio.${HOST_IP:-127.0.0.1}.nip.io:9000
      FILE_AWS_ACCESS_KEY_ID: user
      FILE_AWS_SECRET_ACCESS_KEY: password

  minio:
    ...
    ports:
      - target: 9000
        published: 9000
      - target: 9001
        published: 9001
    environment:
      MINIO_DOMAIN: minio.${HOST_IP:-127.0.0.1}.nip.io
      MINIO_ROOT_USER: user
      MINIO_ROOT_PASSWORD: password
    networks:
      default:
        aliases:
          - minio.${HOST_IP:-127.0.0.1}.nip.io
          - file.minio.${HOST_IP:-127.0.0.1}.nip.io
HOST_IP=192.168.1.10 docker compose up

MinIO を管理できる mc を使ってバケットの管理を楽にする

mc を導入する

MinIO にはWebコンソールが用意されているので、そこにアクセスすることでバケットを作成したりファイルをアップロードしたりできます。しかし、バケットを作り直してまっさらにした場合はコンソール画面からだとすべてのファイルを手動で削除しないといけないので面倒です。そのため、これを簡単にするために MinIO を管理できるCLIツールである mc を導入して簡単にできるようにします。

mc は Dockerイメージが用意されているのでこれを使います。ここでは、compose.yaml で mc コンテナを起動して、MinIOコンテナに簡単にアクセスできるようにします。

まず、mc で minio という名前で対象の MinIO コンテナにアクセスできるようにするために ~/.mc/config.json に設定を記述します。

docker/mc/config.json
{
  "version": "10",
  "aliases": {
    "minio": {
      "url": "http://minio:9000",
      "accessKey": "user",
      "secretKey": "password",
      "api": "s3v4",
      "path": "auto"
    }
  }
}
compose.yaml
---
services:
  ...

  mc:
    image: minio/mc:RELEASE.2023-07-11T23-30-44Z
    volumes:
      - type: volume
        source: mc
        target: /root/.mc
      - type: bind
        source: ./docker/mc/config.json
        target: /root/.mc/config.json

volumes:
  mc:

つぎに、mc は docker compose run mc で実行できるようにしておきたいですが、mc コンテナ起動時に minio コンテナの起動およびデーモンの起動が完了している必要があるので、Docker のヘルスチェック機能と depends_on を使います。ここで、MinIO には GET /minio/health/live というヘルスチェックのためのエンドポイントが用意されているので、これを使います。mc コンテナは depends_onminio コンテナに対して service_healthy を指定すれば minio コンテナのヘルスチェックが通った後に起動します。

compose.yaml
---
services:
  ...

  minio:
    ...
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 1s
      timeout: 20s
      retries: 20

  mc:
    ...
    depends_on:
      minio:
        condition: service_healthy

このように指定することで、以下のように mc コマンドで MinIO コンテナのバケットを作ったり、削除したりできるようになります。

# file バケットを作成する
docker compose run --rm mc mb minio/file

# file バケットを削除する
docker compose run --rm mc rb --force minio/file

# file バケットを public アクセスできるようにする
docker compose run --rm mc anonymous set public minio/file

profiles を指定して docker compose up の時は mc コンテナを起動しないようにする

現在だと docker compose up をすると mc コンテナも同時に起動してしまいますが、profiles を指定することで、通常は起動しないようにできます。 profiles は本来の使い方だとオプショナルなサービスを起動するために指定するものですが、今回は docker compose up で起動しないようにするために使います。

compose.yaml
---
services:
  ...

  mc:
    ...
    profiles:
      - tools

まとめ

以上で、Docker Compose と MinIO を使って、ActiveStorage の開発環境を構築する方法を紹介しました。compose.yaml 等はまとめると以下のようになります。

.envrc
# direnv
export HOST_IP=127.0.0.1
compose.yaml
---
services:
  rails:
    build:
      context: .
      dockerfile: docker/app/Dockerfile
    init: true
    depends_on:
      db:
        condition: service_healthy
      minio:
        condition: service_healthy
    command: >
      bash -c "
        rm -rf tmp/pids/server.pid &&
        rails s -b 0.0.0.0 -p 3000
      "
    ports:
      - target: 3000
        published: 3000
    volumes:
      - type: bind
        source: .
        target: /app
      - type: volume
        source: rails_bundle
        target: /usr/local/bundle
      - type: volume
        source: rails_log
        target: /app/log
      - type: volume
        source: rails_cache
        target: /app/tmp/cache
    environment:
      DB_HOST: db
      FILE_AWS_S3_BUCKET: file
      FILE_AWS_S3_ENDPOINT: http://minio.${HOST_IP}.nip.io:9000
      FILE_AWS_ACCESS_KEY_ID: user
      FILE_AWS_SECRET_ACCESS_KEY: password
      HOST_IP: ${HOST_IP}

  db:
    image: mysql:8
    volumes:
      - type: volume
        source: mysql_data
        target: /var/lib/mysql
    ports:
      - target: 3306
        published: 3306
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
    healthcheck:
      test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
      interval: 1s
      timeout: 20s
      retries: 20

  minio:
    image: minio/minio:RELEASE.2023-07-11T21-29-34Z
    ports:
      - target: 9000
        published: 9000
      - target: 9001
        published: 9001
    command: minio server /data --console-address ':9001'
    environment:
      MINIO_DOMAIN: minio.${HOST_IP}.nip.io
      MINIO_ROOT_USER: user
      MINIO_ROOT_PASSWORD: password
    volumes:
      - type: volume
        source: minio
        target: /data
    networks:
      default:
        aliases:
          - minio.${HOST_IP}.nip.io
          - file.minio.${HOST_IP}.nip.io
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 1s
      timeout: 20s
      retries: 20

  mc:
    image: minio/mc:RELEASE.2023-07-11T23-30-44Z
    profiles:
      - tools
    depends_on:
      minio:
        condition: service_healthy
    volumes:
      - type: volume
        source: mc
        target: /root/.mc
      - type: bind
        source: ./docker/mc/config.json
        target: /root/.mc/config.json

volumes:
  mysql_data:
  rails_bundle:
  rails_cache:
  rails_log:
  minio:
  mc:
config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: S3
  access_key_id: <%= ENV.fetch('FILE_AWS_ACCESS_KEY_ID', '') %>
  secret_access_key: <%= ENV.fetch('FILE_AWS_SECRET_ACCESS_KEY', '') %>
  region: ap-northeast-1
#<% if ENV['FILE_AWS_S3_ENDPOINT'].present? %>
  endpoint: <%= ENV.fetch('FILE_AWS_S3_ENDPOINT', '') %>
#<% end %>
  bucket: <%= ENV.fetch('FILE_AWS_S3_BUCKET') %>
  public: true
docker/mc/config.json
{
  "version": "10",
  "aliases": {
    "minio": {
      "url": "http://minio:9000",
      "accessKey": "user",
      "secretKey": "password",
      "api": "s3v4",
      "path": "auto"
    }
  }
}

また、上記の方針で実装したサンプルが mrk21/rails-activestorage-minio-dev-sample にあります。

ハートレイルズ

Discussion