Skip to content

CreateInstanceOnHeroku

zunda edited this page Aug 23, 2024 · 109 revisions

Herokuにクレジットカードを登録してあるアカウントを持っている場合の手順をまとめてみます。

Heroku CLIとgitコマンドを使ってデプロイする

Herokuアプリの準備

Herokuアプリを作成し、必要なビルドパックを設定し、assets:precompileに必要なconfig varを設定します。

export HEROKU_APP=<アプリ名>しておくことで、Heroku CLIに-aオプションを渡す必要がなくなります。

export HEROKU_APP=<希望のアプリ名>
heroku create $HEROKU_APP
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-activestorage-preview
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-apt
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add heroku/ruby
heroku addons:create heroku-postgresql
heroku addons:create heroku-redis

https://github.com/heroku/heroku-buildpack-activestorage-preview はffmpeg関連のバイナリを提供してくれます。圧縮後のSlugの大きさが、heroku-24でAptfileでffmpeg関連のパッケージを入れた場合の630MB前後から270MBへと改善されます。このビルドパックを利用する場合は、Aptfileから関連のパッケージを除き、.profileから関連のパスを除くことができます。

アセットのプリコンパイルの時にOTP_SECRETが設定されている必要があるようです。

heroku config:set OTP_SECRET=`ruby -r securerandom -e 'puts SecureRandom.hex(64)'` \
    RAILS_SERVE_STATIC_FILES=true

データベースの初期設定の時にドメイン名でインスタンスアカウントが作られるのでLOCAL_DOMAINも先に設定しておきます。カスタムドメインを利用する予定ならそちらを設定しましょう。Herokuアプリケーションのデフォルトドメインは推測不能なものになりました。設定されたデフォルトドメインをheroku domainコマンドで参照してLOCAL_DOMAINを設定します。

heroku domains
=== example-app Heroku Domain

example-app-1234567890ab.herokuapp.com

jqコマンドが利用可能なら下記のようなコマンドで設定できます。

heroku config:set LOCAL_DOMAIN=`heroku domains --json | jq -r '.[] | select(.kind=="heroku").hostname'`

2024年4月17日にActiveRecord Encryptionを利用するようになってから、下記のように追加の環境変数の設定が必要になりました。

The ActiveRecord encryption feature requires that these variables are set:

  - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
  - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
  - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY

Run `bin/rails db:encryption:init` to generate values and then assign the environment variables.

手元にMastodonの走る環境を用意してキーやソルトを生成し、Herokuアプリのconfig varに設定してみました。

$ bundle install
$ heroku config:set `RAILS_ENV=production bin/rails db:encryption:init | grep ^ACTIVE_RECORD_ENCRYPTION_'`

アセットのプリコンパイルがNode.jsからのERR_OSSL_EVP_UNSUPPORTEDのようなエラーで失敗する場合には上記のようにNODE_OPTIONS--openssl-legacy-providerに設定しておく必要があるようです。

Railsが7に更新された頃から、heroku/nodejsビルドパックを追加しておかないと、assets:precompileerror Command "webpack" not found.といったエラーで失敗するようになりました。

RAILS_SERVE_STATIC_FILESは、Add cache headers to static files served through Railsのマージされていない環境ではenabledとします。

アプリケーションログの保存にPapertrailなどのログ収集add-onも付けておくとよいでしょう。

mastodon-3.5.1のリリース後、PumaからRedisへの接続がプールされるようになりました。これに伴って、Heroku Redisへの接続数が制限を越えてしまう可能性が高くなったようです。最初から、キャッシュ用のRedisSidekiq用のRdisを最初から作成しておくのがよいでしょう。

heroku addons:create heroku-redis --as CACHE_REDIS
heroku addons:create heroku-redis --as SIDEKIQ_REDIS

稼働中のサーバにHeroku Redisを追加した記録もあります。

デプロイ

コードをHerokuにデプロイします。

例えばv2.8.0タグを使い、ワーキングコピーではheroku-branchブランチで管理するなら、

VERSION=v2.8.0
git clone https://github.com/tootsuite/mastodon.git
cd mastodon
heroku git:remote
git checkout -b heroku-branch refs/tags/$VERSION
git push heroku heroku-branch:master

git pushコマンドでHerokuへのコードのデプロイとHerokuでのアプリケーションのビルドが行われます。数分かかるのでしばらく待ちます。うまくいかなかった場合には端末にエラーメッセージが表示されているかもしれません。

Slugの大きさが制限を越えてデプロイできなかった場合には、Heroku Helpよりサポートチケットで関連するアプリのSlugの大きさの制限の緩和を依頼する必要があります。

