MogLog

日記 兼 備忘録

AWSアカウントとIAMユーザ

AWSのアカウントとかユーザといった概念をちゃんと理解できていなかったので調べてポイントになりそうなところだけ整理した。

AWSアカウント、ルートユーザ

  • AWSを使うぞ」となったらまずアカウント登録が必要になるが、このとき最初に作成するアカウントがAWSアカウント。TwitterFacebookのようなWebサービスを使うときと同じように最初にアカウントを作るが、それと同じような感じ。
  • AWSアカウントは ルートユーザ とも呼ばれており、名前の通りAWS上の全権限を持っている。
    • ルートユーザを使って日常的なタスクを行うことは、たとえ管理者タスクであっても推奨されていない。代わりに後述のIAMユーザを使うことが推奨されている。
  • AWSアカウントでマネジメントコンソールにログインするときは、メールアドレスとパスワードで認証する

IAMユーザ

  • AWSのIAM(Identity and Access Management) というサービスを使って作られるユーザがIAMユーザである
    • よって、AWSアカウントの下にIAMユーザが紐づくような関係性になる
    • そしてIAMユーザが紐づくAWSアカウントはただ1つ
  • IAMユーザは人が使うように作ることもできるし、プログラムのみから使うように作ることもできる。
    • 仕事で使う場合、おそらくAWSの管理チームからIAM Userを作って渡されるような形になることが多いと思う
  • IAMアカウントと同じようにマネジメントコンソールからログインできるが、あくまでもIAMユーザはAWS上のリソースであり、AWSアカウントの制御下にある概念。
  • IAMユーザでマネジメントコンソールにログインするときは、AWSアカウントのIDとユーザ名とパスワードで認証する

余談だが、Linuxのroot/ユーザの関係とかなり似ていると思い、Linuxを知っている人ならそう考えると理解しやすいんじゃないかと思った。

参考リンク

aws-sdk-s3 for Rubyを使ってみた

今更感があるが、Rubyaws-sdk-s3 gemを使ってS3と戯れた記録を残す。

事前準備

Gemとしては aws-sdk-s3 だけ使えれば良いので、これをGemfileに書く。

# ./Gemfile
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'aws-sdk-s3'

bundle install を実行。Gemのセットアップはこれで完了。

$ bundle install --path vendor/bundle

次、sdkからAWSにアクセスするためにはアクセスキーを作成する必要があるので、マネジメントコンソールから作成する。作成方法は省略するが、下記のドキュメントを見て行った。 aws.amazon.com

入手したアクセスキーとシークレットアクセスキーをaws cliを使ってセットアップする。awscliはhomebrewを使ってインストールした。

$ brew install awscli

awscliをインストールしたら、aws configure で対話的に認証情報の設定ができる。

$ aws configure
AWS Access Key ID [None]: xxx(入手したアクセスキーを入力)
AWS Secret Access Key [None]: yyy(入手したシークレットアクセスキーを入力)
Default region name [None]:(Enter)
Default output format [None]:(Enter)

実行後、~/.aws/credentials~/.aws/config が作られているはず。これで事前準備は完了。 なお、認証情報の設定については以下のドキュメントが参考になった。 docs.aws.amazon.com

バケットの作成

まずはバケットの作成から。Aws::S3::Resource#create_bucket で作ることができる。
ここでは mogulla3 という名称のバケットを作った。

require 'aws-sdk-s3'

s3 = Aws::S3::Resource.new
bucket = s3.create_bucket(acl: 'private', bucket: 'mogulla3') 
# => #<Aws::S3::Bucket:0x00007fae70403be0 ..>

バケットの存在確認

バケットの存在は Aws::S3::Bucket#exists? で確認できる。

require 'aws-sdk-s3'

s3 = Aws::S3::Resource.new
bucket = s3.bucket('mogulla3')
bucket.exists?
# => true

バケットの一覧を取得する

Aws::S3::Resource#bucketsバケットの一覧(Aws::S3::Bucket::Collection)を取得できる。

require 'aws-sdk-s3'

s3 = Aws::S3::Resource.new
s3.buckets.each do |bucket|
  pp bucket
  # => #<Aws::S3::Bucket:0x00007fd06f0d3ee0 ..>
end

ローカルのファイルをパス指定でS3にアップロードする

ローカルPC上にあるファイルをS3上にアップロードしたいようなケースを想定。
Aws::S3::Object.upload_file と使うとアップロードできる。

require 'aws-sdk-s3'

