RailsのAPIサーバーのエラーレスポンスで例外に対応するエラーコードを返却する

はじめましてこんにちは。
夏が本気を出してきて最近麺類しか口にしていないサーバサイドエンジニアのかしまです。

この度APIにてHTTP Status Codeとは別に、例外に対応するエラーコードを返すよう奮闘したのでその知見を共有したいと思います。

やりたいこと

APIにて例外が発生した場合、以下の形式でレスポンスを返すようにします。

{
  "errors":
  [
     {
        "code":  "code1",
        "message": "message1",
     },
     {
        "code":  "code2",
        "message": "message2",
     },
  ]
}

Why?

今回追加したAPIは外部のアプリケーションとの連携に使うため、以下の要件を満たす必要がありました。

  • クライアントサイドがハンドリングしやすいような設計にする
  • エラーの識別子は不変であること

そのためエラーメッセージとは別に、コードを返すことにしました。

次の章から実際にどのように実装したかを紹介したいと思います。

rescue_fromにて例外をキャッチする

rescue_fromとは例外をキャッチして指定したメソッドを実行してくれるという大変便利なものです。

railsguides.jp

これをAPIの基盤となるcontrollerに埋め込みます。

  class APIController < ActionController::Base
    rescue_from StandardError, with: :error500

    def error500(error)
      render json: { errors: [{ code: 'E9999', message: '例外が発生しました' }] }, status: :internal_server_error
    end
  end

このクラスを継承したcontrollerにてStandardErrorが発生するとerror500メソッドが呼び出され、期待するエラーレスポンスを返すことができます。簡単ですね。
他の例外も同じように実装していけば良さそうです。

Modelのvalidationエラーの場合

さてここからが本題なのですが ActiveRecord::RecordInvalid (modelのvalidationエラー)が発生した場合、例外の原因に対応したエラーコードを動的に取得して返すようにします。
独自で定義したModelのvalidationはサービスのドメインに紐づくものも多いため、同じ例外でも原因をコードで識別できるようにするためです。

  class Offering < ApplicationRecord
    # Rails標準の汎用的なvalidation
    validates :start_at, presence: true
    validates :end_at, presence: true
    # 独自のvalidation
    # これらが発生した場合はそれぞれ定義したエラーコードを返したい
    validate :check_minimum_hourly_wage
    validate :check_rests_presence_and_minutes
    
    private

    def check_minimum_hourly_wage
      # 処理

      errors.add(:hourly_wage, :greater_than_prefecture_minimum_wage)
    end

    def check_rests_presence_and_minutes
      # 処理

      errors.add(:base, :greater_then_or_eq_default_rest_minutes)
    end
  end

どうやってエラーコードを取得するか

ActiveRecord::RecordInvalid をrescue_fromにてキャッチすると、以下のように ActiveModel::Errorsインスタンスにアクセスできます。

    def error404(error)
      error.record.errors.class
      # => ActiveModel::Errors
    end

ActiveModel::Errorsインスタンスdetails というattributesを持っています。
details 内にエラーが発生したattribute名をkey、エラーメッセージkeyをvalue*1 として配列で持っています。

error.record.errors.details
=> {:base=>[{:error=>:greater_then_or_eq_default_rest_minutes}]} # 配列内ではerrorをkey、エラーメッセージkeyをvalueとして持っている

これらの情報からエラーコードを取得すれば良さそうです。

エラーコードの管理について

タイミーでは定数管理にConfigを使っているのでエラーコードもymlで管理することにしました。

error_codes:
  bad_request: E0400
  unauthorized: E0401
  forbidden: E0403
  not_found: E0404
  unprocessable_entity: E0422
  internal_server_error: E9999
  models:
    offering:
      base:
        greater_then_or_eq_default_rest_minutes: E2000
      hourly_wage:
        greater_than_prefecture_minimum_wage: E3000

これにより、以下のようにエラーコードを取得できます。

Settings.error_codes.models.user.base.hoge
=> E2000

エラーメッセージとエラーコードを対応させる

エラーコードの取得方法が決まったのであとはエラーメッセージとペアにすれば良さそうです。
ActiveModel::Errorsインスタンスmessagesというattributes名でエラーが発生したattribute名をkey、エラーメッセージkeyからlocaleに対応したエラーメッセージをvalueとして配列で持っています。

error.record.errors.messages
=> {:base=>["法的休憩時間を満たしていません"]}

