行動すれば次の現実

テック中心の個人ブログ

Heroku Data for Redisをアップグレードする方法(v4 to v6)

Herokuからredisのバージョンをv6にアップブレードするようにと催促メールが届いたので対応しました。 公式のアップグレード手順に従って対応しましたが、それだけだとエラーが発生してしまいましたので記事にすることにしました。同じ轍を踏まないためにも参考なれば幸いです。

Herokuからのメール(抜粋)

Running up-to-date software versions is essential for maintaining a highly-available and secure fleet of Heroku Data for Redis instances. Your Redis database redis-xxx on your-app is running a deprecated version (4.0.14) and will not be supported after 30 Jun, 2023.

Redis6ではTLS接続が必須になるため設定ファイルに注意!

Redis6からはPremiumプラン以上の場合はTLS接続が必須となったようなので、Sidekiq等でRedisの接続定義を変更する必要があります

Sidekiq.configure_server do |config|
  config.redis = {
    url: ENV["REDIS_URL"],
    # ↓この部分を追加
    ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
  }
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: ENV["REDIS_URL"],
    # ↓この部分を追加
    ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
  }
end

この定義を追加しなかった場合、OpenSSL::SSL::SSLError (hostname "xxxxxx" does not match the server certificate):というエラーが発生してアプリからRedisに接続ができなくなってしまいます。

なぜVERIFY_NONEなのかというとHeroku内部ではSSLを使用していないためです。HerokuのルーターレベルでSSLを終了させて、内部接続にはHTTPSを使用しているため、VERIFY_NONEで問題ないとのことでした。

https://stackoverflow.com/questions/65834575/how-to-enable-tls-for-redis-6-on-sidekiq

アップグレード手順

上記の考慮を踏まえてアップグレードを実行します。 基本的には公式のアップグレード手順と同じことをしています。

1. メンテナンスモードをON

heroku maintenance:on

2. アップグレード実行

heroku redis:upgrade --version 6.2

3. アップグレード進捗状況の確認

heroku redis:info

=== redis-angular-51206 (REDIS_URL)
Plan:                   Premium 0
Status:                 preparing (version upgrade in progress)
Created:                2021-09-08 20:41
Version:                5.0.12
Timeout:                300
Maxmemory:              noeviction
Maintenance:            not required
Maintenance window:     Tuesdays 21:00 to Wednesdays 01:00 UTC
Persistence:            AOF
HA Status:              Available
Requires TLS:           No
Keyspace Notifications: Disabled

私の環境の場合、preparingからavailableに変わるまで20分程度の時間が掛かりました。

4. メンテナンスモードをOFF

heroku maintenance:off

終わりに

以上でアップグレード作業は完了です。 途中OpenSSL::SSL::SSLErrorが発生してかなり焦りましたがすぐに対応できて良かったです。

テスト環境でもリハーサルでアップグレードをしたのですが、その時はエラーが発生しませんでした。なぜ発生しなかったかというとテスト環境ではRedisをPremiumプランにしていなかったためです。Premium未満の場合、TLS接続は必須ではないためOpenSSL::SSL::SSLErrorのエラーが発生しないため防ぐことが出来ませんでした。

テスト環境でも同スペックの構成をしておく必要があるのだと改めて痛感しました。。

参考

https://ogirginc.github.io/en/heroku-redis-ssl-error https://devcenter.heroku.com/ja/articles/connecting-heroku-redis#connecting-in-ruby

【超簡単】HerokuのpostgresDBのbackupデータをrestoreする方法

以下のコマンドで簡単にバックアップしたファイルを使って、簡単にリストアすることが出来ます。

heroku pg:backups:restore b042 DATABASE_URL --app your-appname
  • b042の部分はバックアップデータの名称を指定します
  • DATABASE_URLの部分は、本番環境を指定する場合はこのままで良いです

参考

Heroku PGBackups | Heroku Dev Center

外部キー(null許可)を複数持っているテーブルで検索する時のSQLクエリのサンプル

レガシーシステムの改修案件で以下のような正規化されていない条件で検索することがありましたので、記事にまとめました。

テーブル設計

  • orders (注文)

    • id
    • supplier_id
    • customer_id
  • suppliers (取引先)

    • id
    • is_active (boolean)
  • customers (顧客)

    • id
    • is_active (boolean)