Redisへの接続が自己署名証明書によるTLSの場合には、デプロイ前に、PumaとSidekiqへの変更ストリーミングへの変更が必要でしょう。

データベースの初期設定をします。

heroku run rails db:migrate

アプリケーションの設定をします。SECRET_KEY_BASEはRubyビルドパックが設定してくれます。

heroku config:set \
  HEROKU=true \
  SMTP_FROM_ADDRESS=notifications@localhost \
  `heroku run rake mastodon:webpush:generate_vapid_key | grep ^VAPID_`

ユーザー登録

最初のユーザーをadminとして登録します。初期パスワードは端末に表示されます。

heroku run tootctl accounts create \
  <表示名> \
  --email <メールアドレス> \
  --confirmed \
  --role Owner

必要な場合には、念のためアカウントの登録はできないようにしておきます:

heroku run tootctl settings registrations close

Dynoをスケールする

heroku ps:scale web=1 worker=1

ここまででMastodonを使ってみることができるはずです。

heroku open

でMastodonを開き、Log inのリンクからログインしてみてください。アップロードされたファイルはDynoの再起動で消えちゃうので注意してください。

うまくいかない時は

アプリケーションのログを見ながらアクセスしてみるとエラーを確認できるかもしれません。

heroku logs -t -a $APP_NAME

追加の設定

ファイルストアの設定

Mastodonはデフォルトではdynoのファイルシステムにアイコンやメディアなどのファイルを格納します。これらのファイルは1日に1度のdynoの再起動の際に消えてしまいます。

Amazon S3のバケットを作成し、そこにファイルを格納するようにします。著者はAWSについて詳しくないので下記の記述には誤りや危険な設定が含まれる可能性があります。気づいたことがあればお知らせください。

バケットの作成

https://aws.amazon.com/ からログインし、AWS Management Consoleに進み、AWS servicesからS3を検索し、クリックして、

  • Create bucketをクリック、
  • bucket nameを設定 (ファイルのダウンロード元のホスト名の一部として使われます)、
  • regionを設定、
  • versioning、logging、tagsを適宜設定、
  • permissionはデフォルト (Ownerのみ読み書き)、

して作成する。

(2023-06-03追記) 現在は、Access Control List (ACL)を有効にする必要があるようです。このサーバでは、バケットのダッシュボードからPermissionsタブを開きAccess control list (ACL)節を確認すると下記のようになっていました。

Grantee Objects Bucket ACL
Bucket owner (your AWS account) List, Write Read, Write
Everyone (public access) - -
Authenticated users group (anyone with an AWS account) - -
S3 log delivery group - -

IAMユーザーの作成

まずIAMユーザーに付与する権限を定義します。

https://aws.amazon.com/ からログインし、AWS Management Consoleに進み、AWS servicesからIAMを検索し、クリックして、

  • Policiesをクリック、
  • Create policyをクリック、
  • Create Your Own Policyをクリック、
  • Policy NameとDescriptionを適宜設定、Policy Documentに下記をペースト、
  • Create Policyをクリック
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::<バケット名>",
                "arn:aws:s3:::<バケット名>/*"
            ]
        }
    ]
}

次にIAMユーザーを作成します。

https://aws.amazon.com/ からログインし、AWS Management Consoleに進み、AWS servicesからIAMを検索し、クリックして、

  • Usersをクリック、
  • Add userをクリック、
  • User nameを設定、
  • Programmatic accessを選択、
  • Attach existing policies directlyを選択、
  • 上記で作成したポリシーを検索して選択、
  • Reviewをクリック、
  • Create userをクリック

Access key IDとSecret access keyが表示されている状態で次に進みます。

アプリケーションの設定

Config varを設定します。S3_HOSTNAMEはリージョンによって異なるかもしれません。下記の例はus-east-1のものです。

$ heroku config:set S3_ENABLED=true
$ heroku config:set S3_BUCKET=<バケット名>
$ heroku config:set AWS_ACCESS_KEY_ID=<作成したIAMユーザーのものをペースト>
$ heroku config:set AWS_SECRET_ACCESS_KEY=<作成したIAMユーザーのものをペースト>
$ heroku config:set S3_REGION=<us-east-1などのリージョン名>
$ heroku config:set S3_PROTOCOL=https
$ heroku config:set S3_HOSTNAME=s3.amazonaws.com

ここまででアイコンやメディアファイルがS3に保存されるようになったはずです。Mastodonから何かアップロードし、AWSのS3のコンソールから確認してみてください。

Pipelinesを使う場合には、slugをビルドするアプリケーションではS3_ENABLEDfalseにしておくと、precompileされたassetはアプリケーションから、稼働中にアップロードされたファイルはS3から利用するようになります。

