自分用の個人的な Redmine を使い始めてから5年以上になる。 私は Redmine を電子版のスケジュール帳、メモ帳、ライフログとして利用しており、 立ち上げてからこれまでの活動、イベント、収集した情報、メモ等が記録されている。 現時点でチケット数は 1,723 件になった。 インターネットに接続できさえすれば、いつでも、どこでも、それらを振り返ることができる。

Redmine はオープンソースのプロジェクト管理ツールであり、 システム開発のプロジェクトで課題、タスクを管理するために一般的に使われている。 基本的には Web ブラウザで Web ページを開いて使う。 見た目は大体 Redmine プロジェクト公式ページ の通り。

1. 自分用 Redmine のすすめ

Redmine の用途は普通、仕事で業務上の様々な事柄 (タスク、質問等) を チケットという単位で整理し、数人から数十人のプロジェクト進行をうまく管理するためのものであるが、 実は個人のプライベートな予定とか、メモを集約するツールとしてもかなり便利に使うことができる。

例えば私は温泉巡りの旅行をしながら花火大会も同時に回るのが趣味だったりするわけだが、 地域ごとに行きたい温泉地をあらかじめメモしておいたり、 毎日のように各地で開催される花火の予定を記しておくことで、 旅行の計画を効率的に立てることができる。 また、そのために宿や交通機関を予約した場合には、それも Redmine 上にまとめることができる。私はバイクも乗るので、メンテナンス記録も残している。

さらに情報をまとめるだけでなく、過去の記録を簡単に参照できるのが素晴らしい。旅行の例であれば、前回の訪問時の記録を参照し、移動時間や行けなかった場所等の知識を今回に活かすことができる。

そもそも我々は、生活費を稼ぎつつ、家庭とか人間関係とか生活の色々をこなし、 趣味でプログラミングをして、旅行にも行き、 短い年月の中で膨大な TODO をこなしていかなければいけないのだから、 プライベートというのは仕事のプロジェクトと同等、もしくはより複雑であり、 プロジェクト管理ツールが適合するのは当然なのである。

2. 動作環境

さて、これまでの Redmine 運用では、さくらの VPS (1Gプラン) 上にて Redmine や、その他いろいろなシステムを相乗りで動かしてきたのだが、

  • 最近どうも動作がもっさりするし、
  • 他システムとの依存関係が壊れるのも怖くサーバー全体のバージョンアップが滞っている

という悩みがあった。

そこでこのたび、Amazon EC2 + Docker + Redmine 環境で再構築することにより、 環境のポータブル化とバージョンアップを行うことにした。

本稿では構築、移行の手順を記録した。 お約束だが、この記事は私が単に趣味で書いたものであり、内容の正しさについて責任を負うことはできない。また参考にして誰かが何かしらやらかしたとしても私のせいではない。

本編に入る前に構成をもう少し詳しく書くと以下のようになる。

  • 設備: AWS EC2
    $ uname -a
    Linux ip-172-31-12-243.ap-northeast-1.compute.internal 4.14.181-142.260.amzn2.x86_64 #1 SMP Wed Jun 24 19:07:39 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
    
  • コンテナエンジン: Docker
  • コンテナ管理: Docker Compose
  • SSL 証明書: Let’s Encrypt 発行
  • Web サーバー: Nginx 1.19.1
  • DB: PostgreSQL 9.5.22
  • Redmine 4.1.1

2.1. Amazon EC2

Amazon EC2 の説明は省略する。Amazon クラウド上の VPS サービスである。

EC2 には Amazon Marketplace という仕組みがあり、 そこに登録された AMI を利用することによりサーバーを1から構築せずとも、 既にできあがった状態でいきなりインストールすることができるが、今回は利用しない。 Redmine 構築でよく使われる Bitnami 社提供の AMI は Redmine + MySQL の構成だが、 私の Redmine は PostgreSQL を使っているためだ。

2.2. Docker

本稿では Docker と Docker Compose を使って構築する。