file = File.new('./sample.txt') 
s3 = Aws::S3::Resource.new
obj = s3.bucket('mogulla3').object('sample.txt') # S3オブジェクトのキー名
obj.upload_file(file.path)

ローカルのファイルのコンテンツを読み込んでS3にアップロードする

今度は、ローカルPC上にあるファイルのコンテンツを直接渡して、S3上にアップロードしたいようなケースを想定。 Aws::S3::Bucket#put_object を使うとアップロードできる。

require 'aws-sdk-s3'

file = File.new('./sample.txt') 

s3 = Aws::S3::Resource.new
bucket = s3.bucket('mogulla3')
bucket.put_object(acl: 'private', body: file.read, key: 'sample2.txt')
# => #<Aws::S3::Object:0x00007fae6ecaa4f8 ..>

S3オブジェクトのダウンロード & 読み込み

S3上にあるファイルをローカルPC上にダウンロードして、そのコンテンツを読み込む。

require 'aws-sdk-s3'

s3 = Aws::S3::Resource.new
bucket = resource.bucket('mogulla3')
obj = bucket.object('sample.txt')
get_object_output = obj.get
io = get_object_output.body

puts io.read
# => ファイルの中身が出力される

Aws::S3::Object#getAws::S3::Types::GetObjectOutputインスタンスを取得し、そのインスタンスに対して #body を呼び出すと StringIO 形式でコンテンツを取得できる。

docs.ruby-lang.org

同じことは、Aws::S3::Client を使うと少しスッキリ書けそうだった。

require 'aws-sdk-s3'
 
client = Aws::S3::Client.new
obj = client.get_object(bucket: 'mogulla3', key: 'sample.txt')
io = obj.body 
 
puts io.read
# => ファイルの中身が出力される

オブジェクトを削除する

オブジェクトの削除は Aws::S3::Bucket#delete_objects でやる。引数に渡すHashの構造が地味に面倒。

require 'aws-sdk-s3'
 
s3 = Aws::S3::Resource.new
bucket = resource.bucket('mogulla3')
res =  bucket.delete_objects(
  delete: {
    objects: [
      { key: 'sample.txt' },
    ]
  }
)

バケットを削除する

バケットの削除はAws::S3::Bucket#deleteAws::S3::Bucket#delete! を使うとできるが、#delete はオブジェクトが空じゃないと実行できない。ここでは delete! を使う。

require 'aws-sdk-s3'
 
s3 = Aws::S3::Resource.new
bucket = s3.bucket('mogulla3')

bucket.delete!
# => #<struct Aws::EmptyStructure>

終わりに

以上、使ってみた記録を残した。APIドキュメントとにらめっこしながら実行してみたログなので、適切な使い方かはちょっと自信がない。
Clientオブジェクトを使っても色々できそうだったが、今回は基本的に Resource からたどる形式でやってみた。

参考URL

Rails6で導入されたinsert_all、insert_all!、upsert_allを使ってみる

Rails6にて insert_all, insert_all!, upsert_all という一括作成・更新の機能が導入された。 railsguides.jp

これらのメソッドを、公式のAPIドキュメントを読みつつ手元の環境で実際に使ってみた記録を残す。

環境

検証用データベースとして、次のスキーマのusersテーブルがあるものとする

Column Type Nullable Default
id integer not null nextval('users_id_seq'::regclass)
name character varying not null
hobby character varying
created_at timestamp without time zone not null
updated_at timestamp without time zone not null

insert_all, insert_all!

まずかんたんな例として、3件のuserデータの一括作成を試みる。
APIドキュメント によると、引数はHashの配列を渡す必要があるとのこと。name キーだけ含んだHashの配列を与えて実行してみる。

users = [
  { name: 'Alice' },
  { name: 'Bob' },
  { name: 'Charles' },
]
User.insert_all(users)
# => ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  null value in column "created_at" violates not-null constraint

created_at のNOT NULL制約に引っかかって、ActiveRecord::NotNullViolation 例外が発生してしまった。 NOT NULL制約がついているカラムは明示的に指定しなければならないようだ。

users = [
  { name: 'Alice', created_at: Time.current, updated_at: Time.current }, 
  { name: 'Bob', created_at: Time.current, updated_at: Time.current }, 
  { name: 'Charles', created_at: Time.current, updated_at: Time.current },
]
res = User.insert_all(users)
# => #<ActiveRecord::Result:0x00007f92fc3c3728 @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007f92fb5ca180 @limit=4, @precision=nil, @range=-2147483648...2147483648, @scale=nil>}, @columns=["id"], @hash_rows=nil, @rows=[[2], [3], [4]]>