しかしこれではどのメッセージがどのエラーメッセージkeyに対応するのかわかりません。
そのため messages の情報を使わず、 details の情報からエラーメッセージも取得する必要があります。

エラーメッセージを取得するため、どのようにエラーメッセージが格納されているのかを把握するため、ActiveModel::Errors#add メソッドをソースを読んでみると、

https://github.com/rails/rails/blob/3f470a7e3a22304f095535b8cc730b4926828175/activemodel/lib/active_model/errors.rb#L311 https://github.com/rails/rails/blob/3f470a7e3a22304f095535b8cc730b4926828175/activemodel/lib/active_model/errors.rb#L535

    def add(attribute, message = :invalid, options = {})
      message = message.call if message.respond_to?(:call)
      detail  = normalize_detail(message, options)
      message = normalize_message(attribute, message, options)
      if exception = options[:strict]
        exception = ActiveModel::StrictValidationFailed if exception == true
        raise exception, full_message(attribute, message)
      end

      details[attribute.to_sym]  << detail
      messages[attribute.to_sym] << message
    end

~ 略 ~

  private
    def normalize_message(attribute, message, options)
      case message
      when Symbol
        generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
      else
        message
      end
    end

どうやら generate_message をいうメソッドを使えば messages に格納されているメッセージを生成できそうです。

以上を踏まえた上で完成したメソッドがこちら

    def error422(err)
      class_name = err.record.class.name.underscore
      active_model_errors = err.record.errors
      errors = []
      active_model_errors.details.each do |attribute, attribute_errors|
        attribute_errors.each do |error_info|
          # エラーコードとエラーメッセージをセットにするため、full_messagesではなく
          # generate_messageとfull_messageを使ってエラーメッセージを生成している
          error_key = error_info.delete(:error)
          message = active_model_errors.generate_message(
            attribute, error_key, error_info.except(*ActiveModel::Errors::CALLBACKS_OPTIONS),
          )

          errors <<
            {
              code: Settings.error_codes.models.send(class_name)&.send(attribute)&.send(error_key) ||
                    Settings.error_codes.unprocessable_entity, # error_codesにて定義のないエラーの場合、汎用的なエラーコードを返す
              message: active_model_errors.full_message(attribute, message),
            }
        end
     end

     render json: { errors: errors }, status: :unprocessable_entity
  end

試しにvalidationエラーを起こしてみた結果以下のレスポンスが返りました🎉

{
  "errors":
  [
     {
        "code":  "E2000",
        "message": "法的休憩時間を満たしていません",
     }
  ]
}

問題点

一見良さそうですが、以下の問題が発覚しました。

1. errors.add時にoptionでerrorという名のkeyを渡されると、エラーメッセージkeyが上書きされる

errors.addメソッドでは、以下のようにoptionを渡せます。

errors.add(:base, :hoge, reason: 'reason')

渡したoptionはlocaleファイル内で参照できたりします。

ja:
  activerecord:
    errors:
      models:
        user:
          attributes:
            base:
              hoge: %{reason} のため登録できません

そして ActiveModel::Errorsdetails の内容は以下のようになります。

error.record.errors.details
=> {:base=>[{:error=>:hoge, :reason=>'reason'}]}

では errors.add時にerrorというオプションを渡したらどうなるかというと。。。

errors.add(:base, :hoge, error: 'error')
errors.details
=> {:base=>[{:error=>'error'}]}

はい。エラーメッセージkeyが上書きされてますね。