Docker は流行りのコンテナ技術、あるいはコンテナ管理ツールである。 Docker コンテナ上でシステムを起動することで、その起動環境をコンテナ内に閉じ込めることができる。

  • ホストサーバへの依存性を最小限にできるため、ホスト側環境を清潔に保てることや、
  • 起動環境の構成が Docker の設定ファイルで記述されるため、同じ環境を立ち上げやすいこと に大きな魅力を感じる。

Docker Compose は複数 Docker コンテナを一括で制御できるツールである。 Docker Compose の設定ファイル (docker-compose.yml) に、 関連するコンテナの情報を記述しておけば、コマンド一発で一連のサーバーを起動できる。

Docker Compose の高機能版みたいなものとして、Docker Swarm や Kubernetes といった オーケストレーションツールもあるが利用しなかった。 私の Redmine は自分用であり、もちろん利用者もほぼ自分だけのため、 負荷が増大することは考えにくく、スケールのことを考慮する必要が無いからだ。

AWS、GCP にコンテナのマネージドサービスもあるが、 費用がかなり高くつくので見なかったことにした。

リモート環境に Docker 環境を構築するという Docker Machine にも少し挑戦したが、 いまいち綺麗に動かないため、労力にメリットが合わないと思い利用を見送った。

2.3. Redmine

まず移行前のバージョンは下記の通り。 私の Redmine 上の作業ログによると、2018年末にバージョンアップしたのが最後らしい。

Environment:
  Redmine version                3.4.3.stable.17022
  Ruby version                   2.2.1-p85 (2015-02-26) [x86_64-linux]
  Rails version                  4.2.8
  Environment                    production
  Database adapter               PostgreSQL
SCM:
  Subversion                     1.6.11
  Mercurial                      1.4
  Bazaar                         2.1.1
  Git                            1.7.1
  Filesystem                     
Redmine plugins:
  redmine_agile                  1.4.1
  redmine_dmsf                   1.5.7

移行後のバージョンは以下のようになった。 Redmine 本体は現時点の最新版である。 表示は無いが、PostgreSQL は移行前と同じマイナーバージョンを利用し、データ復元時のトラブルリスクをできるだけ回避できるようにした。

Environment:
  Redmine version                4.1.1.stable
  Ruby version                   2.6.6-p146 (2020-03-31) [x86_64-linux-musl]
  Rails version                  5.2.4.2
  Environment                    production
  Database adapter               PostgreSQL
  Mailer queue                   ActiveJob::QueueAdapters::AsyncAdapter
  Mailer delivery                smtp
SCM:
  Subversion                     1.12.2
  Mercurial                      5.3.2
  Bazaar                         2.7.0
  Git                            2.24.3
  Filesystem                     
Redmine plugins:
  no plugin installed

ちなみに、今回の手順では環境の移植 (さくらVPS → Amazon EC2 + Docker) と、 Redmine のバージョンアップ (3.4.3 → 4.1.1) を同時に行なっているため そこそこ危ういことをしている。安全のためには、さくらVPS上で Redmine 4.1.1 に上げてから AWS に持って行くか、EC2 で Redmine 3.4.3 を構築してからバージョンアップするかして 二段階で移行するべきだ。でも一段階でやっても問題が起きなかったので、そのまま紹介する。

3. Redmine データのバックアップ

まずはデータのバックアップから始める。

Redmineのチケットや設定の情報はデータベースに保持されている。 さくらVPS側の PostgreSQL で redmine データベースをダンプした。

# su - postgres
$ pg_dump -U postgres -Fc -h localhost -p 5124 -f redmine-20200703.sqlc redmine

アップロードしたファイルとプラグインは Redmine のディレクトリ内にファイルとして保存されている。

# su - redmine
$ cd redmine
$ tar zcvf ~/files.tar.gz files
$ tar zcvf ~/plugins.tar.gz plugins

その他、必要ならテーマ等 public も取っておけばいいだろう。 バックアップしたファイルは AWS にアップロードするため、いったんローカルにダウンロードした。

なおプラグインは移植後うまく動かなかったが、別に使っていなかったので無かったことにした。

