環境
- 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 が完了したら、prepare
、tmp
を削除
└── storage
└── main
途中で失敗したら?
それぞれ STEP で何かしらの理由で失敗しても
ゴミがのこるだけで、main
への影響はありません。
「STEP4. のmain
を削除したあとに失敗したら結局だめじゃん」という意見もあるかと思いますが
そこは「限りなく安全に」というポリシーなので、 手動でtmp
に残っているファイルを移動して元に戻すということになります。
※もし、この状況でよりよい方法があれば教えていただきたいです!
実装
# @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?