Bucketterアドオンが利用できるかもしれません。$5/月から。

CORSヘッダの設定

MastodonのWebUIからJavaScriptによってリソースを読み込むことができるよう、Allow-Access-Control-Allow-MethodsAccess-Control-Allow-Originレスポンスヘッダを追加します。AWSにログインし、S3のダッシュボードからPermissions-Cross-origin resource sharing (CORS)と進んで、下記のようなJSONを入力してSaveボタンをクリックしてください。

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

ソースコードへのリンク

MastodonはAGPLでライセンスされているので、利用者の求めがあればソースコードを提供する必要があります。コードにインスタンス独自の改修をしている場合には、公開レポジトリへのリンクを公開しておくと良いかもしれません。下記のようなイニシャライザを例えばconfig/initializer/source.rbとして追加しておくことで、/about/about/moreで閲覧できるページ下部の「ソースコード」へのリンクが指定したものになります。

# frozen_string_literal: true
module Mastodon
  module Version
    module_function
    def source_base_url
      'https://github.com/<ユーザー名>/mastodon'
    end
  end
end

デプロイ時のコミットハッシュも記録しておく場合には、下記のようなrakeタスクを例えばlib/tasks/version.rakeとして置いておくことで、デプロイ中にイニシャライザを追加できます。

namespace :source do
  desc 'Record source version'
  task :version do
    hash = ENV['SOURCE_VERSION']  # available on Heroku while build
    if hash.blank?
      begin
        hash = `git show --pretty=%H 2>/dev/null`.strip
        # ignore the error: fatal: Not a git repository
      rescue Errno::ENOENT  # git command is not available
      end
    end
    unless hash.blank?
      hash_abb = hash[0..7]
      mastodon_version_to_s = Mastodon::Version.to_s
      File.open('config/initializers/version.rb', 'w') do |f|
        f.write <<~_TEMPLATE
          # frozen_string_literal: true
          module Mastodon
            module Version
              module_function

              def to_s
                "#{mastodon_version_to_s} at #{hash_abb}"
              end
              def source_base_url
                'https://github.com/ユーザー名/mastodon'
              end
              def source_tag
                "#{hash}"
              end
            end
          end
        _TEMPLATE
      end
    end
  end
end

task 'assets:precompile' => ['source:version']

メールの設定

ここではMailgunを使ってみます。

heroku addons:create mailgun
heroku addons:open mailgun

電話番号を入力してアカウントを認証してもらい、一番下のAuthorized RecipientsにMastodonのユーザー登録に利用するメールアドレスを追加し、ベリファイしておく。

Config varを設定する

$ heroku config:set SMTP_FROM_ADDRESS=<メールアドレス> 
$ for v in SMTP_SERVER SMTP_PORT SMTP_LOGIN SMTP_PASSWORD; do
  heroku config:set $v=`heroku config:get MAILGUN_$v`
done

カスタムドメインでの運用

下記のコマンドで、カスタムドメインを使ってアプリケーションにアクセスできるようになります。

heroku domains:add <インスタンスのFQDN>

DNSの設定

インスタンスのFQDNが、CNAMEで、上記のコマンドで表示されたターゲット<インスタンスのFQDN>.herokudns.comを指すように設定する。 トップレベルドメインを使う場合には、ANAMEあるいはALIASレコードにしてください (業者によっては利用できません)。

HTTPSの利用

Automated Certificate Managementを利用する場合には、Hobby以上のdynoを利用する必要があります (dynoの稼働時間に比例した料金がかかります)

heroku ps:type hobby
heroku certs:auto:enable

しばらくして、下記のコマンドがOKを表示するようになれば、HTTPSでのアクセスが可能です。

heroku certs:auto

下記でMastodonがHTTP経由のアクセスをHTTPSにリダイレクトします。自ドメインの設定も直しておく。

heroku config:set LOCAL_HTTPS=true
heroku config:set LOCAL_DOMAIN=<インスタンスのFQDN>

Streaming APIの有効化

Mastodonではブラウザのリアルタイムに近い更新にNode.jsによって実装されているstreaming APIを利用します。上記で作成したアプリケーションではリクエストはRailsが処理しますので、streaming APIが利用できません。下記のように2つめのアプリケーションを作成してリクエストをそちらに送ることで対応できます。(ActionCableを使うようにコードを書き換えられればいいのかな?)