4. Amazon EC2 インスタンス作成

普通?に Amazon EC2 のインスタンスを作成した。ざっくり要点だけ挙げておく。

  • AMI: Amazon Linux2
  • Instance Type: とりあえず t2.micro (あとで費用削減のため変更)
  • Volume: 汎用 SSD 16GB (8GB でも十分だった)
  • Elastic IP: 固定IPアドレスを1つ発行
  • Security Group: HTTP (80), HTTPS (443) を 0.0.0.0 に公開。SSH (22) は MyIP のみ。

その他、IAM グループ、ユーザー、SSH 接続用の鍵ファイルは適当に作成。

発行した固定 IP アドレスは独自ドメインに紐付けて利用するが、 私の場合はさくら VPS で稼動している自前 DNS に登録したため、本稿では省略する。 いつか Route53 に移行するかもしれない。

5. Amazon EC2 環境準備

5.1. Docker インストール

以下は EC2 に SSH で接続して作業する。

Last login: Wed Jul  8 19:22:46 2020 from 略

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/

直接インストールするのは Docker のみ。

$ sudo su - 
# yum update
# yum install docker

Docker の確認:

# docker -v
Docker version 19.03.6-ce, build 369ce74

Docker デーモンの開始と自動起動の有効化:

# systemctl start docker
# systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
# systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
   Active: active (running) since 金 2020-07-03 10:33:01 UTC; 11s ago
     Docs: https://docs.docker.com
 Main PID: 18749 (dockerd)
   CGroup: /system.slice/docker.service
           └─18749 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --default-uli...

ec2-user ユーザーでも Docker を使えるようにするため、 docker グループに ec2-user ユーザーを追加。 変更が反映されるためには、いったん SSH をログアウトする必要があった。

# usermod -a -G docker ec2-user
# exit
$ exit

docker-compose のインストール:

# curl -L "https://github.com/docker/compose/releases/download/1.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# chmod 0755 /usr/local/bin/docker-compose
# exit
$ docker-compose -v
docker-compose version 1.26.1, build f216ddbf

参考 URL:

5.2. SSL 証明書発行

Redmine へは HTTPS で接続したいので、Let’s Encrypt を利用し無料で証明書を発行した。

AWS の機能を使うともっと簡単に済むが、自前で発行した方が安くなる。 Amazon ELB がすごくお高いらしい。

証明書の初回の発行を Docker Compose の中で行うのは無駄に面倒なため、 先に一時的な Nginx を構築して証明書発行だけ行い、それを取り出すことにする。

発行処理のために、サーバー環境を清潔に保つため一時的に Docker を使った。 centos のイメージを使うと円滑に処理が進んだのでおすすめする。 Docker で CentOS を起動し、Bash でログイン。 Let’s Encrypt の証明書発行のため、ホスト側の80番ポートを CentOS の80番ポートに繋ぐ必要がある。

$ docker run --rm -it -v $PWD/data/letsencrypt:/etc/letsencrypt -p 80:80 -p 443:443 centos bash

CentOS 上で Certbot を実行し証明書を発行:

# curl https://dl.eff.org/certbot-auto -o /usr/local/bin/certbot-auto
# chmod u+x /usr/local/bin/certbot-auto
# certbot-auto --standalone

上記のあといくつかの質問がインタラクティブに行われるが、適当にやる。

発行が完了すると data/letsencrypt に必要なファイルが生成されていた。 このディレクトリはあとで Certbot、Nginx コンテナにマウントして使う。

6. システム構築

いよいよ Redmine と、関連システムを構築・起動していく。

先述の通り、サーバー一式の起動には Docker Compose を使う。

6.1. Docker Compose

Docker Comopse を使用するために docker-compose.yml に設定を記述する。 次のように書いてみた。以下が全文である。

docker-compose.yml:

