行動すれば次の現実

テック中心の個人ブログ

別のAWSアカウント間でS3バケットにあるファイルを転送する方法

異なるAWSアカウント間(クロスアカウント)で、S3バケットにファイルを転送させたいことがありました。

accountA        accountB
 S3bucket   ⇐    file

accountAのS3バケットに対して、accountBからファイル転送する場合を例として設定手順を説明します。 同様のことでお悩みの方はぜひ参考にしてみてください。

accountAのS3バケットを操作するIAMポリシーを作成する

まずはaccountAでIAMポリシーを作成します。

以下のようにS3バケットに対する操作権限を付与したポリシーを定義します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
            ],
            "Resource": "arn:aws:s3:::accountA-bucketname"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::accountA-bucketname/*"
        }
    ]
}

accountAでIAMロールを作成する

1.IAMロールを作成

accountAのIAMロール作成画面を開きます。信頼されたエンティティの種類を選択で「別のアカウント」を選択し、accountBのアカウントIDを入力します。

f:id:furu07yu:20211016174540j:plain

2.IAMポリシーの紐付け

先程作成したIAMポリシーを選択してロールの作成を完了させます。

f:id:furu07yu:20211016174916j:plain
ロールにポリシーを紐付ける

このようにすることでaccountBからaccountAのIAMポリシーを使ってaccountAのS3バケットを操作することが可能となります。 (これを信頼ポリシーと呼びます)

3.信頼ポリシーの編集

これまでの設定によりaccountBのルートユーザに対して信頼ポリシーが設定されたことになります。 ルートユーザではない対象(Principal)に信頼ポリシーを設定することもできます。

下記はaccountBのIAMロールに対して信頼ポリシーを設定する場合の手順です。

f:id:furu07yu:20211016180813j:plain

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::222222222222:role/accountB-roleName"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

まとめ

IAMロールを使用することで、簡単にクロスアカウントのファイル転送が実現できます。

IAMロールだけでアクセス制限の管理ができるので保守性も高く非常にオススメの方法です。

プログラム言語は常にバージョンアップしておいたほうが良い理由

オープンソース化によりプログラミング言語やフレームワーク、ライブラリは日々メンテナンスされており、バージョンアップしています。

新しい機能が追加されることで実装の選択肢が増えますので、顧客の課題解決の幅も広がります。 しかし現実にはサービスをリリースしてから一度もバージョンアップしていないというプロダクトが多く存在するように思います。

バージョンアップをするメリットと注意点を解説いたします。バージョンアップ対応を見直すきっかけになれば嬉しいです。

アプリのソースコードを直すよりも効果的!?

バージョンアップをすることで以下のメリットが生まれます。

  1. 新しい機能が使えるようになり、顧客へのサービス提供の幅が広がる
  2. 既存バグが修正されて、アプリの性能が向上する
  3. メモリの効率化によりパフォーマンスが向上することがある

2と3に関しては、アプリのソースコードをほとんど修正せずとも、これらのメリットを享受することが出来ますので、バージョンアップを避ける理由はありません。

場合によってはアプリのパフォーマンスチューニングをするよりも、バージョンアップのほうがパフォーマンス改善に繋がることもあります。 賢いチームほどバージョンアップを軽視せずに積極的にバージョンアップしてメリットを享受しているのです。

情報感度が上がる

また、バージョンアップを心がけると情報の感度が高まるというメリットもあります。 バージョンアップには、新しいトレンドや技術を取り入れることが多いので、バージョンアップ対応を介して情報のキャッチアップができます。

各種言語、フレームワーク等のSNSアカウントをフォローしてアップデート情報をキャッチアップしましょう。

バージョンアップで注意すべきこと

メリットばかりあるように思えるバージョンアップ対応ですが、注意すべき点がいくつかあります。

安定版のみ使用すること

基本的には安定版のみを使用するようにしましょう。 安定版は致命的な不具合などが解消されていているので、安心して使用することが出来ます。

マイナーバージョンアップは1つずつ

マイナーバージョンアップは1ずつ行うようにしましょう。 基本的にはマイナーバージョン番号を飛ばさないようにしたほうが無難です。

番号を飛ばしてバージョンアップすると変更箇所のキャッチアップが難しくなります。もし問題があった場合に、どのバージョンによる原因なのかが把握できない可能性があります。

メジャーバージョンアップは慎重に

メジャーバージョンアップは大幅な仕様変更を伴ないことが多いので、安易な気持ちで行ってはいけません。 大抵はバージョンアップ手順のドキュメントが用意されていますので、それを参考に行いましょう。

また、「ワーニングが出ていないこと」「ユニットテストが全てパスすること」「画面での動作確認が正常であること」などの確認を行った後に反映するようにしましょう。

バージョンアップはコツコツと

日々の開発タスクの中にバージョンアップ対応を盛り込んでおくことで、変更時の影響が少なくなります。

バージョンアップは一気にやるよりもコツコツやることをオススメいたします。

最低限知っておきたい!Railsのトランザクション実装例

Railsでトランザクションを使いたい方向けに実例を踏まえてわかりやすく解説をいたします。

RailsにおけるTransaction

Railsでトランザクションを使用する場合は、トランザクションの範囲をActiveRecord::Base.transactionブロックで囲む必要があります。

ActiveRecord::Base.transaction do
  # ここに処理を書く
end

transactionブロック内で例外が発生した場合、ブロック内で行われたデータベース更新処理が全てロールバックされます。

Railsでtransactionを機能されるためには例外を発生させる必要があるのです。

例外が発生すればロールバックされる

例外が発生すればロールバックされますので、例外を意識して実装する必要があります。

以下のサンプルコードをご覧ください。

def some_method
  # 省略...
  ActiveRecord::Base.transaction do
    user1.save  # 実行される
    raise 'error'  # 例外が発生してuser1.saveの処理がロールバックされる
    user2.save  # 実行されない
  end
  foo # 実行されない
rescue => e
  bar # ロールバック後に実行される
end

raise 'error'が実行されるとuser1.saveで更新された処理はロールバックされます。 user2.saveはそもそも実行されません。

例外が発生した時点でfooも実行されないことになります。 ただし、rescue句に記述されたbarはロールバック後に実行されます。

transactionブロック内でrescueするとロールバックされないので注意!

よくある間違いに、transactionブロック内でrescue句を記述してしまうというのがあります。このように実装してしまうとロールバックされませんので注意が必要です。

例えば下記を実行した場合、ロールバックされません。

def some_method
  # 省略...
  ActiveRecord::Base.transaction do # BEGIN
    user1.save
    raise 'error'
    user2.save
  rescue => e
    bar # 実行されるがロールバックは発動しない
  end
end

なぜこのようになるかというと、Rails本体に実装されているtransactionメソッドでは、与えられたtransactionブロックに対して例外が発生した場合に、rescueしてロールバックするという処理が実装されています。しかし、transactionブロック内で直接rescue句を記述してしまうとそれが実行されなくなってしまうのです。(詳細は下記コードを参照)

rails/transaction.rb at 291a3d2ef29a3842d1156ada7526f4ee60dd2b59 · rails/rails · GitHub

ロールバック後も処理を継続したい場合はどうする?

ときにはロールバック後にも処理を継続したいケースがあるかと思います。 例えば、ロールバック後に処理が失敗したことを特定のテーブルに書き出すといったケースです。

その場合は、ロールバックする処理自体をメソッド化して例外を握りつぶせばロールバック後も処理を継続することが出来ます。

def do_transaction
  ActiveRecord::Base.transaction do
    user1.save
    raise 'error'
  end
rescue StandardError
  false
end

def fuga
  # 省略...
  if do_transaction
    # 成功時の処理
    foo
  else
    # ロールバック後の処理
    bar
  end
end

ルーティングをネストする際のresourcesとresourceの使い分け | Rails

Railsで開発したことがある方は、ルーティングをネストしようとした場合に、resourcesとresourceのどちらを使用すべきか迷ってしまったことがあるのではないでしょうか?(私もその一人です)

この記事ではresourcesとresourceの使い分けについて、親リソースをブログ記事(topicsテーブル)と見立てて、わかりやすく解説をいたします。

子リソースのCRUDを定義したい場合はresources

親リソースに子リソースが属している場合は「resources」を使用します。 例えば、ブログ記事(親リソース)に対して、コメント(子リソース)を付くようなケースが該当します。

「親:子」は「1:N」なのでCRUDの全てのアクションが定義されます。

routes.rb
resources :topics do
    resources :comments, module: 'topics'
end
routing一覧
    topic_comments GET    /topics/:topic_id/comments(.:format)            topics/comments#index
                   POST   /topics/:topic_id/comments(.:format)            topics/comments#create
 new_topic_comment GET    /topics/:topic_id/comments/new(.:format)        topics/comments#new
edit_topic_comment GET    /topics/:topic_id/comments/:id/edit(.:format)   topics/comments#edit
     topic_comment GET    /topics/:topic_id/comments/:id(.:format)        topics/comments#show
                   PATCH  /topics/:topic_id/comments/:id(.:format)        topics/comments#update
                   PUT    /topics/:topic_id/comments/:id(.:format)        topics/comments#update
                   DELETE /topics/:topic_id/comments/:id(.:format)        topics/comments#destroy

親リソースにCRUD以外の振る舞いを定義したい場合はresource

親リソースに対してCRUD以外の振る舞いを定義したい場合は「resource」を使用します。 例えば、ブログ記事(親リソース)に対して、更新予約(子リソース)をするようなケースが該当します。

あくまでも親リソースに対しての振る舞いなので子リソース側にindexアクションは定義されません。

routes.rb
resources :topics do
  resource :comments, module: 'topics'
end
routing一覧
new_topic_comments  GET    /topics/:topic_id/comments/new(.:format)             topics/comments#new
edit_topic_comments GET    /topics/:topic_id/comments/edit(.:format)            topics/comments#edit
     topic_comments GET    /topics/:topic_id/comments(.:format)                 topics/comments#show
                    PATCH  /topics/:topic_id/comments(.:format)                 topics/comments#update
                    PUT    /topics/:topic_id/comments(.:format)                 topics/comments#update
                    DELETE /topics/:topic_id/comments(.:format)                 topics/comments#destroy
                    POST   /topics/:topic_id/comments(.:format)                 topics/comments#create

ルーティング定義はセンスが大事

明確なルールの元、正しくルーティング定義をしなければ保守性の高いシステムは作れません。

ルールを統一すると、ルーティングを見ただけでどのようなシステムなのか把握できる上、コントローラーのスコープが明確になります。 なんとなくルーティングを定義するのではなく、美しくルーティングを定義できるようにセンスを日々磨いていくことが重要であると私は考えます。

RailsアプリでRubyバージョンアップする方法(2.6 to 2.7)

この記事では、Railsアプリをrbenvを使用してv2.6.8からv2.7.4にアップグレードする方法を説明します。

インストールされているrubyのバージョンアップを確認する
 rbenv versions
インストール可能なrubyのバージョン一覧を確認する
rbenv install --list-all
rubyのバージョンを指定してインストールする
rbenv install 2.7.4
全てのプロジェクトでインストールしたrubyを使用する場合
rbenv global 2.7.4
特定のプロジェクトでインストールしたrubyを使用する場合
rbenv local 2.7.4
rubyが適用されたことを確認する
ruby -v
Gemfileのrubyのversionを書き換える
ruby '2.6.8'
↓
ruby '2.7.4'
bundlerのバージョンがruby2.7系に同梱されているversionと不一致なので揃える

bundler2を使用する場合

bundle update --bundler

1.17.2を継続して使用する場合

gem install bundler:1.17.2
下記3ファイルが更新される
  • .ruby-version
2.6.8
↓
2.7.4
  • Gemfile
ruby '2.6.8'
↓
ruby '2.7.4'
  • Gemfile.lock
RUBY VERSION
  ruby 2.6.8p205
  ↓
  ruby 2.7.4p191

BUNDLED WITH
  1.17.2
  ↓
  2.2.28
新しいbundlerでgemをインストールする
bundle install --path=vendor/bundle
railsコマンドが使用できなくなるので再インストールする
gem install rails
その他の設定があれば変更
  • circle ciで使用してるdocker imageのversion変更など

HerokuのR14エラーをScoutAPMで対策する方法 | Rails

Herokuではメモリ使用量がプランの上限を超えるとR14エラーが発生します。 R14はスワップメモリが発生していることを意味しており、発生するとアプリケーションの処理速度が著しく低下してしまいます。

このままメモリ使用量が200%を超えてしまうと、R15エラーが発生してdynoが強制再起動されてしまいます。 このような自体にならないためにもR14エラーが発生した場合は、早急に原因を特定して改善する必要があります。

Scout APMでRailsのパフォーマンスチューニング

スワップメモリを発生させないためにはメモリ使用状態をモニタリングする必要があります。 メトリクス監視サービスとしてNewRelicなどが有名所ですが、筆者はScout APMをオススメします。

NewRelicの有料プランを使用していたのですが、以下の点が不便であると感じていました。

  • 多言語をサポートしているためフレームワーク固有の情報が取得できない
  • 多機能すぎて使いこなすのが難しい
  • サーバー単位でのメトリクスは詳細に把握できるが、どこのURLのアクションが問題を起こしているのかが把握できない

その点、Scout APMはRailsに特化しているので上記の問題点がすべて解消されます。 さらに、Githubと連携することでコード単位での分析も可能となり、ピンポイントに原因を特定することが可能になります。

以下に、R14エラーが発生した際の原因特定からパフォーマンスチューニングまでの流れを説明します。 R14エラーに悩まされている方はぜひ参考にしてみてください。

STEP1. メモリ使用量が大きいアクションを特定する

R14エラーを検知したらScout APMのDashboardから原因のアクションを特定します。

下記画像(Scout APM Dashboard)のようにMEMORYチャートを表示させて、発生した時間帯にシークバーを設定します。 設定すると「Largest Memory Increase」というウィンドウにメモリ使用量の大きい順でアクション情報が出力されます。

StoreLayouts::PrintsController#indexが41MBのメモリを使用していることが確認できます。 このアクションが原因であることが確認できます。

Scout APM Dashboard

STEP2. トレース情報から問題のあるコードを特定する

StoreLayouts::PrintsController#indexの箇所をクリックすると、より詳細なトレース情報が表示されます。 下記画像(Transaction Traces)を参照。

Transaction Traces

Transaction Tracesの該当アクションをクリックすると、実行された処理がタイムライン形式で表示されます。 下記画像(Memory Allocation)を参照。

「Memory Allocation Breakdown」タブを選択するとメモリーアロケーションの回数などが表示されます。 また、N+1が発生した場合はその発生回数が表示されますので、問題のありそうなコードがおおよそ把握できます。

Memory Allocation Breakdown

backtraceというラベルをクリックするとGithub上のコードが表示されます。 下記画像(backtrace)を参照。

backtrace

N+1が多く発生している箇所などを中心に問題のある箇所がないかをチェックしていきます。

R14エラーの場合、N+1以外にも、ActiveRecordのfind時に無駄にincludesしていることで徐々にメモリリークになるケースも多いので、そのような箇所もチェックしていきます。

Scout APMで調査できることはここまでです。 後は上記の調査結果からソースコードの当たりをつけてパフォーマンスチューニングを実際に行っていきます。

STEP3. 実際にパフォーマンスチューニングを行う

ソースコードの当たりがついたらメモリを測定します。

RubyにはObjectSpace.memsize_of_allというメソッドがありますので、それを使用します。 application_controller.rbなどの共通参照できる箇所に以下のようなメソッドを定義しておきます。

def log_memory(label)
    puts '--------'
    puts "--------#{label}"
    puts "#{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"
    puts '--------'
    puts '--------'
  end

実行するとMB単位でメモリ使用量がコンソール出力されますので、メモリを多く使用していると思われる処理の前後に仕込んでおき、メモリ使用量を測定します。

log_memory('before method_a')
method_a
log_memory('after method_a')

メモリ使用量過多の箇所が特定できれば、後は適宜実装を改善した上で改めてメモリ使用量を測定します。

メモリの使用量が改善されればパフォーマンスチューニングは成功です。

実装するよりも実装しないほうが難しい

私はWebサービス(SaaS)を開発・保守しています。

サービス開発をしているとついつい「あの機能も必要かな?この考慮も必要かな?」と考えを巡らせます。 実装が終わってみると予定よりも多くの機能が出来上がり、大きな満足感を得ることが出来ます。

しかし、その機能は本当に必要だったのでしょうか。

そのような経緯で生まれた機能は、実際に開発して運用してみると使わなくなることが往々にしてあります。

私もこのような経験を数多くしてきました。 このような自体に陥らないために、実装するかどうかの私なりの判断基準を解説したいと思います。

サービス開発・保守を担当している方はぜひ一つの参考にしてください。

実装するのは誰でもできる

実装するという選択を安易に選んではいけません。

なぜなら「実装する」ということは、その機能を責任持って保守をし続けると確約することだからです。 (安易にペットを飼ってはいけないという感覚に近いと感じます)

筆者は「実装する」と選択するよりも、「実装しない」と選択するほうが難しいと感じます。

機能を実装する(作る)かどうかを判断する上で以下のカテゴリに分類されます。

  1. 作る必要がある機能
  2. 作る必要がない機能
  3. 作ったほうが良い機能

この内「3. 作ったほうが良い機能」を「作る」という選択を安易にしてしまうのは非常に危険です。 なぜならそのような機能を実装する理由が実装者の思い込みによるところが大きいためです。

この機能があったほうが便利だろう、という理由は実装者の感覚でしかなく何も根拠がないからです。 この機能は絶対使われると確信できないのであれば、いっそのこと作らないと選択したほうが良いです。

作ったが使われなかった場合、その作業に費やした工数。今後その機能をメンテナンスしなければならない工数は全て無駄になってしまいます。

それでも作るのであればそれは実装者のエゴか、保身としての選択になります。 ある意味「逃げの選択」と言い換えることもできます。

※ ただし、新育のためにあえて無駄な機能を作る(作らせる)のはありだと思います。教育という明確な目的があるためです。

実装しないという選択

では、どうすればよいかというと「作る必要がある機能」のみ作れば良いです。

「作る必要がある機能」のみを搭載した最低限の状態(MVP Minimum Viable Product)で顧客に使用してもらい、本当に必要な機能を見極めてもらいます。これはMVP戦略と呼ばれています。

MVPを使用した上で、顧客から意見をフィードバックしてもらいます。そのフィードバックから生まれた機能は「作る必要がある機能」として確信をもって実装することが出来ます。

この時、前提として「作ったほうが良い機能」については、初期開発では無駄な機能は開発しない旨をあらかじめ顧客に提案して合意もらっておく必要があります。

合意をもらうというアクションは極めて重要です。これをしなければ、なぜ作ってもらえないのかと顧客から反発を受ける可能性があるためです。

実装を捨てる

すでに実装してしまった場合は最後まで責任持って保守し続けるしかないのか、というとそうではありません。 根拠を持って実装を捨てれば良いです。

いきなり捨てるのではなく、以下のようなアクションを経て捨てる根拠を見出します。

  1. 顧客からその機能を使っているかヒアリングする
  2. GoogleTagManagerやサーバーログからその機能の使用数を測定する
  3. ABテストでその機能を一時的に塞いで反響を見る

これらのいずれかの方法で捨てても良い根拠を見出だせたらその機能を捨てることが可能となります。