APP_NAME=<上記で作成したアプリ名>
STREAMING_APP_NAME=<上記とは違う、希望のアプリ名>
heroku create $STREAMING_APP_NAME
heroku buildpacks:add heroku/nodejs -a $STREAMING_APP_NAME
heroku addons:attach $APP_NAME::DATABASE --as DATABASE -a $STREAMING_APP_NAME
heroku addons:attach $APP_NAME::REDIS --as REDIS -a $STREAMING_APP_NAME

streaming/index.jsで検出されたCPU数に基づくデフォルトの並列度 7ではFree dynoの512 MBのメモリでは若干足りないようです。ここでは5まで減らしてみます:

heroku config:set STREAMING_CLUSTER_NUM=5 -a $STREAMING_APP_NAME

最近のnodeでは自己署名証明書を提示するPostgreSQLサーバへはERR! Error: self signed certificateというエラーを記録して接続を拒否します。これを避けるには、このようにして環境変数で自己証明書を受け入れるようにコードを変更してそのようにconfig varを設定しておくこともできます。

heroku config:set DB_SSLMODE=ssl_noverify -a $STREAMING_APP_NAME

Mastodonのコードのうち、Procfileを下記のように書き換えて、

web: node ./streaming

レポジトリに登録する。

git add Procfile
git commit -m 'Start streaming service'

このレポジトリを新しく作ったアプリケーションにデプロイする。手元では、ブランチを作成してそこにpushし、GitHub syncでそのブランチからコードをデプロイしています。

Streaming APIに新しく作ったアプリケーションを使うように設定する。

heroku config:set STREAMING_API_BASE_URL=wss://$STREAMING_APP_NAME.herokuapp.com -a $APP_NAME

Puma、Sidekiq、Nodeのプロセス数やスレッド数によっては、PostgreSQLやRedisへの接続数がプランの制限を超える可能性があります。その場合は適宜調整が必要です。

Faviconを変更する

app/javascript/images/logo.svgとして透明な背景のSVGを、app/javascript/images/app-icon.svgとして不透明な背景で余白のあるSVGを用意しておいて、rakeタスクにpngファイルとicoファイルを生成してもらいます。

$ sudo apt install librsvg2-bin
$ RAILS_ENV=development bundle exec rake branding:generate_app_icons

ファイルがごっそり変更されます。

さらに、Safariの非アクティブなタブのアイコンには、黒1色で背景が透明なSVGをapp/javascript/images/logo-symbol-icon.svgとして用意する必要があるようです。不透明部分の色は、app/views/layouts/application.html.haml下記の行で変更します。

%link{ rel: 'mask-icon', href: asset_pack_path('media/images/logo-symbol-icon.svg'), color: '#6364FF' }/
$ git status
  :
	modified:   app/javascript/icons/android-chrome-144x144.png
	modified:   app/javascript/icons/android-chrome-192x192.png
	modified:   app/javascript/icons/android-chrome-256x256.png
	modified:   app/javascript/icons/android-chrome-36x36.png
	modified:   app/javascript/icons/android-chrome-384x384.png
	modified:   app/javascript/icons/android-chrome-48x48.png
	modified:   app/javascript/icons/android-chrome-512x512.png
	modified:   app/javascript/icons/android-chrome-72x72.png
	modified:   app/javascript/icons/android-chrome-96x96.png
	modified:   app/javascript/icons/apple-touch-icon-1024x1024.png
	modified:   app/javascript/icons/apple-touch-icon-114x114.png
	modified:   app/javascript/icons/apple-touch-icon-120x120.png
	modified:   app/javascript/icons/apple-touch-icon-144x144.png
	modified:   app/javascript/icons/apple-touch-icon-152x152.png
	modified:   app/javascript/icons/apple-touch-icon-167x167.png
	modified:   app/javascript/icons/apple-touch-icon-180x180.png
	modified:   app/javascript/icons/apple-touch-icon-57x57.png
	modified:   app/javascript/icons/apple-touch-icon-60x60.png
	modified:   app/javascript/icons/apple-touch-icon-72x72.png
	modified:   app/javascript/icons/apple-touch-icon-76x76.png
	modified:   app/javascript/icons/favicon-16x16.png
	modified:   app/javascript/icons/favicon-32x32.png
	modified:   app/javascript/icons/favicon-48x48.png
	modified:   app/javascript/images/app-icon.svg
	modified:   app/javascript/images/logo.svg
	modified:   app/javascript/images/logo-symbol-icon.svg
        modified:   app/views/layouts/application.html.haml
	modified:   public/favicon.ico

変更点をcommitしてデプロイします。

$ git add public/favicon.ico app/javascript/{icons,images} app/views/layouts/application.html.haml
$ git commit

副作用として、設定画面の左上のアイコンが、app/javascript/images/logo.svgのものになります。

参考文献