User.count
# => 3

今度は成功する。発行されるSQLは以下の通り、1SQLだ。

INSERT INTO "users"(
  "name", "created_at", "updated_at"
) 
VALUES 
  ('Alice', '2019-12-01 14:06:12.151661', '2019-12-01 14:06:12.151710'), 
  ('Bob', '2019-12-01 14:06:12.151717', '2019-12-01 14:06:12.151721'), 
  ('Charles', '2019-12-01 14:06:12.151724', '2019-12-01 14:06:12.151745') 
ON CONFLICT DO NOTHING RETURNING "id"

返り値として ActiveRecord::Resultインスタンスが返ってきており、#to_aで作成した行を参照できるが、ここにはid情報しか含まれていない。

res.to_a
# => [{"id"=>2}, {"id"=>3}, {"id"=>4}]

作成処理を実行したあとにその行情報を参照して何かをしたいことよくあるので、このままだとやや使い勝手が悪いだろう。
そういったときは、#insert_allreturing オプションを使うことで返り値の情報を変更できる。 しかし、APIドキュメントによると returning オプションはPostgreSQL限定のようなので注意されたし。

users = [
  { name: 'Alice', created_at: Time.current, updated_at: Time.current }, 
  { name: 'Bob', created_at: Time.current, updated_at: Time.current }, 
  { name: 'Charles', created_at: Time.current, updated_at: Time.current },
]
res = User.insert_all(users, returning: %i[id name hobby])

res.to_a
# => [
#   {"id"=>8, "name"=>"Alice", "hobby"=>nil},
#   {"id"=>9, "name"=>"Bob", "hobby"=>nil},
#   {"id"=>10, "name"=>"Charles", "hobby"=>nil}
# ]

次に、unique制約に引っかかるデータを同時に作成しようとしたときにどうなるかを見てみる。usersテーブルのnameカラムにはunique制約が付いているので、nameの値が同じレコードを同時に作成してみる。

# 2レコードともnameをAliceとする
users = [
  { name: 'Alice', created_at: Time.current, updated_at: Time.current }, 
  { name: 'Alice', created_at: Time.current, updated_at: Time.current },
]
res = User.insert_all(users)

User.count
# => 1

User.count の結果を見るとわかるように、insert_all の場合は重複した行は無視する。もし例外を起こしたい場合はbangつきの insert_all! を使えば良い。

# 2レコードともnameをAliceとする
users = [
  { name: 'Alice', created_at: Time.current, updated_at: Time.current }, 
  { name: 'Alice', created_at: Time.current, updated_at: Time.current },
]
res = User.insert_all(users)
# => ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_name"

User.count
# => 0

ActiveRecord::RecordNotUnique 例外が発生し、userデータは1件も作成されない。

upsert_all

UPSERTの検証をしたいので、name="Alice"のレコードを事前に作っておく。

User.create!(name: 'Alice')

Aliceのhobbyを更新しつつ、name="Bob"のレコードをINSERTしてみよう。

users = [
  { name: 'Alice', hobby: 'cooking' }, 
  { name: 'Bob', created_at: Time.current, updated_at: Time.current },
]
res = User.upsert_all(users)
# => ArgumentError: All objects being inserted must have the same keys

おっと、ArgumentError が発生してしまった。どうやら配列内のHashの構造はすべて一致していなければならないようだ。

users = [ 
  { name: 'Alice', hobby: 'cooking', created_at: Time.current, updated_at: Time.current },
  { name: 'Bob', hobby: nil, created_at: Time.current, updated_at: Time.current },
]
res = User.upsert_all(users)
# => ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_name"

今度は ActiveRecord::RecordNotUnique が発生してしまった。どうやらname="Alice"のHashが新規作成分として見なされてしまったようだ。
どのキーをUPSERTの判断軸とするかを unique_by オプションで明示的に指定するとこのエラーを回避できる。

users = [ 
  { name: 'Alice', hobby: 'cooking', created_at: Time.current, updated_at: Time.current },
  { name: 'Bob', hobby: nil, created_at: Time.current, updated_at: Time.current },
]
res = User.upsert_all(users, unique_by: 'name')
# => #<ActiveRecord::Result:0x00007f9301a1a838 @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007f92fb5ca180 @limit=4, @precision=nil, @range=-2147483648...2147483648, @scale=nil>}, @columns=["id"], @hash_rows=nil, @rows=[[27], [29]]>

User.count
# => 2

