OpenProject on AWS ECS


# OpenProjectが公開しているdocker-compose.ymlをAWS ECSにデプロイしてみました

(2020/4月ごろに投稿した記事を再度投稿します、内容的に古いところがあるかもです)
OpenProjectコンテナをECS(EC2)で動かしてみまして、それなりに苦労したので記録を残しておきます。

## OpenProjectとは

オープンソースのプロジェクト管理ソフトウェアです。
https://www.openproject.org/
コミュニティ版dockerイメージが公開されています。
https://hub.docker.com/r/openproject/community

## 複数コンテナでの起動

[One container per process (recommended)](https://docs.openproject.org/installation-and-operations/installation/docker/#one-container-per-process-recommended)
にあるように、複数コンテナで動作させるのがおすすめだそうですのでこちらでやってみました。
ちなみにAll-in-one containerのケースだと、その名の通りapache2/PUMA(ruby)/メール処理(postfix)/memcached/postgresqlを一緒くたに動作させるという豪快さです。
(OpenProject本体は、One container per processとAll-in-onでdocker imageは同じ、2Gに迫る巨体というのがトホホ感すごいんですが、わざわざ分けるのも違うような・・・悩ましいところです)

### docker-compose.ymlの入手

上記手順に従って

~~~bash
git clone --depth=1 --branch=stable/10 https://github.com/opf/openproject
~~~
とやるだけです。

作業時点でオリジナルファイルはこんな感じ。

~~~yml:docker-compose.yml
version: "3.7"

networks:
  frontend:
  backend:

volumes:
  pgdata:
  opdata:

x-op-restart-policy: &restart_policy
  restart: unless-stopped
x-op-image: &image
  image: openproject/community:${TAG:-10}
x-op-app: &app
  <<: *image
  <<: *restart_policy
  environment:
    - "RAILS_CACHE_STORE=memcache"
    - "OPENPROJECT_CACHE__MEMCACHE__SERVER=cache:11211"
    - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    - "DATABASE_URL=postgres://postgres:p4ssw0rd@db/openproject"
    - "USE_PUMA=true"
    # set to true to enable the email receiving feature. See ./docker/cron for more options
    - "IMAP_ENABLED=false"
  volumes:
    - "opdata:/var/openproject/assets"
  depends_on:
    - db
    - cache

services:
  db:
    image: postgres:10
    <<: *restart_policy
    stop_grace_period: "3s"
    volumes:
      - "pgdata:/var/lib/postgresql/data"
    environment:
      - POSTGRES_PASSWORD=p4ssw0rd
      - POSTGRES_DB=openproject
    networks:
      - backend

  cache:
    image: memcached
    <<: *restart_policy
    networks:
      - backend

  proxy:
    <<: *image
    <<: *restart_policy
    command: "./docker/proxy"
    ports:
      - "8080:80"
    environment:
      - APP_HOST=web
      - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    depends_on:
      - web
    networks:
      - frontend

  web:
    <<: *app
    command: "./docker/web"
    networks:
      - frontend
      - backend

  worker:
    <<: *app
    command: "./docker/worker"
    networks:
      - backend

  cron:
    <<: *app
    command: "./docker/cron"
    networks:
      - backend

  seeder:
    <<: *app
    command: "./docker/seeder"
    restart: on-failure
    networks:
      - backend
~~~


### 編集

素のdocker-compose.ymlをecs-cli composeに食わせても盛大に怒られますので、以下の点を編集しました。

- version<br>
 今現在のecs-cliはversion3.7非対応ですので、3.xでの拡張部分を修正します。version記載部と変数部とかですね
- network<br>
 ecs非対応なのでこれも削除。
- restart<br>
 これも非対応。
- depends_on<br>
 ECSで対応してないわけではないんですが、ecs-cliでは食ってくれず、依存関係はAWS console上で編集せねばならんようです。本来依存で解決しなければならないところがあるのですが、コマンドだけで完結させたかったのでカッコ悪いですが該当部はbash -c "sleep XX && command"を使いました・・・すいません
- コンテナ間通信<br>
 EC2でも名前解決してくれないので必要なコンテナにlinks:記述を追加します。localhost記述でなんとかなるらしいですが、辛抱できずFargateではなくEC2で動かしました。
- logging<br>
awslogsドライバを使ってCloudWatchに出力してます。
- proxyの外部ポート番号<br>
 これはECS関係ないですが80番で動かしたかったので。
- openproject/communityのタグ<br>
 ${TAG:-10}となっていて、TAG指定しなかったら"10"ってやつを使うことになってるんですが、
 https://hub.docker.com/r/openproject/community/tags
 を見ると latest/10.5.1/10.5/10 が同じハッシュになっててなんとも台無しなので、明示的に10.5.1とすべきですねこれは。
 動かしたあとで気づいたので、もう試す気力もなく、以下引用のファイルではカッコ悪いですが変数部をベタ書きにしてます。

volumeについてはなにやってんだかよくわかりませんが、とりあえず残しました。
また、気持ち悪いのでmemcachedとpostgresqlはコンテナ動作させず、マネージドサービスを使うことにしました(昭和おじさんの性)。なのでdbコンテナとcacheコンテナの起動定義部は削除して、DB接続設定とmemcachedの設定部分も変えてます。
編集後のファイルはこんな感じです。

~~~yml:docker-compose.yml
version: "3"

volumes:
  opdata:

services:

  proxy:
    image: openproject/community:10.5.1
    command: "./docker/proxy"
    ports:
      - "80:80"
      - "443:443"
    environment:
      - APP_HOST=web
      - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    links:
      - web:web
    logging:
      driver: awslogs
      options:
        awslogs-group: xxxx
        awslogs-region: xx-xxxx-x
        awslogs-stream-prefix: proxy

  web:
    image: openproject/community:10.5.1
    environment:
      - "RAILS_CACHE_STORE=memcache"
      - "OPENPROJECT_CACHE__MEMCACHE__SERVER=xxx.xx.xxx.xxx.cache.amazonaws.com:11211"
      - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
      - "DATABASE_URL=postgres://postgres:p4ssw0rd@openproject2.cm6di7rgxd2w.xx-xxxx-x.rds.amazonaws.com/openproject"
      - "USE_PUMA=true"
      - "IMAP_ENABLED=false"
    volumes:
      - "opdata:/var/openproject/assets"
    command: "./docker/web"
    logging:
      driver: awslogs
      options:
        awslogs-group: xxxx
        awslogs-region: xx-xxxx-x
        awslogs-stream-prefix: web

  worker:
    image: openproject/community:10.5.1
    environment:
      - "RAILS_CACHE_STORE=memcache"
      - "OPENPROJECT_CACHE__MEMCACHE__SERVER=xxx.xx.xxx.xxx.cache.amazonaws.com:11211"
      - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
      - "DATABASE_URL=postgres://postgres:p4ssw0rd@openproject2.cm6di7rgxd2w.xx-xxxx-x.rds.amazonaws.com/openproject"
      - "USE_PUMA=true"
      - "IMAP_ENABLED=false"
    volumes:
      - "opdata:/var/openproject/assets"
    command: bash -c "sleep 45 && ./docker/worker"
    logging:
      driver: awslogs
      options:
        awslogs-group: xxxx
        awslogs-region: xx-xxxx-x
        awslogs-stream-prefix: worker

  cron:
    image: openproject/community:10.5.1
    environment:
      - "RAILS_CACHE_STORE=memcache"
      - "OPENPROJECT_CACHE__MEMCACHE__SERVER=xxx.xx.xxx.xxx.cache.amazonaws.com:11211"
      - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
      - "DATABASE_URL=postgres://user:password@xxx.xxx.xxx.rds.amazonaws.com/openproject"
      - "USE_PUMA=true"
      - "IMAP_ENABLED=false"
    volumes:
      - "opdata:/var/openproject/assets"
    command: "./docker/cron"
    logging:
      driver: awslogs
      options:
        awslogs-group: xxxx
        awslogs-region: xx-xxxx-x
        awslogs-stream-prefix: cron

  seeder:
    image: openproject/community:10.5.1
    environment:
      - "RAILS_CACHE_STORE=memcache"
      - "OPENPROJECT_CACHE__MEMCACHE__SERVER=xxx.xx.xxx.xxx.cache.amazonaws.com:11211"
      - "OPENPROJECT_RAILS__RELATIVE__URL__ROOT=${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
      - "DATABASE_URL=postgres://user:password@xxx.xxx.xxx.rds.amazonaws.com/openproject"
      - "USE_PUMA=true"
      - "IMAP_ENABLED=false"
    volumes:
      - "opdata:/var/openproject/assets"
    command: "./docker/seeder"
    logging:
      driver: awslogs
      options:
        awslogs-group: xxxx
        awslogs-region: xx-xxxx-x
        awslogs-stream-prefix: seeder
~~~

こうすると、変数使ってるオリジナルはスマートですねー
もともとはdb/cache/proxy/web/worker/cron/seederの7コンテナだったのが、上記の通りdbとcacheを外だししたので5コンテナ構成になってます。
それぞれ何やってるかですが、公式のドキュメントになにもなさそうなので一応書いておくと

- proxy<br>
 apache2。webコンテナにトラヒックを投げる。
- web<br>
 PUMAが動いている。処理本体。
- worker<br>
 よくわからんがrake jobs:workさせている。<br>
 (seederの処理が終わりきらないと、未処理のテーブル見にいって落ちる。restartしてくれれば救えるのにそれもしてくれないのでsleepさせてから起動)
- cron<br>
 sleep使ってメール処理をループしている。cronって言われるとなんか違う気がしますが。。。
- seeder<br>
 初回起動時にdbの初期化を行う。

あとはecs-params.ymlですがこちらは割愛します。
ポイントはseederに"essential: false"を設定すること。上述のとおり、seederはdbの初期化をするコンテナなんですが、処理が終わるとexitしてしまうのでessential:true(default)だと全体が落ちてしまいます。dbの初期化してるだけなのにメモリをあまりケチるとOOMで落ちます。512MでOKでした。メモリ2Gあたりのインスタンス使うような場合は、このためだけにインスタンスタイプを上げてしまうことになりかねずなので、どうしてもちいさく動かしたい方はdocker-compose.ymlを2つに分けてseederだけまず動かして、その後seeder以外というふうにすればよいかと思います。
外部に持っていったrdsとelasticacheについても割愛。

### 起動

あとは
[チュートリアル: Amazon ECS CLI を使用して EC2 タスクのクラスターを作成する](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-cli-tutorial-ec2.html)
に従って作業すればよしです。

ちなみにopenprojectのimageですが、
[The current Docker image does not support SSL by default.](https://docs.openproject.org/installation-and-operations/configuration/ssl/#docker-based-installation)
です。前段にELBなりなんなり立ててそこで処理してあげるのが手っ取り早いです。