例えば注文テーブルがあり、「取引先の注文」と「顧客の注文」のいずれかを一つのテーブルで管理しているとします。どちらも設定されることはシステムとしてありえないと仮定します。

この注文テーブルから、取引先がアクティブ(is_active=TRUE)である、または顧客がアクティブ(is_active=TRUE)であるテータを抽出したい場合のクエリを紹介します。

SQLのサンプル

SELECT
    orders.*
FROM
    orders
    LEFT OUTER JOIN
        suppliers
    ON  suppliers.id = orders.supplier_id
    LEFT OUTER JOIN
        customers
    ON  customers.id = orders.customer_id
WHERE
    (suppliers.is_active = TRUE OR suppliers.id IS NULL)
    AND
    (customers.is_active = TRUE OR customers.id IS NULL)

Activerecordで実装する場合

Order.left_outer_joins(:supplier, :customer)
  .merge(where(suppliers: { is_active: true })
    .or(where(suppliers: { id: nil })))
  .merge(where(customers: { is_active: true })
    .or(where(customers: { id: nil })))

【Carrierwave+fog-aws】特定バケットへのアクセス権限を持ってS3にアップロードさせる方法

RailsアプリでS3にファイルをアップロードする場合、Carrierwaveとfog-awsを使用することが多いと思います。複数アプリをS3バケットを切り替えて使用する際に、ユーザーごとにバケットのアクセス権限を与えることで、思わぬ事故を防いだり、セキュリティを向上させることができます。

今回は、特定バケットのみアクセスできる権限を持ったCarrierwaveとfog-aws周り環境設定を解説いたします。

インストールとアプリ側の設定

Gem

  • carrierwaveとfog-awsをインストールします
  • 環境変数管理用のdotenvもインストールします
gem 'carrierwave'
gem 'fog-aws'
gem 'dotenv'

Carrierwave設定ファイル

  • Carrierwaveの設定ファイルを作成します

config/initializers/carrierwave.rb

require 'carrierwave/storage/abstract'
require 'carrierwave/storage/fog'

CarrierWave.configure do |config|
  config.storage :fog
  config.fog_provider = 'fog/aws'
  config.fog_directory = ENV['S3_BUCKET_NAME']
  config.fog_public = true
  config.fog_credentials = {
    provider: 'AWS',
    aws_access_key_id: ENV['S3_ACCESS_KEY'],
    aws_secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
    region: 'ap-northeast-1',
    path_style: true
  }
end

Modelへのマウント

  • 例として、Paymentモデルのreceipt_fileにReceiptUploaderをマウントします

app/models/payment.rb

class Payment < ApplicationRecord
  mount_uploader :receipt_file, ReceiptUploader
end

app/uploaders/receipt_uploader.rb

class ReceiptUploader < CarrierWave::Uploader::Base

  def extension_white_list
    %w[pdf]
  end
end

AWSの設定

AWSではS3とIAMのサービスを使用します。

S3の設定

S3バケットの作成を作成します。

S3バケットの作成

  • 任意の名前でバケットを作成します。

  • パブリックアクセスはデフォルトのオフのままにしておきます。それ以外の項目は要件に応じて任意で変更してください。私の場合はデフォルトのままであることが多いです。

IAMの設定

huzzah-bucketのみにアクセスできる権限(ポリシー)を作成して、ユーザーに付与します。

ポリシーの作成

  • ポリシーを使用すると、ユーザーに対して権限を割り当てることができます。JSONエディターで、特定バケットに対してアクセスを許可する操作を設定します。

JSONエディターの内容

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::huzzah-bucket"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::huzzah-bucket/*"
        }
    ]
}
  • 任意のポリシー名を入力してポリシーを作成します

ユーザーの作成

  • 任意のユーザー名を設定します。コンソールへのアクセスは無効のままにしておきます。

  • 「ポリシーを直接アタッチする」を選択して、先程作成したポリシーにチェックしてユーザーを作成します。

ユーザーに対してアクセスキーの設定

  • ユーザーの作成が完了したら、再度ユーザーの詳細画面を開きます。「セキュリティ認証情報」タブに移動して「アクセスキーを作成」をクリックします。

  • 「コマンドラインインターフェース」にチェックを入れてアクセスキーを作成します。

  • アクセスキーとシークレットアクセスキーが作成されますので、忘れないように控えておきます。

環境変数の設定

.envに先程控えたアクセスキーとシークレットアクセスキーとバケット名を登録します。

