去年のはこちら。
2024年振り返り
これまでは時系列で振り返っていましたが、3つのトピックに分けて振り返ろうと思います。
- 業務でやったこと
- 個人の学習
- 就職活動
業務でやったこと
1. 冪等性とロックに向き合う
要件 / 設計と実装
あるユーザーがタグを作成するAPIを実装する必要があった。
APIは「0.xxx 秒間隔で複数回」かつ「同じパラメータ」で実行されるケースもある。
DB上ではユニーク性を保証する必要があった。
例: user_id
と tag_id
の組み合わせは index_user_id_and_tag_id
というユニークインデックスによって一意になる
user_tags テーブル
id | user_id | tag_id |
---|---|---|
1 | 2 | 3 |
2 | 4 | 5 |
3 | 4 | 6 |
4 | 5 | 3 |
Railsで、冪等なAPIエンドポイントを作成する。
すなわち、同じuser_id
と tag_id
の組み合わが、すでにある場合もない場合も問題なく動作する(あれば作成、なければ何もしない)ようなロジックとしたいため、ActiveRecord::Relation#find_or_create_by を活用することにした。
冪等性のデザインパターンについて著名な以下の記事では、
パターン1:IDを付けてCREATEを冪等にする
Create系のAPIは、HTTPの409 Conflictに相当するようなエラーコードを定義しておき、クライアントはそれを他のエラーとは区別して扱う必要がある
https://frsyuki.hatenablog.com/entry/2014/06/09/164559
とあるが、 ActiveRecord::Relation#find_or_create_by
を活用することは、 find によってエラーを返却するわけではないため、追加でコメントされていた以下の方法が該当しそう。
Create に対して ID が重複してたらエラー返すのは冪等って言えるの? 既にリソースがあっても「作成成功」のを返すようにすれば、レスポンスを受け取るまでリトライし続ければ良いことになるんじゃないのかな
https://b.hatena.ne.jp/entry/198838006/comment/taketyan
話は戻り、以下のように実装することで、DBのユニークエラーを発生させる前に、アプリケーションコードレベルでユニーク性の検証が行なわれることを期待した。
user.transaction do
user.user_tags.find_or_create_by!(tag_id: 7)
end
問題
アプリケーションレベルで冪等性の担保を行っているにも関わらず、Sentry にユニーク制約エラーの通知が飛んでいた。
ActiveRecord::RecordNotUnique
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_user_id_and_tag_id"
DETAIL: Key (user_id, tag_id)=(1234, 5678) already exists.
Sentry の通知と同時刻帯のアプリケーションログを確認したところ、「0.xxx 秒間隔で複数回」かつ「同じパラメータ(今回の例ではuser_id: 1234, tag_id: 5678)」で今回のAPIが実行されるケースで、上記のエラーが発生している。
つまり、
- 1回目の
find_or_create_by
では find (≒ SELECT 文の発行)をしてもレコードがないため、 create が実行さる - 2回目の
find_or_create_by
でも find をするとまだ 1回目の create が完了していない ため、 create が実行されてしまい、実際に INSERT 文を発行する時点で1回目の create で作成されたレコードがDBに存在し、ユニーク制約エラーが発生してしまった
ということのよう(※ 若干厳密ではない説明になっていそうです)。
DBエラーによってデータは守られているためユーザー影響はないが、Sentryエラーになっているため修正をしたい。
解決策
悲観的ロックを活用することで、1回目の find_or_create_by
が完了してから2回目の find_or_create_by
が実行されるように、つまりトランザクションの分離性が保証されるようにした。
# By calling `with_lock` with a block, you can:
# 1. start a transaction
# and
# 2. acquire the lock. `select * from users where id=123 for update`
user.with_lock do # =>
user.user_tags.find_or_create_by!(tag_id: 7)
end
上記のように記述することで、ActiveRecord::Locking::Pessimistic#with_lock ブロックで囲った処理のトランザクションが完了するまでは、別プロセスで同じ処理が走らないようになった。
つまり、常に1回目の find_or_create_by
が完了してから2回目の find_or_create_by
が実行されるため、2回目の find_or_create_by
で find するときには1回目の create によって作成されたレコードが常に存在するようになった。
ロックを適切に活用したことでSentryエラー通知は止まり、解決。
雰囲気で知っている風だった明示的ロック SELECT ~ FOR UPDATE
の挙動についておそらくここら辺のタイミングで理解した。
ロックの用語分類を以下にまとめました。
この時に、 Sentry (Railsプロジェクトのため厳密には sentry-ruby という gem)がどのような例外を対象にしているのか少し調べたところ、
基本的には 5xx系ステータスコードの例外が対象で、4xx系の例外クラスは IGNORE_DEFAULT
という定数で指定されたもののみが除外されるようになっている模様。
今回の ActiveRecord::RecordNotUnique
例外が発生した時のステータスコードは4xx系( 422 Unprocessable Entity
か 409 Conflict
)になるように思うが、この例外クラスが IGNORE_DEFAULT
に含まれていないのは、このエラーが発生する = アプリケーションのロジック側に問題が潜んでいる、という意図で含まれていない模様。
なお、Rails 7.1.0以降を使用している場合は、 find_or_create_by
で RecordNotUnique
エラーの場合に find
をリトライするようになった。自前で冪等性を意識した実装を考えなくても、Railsがエラーを区別し、冪等性を担保してくれるようになった。
そのため、Rails 7.1.0以降を使用している場合は上記のような問題に遭遇することはなさそうです。
find_or_create_byでRecordNotUniqueが発生した場合はfindをリトライするようになった。
2. Lambda のサービスクォータに向き合う(その1)
要件・設計・実装
昨年参画していた会社では、マイクロサービスのアプリケーションサーバーとして API Gateway と Lambda を活用していることが多かった。
そのようなアーキテクチャで構成されているアプリケーションの機能として、30MBという大きいサイズのPDFをアップロードする必要があった。
問題
30MBのPDFアップロードでは、当初はリクエストボディにBase64形式のPDFファイルを直接付与する形で実装していたが、レスポンスとして 413 Payload Too Large
が返るようになってしまった。
原因は以下。
AWSリソースにはサービスクォータと呼ばれる上限数がある。
API Gatewayのクォータは、APIペイロードサイズ(= リクエストボディやレスポンスボディのサイズ)に、10MBという制限がある。
LambdaのクォータはEC2やECSなどのサーバーと比較すると、クォータの割り当てが小さい。呼び出しペイロードに、6MBという上限がある。
Invocation payload (request and response)
6 MB each for request and response (synchronous)
10MB < 30MB 、6MB < 30MBであり、通信経路としてはクライアントからのリクエストをAPI Gateway が Lambda よりも先に受け付けるため、API Gateway からステータスコード413のレスポンスが返ってしまっていたということになる。
解決策
1. クォータの引き上げリクエスト
AWSリソースのクォータの問題に対する共通の最も簡易な解決策として、「クォータの引き上げリクエスト」を行うことができる。
ただし、引き上げられるクォータにも上限があったり、引き上げが認められない場合もあるため、その場合は他の方法を考える必要がある。
2. 署名付きURLの使用
PDFなどのファイルアップロードのベストプラクティスとして、署名付きURLを使用したファイルアップロードがある。
署名付きURLは「一時的にアップロード先となるS3バケットへのアクセスを許可する方法」とされているが、自分としては「ファイルアップロードの際にWebサーバー/アプリケーションサーバーへの負荷を避ける方法」として理解している。
今回の例では署名付きURLを使用することで、アップロード時にAPI GatewayとLambdaを経由せず、直接ファイルストレージであるS3にアップロードすることができるようになる。
その結果、413エラーを避けて30MBのPDFをアップロードすることができるようになる。
書いている途中で、ファイルアップロードに関する詳細な記事を見つけました
また、S3ではなくCloudFrontで署名付きURLを発行することができるようで、該当プロジェクトのインフラ構成ではCloudFrontを最前段に置いていたことからも、こちらの方法の方が良いのかもしれない、というのも書いている途中で見つけました
結果として、該当のRailsプロジェクトでは CarrierWave と fog を使用しており、CarrierWave に依存した既存のファイルパス形式の考慮など、データマイグレーションやそれなりのコードの書き換えが必要で、署名付きURLへの移行が工数的に難しいという判断になり、「API Gateway と Lambda」から「ALB と ECS」への移行で対応することになった。
3. API Gateway と Lambda
ALB と ECS への移行
ALB や ECS にはリクエストボディのペイロードサイズの上限がないため、API Gateway と Lambda ALB と ECS へ移行することによってもファイルアップロードの上限の問題は解消できる。
社内のスーパーインフラエンジニアにお任せして対応完了。
学びとしては、アプリケーションサーバーとしてLambdaを利用するのは結構制約が厳しいように感じる。
2. Lambda のサービスクォータに向き合う(その2)
要件・設計・実装
BFFとして動作するAPIの実装で、クライアントから渡された10万件のユーザーデータを、API Gateway と Lambdaで動作するバックエンドのマイクロサービスに転送する必要があった。
問題
その1のPDFアップロードの話とほぼ同様だが、バックエンドのマイクロサービス側のAPI Gateway or Lambda のペイロードサイズの上限問題に引っかかり、413 Payload Too Large
が返るようになった。
バイト数まで調査したわけではなかったが、リクエストボディに含めた10万件のユーザーデータが、おそらく原因と思われる。
解決策
その1で行ったようなバックエンドのマイクロサービス側のALB と ECSへのインフラアーキテクチャ変更は、担当メンバーの工数などを考えても難しい状況だった。
そのため、10万件のユーザーデータを、5000件のユーザーデータに20分割(5,000 x 20 = 100,000)してリクエストを送信するようにした。ペイロードサイズの上限問題はこれで解消された。
多分似たような意図の記事
(上記の記事の関連本が出ていそうで、しかもアプリケーションエンジニア向けに書いてくれているとのことなので、とても読みたくなった )
しかし、バックエンドのマイクロサービスAPIのレスポンスに含まれる、1つのリクエストを識別するキーが複数になってしまうという問題が発生した。
この1つのリクエストを識別するキーは、バックエンドのマイクロサービス側の結果整合性を保証するために必要だった。エラーになったリクエストを特定して、補償トランザクションによって打ち消すために必要だった。
例えば、10万件を20分割したうちの、12回目のリクエスト(60,001 ~ 65,000)でエラーが出た場合、1~12回目のリクエスト(1 ~ 65,000)によって作成されたデータ全てを打ち消さないといけないが、既存の実装だと、12回目のリクエストによって作成されたデータだけしか打ち消すことしかできなくなってしまっていた。
そのため、1つのリクエストを識別するキーを、リクエスト分割時には各リクエストが同じキーを返すように、バックエンドのマイクロサービスAPI側の実装変更をしてもらう必要は出てしまった。
ここら辺の話について詳しかった記事1
Step Functions を活用してはいなかったが、以下のような「1-3-b. オーケストレーションにおける結果整合性」を自前で実装していたようなアーキテクチャであった。
このオーケストレーションを実現するための AWS サービスには、AWS Step Functions があります。
これを利用することで、例えば、(1) 配送処理サービスは正常終了、(2) メール通知処理サービスが正常終了、(3) 請求処理サービスでデータ不整合があった際には、(4) 請求処理の前段で行われていた処理の打ち消しを実施する、といったワークロードを実現することができます。
こちらはQAのフェーズで検知していただき、修正工数もそれほど大きくなかったため、ユーザー影響等の問題はなかった。
3. Lambda のサービスクォータに向き合う(その3)
要件・設計・実装
こちらも API Gateway と Lambdaで動作するバックエンドと、SPAのフロントエンドで構成されたアプリケーションだった。
CSVファイルのアップロードで、バックエンドではCSVデータの加工やDB保存に時間がかかり、タイムアウトのリスク(CloudFrontが60秒、API Gatewayの30 秒、Lambdaが900秒)があったため、CSVデータの加工やDB保存は非同期処理によって行い、フロントエンドはバックエンドの非同期処理によって更新されたデータをポーリングによって取得する必要があった。
問題
CSVファイルアップロード機能を検証環境にデプロイしてからしばらくして、以下のようなエラーが出た。
failed to initialize database, got error failed to connect to `host=hoge.abcdef.ap-northeast-1.rds.amazonaws.com user=hoge database=hoge_production`:
hostname resolving error (
lookup [host=hoge.abcdef.ap-northeast-1.rds.amazonaws.com](http://hoge.abcdef.ap-northeast-1.rds.amazonaws.com/)on 168.253.79.1:52:
dial udp 168.253.79.1:52: socket: too many open files)
too many open files
というエラーは、TCPコネクション(≒ DBコネクション)が増えすぎた結果、1つのTCPコネクションごとに生成されるファイルディスクリプタの数が、サーバーのファイルディスクリプタの上限数(ここではLambdaのファイルディスクリプタのクォータ1,024)を超えてしまった、ということを表している。
問題深掘り1
Lambda実行環境のライフサイクルは3つに分かれています。INITとINVOKEとSHUTDOWNです。
INITフェーズでは、関数インスタンス(Lambda関数が実行される環境のこと)作成やハンドラ関数外に実装されている初期化処理が行われます。
たとえばGoの場合、AWSのサンプルにある 2 以下のような init() の処理はINITフェーズで行われます。
なお、上のサンプルで LambdaHandler() に該当するハンドラ関数内の処理はINVOKEフェーズで実行されます。
package main
import (
"log"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/aws"
)
var invokeCount = 0
var myObjects []*s3.Object
func init() {
svc := s3.New(session.New())
input := &s3.ListObjectsV2Input{
Bucket: aws.String("examplebucket"),
}
result, _ := svc.ListObjectsV2(input)
myObjects = result.Contents
}
func LambdaHandler() (int, error) {
invokeCount = invokeCount + 1
log.Print(myObjects)
return invokeCount, nil
}
func main() {
lambda.Start(LambdaHandler)
}
要は、1回目のリクエストがLambdaに来て、Lambdaの実行環境(インスタンス)が起動したとき、 init()
関数が呼び出され、その後LambdaHandler()
が呼び出される。
2回目のリクエストがLambdaに来て、起動中のインスタンスがリクエストを受け付けたときは、1回目の init()
によって処理されたコードが再利用されつつ、 LambdaHandler()
が呼び出される。
というライフサイクルになる。
既存実装では、 LambdaHandler()
が呼び出された時にDBコネクションを行っていたが、DBコネクションの解放が行われておらず、これによってTCPコネクションがLambdaのインスタンスが終了するまで残りっぱなしとなっていた。
問題深掘り2
さらに注意深くログを追うと、以下のようなログも出ていた。
caused by: Post "https://cognito-idp.ap-northeast-1.amazonaws.com/":
dial tcp: lookup cognito-idp.ap-northeast-1.amazonaws.com on 168.253.79.1:52:
dial udp 168.253.79.1:52: socket: too many open files
LoadLocationErorr:too many open files
これは、DBだけではなく、Cognitoへの通信やタイムゾーンの初期化などでもTCPコネクションが行われており、ファイルディスクリプタを生成 増加 しているということを表している(Cognito のような他リソースへの接続でTCP通信が行われるのは理解できるが、タイムゾーンの初期化でもTCP通信が発生するのはなるほどとなった)。
解決策
1. INITフェーズでのTCPコネクションと、TCPコネクションの解放によるファイルディスクリプタの節約
各サービスへの接続を、 init()
で行うようにし、2回目以降のリクエストではTCPコネクションを再利用する方法が考えられた。
RDSへの接続はパスワード認証ではなくIAM認証で行っていたため、DB接続を行ってから15分間経つと認証が切れてしまう問題があった。
そのため、DBコネクションのみ引き続き LambdaHandler()
で行うようにし、DBコネクションの解放をLambdaのインスタンスが終了するまで待つのではなく、リクエストの完了時に行うようにする。
こうすれば最大限TCPコネクション(≒ ファイルディスクリプタ)を節約することができる。
しかし、先の記事に出てきたようにLambdaの予約済同時実行数が1の場合に予期せぬ問題が発生する、であったり、上記のような方法でTCPコネクションを節約したところで、今回の要件としてクライアントからポーリングを受け付けるため、Lambdaライフサイクルあたりのポーリング数に比例してどうしてもTCPコネクションが増えてしまうことから、あまり安心できる解決策ではないと考えられた。
2. API Gateway と Lambda
ALB と ECS への移行
結局、「Lambda のサービスクォータに向き合う(その1)」と同様、ALB と ECS への移行によって解決することとなった。
ALB と ECS においては、ググるかぎりファイルディスクリプタのクォータは存在しなさそうで、実際に移行して too many open files
エラーは発生しなくなり、問題は解決。
ちなみに、自分が担当したのは、Lambdaのファイルディスクリプタのクォータ1,024を超えてしまったことの特定と、DBコネクション以外でもTCPコネクションが行われていたことをアプリケーションログから見つけたくらいです。
また、こちらは開発フェーズにて機能がユーザーによって本格利用される前に検証環境で発覚し、リリース日までそれなりに時間があったため、対応には関係者苦労があったが大事には至らず済みました。
4. データベーススキーマの移行
RDSはAWSリソースの中でもコストが高いため、とあるRDSクラスター(インスタンス)を他のRDSクラスターに統合できないか、という依頼を受けた。
RDSクラスターの(RDSインスタンスの)Aurora PostgreSQLエンジン上で動作しているPostgreSQLのデータベースを、他のRDSクラスターに統合した。
統合は、踏み台サーバーに移行元のRDSクラスターのデータベースのバックアップを取得して、移行先のRDSクラスターに対してデータベースをリストアすることで実現した。
RDSのスナップショットという機能があるが、スナップショットはRDSクラスタやRDSインスタンス単位のバックアップ & リストア機能であるため、データベーススキーマは、 psql
コマンドを使用したバックアップ(ダンプ) & リストアによって実行した。
加えて、以下の後続作業が必要だった。
- アプリケーションサーバー(ECS)の参照先の RDSクラスターエンドポイント が移行先のRDSクラスターエンドポイントになるよう、環境変数を変更 & ECSサービスのk
- 移行先のRDSインスタンスのセキュリティグループのインバウンドルールに、ECS用のセキュリティグループを送信元として追加
このタスクを経て、自分の中でふわふわしていた以下の用語の区別がつくようになった。
- RDSクラスター
- RDSインスタンス
- データベースエンジン
- データベース(データベーススキーマ)
- テーブル
こちらの移行作業は、
- 開発環境と検証環境のデータベースのみが対象で、本番環境のデータベースは非対象
- データベースのテーブル数は「5個」、各テーブルのレコード数は「~1000件」ほど
という前提から、神経質になり過ぎることもないものではあったのだが、移行作業時はダウンタイムが発生してしまうことは避けられなかったので、
- 作業内容はドキュメントで手順を明確にしておく
- 開発者やQAメンバーに作業日時や想定完了時間を事前周知する
などして実行していった。
インフラの変更作業は顧客に対してや社内のチーム横断で影響が発生することも多く、どのように顧客や社内への影響を小さくするかを計画し、報連相することが重要だと思う。
5. データベースエンジンの移行
自分が新規構築を担当したサービスでRDSのDBエンジンとしてAurora Serverless v2を採用したが、Provisioned Aurora (通常のAurora)のDBインスタンスクラスに相当するACU(Aurora Capacity Unit)ではコストが高いことが判明し、Provisioned Auroraに戻すことはできないか、という依頼を受けた。
今調べてみれば、実際にそのように述べている記事も多い。
ピークの状態がoffpeakの8倍のリクエスト量であり、ピーク時間が144分間続くようなユースケースであれば、コストメリットが得られることになります。
これを踏まえて考えると、ログインサービスのソーシャルゲームだったり、締め切り間際のオークションサービスといったような特定の時間にリクエストが集中するようなサービスがやはりユースケースとして上がってくるんじゃないかと思います。
そのため、元はと言えば最初から Provisioned Aurora を根拠を持って採用できていればよかったのだが、調査力不足だったなと思う。
以下の記事の検証なんかはとてもすごい。
ここまでできなかったとしても、開発環境などでServerless v2 と Provisioned Aurora を(サービスのリクエスト数に差が出るため、I/O料金が正確には比較できないことは許容する前提で)同等スペックで同じ期間運用し、料金を比較してみる、などできればよかったかもしれない。
関連して、会社のAWSのコストを見る習慣は(インフラメンバーに任せっぱなしにするのではなく)持っていた方が良いな、など思ったりした。
Aurora Serverless v2 を含む Aurora のDBエンジンの移行はボタン1つで出きるため、RDSのメンテナンスウィンドウで変更するだけで完了できました。
その他
他にも何かやった気がするため、思い出したら追記したいとも思いつつ、長くなってしまうので追記しないかもしれないです。
個人の学習
AWSの学習塾
何か自分のスキルが伸び悩んでいるような課題感から、Xで見つけたAWSの学習塾に入ることにした。
各AWSリソースもそうだが、ネットワークを中心とした情報科学やWebの基礎的な内容を一緒に学ぶことができて、とても勉強になっている。
例えば、AWSのサービスが裏側でどのように動いていると考えるべきか、といった観点に加えて、digコマンドを叩いた時のDNSクエリの再帰的な流れ、SSL/TLS証明書の目的、認証局の役割、TCP/IPを意識した通信の流れなど、ここで学ばなかったら永遠に理解できなかったことがいくつもあるように感じている。
自分の独学力を見つめ直すこともできてよかった。1人で勉強するだけでなく、教えを乞う大事さを感じた気がする。
Qiita
就職活動のコーディング面接に備えようと思い、データ構造とアルゴリズムの勉強をしていた。
問題を解きながら身につけるというよりは、1個1個のデータ構造やアルゴリズムの用語や解説を読んでいくことで、ようやく理解が進むようになった実感があった。
AWSの学習塾でネットワークの知識について理解が深まったので、以下の記事を更新していた。
ロックを含むトランザクションや冪等性についての理解が(同僚エンジニアから丁寧に教えてもらえたりもあり、)業務で深められたので、以下の記事を更新していた。
しかし明示的なロックをちゃんと書く会社は前の会社まで出会ったことがなかった。他の会社ではそういった要件に遭遇していなかっただけだろうか。
RDS Performance InsightsやECSのメトリクスのグラフを見ても、何のことかいまいちピンとこないなーと思っていたので、
CPU使用率、メモリ使用率、ディスクI/O、ロードアベレージといったメトリクスが何を指しているのか、というようなところを勉強した。
あとは認証認可の記事も書いた。
自分の整理をひとことでは、以下のようになる。
- 認証
本人確認
- 認可
アクセス許可
認証認可では、OAuth / OIDC 、クッキーベースの認証とトークンベースの認証あたりも理解が深まってきたので1つ記事を出したかった。
他には某外資系企業の面接でけっこう勉強になる情報科学基礎系の質問が多かったので、これも別記事でまとめて、今年中にそのうち出したい。
総じて去年の振り返りの時と同じようなことを言っている気もする。
就職活動
外資系企業
外資系でのキャリアに実は昔から少し憧れがあったため、Xの関連アカウントにもインスパイアされ、就職活動を始めてみた(※転職活動ではなく就職活動と言っているのは、個人事業主 正社員のため)。
今後を見据えて、現時点の自分の状態を把握するつもりで、まずは面接を受けてみようと、Linkedinを適当に記載し、レジュメをそれっぽく書いて、Linkedinから直接五月雨に応募したり、グローバル企業の求人が多い転職サービスを利用してみたりした。
ビジネスインパクト
レジュメに関しては、グローバルなテック企業で働く方2名にレビューをしてもらったりもした。
転職斡旋サービスを活用する中で、そのような方々にレビューしていただくことができた。
これは本題ではないのだが、以下のような話が聞けて結構面白かった。
- 本当に実力があれば、どのような会社からでもGAFAには入れる
- 一方でビッグテックへの踏み台になるような、(転職しないよりは)入社できるのであればした方が良い企業もある
- GAFAのエンジニアであれば、レジュメを見るだけでそのエンジニアがどれくらいの実力かがわかる
- レジュメのスクリーニングは、エンジニアではない人事担当者が行なっており、応募されたポジションとレジュメが合っているかを2、3分で判断しないといけない
- 実力のあるエンジニアは、書類選考で落ちることはあるが、面接で落ちることはない。面接では実力を実際に示すことができるため
- 就職活動の戦略として、外資の有名どころはポジションが少ないので、有名ではないが高給な会社を狙っていくのもアリ
- たくさん学べる会社に入るのではなく、たくさん開発ができる会社に入るべき
- 面接でシステム設計が出た時は、要件の確認と、アプローチのメリット・デメリット両方を話せると良い
また、両名の方からいただいたフィードバックの中で、「レジュメには"ビジネスインパクト"を書いてください」というものがあり、この"ビジネスインパクト"というワードが自分に結構刺さった。
自分のエンジニアキャリアとして、将来的に給料の高い会社でゆっくり働けるようになることが理想だと、ぼんやりと考えていた。
給料の高い、例えば外資系の企業に入ればそのような理想が叶うんじゃないか、そのためにはコーディングやシステム設計のスキル、あとは多少の英語が身につけば、それらの企業の面接でも成果が出るようになるんじゃないかと考えていた。
実際にいくつか応募してみると、まずレジュメがなかなか通らずで面接に進むことすらできず、スキルを試すにも至らなかった。
これはきっと、"ビジネスインパクト"がレジュメから伝えることができておらず、"ビジネスインパクト"、つまり事業の売上増加やコスト削減、KPI、SLOの改善といった数値で具体的に表せる実績を積まないままでは、外資系の企業に入ることは難しいんだろうと理解が進んでいった。
リーダシッププリンシプル
選考を受けた会社の1つとして、AWSがある。
Xでサポートエンジニアのポジションが積極採用中というのを見かけたのがきっかけだった気がする。
手応えはないながら最終面接まで進み、そこではLP(Leadership Principles)面接と呼ばれる面接を受けた。
Amazonが求める人物像とはリーダーシップ・プリンシプル に共感し、大切にできる方です。
面接を受けるにあたっては、 これまでの職歴(どのような場面)で どのリーダーシップ・プリンシプルをどのように発揮してきたかを考えておくと良いでしょう。
以下、行動に基づく面接の質問例です。
- 過去に問題に直面し、数多くのソリューションを見い出した経験について教えてください。どのような問題で、どのように行動方針を決めましたか? その際にとった行動により、どのような成果が出ましたか?
Amazonはデータ駆動型(データドリブン)の企業です。質問に答える際には、聞かれている内容に合致した回答をすること、論理立てて話をすること、差支えない範囲で指標やデータを用いた例を出していただくこと、できるだけ直近の事例をお話いただくことをお勧めいたします。
要は、Amazon のカルチャーではリーダーシップを重視していて、LP面接ではそれに沿うエピソードを、ときにはデータに基づいて話すことが求められる。
面接が終わった時には、用意していたストーリーを問題なく話すことができたつもりでいたのだが、振り返ってみると、リーダーシップとは何か、という自分の認識に誤解があったように思う。
結果として選考は不採用になってしまった。
決められらたスケジュールが厳しかったとしてもそれを守ることを頑張った、というようなエピソードを中心に話してしまったのだが、今となってはそれが「リーダーシップ」を表すエピソードとしてはズレていたのではないかと思う。
シニアなエンジニアの振る舞いとリーダーシップについて
もし本当にリーダーシップのある人であれば、難しいタスクであった場合は決められたスケジュールの調整を自ら申し出るなど、よりリーダーやマネージャーの目線で、自分の担当領域を超えた範囲でアクションを取ることができるはずで、そのようなエピソードが求められていたのではないかなと、何かの拍子に以下の記事を拝読して、思ったりした。
自分はこれまでフリーランスとして数人から千人規模の会社まで大体5社くらいで働かせてもらったが、そこで評価されていたエンジニアの方たちのことを思い出してみても、上記の記事に書かれているようなことは通ずる部分があり、すごく納得したというか、そういう風に仕事をしないといけなかったのか、という気持ちになった。
そんな感じで思っていたことをまとめてみようと、以下の記事を書いた。
記事へのコメントで、これはマネージャーの仕事でエンジニアの仕事ではない、といったことを言われているのも見たが、今行っている自分の仕事が事業の成長のために必要な/重要な仕事なのか、といったようなことを考えたことがないとそのような感想になるだろうと思う。
自分も元々はそのようなことを真剣に考えることに向き合っておらず、数年前の自分が読んだとしたらピンと来なかっただろうと思う。参画した企業で出会った方々や、先に書いてきたような就職活動での学びを経て、キャリアアップを目指すならそういった行動をしていくべきなのだろうと考えるようになった。これは仕事論のような話で、自分の場合はこうして重要性に気づくに至ったが、その人の性質や、生来の仕事に対する向き合い方、今まで働いた環境、上司、同僚がそうだった、などない限りは、人によって一生縁の無い話でもあるように思う。
あとは、例えば記事中の「選択肢の提案」を行うためには、技術のトレードオフについて知っていたり未知の技術に素早くキャッチアップできたりする必要があるなど、そもそもシニアとしての振る舞いを行うために前提として求められる技術レベルはそれなりにある。
結果
レジュメを修正したおかげもあってか何社か選考に進めるようになり、最終的には昔からすごいと思っていたような会社からオファーをもらうことができた。
Senior Software Engineerのポジションに応募したが、Software Engineerというタイトルでのオファーとなった。ただこれは自分の実力からして納得かなという感想だった。
カジュアル面談を受けた会社の中には自分と経験年数がそれほど変わらないにも関わらず上場が近い企業でCTOを務めている方がいたり、かつての同期エンジニアの友人が技術顧問業等で大活躍していたりと、エンジニア歴が自分と同じくらいであっても大きな成果を出している方々がいるので、そのような方々を目標に頑張っていきたいなと思う。と同時に、就職活動が自分が期待していた以上に満足する結果となってしまったことで、しばらくゆっくりやりたい気持ちもある。
目先の1、2年の目標としては、今の会社でシニアになれるようやっていきたいのと、外資系なので英語力も今よりは上がっているといいなと思う。