version: '3.1'
services:
  certbot:
    image: certbot/certbot
    restart: always
    volumes:
      - ./data/letsencrypt:/etc/letsencrypt
      - certbot:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
  nginx:
    image: nginx:1-alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./data/nginx/conf:/etc/nginx/conf.d
      - ./data/nginx/ssl:/etc/ssl/private
      - ./data/letsencrypt:/etc/letsencrypt
      - certbot:/var/www/certbot
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
  redmine:
    image: redmine:4.1-alpine
    restart: always
    volumes:
      - ./data/redmine/conf/configuration.yml:/usr/src/redmine/config/configuration.yml
      - ./data/redmine/files:/usr/src/redmine/files
      - ./data/redmine/plugins:/usr/src/redmine/plugins
    environment:
      REDMINE_DB_POSTGRES: db
      REDMINE_DB_DATABASE: redmine
      REDMINE_DB_USERNAME: postgres
      REDMINE_DB_PASSWORD: YOUR_PASSWORD
      #REDMINE_PLUGINS_MIGRATE: 1
  db:
    image: postgres:9.5-alpine
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./backup/db:/backup
    environment:
      POSTGRES_DB: redmine
      POSTGRES_PASSWORD: YOUR_PASSWORD
volumes:
  db-data:
  certbot:

ファイル構成は以下の通り:

.
├── backup
│   └── db
│       └── redmine-20200703.sqlc
├── data
│   ├── letsencrypt
│   ├── nginx
│   │   ├── conf
│   │   │   └── proxy.conf
│   │   └── ssl
│   │       └── dhparam.pem
│   └── redmine
│       ├── conf
│       │   └── configuration.yml
│       ├── files
│       ├── plugins
│       └── themes
└── docker-compose.yml

参考 URL:

6.2. Let’s Encrypt

certbot は Let’s Encrypy の証明書を取得するためのツールである Certbot を起動する。 初回の証明書は先の手順により発行したものをディレクトリごとマウントする。

このコンテナでは証明書の更新だけ行い、12時間毎に certbot renew を起動する。

このテクニックは Medium の記事を参考にした。

参考 URL:

6.3. Nginx

Web サーバーとして Nginx を使う。 外部からのリクエストは Redmine で直接受けるのではなく、 一旦 Nginx で受けて Redmine へリバースプロキシする。

Nginx では以下の機能を担当する:

  • Let’s Encrypt 証明書更新のためのチャレンジ応答
  • HTTP リクエストを HTTPS リクエストへリダイレクト
  • HTTPS 通信のための諸々
  • Redmine へのリバースプロキシ

以下のように proxy.conf を作成して Nginx に読み込ませた。 Docker 上で使う場合、他のファイル名・別の構成でやった方がより良い雰囲気も感じているが、 これでも問題は無いだろう。

data/nginx/conf/proxy.conf:

server {
    listen        80;
    server_name   redmine.example.net;

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
    }

    return 301    https://redmine.example.net$request_uri;
}
server {
    listen        443 ssl;
    server_name   redmine.example.net;

    ssl_certificate          /etc/letsencrypt/live/redmine.example.net/fullchain.pem;
    ssl_certificate_key      /etc/letsencrypt/live/redmine.example.net/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    ssl_trusted_certificate  /etc/letsencrypt/live/redmine.example.net/chain.pem;
    ssl_dhparam              /etc/ssl/private/dhparam.pem;

    add_header Strict-Transport-Security "max-age=63072000" always;

    ssl_session_cache shared:SSL:10m;

    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    location / {
        proxy_set_header     X-Forward-For $remote_addr;
        proxy_pass           http://redmine:3000/;
        client_max_body_size 500M;
    }
}

ドメイン名は適当に redmine.example.net にしておいた。環境に応じて適切に変更する必要がある。

/data/nginx/ssl には、Nginx から使うため dhparam.pem を次のように生成し配置しておく。 これは OpenSSL で Diffe-Hellman Key Exchange を行うときのパラメーターになるらしい。

# openssl dhparam 2048 -out dhparam.pem

私の場合、自分用の Redmine のため、自分の端末からしかアクセスしないので、 HTTPS 設定は全体的に厳しめにしておいた。古い環境は切り捨てていく。