しかし、unique_by オプションはPostgreSQLSQLiteでしか使えないようなので、これもまた注意されたし..。MySQLだとこの状況、どうすればいいんだろう。

その他のメモ

その他ドキュメントを読んでいてのメモ

  • insert_allupsert_all はモデルのインスタンスを作らないので、CallbackやValidationはスキップされる
  • bangつきの upsert_all! は存在しない

参考URL

【Ruby】rescueした例外のmessageを上書きしてre-raiseする

rescueした例外のmessageを上書きしてre-raiseしたい場合、Exception#exception を使うとスッキリ書ける。

https://docs.ruby-lang.org/ja/latest/method/Exception/i/exception.html

引数を指定しない場合は self を返します。
引数を指定した場合 自身のコピー を生成し Exception#message 属性を error_message にして返します。

# exception_test.rb
def override_exception_message_and_reraise
  begin
    raise RuntimeError.new('abc')
  rescue => e
    raise e.exception('xyz')
  end
end

次のテストはpassする。

# spec/exception_test_spec.rb
require_relative '../exception_test.rb'

RSpec.describe do
  it do
    expect { override_exception_message_and_reraise }.to raise_error(RuntimeError, 'xyz')
  end
end

『雰囲気で使わずきちんと理解する!整理してOAuth2.0を使うためのチュートリアルガイド』がわかりやすくてオススメ

最近読んで良かった技術本『雰囲気で使わずきちんと理解する!整理してOAuth2.0を使うためのチュートリアルガイド』の紹介をしたい。

読み始めた背景

OAuth2.0、Webアプリケーションの開発をしたことがある人なら一度は触ったことがあるんじゃないかと思う。 自分は仕事でも使っているし、趣味の個人開発でも使ったことがある。

しかし、なんとなく自信が無く、ちゃんとわかった気がしないもやっとした状態が続いていた。
OAuthに詳しくなくても、ライブラリが裏で良い感じにやってくれて、ライブラリのREADMEを読んでとりあえず試してみたら大体やりたいことができてしまってそのままになる...みたいなパターンも良くなかったのかもしれない。

...というような感じで理解が曖昧な状態だったので「いつかOAuthをちゃんと理解して使えるようになりたい」と思っていたときにこの本の存在を知り、分量も多くなさそうだったので読み始めてみたのだった。

所感

まず、ありきたりな感想ではあるが、非常にわかりやすかった。
読んでいてつまづいたり立ち止まることがほとんど無く、すっと頭に入ってきてくれる。多分、章の構成がうまいんじゃないかと思っている。

チュートリアルガイドという名の通り、後半の半分近くはGoogleのOAuthサービスを使って、curlとブラウザで典型的なグラントタイプを試すことができるようになっているが、自分は前半部分のOAuthの仕様・用語についての方がタメになった。

対象読者としては、この本の「はじめに」にも書かれている内容だが、以下の質問にスッと答えられない場合、一読の価値があると思う。

  • スコープとはなんですか?
  • 認可コードは何が行われた証ですか?
  • サーバサイドのアプリケーションの場合、どのグラントタイプを使うべきですか?

恥ずかしながら、自分は読み始めた時点でまともに答えられたのは1つだけだった。が、今は全てちゃんと答えられるようになった。
せっかくなので、自分の回答を載せておく。 ※ ちゃんと読んだらみんな答えられる

スコープとはなんですか?

→ アクセストークンにひも付いている、アクセス権をコントロールするための仕組み。誰のどのリソースにどのような操作を行ってよいかを示す。

認可コードは何が行われた証ですか?

→ リソースオーナーがクライアントへの権限委譲に同意した証(認可サーバが発行し、クライアントがそれを受け取って、アクセストークンを取得するときに使われる)

サーバサイドのアプリケーションの場合、どのグラントタイプを使うべきですか?

→ 認可コードグラント。クライアントクレデンシャルグラントも条件を満たしさえすれば使っても問題ない認識。

ここには回答しか書いていないので、理由が知りたい方や、回答内に知らない用語が出てきたぞという方は本書をどうぞ!

終わりに

とてもタメになる本だったので、勢いで紹介したくなってこのエントリを書いた。 自分は読むスピードは速い方では無いのだが、サクサク読めて休日1日でほとんど読みきれて、かなりコスパの良い1日になったと思う。著者のAuth屋さんには感謝です。

最後に、Twitterで感想を少しつぶやいたらAuth屋さんが拾ってくれたので記念に貼っておく。続編が出るようなので期待して待っています 😃😃😃