S3_BUCKET_NAME=huzzah-bucket
S3_ACCESS_KEY=xxxxxxx
S3_SECRET_ACCESS_KEY=zzzzzz

お疲れ様でした

作業は以上となります。お疲れ様でした!

HerokuのPostgreSQLをバージョン11から14にアップグレードしてみた

Herokuから以下の通知が来ました。

  • PostgreSQL 11 reaches End of Life on 2023-Nov-09. Due to security and operational concerns, Heroku cannot run unsupported software as a service. Therefore, the following database will need to be updated before 2023-Nov-09:

  • PostgreSQL 11は2023-11-09に終了を迎えます。Herokuでは、セキュリティや運用の観点から、サポート対象外のソフトウェアをサービスとして運用することができません。

PostgreSQL 11の終了の期限が迫ってきたので、重い腰を上げてバージョンを上げることにしました。実際やってみると思っていたより簡単に終わったので思いとどまっている方の参考になれば幸いです。

アップグレード方法の選定

基本的には公式のアップグレードガイドに沿った説明になっていますが、実際作業してみての所感などを追記していますので、合わせて確認するとより理解ができるかと思います。

PostgreSQLをアップグレードする方法は2つの選択肢があります。

  1. pg:upgradeを使用する方法
  2. pg:copyを使用する方法

私はpg:copyを使用する方法を選択しました。 理由は以下のとおりです。

  • pg:copyの方が作業がシンプルであるため
  • DBは10GBを超えているがダウンタイムを許容できるため
  • Database Bloat(使用されなくなったレコードによって占有される余分な領域)を解消したいため

アップグレード手順

1. 新しいデータベースを作成する

アップグレード後のデータベースを構築します。--versionを指定しない場合、2023/01/21時点ではversion 14が適用されます。

heroku addons:create heroku-postgresql:premium-2 -a your-appname

2. アプリをメンテナンスモードをオンにする

データベースへの書き込みを停止させるためにメンテナンスモードにします。

heroku maintenance:on -a your-appname

3. 新しいデータベースにコピーする

既存のデータベースの内容を新しいデータベースにコピーします。 HEROKU_POSTGRESQL_CYAN_URLの部分は新しいデータベースの名称です。PostgreSQLの管理画面やheroku pg:infoコマンドなどで確認できます。

heroku pg:copy DATABASE_URL HEROKU_POSTGRESQL_CYAN_URL -a your-appname

ちなみに私の環境の場合は20GB程度のコピーに15分ほど掛かりました。また、肥大化の部分がカットされて容量が8GB程度に下がりました。

4. 新しいデータベースに切り替える

新しいデータベースを使用するように切り替えます。

heroku pg:promote HEROKU_POSTGRESQL_CYAN_URL -a your-appname

5. アプリをメンテナンスモードをオフにする

メンテナンスモードをオフにします。以後アプリは新しいデータベースを使用するようになります。

heroku maintenance:off -a your-appname

6. 古いデータベースを削除する

アプリが安定稼働していることを確認したら、下記のコマンドまたは管理画面から古いデータベースを削除します。

heroku addons:destroy HEROKU_POSTGRESQL_LAVENDER -a your-appname

終わりに

以上で作業は終了です。思っていたよりも簡単にアップグレードできたので、一安心しました。 細かいことを意識しなくても手順に沿えばアップグレードできてしまうあたりは、さすがのHerokuだと改めて感心しました。

Groverで非同期にPDFファイルを生成する方法 | Rails

PDFファイル生成GemとしてGroverはとても優秀なのですが、処理するデータ量によっては処理が重くなり、メモリ負荷やレスポンスタイムの懸念が付き纏います。

弊社プロダクトでも、もともと非同期処理でGroverを実装していなかったため、度々レスポンスが極端に遅くになりHeroku上でH12(タイムアウト)が発生することが有りました。

そこで今回は、Sidekiqを用いたGroverの非同期処理の実装例を紹介したいと思います。

実装例

下記は非同期処理の全体図になります。

請求書(Invoiceモデル)の内容をもとにPDFファイルを非同期で生成して、画面からダウンロードするというシナリオで実装イメージを説明していきます。

前提として、Invioceモデルにはpdf_fileというカラムを持っており、pdf_fileにはCarrierWaveをマウントしておきます。 aws-fogを使用してS3にPDFファイルを格納してクライアントからダウンロードさせるという構図になります。