本稿を参考にする場合には、要件に応じて適切に変更するべきである。

ssl_ciphers は呪文のようだが Mozilla SSL Configuration Generator で生成した。

参考 URL:

6.4. Redmine Configuration

ほとんどデフォルト設定のまま利用。 通知メールの送信設定だけ変更しており、 SMTP サーバーの設定を configuration.yml に書いた。以下は抜粋である。

data/redmine/conf/configuration.yml:

  # 略
  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      address: SMTP_SERVER
      port: 587
      authentication: :plain
      domain: SERVER_DOMAIN
      user_name: SMTP_USERNAME
      password: SMTP_PASSWORD
  # 略

6.5. Redmine データ復元

序盤で書いたように、Redmine のチケットや設定のデータはデータベースに保持されている。 アップロードしたファイルやプラグインは Redmine 配下のディレクトリにファイルとして保存されている。 データの復元ではこれらを新しい Redmine に与えてやる。

まず、アップロードしたファイルやプラグインについては、バックアップしたファイル群を Docker 経由で所定のディレクトリにマウントする。

ホスト側パス マウント先パス 説明
data/redmine/files /usr/src/redmine/files アップロードしたファイル群
data/redmine/plugins /usr/src/redmine/plugins プラグイン群

次にデータベース上のデータを移すためには、PostgreSQL の pg_restore を使い、 Redmine (Ruby on Rails) の機能でデータマイグレーションを実行する。

そのために一旦各サーバーを起動する必要がある。

PostgreSQL から取得したバックアップをコンテナ内から使えるよに、 バックアップファイルをホスト側の backup/db/redmine-20200703.sqlc に配置し、 PostgreSQL コンテナの /backup/redmine-20200703.sqlc にマウントした。

サーバーは以下のように Docker compose を使って一括起動できる。

$ docker-compose up -d 

PostgreSQL コンテナに入り、データベースにデータを流し込む。

$ docker-compose exec db bash
# pg_restore -U postgres -d redmine -c -n public /backup/redmine-20200703.sqlc

問題が無ければエラー無しに終了する。 -c -n public が無いとエラーが少しまたは大量に出力される。

成功したら PostgreSQL からはログアウトし、 今度は Redmine コンテナに入ってデータマイグレーションを実行する。

ここが一番緊張するところ。姿勢を正してコマンドを実行する。

