Ruby
Rails
GAE
GCS
gcp

GoogleCloudStorage(GCS)へ安全にアップロードする

More than 1 year has passed since last update.

環境

  • ruby歴 初心者
  • rails5
  • ruby 2.4.1p111

これはなに?

GCSへのアップロードフローをBlueGreenDeploymentに見立てて組んでみました。
本来は、切り替え役のRouterなる存在が必要ですが、今回はRouterなしでフローを組まざるを得ず、このような内容となっていますが了承ください。

概要

まず、前提としてアプリはローンチ済みで
アプリは GCS のバケットに置かれているリソースを見ているという状態です。

ざっくりのフローは下記の通りです。

■STEP.1

└── storage
    └── main # ここにアプリが参照しているリソースが配置されている

■STEP.2
アップロードするファイル群を別パスにアップロード

└── storage
    ├── main  
    └── prepare # ここにまずはアップロード

■STEP.3
STEP.2 が完了したら、つぎにmainの中身を別のパスにコピー

└── storage
    ├── main  
    ├── prepare
    └── tmp # mainのコピーをここに置く

■STEP.4
STEP.3 が完了したら、つぎにmainを削除し、prepare の中身をmainへコピー

└── storage
    ├── main # 削除して、prepare をここにコピーする
    ├── prepare
    └── tmp 

■STEP.5
STEP.4 が完了したら、preparetmp を削除

└── storage
    └── main

途中で失敗したら?

それぞれ STEP で何かしらの理由で失敗しても
ゴミがのこるだけで、mainへの影響はありません。
「STEP4. のmainを削除したあとに失敗したら結局だめじゃん」という意見もあるかと思いますが
そこは「限りなく安全に」というポリシーなので、 手動でtmpに残っているファイルを移動して元に戻すということになります。

※もし、この状況でよりよい方法があれば教えていただきたいです!

実装

publisher.rb
    # @storage_keys: すでに存在するファイル群のパスが入った配列
    # @upload_keys: これからアップロードするファイル群のパスが入った配列

    MAIN_PREFIX = "main".freeze    
    PREPARE_PREFIX = "prepare".freeze
    TMP_PREFIX = "tmp".freeze

    def publish
      begin
        upload_to_prepare
      rescue => e
        clean(:prepare)

        Rails.logger.error(e)
        Rails.logger.error(e.backtrace)
        raise UploadError
      end

      begin
        copy(from: :main, to: :tmp)
      rescue => e
        clean(:prepare)
        clean(:tmp)

        Rails.logger.error(e)
        Rails.logger.error(e.backtrace)
        raise CopyCleanError
      end

      begin
        clean(:main)
        copy(from: :prepare, to: :main)
        clean(:prepare)
        clean(:tmp)
      rescue => e
        clean(:main)
        clean(:prepare)
        copy(from: :tmp, to: :main)
        clean(:tmp)

        Rails.logger.error(e)
        Rails.logger.error(e.backtrace)
        raise CopyCleanError
      end
    end

    def copy(from:, to:)
      case from
      when :main
        from_key_prefix = MAIN_PREFIX
        keys = @storage_keys
      when :tmp
        from_key_prefix = TMP_PREFIX
        keys = @storage_keys
      when :prepare
        from_key_prefix = PREPARE_PREFIX
        keys = @upload_keys
      else
        raise "target not found"
      end

      case to
      when :main
        to_key_prefix = MAIN_PREFIX
      when :tmp
        to_key_prefix = TMP_PREFIX
      when :prepare
        to_key_prefix = PREPARE_PREFIX
      else
        raise "target not found"
      end

      keys.each do |key|
        from_key = from_key_prefix.blank? ? key : "#{from_key_prefix}/#{key}"
        file = StorageBucket.files.get(from_key)
        if file.present?
          to_key = to_key_prefix.blank? ? key : "#{to_key_prefix}/#{key}"
          copyed_file = file.copy(StorageBucket.key, to_key)

           # 注意! 
           # fog が用意している copy メソッドはデフォルトで公開設定にしています。
           # もし元のファイルが非公開ファイルの場合は、下記の処理を挟まないと
           # 公開ファイルになってしましますので気をつけて
          if file.public_url.present?
            copyed_file.public = true
            copyed_file.save
          end
        end
      end
    end

    def clean(target)
      case target
      when :main
        prefix = MAIN_PREFIX
        keys = @storage_keys
      when :tmp
        prefix = TMP_PREFIX
        keys = @storage_keys
      when :prepare
        prefix = PREPARE_PREFIX
        keys = @upload_keys
      else
        raise "target not found"
      end

      keys.each do |key|
        key = prefix.blank? ? key : "#{prefix}/#{key}"
        file = StorageBucket.files.get(key)
        if file.present?
          file.destroy
        end
      end
    end

説明

リリース済みのアプリが見ているリソースの更新をかけようとして
GCSへアップロードする際に、そのまま直接更新をかけてしまっては
途中で処理がエラーした時はどうするんだ?という、サーバーのデプロイと同じような問題があがり
今回の実装に至りました。

わりとゴリゴリな実装なのと
「copy が失敗し、元に戻す処理が失敗したら結局だめじゃん」という意見もあるかと思いますが
そこは「限りなく安全に」というポリシーなので起こったらしゃーないということでm(_ _)m

※もし、この状況でよりよい方法があれば教えていただきたいです!

学んだこと

・ストレージにおける BlueGreenDeployment の適用

参考

How to list files in a Google Storage bucket with fog-google
How do I rename a file with Fog?