Grover非同期処理の全体図

① クライアントからPDF生成アクション(POST)を実行します

クライアントは請求書を作成するために画面からPDF生成アクションを実行します。

後述するenumステータスによって、画面上に「PDF生成ボタン」「PDF作成中ローディング」「PDFダウンロードボタン」の出力を切り替えるなどすると良いかと思います。

② PDF生成アクションがPDF生成ワーカー(Sidekiq)にリクエストします

PDF生成アクションではWorkerに対してPDF生成処理のみを要求して、即座にレスポンスを返します。 Invoiceモデルには必要に応じてenumで処理状態のステータスを管理すると良いでしょう。

# endpoint
#    invoices/:invoice_id/pdf_files(post)
# action
#    invoices::pdf_files_controller#create

# スタータスの例
# - requested: 処理をリクエスト済み
# - creating: PDF作成中
# - created: PDF作成済み
# - failure: PDF作成失敗

def create
  invoice = Invoice.find(params[:invoice_id])
  invoice.requested!
  CreateInvoicePdfWorker.perform_async(invoice.id)
  redirect_to invoices_url
end

③ PDF生成ワーカーがGroverでPDFを生成してS3に配置します

ワーカーでは、PDFファイルをローカルに作成して、CarrierWave経由でS3にアップロードさせます。 コメントで処理の内容を記載していますので、詳しくはそちらを参照ください。

class CreateInvoicePdfWorker
  include Sidekiq::Worker
  def perform(invoice_id)
    # 対象のInvoiceを取得してステータスを作成中に変更します
    invoice = Invoice.find(invoice_id)
    invoice.creating!
    # html文字列を生成します
    # 生成処理は簡略化のためにダミーのHTML文字列としています
    html = "<div>請求書</div>"
    temp_file_name = "/tmp/invoice_#{invoice.id}_#{Time.zone.today.strftime('%Y%m%d%S')}.pdf"
    # pathを指定すると任意のローカルファイルにPDFファイルを書き出しをしてくれます
    Grover.new(html, path: temp_file_name).to_pdf
    File.open(temp_file_name) do |f|
      # 事前にInvoiceモデルのpdf_file項目にCarrierWaveをマウントしておきます
      # (aws-fogを使用してS3に配置します)
      # PDFファイルの内容をinvoice.pdf_fileに反映します
      invoice.pdf_file = f
    end
    # ステータスを作成済みに更新します
    invoice.created!
    # 一時ファイルは削除しておきます
    File.delete(temp_file_name)
  end
end

④ クライアントがPDF取得アクション(GET)を実行します

クライアントは請求書が「作成済み」になったら、PDF取得アクションでファイルをダウンロードします。 CarrierWaveでpdf_fileをS3にマウントしていますので、S3のパスは隠蔽したままダウンロードさせることができます。

# endpoint
#    invoices/:invoice_id/pdf_files(get)
# action
#    invoices::pdf_files_controller#index

def index
  invoice = Invoice.find(params[:invoice_id])
  send_data(invoice.pdf_file.read, filename: invoice.pdf_file_identifier)
end

【CarrierWave + aws-fog】S3上のファイルをローカルにダウンロードする方法

CarrierWaveとaws-fogを用いてS3上にアップロードしたファイルをローカルにダウンロードする方法を説明します。 サイズの大きいファイルを一旦ローカルにダンロードしてから処理したいケースなどに活用できるかと思います。

実装

モデルの定義

class Invoice < ApplicationRecord
  mount_uploader :csv_data, CsvUploader
end
  • Invoiceモデルのcsv_dataという項目にCarrierWaveをマウントします

ダウンロード処理

user = User.find(params[:id])

temp_file = Tempfile.new
# ダウロード対象がバイナリの場合は下記も実行
# temp_file.binmode

URI.parse(user.csv_data.file.url).open do |file|
  temp_file.write(file.read)
  temp_file.close
end
  • user.csv_data.file.urlを実行するとS3オブジェクトの署名付きURLが返却されます
  • Tempfileを作成して、URI.openでS3オブジェクトを取得してファイルに書き込みます。

ダウンロードしたファイルを使う

例えば、ダウンロードしたCSVファイル使って取込処理を実装する場合は以下のようになります。

File.open(temp_file) do |file|
  CSV.new(file).each do |row|
    # 取り込み処理を実装する
  end
end