S3バケットを別アカウントへ移設
ダウンタイムなしにS3のアカウントを移設したかったのですが、巷の情報では1回のみのsyncで済ませており、ちょっと困ったので備忘録。
他との違いとしてはs3:GetObjectTagging
の追加がかなめ。
手順
移設用IAMユーザー
移設先のアカウントで以下のポリシーを持ったユーザーを作成してください。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:GetObjectTagging", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::from-bucket", "arn:aws:s3:::from-bucket/*" ] }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:ListBucket", "s3:PutObjectAcl" ], "Resource": [ "arn:aws:s3:::to-bucket", "arn:aws:s3:::to-bucket/*" ] } ] }
configureの登録
後程aws cliで同期を行うので、作成したIAMユーザーのconfigureを登録しておいてください。
$ aws configure --profile s3-sync-profile
移設元バケットポリシー
{ "Version": "2012-10-17", "Statement": [ { "Sid": "ReadAccess", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::xxxxxxxxxxxx:user/sync-user" }, "Action": [ "s3:ListBucket", "s3:GetObject", "s3:GetObjectTagging" ], "Resource": [ "arn:aws:s3:::from-bucket/*", "arn:aws:s3:::from-bucket" ] } ] }
移設実行
$ aws s3 sync s3://from-bucket/ s3://to-bucket/ --profile s3-sync-plofile
s3:GetObjectTagging
これがない場合、2回目のsyncでAn error occurred (AccessDenied) when calling the GetObjectTagging operation: Access Denied
が発生して差分をsync出来ませんでした。
要注意!
備考
aws s3 sync
は差分のみコピーしてくれます。
なのでまず最初に実行し、その後サービス側のS3の向き差を変更、再度aws s3 sync
を実行すれば影響範囲を最小に移設が可能です。
ただしすごく活発なサービスや、少しの問題が発生することは許されない、ということであれば、しっかりメンテ時間をとりませう。
KusanagiでNginxが何度も再起動する
Nginxのログを見ていると何度も再起動が行われていました。
幸い動作に影響は無かったものの、ひどいときには数分に一回レベルで再起動が起こるので原因を調査しました。
原因
Kusanagiはデフォルトでmonitが動いています。
monitの設定でNginxで500番台が計測されればNginxを再起動するよう設定されていました。
普段Kusanagiも使わないし、監視にはmonitも使っていなかったのでなかなか気づけず。。
余談
そもそもなぜ500番台が発生していたかというと、Next.jsでBOTからのアクセス時に500になるが原因でした。
普段ならエラー検知サービスを入れておくのですぐ気づけたんですが、今回はいろいろな制約によりサードパーティ製のアレコレが使えず、気づくのが遅れたのでした。
Next.jsでBOTからのアクセス時に500になる
本番稼働中のNext.js アプリケーションのログを眺めていると、稀にエラーを吐いていました。
環境
Key | Val |
---|---|
Node.js | 20.9.0 |
Next.js | 13.4.13 |
エラーの内容
- error Failed to handle request for / TypeError: fetch failed at Object.fetch (node:internal/deps/undici/undici:11372:11) at async invokeRequest (/var/www/app/sample-app/node_modules/next/dist/server/lib/server-ipc/invoke-request.js:21:12) at async requestHandler (/var/www/app/sample-app/node_modules/next/dist/server/lib/start-server.js:336:33) at async Server.<anonymous> (/var/www/app/sample-app/node_modules/next/dist/server/lib/start-server.js:152:13) { cause: _RequestContentLengthMismatchError: Request body length does not match content-length header at write (node:internal/deps/undici/undici:8302:41) at _resume (node:internal/deps/undici/undici:8276:33) at resume (node:internal/deps/undici/undici:8173:7) at connect (node:internal/deps/undici/undici:8162:7) { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' } }
原因
UND_ERR_REQ_CONTENT_LENGTH_MISMATCH
でググると、お有難いことに下記記事がヒットしました。
本当にありがとうございます。
※Version 13.4.13時点 本番環境で以下のようなエラーが発生することがある
さて、今回の環境を見てみましょう。
Next.js 13.4.13ですね!!! まさに引用元のバージョンでした。
これにより、OGPを取得しに来たBOTのアクセス時にエラーが発生していたようです。
この影響でOGPが表示されていませんでした。
対応
NextJS fails with UND_ERR_REQ_CONTENT_LENGTH_MISMATCH after redirect from server
I was experiencing a similar issue. Upgrading to the latest version of NEXT helped resolve it for me. Version 13.4.19 as of this comment.
対応方法についてもお有難いことに既に試している方がいました。
13.4.19では直っているそうです。
ということでnextのバージョンを上げて終了です。
ruby-handlebarsのプレースホルダーに日本語を使いたい
handlebarsって便利ですよね。
さてプレースホルダー {{hoge}}
に日本語を使いたい場面などありませんか?
ありませんか。まあ普通はそうです。
しかし私にはそんなタイミングがあったのです。
handlebars.rbでは使えたのですが、さすがにこれはもう古すぎですし、V8エンジンが必要だとかでRails7で使うには面倒です。
そこでruby-handlebarsを使いたいわけですが、こちらは何と日本語に対応していません。
例えば{{名前}}
等を使うと、下記エラーが発生します。
Extra input after last repetition at line 1 char 1. (Parslet::ParseFailed)
そこで何とかしていきたいと思います。
環境
Service | Version |
---|---|
Ruby | 3.2.2 |
Ruby on Rails | 7.0.8 |
ruby-handlebars | 0.4.1 |
原因
ruby-HandlebarsではParslet::Parser
を継承したHandlebars::Parser
にルールを定義してparseしています。
しかしここに日本語用のルールが無いためエラーが発生しています。
対応
日本語のルールが無いのであれば、追加すればよいのです。
ということでモンキーパッチを当てていきます。
config/initialize/handlebars.rb
module Handlebars class Parser # 日本語を含む識別子のための新しいルール # \p{Han} は漢字、\p{Hiragana} はひらがな、\p{Katakana} はカタカナを表します rule(:japanese_identifier) { match['\p{Han}\p{Hiragana}\p{Katakana}\-a-zA-Z0-9_\?'].repeat(1) } # 既存のidentifierルールを拡張 rule(:identifier) { (else_kw >> space? >> dccurly).absent? >> japanese_identifier } end end
以上デース
PostgreSQLのシーケンスがずれた!
開発中色々いじっていたらシーケンスがずれた、なんてことは数年に一度くらい経験したりしないでしょうか?
そんな時にぱっと直したいときは、以下を流し込めばOKです。
DO $$ DECLARE r RECORD; s text := 'public'; BEGIN FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = s LOOP BEGIN EXECUTE 'SELECT setval(' || quote_literal(s || '.' || r.tablename || '_id_seq') || ', (SELECT COALESCE(MAX(id), 1) FROM ' || s || '.' || r.tablename || '));'; EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'エラーが発生したテーブル: %', r.tablename; END; END LOOP; END; $$;
schemaやシーケンステーブルの命名規則が異なる場合は適宜修正してください。
エラーが発生したテーブルも適宜良しなに。
Rspecテストで同時アクセスによる排他テストを行う
商品購入に同時にアクセスがあった場合、在庫数がマイナスにならないようにテストしておきたい。
そんなテストをしたいタイミングが多々あるのではないでしょうか?
その際テーブルロックを利用して排他制御を行っていた場合、何も考えずにrspecでThreadを使って同時にロジックを実行させても上手く動きません。
これはrspecはテストの開始時にTransactionを張ってしまうため、Thread内の別プロセスからはメインプロセスで作った未コミットのモデル群にアクセスできないためです。
そこでrspec開始時にtransactionを張らないように修正する必要があります。
前提
今回はDatabaseCleanerを利用しています。
環境
Service | Version |
---|---|
Ruby | 3.2.2 |
Ruby on Rails | 7.0.6 |
rspec | 3.12.0 |
database_cleaner | 2.0.2 |
mariadb | 11.1.2 |
対応
rspecのデータ削除の仕組みであるuse_transactional_fixtures
を非活性化し、特定条件下ではDatabaseCleanerのtransactionも張らないようにします。
spec/rails_helper.rb
- config.use_transactional_fixtures = true + config.use_transactional_fixtures = false config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:truncation) end config.before(:each) do |example| + if example.metadata[:disable_database_cleaner] + DatabaseCleaner.strategy = nil + else DatabaseCleaner.strategy = :transaction DatabaseCleaner.start + end end config.after(:each) do |example| + if example.metadata[:disable_database_cleaner] + # do nothing + else DatabaseCleaner.clean + end end config.after(:all) do DatabaseCleaner.clean_with(:truncation) end
利用方法
上記設定を読むと、disabled_database_cleaner
を設定するとstrategyとしてtransaction
を利用しないようにしていることが分かります。
具体的な使い方としては下記のようになります。
RSpec.describe Order, type: :model do describe "self.create_from_cart!" do context "parallel test", disable_database_cleaner: true do let!(:product) { create(:product, :with_inventory) } let!(:cart_items) { create_list(:cart_item, 100, product: product, quantity: 1) } it "should work on multiple threads" do product.product_inventory.update!(amount: 50) cart_items.map do |cart_item| Thread.new(cart_item.cart) do |cart| ActiveRecord::Base.connection_pool.with_connection do Order.create_from_cart!(cart) rescue nil end end end.each {|t| t.join } expect(product.product_inventory.reload.amount).to eq(0) expect(Order.count).to eq(50) end end end end
context "parallel test", disable_database_cleaner: true do
の部分でtransactionを張らないよう設定をしています。
コード自体の説明はしませんが、create_from_cart!
内ではproduct_inventory
をSELECT FOR UPDATE
でコールし、排他制御を行っています。
所感
最近はやりのChatGTPに解決方法を聞いたりしたけど、全然的を射た回答はくれないし、「transactionが怪しいのでは?」と気づいたあたりで再度聞いても動きもしないコードを示してきたりで逆に時間がかかってしまった。。。
上司としての才能がないということが証明されてしまったようで辛い。。。
Ruby on RailsでInstagram Graph APIを使いたい
Ruby on Rails上でInstagram Graph APIを使う情報が少なかったので、はまりポイントも併せて備忘録を残していく。
環境
Service | Version |
---|---|
Ruby | 3.2.2 |
Ruby on Rails | 7.0.4 |
devise | 4.9.2 |
omniauth-oatuh2 | 1.3.1 |
omniauth-rails_csrf_protection | 0.1.2 |
omniauth-facebook | 9.0.0 |
koala | 3.4.0 |
構築
下準備
まずはどんな環境でも必須となる、下記準備を行ってください。
実装
やることは以下です。
- omniauth-facebookでAccessToken取得
- 取得したAccessTokenからFacebookページ用のAccessToken取得
- Facebookページ用のAccessTokenを使ってInstagram Business Account IDを取得
事前準備
まずはMeta Developerでアプリを作成し、アプリIDとapp secretを取得してください。
そしてFacebookログイン/Instagram Graph API設定を行ってください。
config
まずはomniauthの設定。
一応v16を使おうと思ったのでclient_options
を指定していますが、ここは不要だと思います。
scopeに関してもとりあえず詰め込んでいますが、ご自身で精査してください。私も後で精査予定。
config/initializes/devise.rb
config.omniauth :facebook, Rails.application.credentials.dig(:facebool, :app_id), Rails.application.credentials.dig(:facebool, :app_secret), { client_options: { site: 'https://graph.facebook.com/v16.0', authorize_url: "https://www.facebook.com/v16.0/dialog/oauth" }, scope: 'instagram_basic,instagram_content_publish,instagram_manage_comments,instagram_manage_insights,instagram_shopping_tag_products,pages_show_list,pages_read_engagement,instagram_manage_messages' }
callback
ここまでは普通のomniauthなので難しくはないと思うので、viewだとかは省略。
ここからは取得したTokenを使ってInstagram Graph APIを使用できるTokenを取得します。
omniauth_callbacks_controller.rb
def facebook token = request.env['omniauth.auth']['credentials']['token'] # 実際はページングとかもありますが、この解説では無視します。 accounts = Koala::Facebook::API.new(token).get_connections(:me, :accounts) accounts.each do |account| graph = Koala::Facebook::API.new(account["access_token"]) me = graph.get_object(:me, { fields: :instagram_business_account }) ib_id = me["instagram_business_account"]["id"] access_token = account["access_token"] # something process end # something process end
fetch media
上記で取得したib_id
とaccess_token
を使うことで、Instagram Graph APIを利用できます。
media_field = [:id, :media_url, :caption, :media_type, :thumbnail_url ] graph = Koala::Facebook::API.new(access_token) live_media = graph.get_connections(ib_id, "live_media", { fields: media_field })
やったぜ!!
はまりポイント
その1
Q: Facebook/Instagramって別物なの?
ドキュメントを読むと同じエンドポイントっぽいけど、omniauth-instagram-graphやinstagram_graph_apiのようなInstagram特化gemがあるよ?
A: 同じです。
それらのGemは使う必要はないです。
omniauth-instagram-graphは名前にgraphって入ってるのに、発行されるtokenはInstagram Basic Display APIようのものでした。
それっぽい名前使うんじゃねぇ!と言いたいところ。
何か意図があるのかしら。。。
instagram_graph_apiは使ってもよいかも。
omniauthで取得したAccessTokenを食わせれば、callbackの部分に書いた内容を丸っと肩代わりしてくれるかもしれない。
ただ更新が3年前で止まっているので今回は見送りました。
koalaで普通にいけるしね。
その2
Q: Tried accessing nonexisting field (media) on node type (User)
下準備はしっかり行ったのに、エラーが出るよ?
A: Access Tokenは3種類ある!
最初はomniauthで取得できるAccessTokenを使用し、かつ{ig-user-id}
部分をme
で対応していた。
しかしこれは両方ともダメ。
まずAccessTokenは以下の三種類存在するが、最後のページのTokenを利用する必要がある。
- OAuthで取得できるToken
- 長期化したToken
- 各ページごとのToken
omniauth-facebook
は第2トークンまでは勝手に取ってくれる。
第3トークンは実装編に書いている通り。
第2トークンを使っていると、Tried accessing nonexisting field (media) on node type (User)
というエラーが出る。
これに長らくはまってしまった。
そしてこれまたはまりポイントが、{id-user-id}
はちゃんと指定しなければならない。
この取得方法が最初は分からず、ここも詰まってしまった。
そんな中に見つけた下記サイトにヒントを得て、実践編の形に落ち着いた。
https://tsushiru.com/sns/instagram-graph-api-get-access-token-and-business-account-15-0-2.html
所感
APIはFacebook Graph APi/Instagram Graph API/Instagram Basic Display APIと三種類もあるし、
AccessTokenもInstagram Basic Display API用やら短期/長期/ページ用等複数あるし、
app id/app secretもFacebookのものとInstagram Basic Display API用のがあるし、
まずそもそもInstagram Graph APIを使うにはビジネスアカウントにしたりFacebook Pageと連携が必要だったり、
むっずかしいな!!