(個人用)mac初期設定
新規端末を設定する必要があったのでまとめておきます。
OSはVenturaです。
随時更新
デバイス周り
ディスプレイ
解像度を上げる
システム環境設定->ディスプレイ->解像度:サイズ調整->スペースを拡大
サブモニター
あれば繋ぐ
ダークモード
システム環境設定->外観 -> 外観モード -> ダーク
Dock
左側に置く
林檎マーク -> システム設定 -> デスクトップとDock -> 画面上の位置を左に変更
Dockを隠す 林檎マーク -> システム設定 -> デスクトップとDock -> Dockを自動的に表示/非表示
Dock表示のアニメーションを切る
terminalで
//アニメーション無効化 defaults write com.apple.dock autohide-time-modifier -int 0 killall Dock //戻したい時 defaults write com.apple.dock autohide-time-modifier -int 1 killall Dock
Finder
下部にパスを出す
Finderを有効にする -> 表示 -> パスバーを表示
マウス
マウスカーソル大きく
林檎マーク -> システム設定 -> アクセシビリティ -> ディスプレイ -> カーソルのサイズ(半分ぐらい)
スクロールの向きを逆にする
システム設定 -> マウス -> ナチュラルなスクロールをオフ
(mos入れた方が良さげ)
ホットコーナー
画面の隅にマウスカーソルを持って行った時の挙動の設定
林檎マーク -> システム設定 -> デスクトップとDock -> ホットコーナー(一番下)
左上にデスクトップ、右上にMission Control設定
キーボード
(US配列用)日本語切り替え
control + スペース
CapsLockをcontrolに入れ替え
Macで修飾キーの動作を変更する - Apple サポート (日本)
開発ツール周り
VScode
CLIから使えるようにする
Cmd + Shift + P -> shellで検索 -> Install 'code' command in PATH
(再起動すると消える?)
terminal
Docker
設定から自動で起動するようにする
Docker Desktopの歯車アイコン -> General -> start Docker Decktop when you log in
node
slack
通知のバッジをアイコンに表示
slackアプリ側: 環境設定 -> 通知 -> 新しいアクティビティがあった時にバッジで知らせる
mac側: システム設定 -> 通知 -> slackを許可
Github Copilot1日目
どうでしょう
GraphQLとDataloader
gqlgenのサンプルコードがスッと入ってこなかったので整理
参考URL
Optimizing N+1 database queries using Dataloaders — gqlgen
GitHub - graph-gophers/dataloader: Implementation of Facebook's DataLoader in Golang
【GraphQL × Go】 N+1問題を解決するgqlgen + dataloaderの実装方法とCacheの実装オプション - LayerX エンジニアブログ
gqlgenのGetting Startedで使っているスキーマを使用します。
Building GraphQL servers in golang — gqlgen
type Todo { id: ID! text: String! done: Boolean! user: User! } type User { id: ID! name: String! } type Query { todos: [Todo!]! }
Dataloaderって何
データ取得をまとめ(Batch)たりキャッシュしたりする仕組み。
GraphQLではlazy loadingによるN+1問題の回避に使われる。
resolverの分割とN+1問題
todo resolver内でuserを取得するのではなく、userを取得するresolverを分けることができる。
こうすることでuserフィールドが必要な時だけuser resolverが呼ばれるため、userフィールドが不要なクエリに対してuserの取得処理をスキップすることができる。
一方で取得したN件のtodosに対して、それぞれuser resolverが実行されるためN+1問題が発生する。
lazy loading
短い時間で必要なidを貯めて、時間経過後にデータをまとめて取得する方法。
GraphQLではそれぞれ実行されたuser resolverでidを貯めていき、時間経過後にidを全て受け取った上で取得処理(SQLなど)を実行する。
N件発行されていたSELECT FROM users WHERE id=?
をSELECT FROM users WHERE id IN (?, ?, ?, ?)
の1クエリにまとめるイメージ。
その他の対処法
GraphQLでは有効ではないが一般的なN+1問題回避法について
いずれもresolverの分割と相性が悪く、userフィールドが必要な場合のみuserを読み込むという方法が難しいためGraphQLでは微妙になる
eager loading
Todoを取得するSELECT文と、Userを取得するSELECT文を実行して両方の結果をコード(ORM)側で繋ぎ合わせる方法
JOIN
TodoでのSelect時にUserをJOINで結合してしまう方法
柔軟にフィールドを取捨選択するようなクエリではなかったのと、resolverの仕組みを理解していなかったため私は実務でこれをやっていました。
Go(gqlgenとgraph-gophers/dataloader)での例
loaderの生成
lazy loadingの仕組みから、user resolverで実行するidを貯めていく処理
と貯めたidからuserをまとめて取得する処理
の実装が必要になりそうである。
その前にdataloaderからloaderを作成する。
//dataloaderドキュメントから // create Loader with an in-memory cache loader := dataloader.NewBatchedLoader(batchFn) batchFn := func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { var results []*dataloader.Result // do some async work to get data for specified keys // append to this list resolved values return results }
貯めたidからuserをまとめて取得する処理
上記のbatchFnに相当し、gqlgenドキュメントにおけるgetUsersはこれを実装したもの。
SELECT * FROM users WHERE id IN()
を実行し、型を合わせていく
//gqlgenドキュメントから func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] { //貯めたidからIN句を作る //SELECT id, name FROM users WHERE id IN (?, ?, ?, ?, ?) stmt, err := u.db.PrepareContext(ctx, `SELECT id, name FROM users WHERE id IN (?`+strings.Repeat(",?", len(userIds)-1)+`)`) (略) //result配列を作り result := make([]*dataloader.Result[*model.User], 0, len(userIds)) //SQLの結果をresultに詰めていく for rows.Next() { (略) result = append(result, &dataloader.Result[*model.User]{Data: &user}) } return result }
idを貯めていく処理
dataloader導入前はuser resolver内でSELECT文を実行していたが、その代わりにloader.Load()
を叩く
Loadはidを渡した後に処理がブロックされ、batchFnが実行されたタイミングで返り値としてuserを受け取るイメージ
//gqlgenドキュメントから func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { return loaders.GetUser(ctx, obj.UserID) } // GetUser returns single user by id efficiently func GetUser(ctx context.Context, userID string) (*model.User, error) { loaders := For(ctx)//contextに詰めたloaderを取り出す return loaders.UserLoader.Load(ctx, userID)() }
その他の処理
上記が主な処理であり、gqlgenドキュメントでの残りの処理は
- 複数のloaderをloadersにまとめる(func NewLoaders)
- リクエストに対してloadersを生成してcontextに仕込むmiddleware(func Middleware)
- loadersをcontextから取り出す(func For)
など
実行してみる
userをいくつか生成し、Queryの各所にログを仕込んで処理の流れを確認しました。
// User is the resolver for the user field. func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { fmt.Printf("user resolver for ID %s\n", obj.ID) user, err := loaders.GetUser(ctx, obj.UserID) fmt.Printf("user ID %s loaded!!\n", obj.ID) return user, err }
batchFnであるgetUsersでは引数のuserIdをもとにUserを作って返します。
func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] { fmt.Println("batchFn !!") fmt.Println("userIds: ", userIds) result := make([]*dataloader.Result[*model.User], 0, len(userIds)) for i := 0; i < len(userIds); i++ { user := model.User{ ID: userIds[i], Name: "aaa", } result = append(result, &dataloader.Result[*model.User]{Data: &user}) } return result }
user resolverのLoadで処理が止まり、batchFnが1度実行されてからresolverに値が返されていることがわかります。
todos Query //func (r *queryResolver) Todos user resolver for ID T21 //func (r *todoResolver) User(Load前) user resolver for ID T5 user resolver for ID T61 user resolver for ID T2 user resolver for ID T85 user resolver for ID T19 user resolver for ID T7 user resolver for ID T90 batchFn !! //func (u *userReader) getUsers user ID T61 loaded!! //func (r *todoResolver) User(Load後) user ID T90 loaded!! user ID T85 loaded!! user ID T2 loaded!! user ID T21 loaded!! user ID T7 loaded!! user ID T19 loaded!! user ID T5 loaded!!
配列の場合はどうなる??
サンプルではtodoとuserが1対1でしたが、1対多の場合も気になったのでTaskに紐づくUserを単数のleaderと複数のmembersに分けました。
type Todo { id: ID! text: String! done: Boolean! leader: User! members: [User!]! }
同じbatchFnにleaderのidとmembersのidをごっちゃに入れてloadした時、どうなるでしょうか。
Loadの引数と返り値のIDが変化していないか見てみます。
todo作成時にleaderIdを引数で固定(1)、memberIdをランダムに2つ入れます。
todo := &model.Todo{ Text: input.Text, ID: fmt.Sprintf("T%d", randNumber), LeaderID: input.LeaderID, MemberIDs: []string{fmt.Sprintf("%d", memberRand1), fmt.Sprintf("%d", memberRand2)}, }
gqlgen.ymlにfieldを追加してgenerateすると空のresolver(leader, members)が生えます。
Todo: fields: leader: resolver: true members: resolver: true
生成されたresolverを実装します。渡したidと帰ってきたidをprintでチェックします。
// Leader is the resolver for the leader field. func (r *todoResolver) Leader(ctx context.Context, obj *model.Todo) (*model.User, error) { user, err := loaders.GetUser(ctx, obj.LeaderID) fmt.Printf("leader resolver loaded. before_leaderId = %s, after_leaderId= %s\n", obj.LeaderID, user.ID) return user, err } // Members is the resolver for the members field. func (r *todoResolver) Members(ctx context.Context, obj *model.Todo) ([]*model.User, error) { users, _ := loaders.GetUsers(ctx, obj.MemberIDs) var userIDs []string for _, v := range users { userIDs = append(userIDs, v.ID) } fmt.Printf("member resolver loaded. before_MemberIDs = %v, after_MemberIDs= %v\n", obj.MemberIDs, userIDs) return users, nil }
loaders.GetUsersの内部ではLoadの複数版であるLoadManyを叩いています。
// GetUsers returns many users by ids efficiently func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, []error) { fmt.Println("Load memberIds: ", userIDs) loaders := For(ctx) return loaders.UserLoader.LoadMany(ctx, userIDs)() }
3件入れた後、queryを実行しました。
入力([leaderId, memberID1, memberId2]) userIds: [1 58 34] userIds: [1 84 23] userIds: [1 3 46]
todos Query Load leaderId: 1 Load leaderId: 1 Load memberIds: [58 34] Load leaderId: 1 Load memberIds: [84 23] Load memberIds: [3 46] batchFn !! userIds: [1 58 34 84 23 3 46] // batchFnに渡されたids leader resolver loaded. before_leaderId = 1, after_leaderId= 1 leader resolver loaded. before_leaderId = 1, after_leaderId= 1 member resolver loaded. before_MemberIDs = [3 46], after_MemberIDs= [3 46] member resolver loaded. before_MemberIDs = [58 34], after_MemberIDs= [58 34] member resolver loaded. before_MemberIDs = [84 23], after_MemberIDs= [84 23] leader resolver loaded. before_leaderId = 1, after_leaderId= 1
というわけでbatchFn内でまとめてデータをfetchしても、内部でうまいこと対応するデータを返してくれているようです。
docker-compose.yamlで起動させるサービスを制御
各サービスでprofiles
を記述しておくとdocker compose up
では起動せず、docker compose --profile {指定したprofile名} up
の時だけ起動するようにできるとのこと。
profiles
を記述しなかったサービスについてはコマンド中にprofile指定に関係なく常に起動するようです。
(prisma)Can't reach database server at
nest.jsの学習を始めたところdocker composeでprismaコンテナとpostgresコンテナを立てている状態で、prismaからDBに接続できませんでした。
慣れた作業のはずなのに何故
結論
タイムアウトが原因でした。。。 for.kobayashiii.dev こちらの情報無かったら一生気づかなかったと思います。ありがとうございます。
以下調べてた時の様子になります。
DB設定(docker-compose.yaml)
dev-postgres: image: postgres:14.4-alpine ports: - 5432:5432 environment: POSTGRES_USER: user POSTGRES_PASSWORD: passwd POSTGRES_DB: db restart: always
prismaコンテナ内からサービス名dev-postgres
で接続できる(warning出てますが)
root@a8bdedef3739:/app# psql -h dev-postgres -p 5432 -U user -d db perl: warning: Setting locale failed. perl: warning: Please check that your locale settings: LANGUAGE = (unset), LC_ALL = (unset), LANG = "en_US.UTF-8" are supported and installed on your system. perl: warning: Falling back to the standard locale ("C"). Password for user user: psql (11.21 (Debian 11.21-0+deb10u2), server 14.4) WARNING: psql major version 11, server major version 14. Some psql features might not work. Type "help" for help. db=#
prisma設定箇所
調査のため一時的に直書き
datasource db { provider = "postgresql" //url = env("DATABASE_URL") url = "postgresql://user:passwd@dev-postgres:5432/db?schema=public" }
prisma migrateでエラー
root@a8bdedef3739:/app# npx prisma migrate dev Environment variables loaded from .env Prisma schema loaded from prisma/schema.prisma Datasource "db": PostgreSQL database "db", schema "public" at "dev-postgres:5432" Error: P1001 Can't reach database server at `dev-postgres`:`5432` Please make sure your database server is running at `dev-postgres`:`5432`.
こういうエラーで時間取られると辛いですね。。
Cognitoへの移行
ログイン周りの処理を自前で実装してある程度理解したのでcognitoに移行させたい
ユーザーIDはcognito側から払い出されたものを自前のユーザーテーブルで二重管理するイメージになるんだろうか
IDプロバイダ側(Googleなど)の設定
Adding social identity providers to a user pool - Amazon Cognito
react向けチュートリアル
Amazon Cognito と AWS Amplify を使用して React アプリケーションユーザーを認証する - AWS 規範的ガイダンス
以下に実装しながら気づいた点などメモします。
- Hosted UIを使うかどうか
- AmplifyのUIコンポーネントを使うかどうか
- トークンのバックエンドでの検証
- Amplify.configureの引数について
- user_idに相当する値
- どのトークンをバックエンドの認証に使うのか
- トークンのリフレッシュ処理について
- googleソーシャルログインに審査が要るのかどうか
- terraformでaws_cognito_identity_providerを管理しようとすると差分が出る
- cognito関連のuser_pool_idなどをどこに保管するか??
- 後記
Hosted UIを使うかどうか
AWS側で用意されたページに飛ばされるので使わないケースの方が多そう
AmplifyのUIコンポーネントを使うかどうか
Hosted UIと同じでカスタマイズ性に欠けるので理想的には使わないのが良さそうだが、UIの作成と認証処理をかなり省けそう(+安全)なので一旦使ってみる
特にIDプロバイダからのサインインは作り方がさっぱりなので助かる
トークンのバックエンドでの検証
JSON Web トークンの検証 - Amazon Cognito
Amplify.configureの引数について
もしかするとCLIで作成するのがマナーなのかも
user_idに相当する値
accessTokenのusernameがユニークな値なのでこれが使える
またidTokenのcognito:usernameも同じ値だと思われる
お客様のアプリケーションでユーザー名が不要の場合は、ユーザーにユーザー名を指定するように求める必要はありません。アプリでユーザー用の一意のユーザー名をバックグラウンドで作成できます。 username はユーザープール内で一意である必要があります。username は再利用できますが、削除されて使用されなくなった場合のみです。
またcognitoのユーザープールを作成する際にサインインオプションとしてusernameとemailを必須にすると、usernameが固有な代わりに同じemailで複数のユーザーが登録できてしまい、想定する仕様(emailがunique)と異なるのでやめました。
ただcognitoでの(フォーム経由の)登録とソーシャルIdPからの登録が同じemailだった場合、それは重複してしまうようです。
どのトークンをバックエンドの認証に使うのか
cognitoから渡されるトークンはaccessToken, idToken, refreshTokenの3種類。
もちろんaccessTokenだろ!と思ってたのですが、それぞれの役割やpayloadを見てみると、IDトークンを使うのが正しそうです。
IDトークン:連携サービスの認証(例:API Gateway)と認証されたユーザー情報の参照 アクセストークン:Cognitoユーザープールのユーザー属性更新 更新トークン:新しいIDトークン、アクセストークンの取得
Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する | DevelopersIO
accessTokenはあくまでもcognitoに対するアクセスを許可するトークンという意味合いっぽいですね。
トークンのリフレッシュ処理について
アクセストークンの寿命を最短(5分)にして確認したところ、amplifyを使ってれば自動で更新してくれるようです。ありがとう。
googleソーシャルログインに審査が要るのかどうか
機密性の高いデータにアクセスする場合は必要だそうですが、機密性の高いデータとは何だろうか。
emailだけ取れれば十分なんですが・・・
AWS側のドキュメントによると最低限必要なscopeは3つ(email, profile, openid)ですが、OAuth同意画面を再確認したところ全て非機密のスコープにカテゴリされていたので審査は必要なさそうです。
terraformでaws_cognito_identity_providerを管理しようとすると差分が出る
authorize_url, oidc_issuerなどのドキュメントのexampleに載ってないパラメータがnullになる差分出ますが、applyして動作を確認したところ問題なさそう。
~ provider_details = { - "attributes_url" = "https://people.googleapis.com/v1/people/me?personFields=" -> null - "attributes_url_add_attributes" = "true" -> null ~ "authorize_scopes" = "openid email profile" -> "profile email openid" - "authorize_url" = "https://accounts.google.com/o/oauth2/v2/auth" -> null - "oidc_issuer" = "https://accounts.google.com" -> null - "token_request_method" = "POST" -> null - "token_url" = "https://www.googleapis.com/oauth2/v4/token" -> null
とはいえapplyする度に差分が出るので、ignore_changesで無視しようとのこと。 "aws_cognito_identity_provider" always have some changes on "provider_details" when "provider_type" is "Google" or "Facebook" · Issue #4831 · hashicorp/terraform-provider-aws · GitHub
cognito関連のuser_pool_idなどをどこに保管するか??
払い出されるid等はsecretとは書かれてないとはいえenvファイルに含めない方が良い感じがしますが、reactでは(webフロントエンド全般?).envをコミットしないようにしても最終的にビルドに埋め込まれるため閲覧可能になってしまうようです。
じゃあどうすんのよって話ですが、仮に外部からcognitoへ不正アクセスされても、サインアップ後のemailやSMS検証で弾けるから大丈夫とのこと。(検証してなかったらアウト)
というわけで.envに書いてgit管理しても致命的な状況にはならなそうですが、念のためCICD中に埋め込むようにしときます。
後記
移行完了しました。
バックエンド側で実装したトークン認証処理をcognito用に置き換えましたが、このトークンを自前で生成できないのとパースするために外部通信が必要でテストに困りました。
同じpayloadを持つJWTを作ってパースできれば認証処理以外のテストは通せるかということで、結局移行前に実装した処理をテストで引き続き使っています。