以下の処理でoptionのmergeを行った結果をdetailsに格納しているのでこのような結果になります。 https://github.com/rails/rails/blob/3f470a7e3a22304f095535b8cc730b4926828175/activemodel/lib/active_model/errors.rb#L545

    def normalize_detail(message, options)
      { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
    end

対応策

幸い現時点でerrorオプションを使用しているコードはありませんでしたが、今後もしerrorオプションを渡された場合、エラーメッセージkeyの取得は不可能です。
そのため、エラーメッセージkeyが上書きされgenerate_messageにて適切なメッセージが取得できなかった場合は汎用的なメッセージを返すと共に適切なメッセージを返せなかったことを検知*2できるようにしました。

    def error422(err)
      class_name = err.record.class.name.underscore
      active_model_errors = err.record.errors
      errors = []
      active_model_errors.details.each do |attribute, attribute_errors|
        attribute_errors.each do |error_info|
          # エラーコードとエラーメッセージをセットにするため、full_messagesではなく
          # generate_messageとfull_messageを使ってエラーメッセージを生成している
          error_key = error_info.delete(:error)
          message = active_model_errors.generate_message(
            attribute, error_key, error_info.except(*ActiveModel::Errors::CALLBACKS_OPTIONS),
          )

          if message.match?(/\Atranslation missing:/)
            message = I18n.t('errors.messages.exceptions.unprocessable_entity')
            # Sentry通知
            LogService.error_with_sentry(error, '外部向けAPIにて適切なエラーメッセージを返せませんでした。')
          end

          errors <<
            {
              code: Settings.error_codes.models.send(class_name)&.send(attribute)&.send(error_key) ||
                    Settings.error_codes.unprocessable_entity, # error_codesにて定義のないエラーの場合、汎用的なエラーコードを返す
              message: active_model_errors.full_message(attribute, message),
            }
        end
     end

     render json: { errors: errors }, status: :unprocessable_entity
  end

追加した処理は以下の部分です。

  if message.match?(/\Atranslation missing:/)
    message = I18n.t('errors.messages.exceptions.unprocessable_entity')
    # Sentry通知
    LogService.error_with_sentry(error, '外部向けAPIにて適切なエラーメッセージを返せませんでした。')
  end

generate_message内部ではI18nを使用しています。
オプションで渡す場合、locale keyが存在する確率は低いだろうという思想のもと、戻り値に translation missing: が含まれる場合、汎用的なメッセージを返すと共にSentryに通知するようにしました。 *3

2. 関連テーブルのvalidationに引っかかった場合、generate_messageで例外を吐く

ActiveRecordでは関連テーブルを含めたデータ保存を以下のように行うことができます。

offering = Offering.new(offering_params)
offering.rests.new(rests_params)
offering.save!

上記の例で、restsの保存処理にてvalidationエラーが発生すると、generate_messageメソッドの内部でエラーが発生しました。

NoMethodError: undefined method `rests.start_at' for #<Offering:0x00005570d93a1de0>
Did you mean?  rests_attributes=
from /usr/local/bundle/gems/activemodel-6.0.3.2/lib/active_model/attribute_methods.rb:432:in `method_missing'
Caused by ActiveRecord::RecordInvalid: 休憩開始時間は休憩終了時間より大きい値にしてください
from /usr/local/bundle/gems/activerecord-6.0.3.2/lib/active_record/validations.rb:80:in `raise_validation_error'

なんだこれは。。ということでgenerate_messageメソッドのソースを読んでみると

https://github.com/rails/rails/blob/3f470a7e3a22304f095535b8cc730b4926828175/activemodel/lib/active_model/errors.rb#L482

    def generate_message(attribute, type = :invalid, options = {})
      type = options.delete(:message) if options[:message].is_a?(Symbol)
      value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)

attribute名がbaseではない場合、レコードに対してattributeのメソッド呼び出しを行なっています。*4

つまりdetails に格納されているattribute名が rests.start_at になっているため、そんなメソッドはねぇ!と怒られているようです。

対応策

どうやら関連テーブルのデータにてvalidationエラーが起きた場合、attribute名は#{relation名}.#{attribute名} になるということがわかりました。 *5 そのためattribute名に.が存在したらrelation名とattribute名に分割し、関連レコードをチェックして、エラーが発生していたらコードを取得してメッセージを生成を行うようにしました。

    def error422(err)
      errors = ::ExternalCoordinationAPI::ErrorResponse.new(err).create.errors
      render json: { errors: errors }, status: :unprocessable_entity
    end
module ExternalCoordinationAPI
  class ErrorResponse
    attr_reader :record, :error, :errors, :active_model_errors

    def initialize(error)
      @error = error
      @record = error.record
      @errors = []
      @active_model_errors = record.errors
    end

    def create
      active_model_errors.details.each do |attribute, options|
        if attribute.to_s.include?('.')
          relation_name, _attribute_name = attribute.to_s.split('.')
          record.send(relation_name).each do |relation_record|
            next if relation_record.errors.empty?

            relation_record_active_model_errors = relation_record.errors
            relation_record_active_model_errors.details.each do |relation_record_attribute, relation_options|
              relation_options.each do |option|
                error_key = option.delete(:error)
                message = generate_message(relation_record_attribute, error_key, option)
                add_error(relation_record.class.name.underscore, relation_record_attribute, error_key, message)
              end
            end
          end
        else
          options.each do |option|
            error_key = option.delete(:error)
            message = generate_message(attribute, error_key, option)
            add_error(record.class.name.underscore, attribute, error_key, message)
          end
        end
      end
      self
    rescue StandardError => e
      LogService.error_with_sentry(e, '外部向けAPIにて422エラー生成時にエラーが発生しました。')
      errors <<
        {
          code: Settings.error_codes.unprocessable_entity,
          message: I18n.t('errors.messages.exceptions.unprocessable_entity'),
        }
      self
    end

    private

    def generate_message(attribute, error_key, options)
      # エラーコードとエラーメッセージをセットにするため、full_messagesではなく
      # generate_messageとfull_messageを使ってエラーメッセージを生成している
      message = active_model_errors.generate_message(
        attribute, error_key, options.except(*ActiveModel::Errors::CALLBACKS_OPTIONS),
      )

      if message.match?(/\Atranslation missing:/)
        message = I18n.t('errors.messages.exceptions.unprocessable_entity')
        LogService.error_with_sentry(error, '外部向けAPIにて適切なエラーメッセージを返せませんでした。')
      end

      message
    end

    def add_error(class_name, attribute, error_key, message)
      errors <<
        {
          code: Settings.error_codes.models.send(class_name)&.send(attribute)&.send(error_key) ||
                Settings.error_codes.unprocessable_entity,
          message: active_model_errors.full_message(attribute, message),
        }
    end
  end
end

もしgenerate_messageにてまだ予想できていないエラーが発生した場合に備え、StandardErrorをキャッチして汎用的なメッセージを返すようにしました。

終わりに

正直かなり力技に頼った実装になってしまっており、もっとうまくできるのではないかと思っています。
特にerrorオプションを渡せないなど本来の機能を制限してしまっている箇所はどうにかしたい気持ちが強いです。
もし良い方法をご存知の方がいらっしゃったら是非アドバイスをいただけたらと思います。

*1:errors.add時に第二引数に渡した値が入ります。タイミーではエラーメッセージは全てlocaleで定義しているので、errors.add(:base, 'エラーメッセージ')というように文字列を渡さないようにしています。

*2:タイミーではSentryを使ってます

*3:対応するlocale keyが存在したら。。。オプションkeyにerrorは使用しないというルール化をするほうが良さそうです。

*4:read_attribute_for_validationはsendのaliasでした。
https://github.com/rails/rails/blob/e6c6f1b4115495b27a2d32a4bd5c95256db695b1/activemodel/lib/active_model/validations.rb#L402

*5:どの箇所でこのようなattribute名をセットしてるのかまで追いたかったのですが、力及ばすでした。無念。

オフィスのネットワーク構築についてのお話

SREとコーポレートエンジニアをやっている @sion_cojp です。

今回は新オフィスのネットワーク構築を実施したので、こちらについてお話します。

私自身、学生時代の研究や、10年前にデータセンターのネットワーク構築しか経験がないため、所々おかしな点があるかもしれませんが、ご了承ください。

また今回相談に乗ってくださった @kajinari さんに感謝の意を表します。

TL;DR

  • 旧オフィスはとても不安定なネットワークでしたが
  • coltの回線とmerakiのネットワーク機器で
  • 快適なネットワークができました
  • 予算問題で達成できなかったこと: L3/L2機器 + ネットワーク回線の冗長化

なんで今オフィス移転?

旧オフィスの契約終了が7月末だったため、新しいオフィスはコロナが流行する前に契約しました。

違約金など考慮した結果、オフィス移転の判断をしております。

もちろん新オフィスを構えましたが、会社としては現在もリモートワーク推奨です。

旧オフィスの課題

回線が家庭用だったため、数百人規模には耐えれませんでした。

docker pullしてネットワークが落ちたこともありますし、日常的に遅くなったりとても不安定でした。

またネットワーク構築をアウトソースしているため、ネットワーク機器にアクセス出来ず、設定変更するにも連絡しないとダメでした。

これを踏まえて、自分たちでネットワーク構築する判断をしました。

設計

f:id:timee_dev:20200717142724p:plain:w300

こちらの図はdraw.ioで書いて、png + drawioファイルをGitHubで管理しております。

L3, L2ですが、予算の兼ね合いで一旦シングル構成にしております。APに関しても工事と予算の関係上、半分の台数のみ購入となっております。

そのため弊社で根幹となっている、カスタマーサポートと「ネットワークが止まった場合のフロー」を相談し合意の上でシングル構成にしました。

いずれ冗長構成にする予定です。

配線

f:id:timee_dev:20200717142854p:plain:w300

f:id:timee_dev:20200717142916p:plain:w300

天井と床下があり、それぞれ図面から長さを測って作成しました。

同じようにGitHubで管理しております。

こちらをオフィス移転の工事業者に提出して配線依頼しました。

また年内は諸事情により、図の右半分だけのオフィス運用になっておりますが、配線だけは全部完了させて、使ってないLANケーブルはネットワークポートのほうでshutdownしております。

業者を呼ぶ手間と料金を省きたかったからです。

L3, L2

f:id:timee_dev:20200717152043p:plain:w300f:id:timee_dev:20200717152049p:plain:w300

コンソールケーブルやSSHで設定...私は可能性ですが、今時代だと管理コストが高いため、全てcisco merakiにしました。

選定理由はオフィスネットワークで安定した結果を出しておりますし、dashboardから機器の管理, 設定が可能なのである程度ネットワーク知識さえあれば設定できると思ったからです。

またmerakiだとWLC(ワイヤレスコントローラー)が不要なのもメリットです。

こちらはCTCから下記を購入しております。

L3(MX100) ×1台, L2(MS120-48) ×1台, AP(MR36) ×3台(1台は今の所予備機)

機種の選定理由は下記。

  • MX100...数年後見据えた人員計画を元に、最大収容人数と合致したのがこれでした。
  • MS120-48...1000Base対応 + 要件的に48ポート欲しかったのでこちらに。
  • MR36...将来クライアントで利用できるWiFi6(802.11ax)対応 + PoE。ある程度長く使えそうだと思いました。

VLANや設定周り

有線LANはMACアドレスフィルタリングと固定IPで管理するようにしております。これにより第三者が有線LANを物理的に奪ってからのクラッキングリスクを防ぎます。

WiFiに関しては、ゲスト用はプリンタに接続出来ないようにしてます。プリンタを踏み台にしたクラッキングリスクを防いでます。

また弊社にはネットワークカメラがあるので、そちらにはどの端末からもLocal IPアクセス出来ないようVLANを切ってます。

AP

f:id:timee_dev:20200717152017p:plain:w300

APの配置は柱などで電波が届かない場所を考慮しながら設置します。

そしてAPの台数を決めるのにクライアント数の計算は必要です。

社員1人あたり、2~3クライアント(PC, スマフォ,検証端末)* 100 = 200~300クライアント。

また、いずれできるオープンスペースの収容人数も追加計算しました。

一般的には1APあたり20~25らしいですがあくまで目安なので、もっと多くのクライアントを捌くことも出来ます。

そこら辺は経験の上、台数を決めて、もし足りなくなったら増やす方針にしました。

またLANケーブルから電力供給するため、PoE対応のものが必須です。

ラック

f:id:timee_dev:20200717152250p:plain:w300

摂津金属工業のサイトから購入しました。

選定理由はデータセンターでも実績があり、安心感がありました。

ポイントはONU(実際は回線業者から提供されたスイッチ)を置く棚板 + L3 ×2台 + L2 ×2台分のU数は確保したいですね。

またLANケーブルを地面に置いたりする可能性があるので、その分のスペースも確保すると良いです。

最終的に購入したのは下記となります。

- SKR-16U60A0VB
- 背面マウントのSKRO-16UPF-B(黒色)追加、
- コインロックのSKRO-3CL追加
- 棚板(D600㎜)1セット、耐サージ機能付OAタップ(8口)2本

電源

電源の冗長化のために、オフィスのビルに2系統引いてます。

(と言っても今はシングル構成なので、意味ないですが将来のため。。)

回線

Colt 300M Fixed(帯域保証)の回線を利用してます。

選定理由は金融機関でも使われてるため安心して利用出来そうだったからです。

社員が増えれば1Gにupgradeも良いでしょう。

またネットワーク冗長で、Nuroあたりの従量課金もサブで利用したかったのですが、予算のため今回はやっておりません。

ラック内配線のポイント

f:id:timee_dev:20200717152844p:plain:w400

実際のラックです。

配線のポイントとしては、ラックの外側にLANケーブルを這わせることです。

もし面倒くさがってラックの内側からケーブリングしてしまうと、機器を追加マウントする時にケーブルが邪魔になる可能性があります。

また今後追加予定の機器を想定してスペースを確保してます。

GitHubで管理してるもの

  • ネットワーク設計図
  • 天井、床下の配線図
  • 固定IP/VLAN/SSIDのリストとdescription
  • 購入->初期設定の手順やこの構成になった経緯
  • 各機器の問い合わせ先と契約内容

などを管理してます。余談ですが、ネットワーク以外にもプリンタ周りや、セキュリティソフト関連もGitHubで管理しております。

どれくらい期間かかるの?

基本的にネットワーク機器、ラック、回線の手配で2ヶ月はみた方がいいでしょう 特に回線ですが、コロナ禍やオリンピックの影響で手配が遅れる可能性が高いです。

参考にした資料は?

出たエラーに関してgoogleで検索すれば大抵の内容はhttps://community.meraki.com/ で議論されてるので、それを読めば解決しました。

アウトソースしなかったメリットについて

ネットワーク機器のアクセス権があるので、好きなタイミングで自分たちで設定出来るのが嬉しいですね。 ある程度ネットワークの知識があるならアウトソースしないほうがコストと運用面でメリットが大きいと思います。

まとめ

素人ながらネットワーク設計をしっかり調べてやった結果、旧オフィスと比べ物にならないくらい快適なネットワーク環境になりました。

移転後、ネットワーク周りの問い合わせが来ないかとても不安でしたがそれもなかったです。

merakiには様々な設定があるみたいなので、もっと学んで強化していきたいと思います。

Rails + RSpec + OpenAPI3 + Committeeでスキーマ駆動開発を運用するTips

こんにちは、タイミーデリバリー開発チームの宮城です。
今回は弊社のOpenAPI3ベースのスキーマ駆動開発の運用方法を紹介します。

TL;DR

  • 技術スタックは OpenAPI3, Swagger UI, Committee, ActiveModelSerializers
  • Committeeを利用してOpenAPI準拠のRequest Specを行う
  • OpenAPI3のrequiredキーワードに注意する

背景

タイミーデリバリーでは、RailsによるAPIサーバーと、Web管理画面としてVue.jsによるSPA、ユーザー向けiOSアプリとしてSwiftを採用しています。
1つのモノリスRailsで利用者別にネームスペースを区切り、それぞれエンドポイントを提供しています。
サーバーサイドとクライアントサイドを分離し並行して開発を進めるためにスキーマ駆動開発を導入しました。スキーマ駆動開発の詳しい説明やメリットについては既に多くの記事が存在するため、ここでは参考にさせていただいた記事の紹介までとさせていただきます。

RubyKaigi 2019でOpenAPI 3について登壇しました - おおたの物置

スキーマファースト開発のススメ - onk.ninja

スキーマ駆動開発のススメ - Studyplus Engineering Blog

この記事では、実際にスキーマ駆動開発を開発フローに導入する際のTipsを記したいと思います。

続きを読む

RedashをFargate, Datadog, Terraformで構築/運用する

こんにちは、タイミーSREチームの宮城です。
今回は弊社がRedashをFargateで構築/運用している話を紹介します。

背景

タイミーでは、CSやセールスのKPI策定から毎月の事業数値に至るまで、Redashが様々な用途で活用されています。
Fargateで構築する以前はEC2上のdocker-composeで運用されていましたが、以下の課題がありました。

  • オートスケールできないため、クエリが詰まってCPUが100%になってサービスが停止する。
    • その度slack上から再起動していた
  • セットアップしたエンジニアが退社しており、インフラ構成図やノウハウの共有、IaCによる管理ができていない。
  • クエリやダッシュボードなどのデータの定期的なバックアップができていない。
  • v7系からv8系へのアップデートがしたいが、アップデートによる影響範囲がわからず恐怖感がある。
  • 事業に大きく関わるサービスなのにも関わらず、モニタリングやアラートができていない。

上記をFargateに移行することで解決することができました。

移行後のアーキテクチャ

f:id:MH4GF:20200418180434p:plain

Redashで利用するミドルウェアに関しては下記コンポーネントを使い、全てをterraformで管理しています。
- PostgreSQL -> RDS
- Redis -> ElastiCache

それぞれの構成の紹介

ここからは、それぞれの構成をTerraformのソースコードやタスク定義のJSONなどを交えつつ説明していきます。

続きを読む