$ docker-compose exec redmine bash
# RAILS_ENV=production bundle exec rake db:migrate
bash-5.0# RAILS_ENV=production bundle exec rake db:migrate
I, [2020-07-03T11:54:33.442284 #55]  INFO -- : Migrating to RenameCommentsToContent (20170723112801)
== 20170723112801 RenameCommentsToContent: migrating ==========================
-- rename_column(:comments, :comments, :content)
   -> 0.0031s
== 20170723112801 RenameCommentsToContent: migrated (0.0035s) =================

I, [2020-07-03T11:54:33.453236 #55]  INFO -- : Migrating to AddAuthorIdToTimeEntries (20180501132547)
== 20180501132547 AddAuthorIdToTimeEntries: migrating =========================
-- add_column(:time_entries, :author_id, :integer, {:default=>nil, :after=>:project_id})
   -> 0.0009s
== 20180501132547 AddAuthorIdToTimeEntries: migrated (0.0104s) ================

I, [2020-07-03T11:54:33.466950 #55]  INFO -- : Migrating to AddVerifyPeerToAuthSources (20180913072918)
== 20180913072918 AddVerifyPeerToAuthSources: migrating =======================
-- change_table(:auth_sources)
   -> 0.0079s
== 20180913072918 AddVerifyPeerToAuthSources: migrated (0.0083s) ==============

I, [2020-07-03T11:54:33.480175 #55]  INFO -- : Migrating to ChangeSqliteBooleansTo0And1 (20180923082945)
W, [2020-07-03T11:54:33.499343 #55]  WARN -- : Creating scope :sorted. Overwriting existing method Group.sorted.
W, [2020-07-03T11:54:33.530163 #55]  WARN -- : Creating scope :sorted. Overwriting existing method User.sorted.
W, [2020-07-03T11:54:33.535305 #55]  WARN -- : Creating scope :system. Overwriting existing method Enumeration.system.
== 20180923082945 ChangeSqliteBooleansTo0And1: migrating ======================
== 20180923082945 ChangeSqliteBooleansTo0And1: migrated (0.0000s) =============

I, [2020-07-03T11:54:33.681166 #55]  INFO -- : Migrating to ChangeSqliteBooleansDefault (20180923091603)
== 20180923091603 ChangeSqliteBooleansDefault: migrating ======================
== 20180923091603 ChangeSqliteBooleansDefault: migrated (0.0001s) =============

I, [2020-07-03T11:54:33.684446 #55]  INFO -- : Migrating to ChangeCustomValuesValueLimit (20190315094151)
== 20190315094151 ChangeCustomValuesValueLimit: migrating =====================
== 20190315094151 ChangeCustomValuesValueLimit: migrated (0.0000s) ============

I, [2020-07-03T11:54:33.687314 #55]  INFO -- : Migrating to AddTrackersDescription (20190315102101)
== 20190315102101 AddTrackersDescription: migrating ===========================
-- add_column(:trackers, :description, :string, {:after=>:name})
   -> 0.0023s
== 20190315102101 AddTrackersDescription: migrated (0.0027s) ==================

I, [2020-07-03T11:54:33.693039 #55]  INFO -- : Migrating to AddUniqueIdToImportItems (20190510070108)
== 20190510070108 AddUniqueIdToImportItems: migrating =========================
-- change_table(:import_items)
   -> 0.0034s
== 20190510070108 AddUniqueIdToImportItems: migrated (0.0037s) ================

I, [2020-07-03T11:54:33.700075 #55]  INFO -- : Migrating to ChangeRolesNameLimit (20190620135549)
== 20190620135549 ChangeRolesNameLimit: migrating =============================
-- change_column(:roles, :name, :string, {:limit=>255, :default=>""})
   -> 0.0011s
== 20190620135549 ChangeRolesNameLimit: migrated (0.0015s) ====================

うまくいったようだ。これにて作業完了である。

実施する場合には下記の Redmine 公式ドキュメントも見て理解しておくべき。

参考 URL:

7. 構築完了

全てが完了したら、Redmine を Web ブラウザで開き、正常動作すること、 データがちゃんと移行できたことを確認できる。

想定外にもめちゃくちゃサクサク動くようになった。 Redmine が高速に反応すると、生活も明るくなるようだ。

なお、起動がうまくいかない場合には Docker compose を使ってログを表示してみよう。

Nginx, Redmine のログ:

$ docker-compose logs nginx
$ docker-compose logs redmine

または、非デーモンで起動してみると結構わかることが多い。

$ docker-compose up

8. コスト削減

趣味の Redmine は一切の利益を生み出さないため、とにかく費用を節約したい。 今回の構成で一番費用がかかるのは、EC2 インスタンスである。

AWS 利用開始1年目は無料枠があるので t2.micro がタダで使えるが、 2年目以降になっているとインスタンスの起動時間に応じ課金されてしまう。 t3.micro の利用料金は $0.0152/Hour なので、年間にすると 15,000 円近くになる。高い。

というわけでコスト削減のために色々と思案したのだが、 結論からいくと、EC2 の t3.nano または t3a.nano の3年前払い (リザーブドインスタンス) が年 $17.52 で一番安い。 実際にはストレージの料金が発生するのでもう少し高くなるとは言え、 年間 2,000 円というのは他社サービスと比べても圧倒的な安さだ。

t3.nano のリザーブドインスタンスで運用する予定である。

ただし t3.nano の RAM 512 MiB 環境だと、 上記の docker-compose.yml では一回で全サービスを起動できないことが多い。 この解決方法はまだ見付けていないので、わかったらまた書くつもりだ。

8.1. 価格比較

EC2 Ondemand Instance

Plan Cost/Hour Cost/Year Spec.
t3a.nano $0.0061 $53.44 2 vCPUs, 512 MiB
t3.nano $0.0068 $59.57 2 vCPUs, 512 MiB
t2.nano $0.0076 $66.58 1 vCPU, 512 MiB
t3a.micro $0.0122 $106.87 2 vCPUs, 1024 MiB
t3.micro $0.0136 $119.14 2 vCPUs, 1024 MiB

注意すべきは、EC2 のオンデマンドインスタンス (普通のインスタンス) 停止中は 別途 Elastic IP $0.005/Hour ($43.8/Year) がかかることだ。 インスタンスの利用時間に応じて課金されるならば、 使わないときはインスタンスを止めておけばよくね? と考えるのは自然だが、 Elastic IP で固定 IP アドレスを付与していると、 インスタンス停止中だけ課金される。これが年間 4,500 円位になってしまうため、 オンデマンドインスタンスを使っている限りこれより安くなることはない。

よく考えられた方式だと思う。

EC2 Reserved Instance (1-3 years pre-payed)

Plan Cost/Hour Cost/Year
t3.nano 3年前払い $0.002 $17.52
t3a.nano 3年前払い $0.002 $17.52
t2.nano 3年前払い $0.004 $35.04
t3.micro 3年前払い $0.005 $43.80
t3a.micro 3年前払い $0.005 $43.80
t3.nano 1年前払い $0.004 $35.04
t3a.nano 1年前払い $0.004 $35.04
t2.nano 1年前払い $0.005 $43.80
t3.micro 1年前払い $0.009 $78.84
t3a.micro 1年前払い $0.009 $78.84

リザーブドインスタンスならば、常時起動していても安い。

Amazon Lightsail

Plan Cost/Hour Cost/Year Spec.
$3.50/Month - $42.00 1 core, 512 MiB
$5.00/Month - $60.00 1 core, 1024 MiB

EC2 の他に Web サイト運用のための機能が一式揃った Lightsail というサービスもある。 こちらは月額課金のシンプルな価格設定だが、年 4,500 円位が確定することになる。 Elastic IP を付与したインスタンスを1年中停止した時の課金額より微妙に安く、 ライトユーザーを Lightsail に誘導したい Amazon の意図が感じられる。

8.2. 利用時だけ EC2 インスタンスを起動する方法 (使っていない)

上記の検討の結果、使わないことになったが、せっかく作ったので公開しておく。

普段は EC2 インスタンスを停止しておき、 Redmine へのアクセスがあったら都度インスタンスを起動する仕組みである。

利用頻度の低いサービスならばインスタンス起動時間を最小に抑えることができる。

ただし以下の残念な点がある:

  • Elastic IP 料金がかかるので結局年額 4,000 円を下回ることは無い。
  • インスタンスが起動し Redmine が利用可能になるまで1分弱かかり、利用感はかなり悪い。
  • Nginx はインスタンスの外側に常時起動しておく必要があり、運用が面倒くさい。

とはいえ、パッと思い付かないが、特定状況においては意外と役立ちそうな気もする。

8.3. CloudWatch alarms

EC2 の CPU 使用率がある水準より低い状態が一定期間続いたら、 Redmine が使われていないと判断しインスタンスを停止する。

これは CloudWatch alarms (EC2 Dashboard > Instances > Instance > Monitoring > CloudWatch alarms) を使って実現できた。

以下の設定で大体イメージ通りの動きになった。

  • Take the action: Stop this instance
  • Whenever: Maximum of CPU Utilization
  • Is: <= 0.5 Percent
  • For at least: 2 consecutive period(s) of 5 Minutes
  • Missing data treatment: Treat missing data as missing

8.4. Nginx

インスタンスとは関係無い常時起動のサーバーに Nginx を起動しておく。

仕組みは以下:

  • /projects, /issues, /search のロケーションへリクエストがあったら、EC2 インスタンス起動 API (後述) を実行する。
  • EC2 インスタンス起動 API は Nginx の Lua 拡張で ngx.location.capture を使って非同期に呼び出す。
  • EC2 インスタンス起動 API が "pending" を応答してきたら、インスタンス起動中なので ngx.sleep で処理を遅延させ、その後リバースプロキシでリクエストする。
  • 上記以外のロケーションは従来通り。

Nginx の設定ファイルは以下:

server {
    listen    80;

    proxy_set_header      Host redmine.example.net;
    proxy_set_header      X-Forwarded-Proto https;
    proxy_set_header      X-Forward-For $remote_addr;

    location / {
        proxy_pass            http://redmine:3000/;
        client_max_body_size  500M;
    }

    location /issues {
        if ($request_method = GET) {
            rewrite ^/(.*) /redmine_start_with/$1 last;
            proxy_pass http://localhost;
        }
        proxy_pass            http://redmine:3000/issues;
    }

    location /projects {
        if ($request_method = GET) {
            rewrite ^/(.*) /redmine_start_with/$1 last;
            proxy_pass http://localhost;
        }
        proxy_pass            http://redmine:3000/projects;
    }

    location /search {
        if ($request_method = GET) {
            rewrite ^/(.*) /redmine_start_with/$1 last;
            proxy_pass http://localhost;
        }
        proxy_pass            http://redmine:3000/search;
    }

    location /redmine_start_with {
        access_by_lua_block {
            local res = ngx.location.capture("/redmine_start")
            if res.body == "pending" then
                ngx.sleep(60)
            end
        }
        rewrite               ^/redmine_start_with/(.*) /$1 break;
        proxy_pass            http://redmine:3000;
    }

    location = /redmine_start {
        proxy_set_header  Host hogehoge.execute-api.ap-northeast-1.amazonaws.com;
        proxy_pass https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/default/ControlRedmineInstance;
    }
}

8.5. Lambda

EC2 インスタンス起動 API は AWS Lambda で作成し、 API Gateway で HTTP から利用できるようにしておく。

Lambda 関数:

var AWS = require('aws-sdk');

var region = 'ap-northeast-1';
var instances = ['i-0123456789abcdef'];

var ec2 = new AWS.EC2({
    region: region
});

exports.handler = async (event) => {
    console.log('Event:', event);
    var req = ec2.startInstances({
        InstanceIds: instances
    }, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            console.log(data);
            return data;
        }
    });
    var res = await req.promise();
    if (res && res.StartingInstances && res.StartingInstances.length >= 0) {
        const instance = res.StartingInstances[0];
        return instance.CurrentState.Name;
    } else {
        return 'error'
    }
};

参考 URL:

8.6. Docker Comopose

この構成では Docker 上に Nginx を起動する必要が無く、Redmine で直接リクエストを受信する。 EC2 の Security Group 等で Redmine の 3000 番ポートだけ空いていることを要確認。

$ cat docker-compose.yml.3000 
version: '3.1'
services:
  redmine:
    image: redmine:4.1-alpine
    restart: always
    ports:
      - "3000:3000"
    volumes:
      - ./data/redmine/conf/configuration.yml:/usr/src/redmine/config/configuration.yml
      - ./data/redmine/files:/usr/src/redmine/files
      - ./data/redmine/plugins:/usr/src/redmine/plugins
      - ./data/redmine/themes/bleuclair:/usr/src/redmine/public/themes/bleuclair
    environment:
      REDMINE_DB_POSTGRES: db
      REDMINE_DB_DATABASE: redmine
      REDMINE_DB_USERNAME: postgres
      REDMINE_DB_PASSWORD: PASSWORD
  db:
    image: postgres:9.5-alpine
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data
      #- ./backup/db:/backup
    environment:
      POSTGRES_DB: redmine
      POSTGRES_PASSWORD: PASSWORD
volumes:
  db-data:
  certbot:

9. まとめ

5年間育てた個人的 Redmine を無事に引越すことに成功した。 さくら VPS 相乗り環境から、Amazon EC2 + Docker 環境に移行したところ、 すごくサクサク動くようになり満足している。