2023年5月

育児

2 歳 6 ヶ月になった。

  • 股からふとももにかけて虫刺され?が広範囲にでた
    • 小児科いったら蕁麻疹っぽい
    • そのあと皮膚科いったら湿疹といわれた
    • 強めのステロイドだしてもらった
  • 乃が美の食パンたべた
    • 美味しそうにたべていた
  • 大きめの公園いってピクニックした
  • トーマスのパジャマきたくてギャン泣き
    • 着させるつもりだったが先に肌着きてほしいんよ..
    • 無理やり肌着着せたらギャン泣き
  • ガソリンスタンドごっこをする
  • 肉まんづくりをした
  • 初回転寿司
    • きゅうりの巻き寿司
    • ポテトフライ
    • シャリ
    • カステラ
    • ストロベリーアイスを食べた
    • 斜め前の席の親子をずっと見てた
    • 皿を入れるアレ楽しそうだった
  • 園で掃除機が左目の下にあたった
    • 先生が掃除してて引いた時にあたったみたい
    • 自宅でも 1 回あてちゃったなぁ
    • 少し青くなっていた
    • 念のため翌日眼科にいった
      • 大丈夫だった
      • めっちゃギャン泣き
      • 先生方ありがとうごさいました。。
  • 初めてバスにのった
    • 珍しく大人しく椅子に座って待ってた
    • 降りた後も整理券を大事にもっていた
  • 近くのスポーツセンターまでお散歩
  • パパがおもちゃ渡さなかったからギャン泣き
    • 投げるから取り上げていた
  • 初めてのマックシェイク(ストロベリー)
    • 初めて店内でたべた
    • テーブルが高くてうまく飲めない
  • 夜寝室にいきたがらない
    • 保湿ローションに輪投げさせて寝室まで追い込んだ
    • 輪っかはトーマス弁当箱のパッキン
  • バルタン星人を白バイに乗せてジャンプさせる
    • パパのマネ
    • めっちゃ笑う
  • 園でお友達に押されるのをパパが見た
    • 先生に聞いたら仲良いとのこと
    • おもちゃを取られて悶々として時間が経ってから怒る子もいるらしい
    • そのタイミングで声を掛けられなかったのは反省
    • 息子氏は嫌な気持ちだったかもしれない
  • 夕ご飯を一緒に食べてくれない
    • 時間をおいて好きな時に食べさせるようにした
    • あとは興味を引いたり
    • ご飯がワンパターン化してるのが原因かも...
  • 「〇〇じゃないよ〜」と言う

今月は毎週病院に行ってる気がする。。寒暖差でまた風邪っぽくなってきたので気をつける。

仕事

その他

ブログ 1 件かいた。想像以上にブクマもらってホッテントリも入って驚いている。技術記事でホッテントリ入りたいわね。。 1 人でも参考になる方がいたら幸い。

enokawa.hatenablog.jp

アルゴリズムとか疎かったので↓の本を買った。いま読み進めている途中。

gihyo.jp

インフラエンジニアからアプリケーションエンジニアになって1年経った

気づいたら入社して 8 年も経っていた。 2022 年の 4 月にアプリケーションエンジニアへ転向したので、その経緯や転向してやったこと、今やっていることをまとめてみる。

転向の経緯

自分で作りたいものを作りたかったからというのが一番大きい。 アイレットに入社してから 7 年間インフラエンジニアとして生きてきて、ほぼ毎日楽しく業務に励んでいた。5 年目くらいから「あーこれつくれたら便利だな」という場面に何度も遭遇した。 例えば、日々の業務を効率化するような Web アプリケーションや CLI、内部向け API などあったら便利なモノだ。 「よしつくったろ!」と意気込むも手が進まず、最終的には諦めて要件を纏め、コードを書くのが得意な人にお願いしたりしていた。そして要件通りのモノができあがって喜びつつも、「あぁこうやって実装すればいいのか」「実装できてすごいなぁ」「オレにもできたらなぁ」と複雑な気持ちになっていた。「いやそう思ってる暇があるならコード書けよ」と言われたら何も言い返せない。

このままじゃいかんなと思っていた矢先、育児休業から復帰するタイミングで所属していたチームが解散することとなった。 当時の上司からも「開発に異動したらもっと成長できると」とのコメントをもらい、確かにそうかもと思い自分の今後のキャリアを考える時間をつくった。

このタイミングで転職も考えた。7 年も働いてきたし、そろそろいいかなという気持ちがあったのと、業務に対するモチベーションが低下していたから。転職サイトでいくつかの企業にコンタクトをとってカジュアル面談を受けたりもした。主に自社のサービスを提供する事業会社の方と、会社やサービスのこと、ワークライフバランス(育児と仕事)、技術について話したりした。話して分かったのは、オレはこの業界・サービスの開発に携わりたいという想いがなかったということ。今までさまざまな業界のサービスのインフラ構築に携わってきたが、例えば金融系だとかゲーム系などといった特定の業界の会社にいきたいという気持ちがなかった。今までのキャリアも小学生時代から振り返ってみてもそうだった。

最終的に、特定の業界にこだわりがないので開発チームに異動することに決めた。1 社だけ、このサービスの開発に携わりたいと強く思える企業に出会いコンタクトをとってみたが、残念ながら縁がなかった。心残りはあるが、またどこかでチャレンジの機会を伺いたい。 他の企業の選考に進む選択肢もあったが、書いたとおり何がやりたいのかが明確になっていないまま進むのはよくないし、何よりもアイレットが大好きで居心地も良いというのが大きかった。ママパパの働き方にも理解があって自分の裁量で仕事できるのがありがたい。居心地の良さを求めて自社に留まることによって、成長曲線が横ばいとなってしまうことを懸念していたが、当時は家族を最優先にしたかったのでそうした。この選択が正しかったかどうかは分からないけど、現時点では技術者として成長できていると実感していて、プライベートも幸せなのでよかったと思える。

1 年間何をしてきたか

主にバックエンドを担当していて、Web サービスの API を開発している。技術スタックとしては PythonMySQLAPI Gateway、Lambda、AWS SAM などで、サーバレスで開発を進めている。直近だとキャンペーンサイトの API やバッチの実装だったり、チャットボットサービス向けのイベントドリブンで動作するコードを運用している。実際に関わっていはいないが、以下事例の構成と似たようなことをやっている。

www.iret.co.jp

異動後はトレーニングから始まり、Vue + API Gateway + Lambda + DynamoDB で簡単なサーバレスアプリケーションを構築した。教材がとてもよくできていて、フロントエンドとバックエンド間通信の概要が理解できた。このように Web アプリケーションが作られているのだなとイメージが沸いた。トレーニング終了後は少しテーマを変えて DB を MySQL に変えてみたりした。トレーニングは約 1 ヶ月で終了した。

レーニング後はチャットボット向けサービスのプロジェクトにジョインした。これが初めての複数人での開発で、どのようにチーム開発を進めていくのかを体験できた。コードレビューのフローであったりリリースフローが確立されていなかったため提案したりもした。以前からドキュメントを書くのが得意であったこともあり CONTRIBUTING.md を書いて実際に運用して、開発メンバーから良い評価をもらった。実装の部分は、SQS トリガーで動作する Lambda のリファクタリングを行い、メンバーにフォローしてもらいながら進めた。テストコードを書いてなくて、挙動を確認するにはデプロイして手動で動作確認を行う必要があった。当初はそのままテストコードを書かずに開発を進めてしまったが、今思い返すとまずテストコードを書くところから始めるべきだったと反省。

同プロジェクトで新規に API を開発することとなり、API 設計やテーブル設計にも関わらせてもらった。メンバーとあーでもないこーでもないと議論しながら設計を進めていくのは面白かった。オレ自身はじめての設計だったため分からないことだらけだったが、とてもいい経験になった。テストコードも書いた。Aurora Serverless v1 の Data API を用いたテストで実行時間が非常に長い問題にも直面した。このあたりは以下の記事に書いた。

enokawa.hatenablog.jp

チャットボットのプロジェクトが一段落したあとで、キャンペーンサイトプロジェクトにジョインした。主に API の開発やインフラの構築を担当した。良い API 設計をしてくれたメンバーのおかげで特に問題なくリリースできた。前回の反省を踏まえて開発はテストファーストで進めて、スピーディに高品質なコードが書けたと考える。テスト実行も 10 秒程度で済んだのはよかった。諸事情あって CI を整備できなかったのが心残りで、次に活かしていきたい。

インフラが強みであったことからインフラの改善なども推進した。CloudFormation のテンプレート充実化であったり、OpenAPI(Swagger UI)の AWS Amplify でのホスティングを行ったりした。OpenAPI のホスティングGitHubopenapi.yaml が commit されると Amplify でビルドが走りデプロイされる。複数のプロジェクトで、同じ仕組みで動いていて嬉しい。今後はインフラエンジニア時代に使っていた Terraform をやっていきたいと考えている。CloudFormation も好きだが、やはり terraform plan の安心さが欲しい。オレが CloudFormation をちゃんと理解できていないだけだが、デプロイ時に意図しない挙動(変更されるはずのないリソースが更新されたり)をしたりしている。change set の差分わかりづらい。

とあるプロジェクトで Flutter も触った。アプリケーションエンジニアという肩書なのでチームでは iOSAndroid 向けのアプリケーションも開発していて、ゆくゆくはクライアント側のコードも書けるようにしたいので積極的に取り組んだ。簡単な bugfix から始まり、レイアウトの微調整、環境(dev/stg/prd)毎の遷移先リンクの振り分け、画面遷移時に発生するエラーの調査修正などを行った。Python を触ったあとに Dart を書いたので、静的型付け良いなぁというのと、ドキュメントがすごく丁寧な印象をもった。関わる期間が短かったため深くは学んでいないが、まだプロジェクトは動いているので今後も機会があれば貢献したい。

チームの雰囲気とか

上長以外は初対面で、かつリモートのため馴染むのに時間がかかった。少しずつ慣れていった。 異動してから毎日夕会を実施していて、トレーニングや業務で詰まったときに画面共有しながら質疑応答してくれたのはよかった。 Slack での質問も快く回答してもらった。当時オレを除いたメンバーは 2 人いて、1 人はバックエンド開発に長けていて、もう 1 人はフロントエンドに長けている。上長は何でもできるって感じだったので、ほとんどの問題は質問することで解決できた。特に質問しづらいといった雰囲気は感じなかった。

困ったことは、開発フローやコーディングガイドがなかった点。どのような流れで PR を作成してレビュー依頼をするのか、Python のフォーマッターや Linter は何をつかうのかとか。あとはコードの書き方に統一感がなかったのも気になった。Lambda で API を書く場合は特に Web フレームワークを利用しない(コンテナ使えばできるが)ため、例えば共通処理はどのように切り出すかであったり、変数宣言時に型も明記するかなどが不明瞭だった。これらの問題は今後の課題。

直近のプロジェクトでは積極的にモブワークをした。素早く相談したり意思決定をしたかったり、コードレビュー時にお互いの認識が合わないといった場合に「モブりませんか?」と気軽に話せるのはよかった。話した内容は Wiki なり PR に明記するように心がけた。後で絶対に話した内容を忘れるので。VSCode やターミナル、Wiki を画面共有しながらあーでもないこーでもないとフラットに話せるのが心地よかった。画面共有は Gather を使った。常駐しているわけではないが、「いつもの部屋で」ができるのはとてもいい。Meet や Zoom だと都度ミーティングを払い出さないといけなくて手間だが、Gather は広いマップに複数の部屋があるイメージで他の人と被ったりすることもない。

www.gather.town

これからどうしていくのか

この 1 年はがっつりバックエンドをやってきたので、今後はフロントエンドもやっていきたい。 多くの場合、API はフロントエンドから叩かれてはじめて意味を成すため、フロントエンドを分かっていると、よりよい API を設計・実装できそうだと強く感じたから。 あとは、DynamoDB などの NoSQL もやっていきたい。関わったプロジェクトは RDBMS しか使っていないので、以降関わるプロジェクトでやっていきたい。

リリース作業も積極的に行った。まだまだ手作業な部分が多く、例えば DB のマイグレーションやサービスの動作確認、IAM ポリシーの権限修正などを手動で行っている。 人間なので手作業でやると必ずどこかでミスが発生する。挙げた手作業は全て自動化できる内容なので、開発初期の段階で自動化や IaC などを推進していきたい。 とはいえ開発中は機能の実装やバグ修正、打ち合わせなど様々なタスクに忙殺されることが多いので、日頃から検証しておくなどの素振りが必要。

インプット

読みきったものは少ないが、この 1 年は以下の書籍を読んだ(購入した)。

techbookfest.org

www.shoeisha.co.jp

gihyo.jp

www.oreilly.co.jp

www.oreilly.co.jp

gihyo.jp

gihyo.jp

www.seshop.com

www.shoeisha.co.jp

www.seshop.com

アウトプット

業務に関わるアウトプットはこれくらい。

enokawa.hatenablog.jp

enokawa.hatenablog.jp

enokawa.hatenablog.jp

おわりに

あまりまとまりがないが、インフラエンジニアからアプリケーションエンジニアへ転向した経緯ややってきたことをまとめた。 プログラミングの経験はかじる程度しかなかったが、この 1 年でフロントからバックエンド、インフラまで幅広く経験できて成長した実感がある。 まだまだ課題は山積みで、作りたいモノを作れるようにはなっていないが、こつこつやっていく。

2023年4月

育児

2 歳 5 ヶ月になった。

  • 桜を見に行った
  • いっぱい目薬さしたくて泣く
  • 夜めっちゃ起きる
    • のでいっぱい遊びまくった
    • 目悪くしそうだな。。
  • 夜中に起きてリビングまで歩いてきた
    • めっちゃビビった
    • 起きて誰もいなくて寂しかったのかな
    • 泣いてはいなかった
    • ATOM Cam の録画みたら上手にベッドから降りててドアもちゃんと閉めてて笑ってしまった
  • 新年度初の登園
    • 新しいお部屋やイスで戸惑っていたらしい
    • 徐々に慣れてくれるといいな
  • 園でトイレでおしっこできた
    • 家ではまだなので誘っていきたい
  • 歯医者でもらったドーナツの消しゴム食べれなくてキレた
  • 3 時のおやつ用にホットケーキを焼くようになった
    • 虫歯対策の一環
    • ゆくゆくは米粉とベーキングパウダーでやりたい
  • ホットケーキおとして食べれなくてギャン泣き
  • キッチン台に頭うって打撲
  • おもちゃに顎うってすりむいた
  • おくるみを卒業した
    • 歯医者で少し出っ歯気味と言われ卒業を決断
    • 生まれた時からずっと一緒だったので心が痛い
    • ホント息子氏に申し訳ない
  • ご飯食べる時の姿勢が悪い
    • ちゃんと座らせよう
    • ママパパも姿勢よく食べよう
  • 寝室にいきたがらない
    • おくるみ卒業したからかな
    • 寝つきも遅くなった
  • 寝起きに泣く
    • これもおくるみ卒業したからかな
  • お隣さん家族をお招きして遊んだ
    • ホットケーキ祭りと題した会だったけどお隣さんの子は食べず
    • おもちゃに集中してた
  • 車道に飛び出した
    • 運よく車は通ってなかった
    • 手を繋ぎたがらなかったので離してた油断した
    • 肝冷やした...気をつけよう
  • 園でシュシュつけてもらってそのまま降園した
  • 歯磨きの時「ウーイースーキー」って言ったらマネした
    • オレが昔受けたプレゼン研修で習った口角をあげる方法
    • キレイに歯が磨けるからありがたい
  • 歯磨き前に『はみがきれっしゃ』を持ってくる

今月のビッグニュースはついにおくるみを卒業したこと。生まれてから 2 年と 4 ヶ月、ほぼ常に身につけていて、ずっとおくるみと一緒に過ごしてきた。息子氏はおくるみがあると落ち着くらしく、寝る前や癇癪のときに与えると寝付いたり落ち着いたりする。そんな大切なものをたった一瞬で奪ってしまったのはとても心苦しかった。すべては親の都合で、息子氏にとって良いことなんてひとつもなかった。実際、歯並びに影響が出たり、これは定かではないが、発語に影響が出ていたとも思う。まだ十分に 2 語を話せていないので責任を感じている。

歯医者で指摘されてから、その後はおくるみを与えずに隠して昼寝につかせた。奥さんとも話して、このまま夜も与えずにいこうと決めた。その日はいつも寝付きのよい息子氏もなかなか寝付けなかった。寝る前はおくるみが必ずベッドに置いてあるから不安に思ったのだろう。そんな日が何日か続いて、最近ようやくおくるみなしでも落ち着いて寝付けるようになってきた。

何事も親の都合で解決するのは良くないなと痛感した。今日も疲れたから YouTube を観せて楽をしてしまった。これは短期的には、親にとっては楽になるかもしれない。ただ YouTube を観ているあいだ息子氏は言葉を殆ど発しない。つまり、親の都合で子どもの成長を妨げているということになる。そのことを忘れずにいたい。全ての労力を子どものために費やして生活するのはなかなか難しいが、うまくバランスをとって過ごしていきたい。例えば YouTube を観る時間を制限するなど。これは子どもにとってもそうだが、親にとっての制限にもなる。1 日 1 時間は YouTube を観せても/観てもよい。が、それ以外の時間はお絵かきをしたり、おもちゃで遊んだり、公園で遊んだりするといった具合で。

最終的には全部自分に返ってくるんだよな。将来「お父さんなんで僕は歯並びが悪いの?」「なんで YouTube ばっか観てちゃダメなの?」 「なんでゲームしてばっかじゃダメなの?」なんて言われたらオレは言い返すことはできるだろうか?できない。これ以上かくのはやめよう。。

仕事

その他

ブログ 2 件かいた

enokawa.hatenablog.jp

enokawa.hatenablog.jp

AWS KMSのエンベロープ暗号化を理解する

AWS Certified Security - Specialty の学習の一貫で KMS について勉強していた。 Black Belt を観て CLI を叩いたりしていて、GenerateDataKey APIユースケースが思い浮かばなかったので整理する。

前提

  • 対称暗号化 KMS キー(SYMMETRIC_DEFAULT)

処理の流れを整理

通常の Encrypt / Decrypt を利用した場合と、エンベロープ暗号化を利用した場合の流れを整理する。

Encrypt / Decrypt

特に手元に鍵を用意することなく、カジュアルに利用することが可能。 AWS CLI で Encrypt API を呼び出す際は、渡す平文(--plaintext)を事前に base64 エンコードしておく必要がある。 Decrypt API を呼び出す際は、返却された平文(Plaintext)を base64 デコードしてあげる。

  1. Encrypt API で CMK を利用しての平文を暗号化
  2. Decrypt API で CMK を利用して暗号データを復号

エンベロープ暗号化

Encrypt / Decrypt と比較すると手順が多い。暗号化と復号それぞれで手順をみていく。

暗号化

  1. GenerateDataKey API で CMK を利用して CDK を作成
    • データキーと暗号化されたデータキーを返す
  2. データキーを利用して任意の平文を暗号化
    • データキーはメモリ上に保持する
  3. 暗号化されたデータキーをディスクなどに保存
  4. データキーを削除

復号

  1. Decrypt API で CMK を利用して暗号化されたデータキーを復号
    • "暗号化"の 4 で保存したデータキーを送信
  2. 1 で返却されたデータキーを利用して暗号化されたデータを復号
    • データキーはメモリ上に保持する
  3. データキーを削除

Encrypt / Decrypt とエンベロープ暗号化の比較

簡単にまとめてみる。

Encrypt / Decrypt エンベロープ暗号化 備考
データ上限 4 KB なし
API 呼び出し頻度 多い 少ない
実装の容易さ △(※) ※ Encryption SDK 利用推奨
データキーの発行有無 なし あり
意図せず CMK が無効化もしくは削除された場合の影響

データ上限については分かりやすくて、4 KB 以上のデータを暗号化する必要がある場合はエンベロープ暗号化を利用せざるを得ない。API の呼び出し頻度については、Ecrypt / Decrypt の場合は都度呼び出す必要があるが、エンベロープ暗号化の場合は暗号化の処理を言語のライブラリ(Python だと cryptography など)で実装することができて、スロットリングが発生する可能性も低い。実装の容易さは Encrypt / Decrypt に軍配が上がるが、AWS から Encryption SDK というエンベロープ暗号化を簡単に実現できるライブラリが提供されている。Encryption SDK を利用することで、簡単に、かつ耐障害性のあるアプリケーションの実装が可能となる。

docs.aws.amazon.com

意図せず CMK が無効化もしくは削除された場合の影響について、CMK の場合はどうしようもないが、エンベロープ暗号化の場合はどうにかなる。仮に CMK が削除された場合、既にデータキーを発行している状態であれば引き続き暗号化は可能である。復号する場合どうすればいいのかはまだ理解できていないが、KMS のベストプラクティス(PDF)には DR の設計にも有用との記載があった。

どちらの方法を選択するにせよ、セキュアであることに変わりはなさそう。CMK の保守性であったり、可用性を高める場合はエンベローブ暗号化を使ったほうがよさそう。暗号化されたデータキーをどこに保持しておくかは検討する必要がありそう。どこにおいたほうがベストかはまだ分かっていないが、Encryption SDK では暗号化されたデータキーをキャッシングする機構もあるようだ。

ユースケース

雑に書くと、以下の条件に当てはまる場合はエンベロープ暗号化を利用したほうがよさそう。

  • 4 KB 以上のデータを暗号化する必要がある
  • バックエンドの Web API などで、多くのリクエストが見込まれる
    • 毎リクエストで暗号化する必要がある前提
  • 高可用性が求められる

具体的なユースケースは思い浮かばなかったが(ごめんなさい)、EC2 の EBS 暗号化でも、実際にエンベロープ暗号化を利用しているそう。高可用性が求められるサービスのため、エンベロープ暗号化を利用する必要があったといえる。

サンプルコード

実際にエンベロープ暗号化を実装したら理解が進んだ。合っているかどうか怪しいので参考までに。

github.com

参考

docs.aws.amazon.com

aws.amazon.com

docs.aws.amazon.com

qiita.com

stackoverflow.com

おわりに

GenerateDataKey API はどのようなユースケースで使うのだろうか。という疑問から、実際に実装しながら、ドキュメントを読みつつ理解を深められた。調べれば調べるほど KMS は奥が深くて、他の AWS サービスとも連携していて面白いなと感じた。KMS 完全に理解した。

今回は学習のため Encryption SDK は利用しなかったが、こんど実際に使ってみて便利さに驚愕したい。おそらくとても便利。

pytest + motoでSecrets Managerをテストする

Secrets Manager に限らず、いつもやり方を忘れてしまうので備忘録として残しておく。

前提

  • macOS(Big Sur) 11.7.2
  • AWS CLI 2.5.6
  • Python 3.9.12
  • pytest 7.1.3
  • boto3 1.18.56
  • moto 3.1.16

事前準備

AWS アカウントで事前に Secrets を作成しておく。今回は例として、DB の接続情報を格納する。エンジンは MySQL を想定している。 Secret name は sample/pytest とし、値は以下のようになっている。ちなみに password はこのブログのために用意した一時的なもの。

AWS CLI で取得すると以下の出力が取得できた。これで準備は完了。

$ aws secretsmanager get-secret-value --secret-id sample/pytest
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:sample/pytest-xxxx",
    "Name": "sample/pytest",
    "VersionId": "024aedb2-0c51-43d1-be6d-e45f80ec0516",
    "SecretString": "{\"username\":\"admin\",\"password\":\"FLsr(uZoI@@Z+Aa?\",\"engine\":\"mysql\",\"host\":\"0.0.0.0\",\"port\":\"3306\",\"dbname\":\"pytest\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2023-04-21T21:18:04.919000+09:00"
}

仮実装

まずはまっさらな状態からテストコードを書く。今回は Secrets Manager から SecretString を取得し、それを戻り値として dict 型で返す関数を書きたい。呼び出す Secrets Manager の API は GetSecretsValue を想定している。

docs.aws.amazon.com

モジュール名は get_secret_string としよう。関数名も get_secret_string とする。期待値は先ほど AWS CLI で叩いた結果の SecretString の値を dict 型にする。テストコードは以下となった。本来 username や password などの機微な情報は環境変数から取得するようにすべきなのは理解している。

# test_get_secret_string.py
def test_get_secret_string():
    expected: dict = {
        "username": "admin",
        "password": "FLsr(uZoI@@Z+Aa?",
        "engine": "mysql",
        "host": "0.0.0.0",
        "port": "3306",
        "dbname": "pytest"
    }

    secret_value: dict = get_secret_string()
    assert secret_value == expected

テストを実行する。まだモジュールも get_secret_string 関数も存在しないためインポートエラーとなりそうだ。

$ pytest
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.12, pytest-7.1.3, pluggy-1.0.0
rootdir: /Users/enokawa/workspace/github.com/enokawa/pytest-sandbox/secrets_manager
collected 1 item

test_get_secret_string.py F                                                                                                                     [100%]

====================================================================== FAILURES =======================================================================
_______________________________________________________________ test_get_secret_string ________________________________________________________________

    def test_get_secret_string():
        expected: dict = {
            "username": "admin",
            "password": "FLsr(uZoI@@Z+Aa?",
            "engine": "mysql",
            "host": "0.0.0.0",
            "port": "3306",
            "dbname": "pytest"
        }

>       secret_value: dict = get_secret_string()
E       NameError: name 'get_secret_string' is not defined

test_get_secret_string.py:11: NameError
=============================================================== short test summary info ===============================================================
FAILED test_get_secret_string.py::test_get_secret_string - NameError: name 'get_secret_string' is not defined
================================================================== 1 failed in 0.02s ==================================================================

NameError だった。恥ずかしい。 get_secret_string が定義されていないため FAILED となった。次に get_secret_string モジュールと関数を作成する。関数では仮実装として None を返すようにしよう。

# get_secret_string.py
def get_secret_string() -> dict:
    return

テストモジュールで get_secret_string モジュールの get_secret_string 関数をインポートする。これでテストを実行したら AssertionError となるはず。

# test_get_secret_string.py
from get_secret_string import get_secret_string

def test_get_secret_string():
    expected: dict = {
        "username": "admin",
        "password": "FLsr(uZoI@@Z+Aa?",
        "engine": "mysql",
        "host": "0.0.0.0",
        "port": "3306",
        "dbname": "pytest"
    }

    secret_value: dict = get_secret_string()
    assert secret_value == expected
$ pytest
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.12, pytest-7.1.3, pluggy-1.0.0
rootdir: /Users/enokawa/workspace/github.com/enokawa/pytest-sandbox/secrets_manager
collected 1 item

test_get_secret_string.py F                                                                                                                     [100%]

====================================================================== FAILURES =======================================================================
_______________________________________________________________ test_get_secret_string ________________________________________________________________

    def test_get_secret_string():
        expected: dict = {
            "username": "admin",
            "password": "FLsr(uZoI@@Z+Aa?",
            "engine": "mysql",
            "host": "0.0.0.0",
            "port": "3306",
            "dbname": "pytest"
        }

        secret_value: dict = get_secret_string()
>       assert secret_value == expected
E       AssertionError: assert None == {'dbname': 'pytest', 'engine': 'mysql', 'host': '0.0.0.0', 'password': 'FLsr(uZoI@@Z+Aa?', ...}

test_get_secret_string.py:14: AssertionError
=============================================================== short test summary info ===============================================================
FAILED test_get_secret_string.py::test_get_secret_string - AssertionError: assert None == {'dbname': 'pytest', 'engine': 'mysql', 'host': '0.0.0.0',...
================================================================== 1 failed in 0.02s ==================================================================

想定通り AssertionError となった。これで TDD における "RED" には到達した。次に明白な実装をして "GREEN" にする。get_secret_string 関数の返り値を、テストコードで書いた expected 変数の値と同じにして、再度テストを実行する。GREEN になるはずだ。

# get_secret_string.py
def get_secret_string() -> dict:
    value: dict = {
        "username": "admin",
        "password": "FLsr(uZoI@@Z+Aa?",
        "engine": "mysql",
        "host": "0.0.0.0",
        "port": "3306",
        "dbname": "pytest"
    }

    return value
$ pytest
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.12, pytest-7.1.3, pluggy-1.0.0
rootdir: /Users/enokawa/workspace/github.com/enokawa/pytest-sandbox/secrets_manager
collected 1 item

test_get_secret_string.py .                                                                                                                     [100%]

================================================================== 1 passed in 0.00s ==================================================================

やったね。これからリファクタリングしていく。参考までに、この時点でのディレクトリ構成を記載しておく。

$ tree .
.
├── get_secret_string.py
└── test_get_secret_string.py

0 directories, 2 files

GetSecretsValue APIを呼び出す

boto3 のドキュメントを参考に、get_secret_value メソッドを呼び出す。

boto3.amazonaws.com

必須の引数は SecretId のみで OK なので、作成済みの sample/pytest を指定する。戻り値は dict で返却されるため、SecretString キーの値を json.loads() してあげればよさそうだ。

# get_secret_string.py
import json
import boto3

def get_secret_string() -> dict:
    client = boto3.client('secretsmanager')
    response = client.get_secret_value(
        SecretId='sample/pytest'
    )
    secret_string: str = response["SecretString"]

    return json.loads(secret_string)

これで AWS Credential を仕込んだうえでテストを実行すれば通りそうな気がする。やってみる。

$ export AWS_PROFILE=enokawa
$ pytest
中略
=============================================================== short test summary info ===============================================================
FAILED test_get_secret_string.py::test_get_secret_string - botocore.exceptions.NoRegionError: You must specify a region.
================================================================== 1 failed in 2.30s ==================================================================

NoRegionError が出たので、SecretsManager Client の作成時にリージョンも指定する。これでいけるかな。

# get_secret_string.py
import json
import boto3

def get_secret_string() -> dict:
    client = boto3.client('secretsmanager', region_name="ap-northeast-1")
    response = client.get_secret_value(
        SecretId='sample/pytest'
    )
    secret_string: str = response["SecretString"]

    return json.loads(secret_string)
$ pytest
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.12, pytest-7.1.3, pluggy-1.0.0
rootdir: /Users/enokawa/workspace/github.com/enokawa/pytest-sandbox/secrets_manager
collected 1 item

test_get_secret_string.py .                                                                                                                     [100%]

================================================================== 1 passed in 0.27s ==================================================================

いけてそう。AWS Credential を指定しない状態でテストを実行すると認証エラーが出るはず。

$ unset AWS_PROFILE
$ pytest
中略
=============================================================== short test summary info ===============================================================
FAILED test_get_secret_string.py::test_get_secret_string - botocore.exceptions.NoCredentialsError: Unable to locate credentials
================================================================== 1 failed in 2.25s ==================================================================

想定通り NoCredentialsError が出た。次に進む。

motoを使わずにモックできないのか?

pytest-mock でモックできないか試してみる。

pypi.org

# test_get_secret_string.py
from get_secret_string import get_secret_string

def test_get_secret_string(mocker):
    expected: dict = {
        "username": "admin",
        "password": "FLsr(uZoI@@Z+Aa?",
        "engine": "mysql",
        "host": "0.0.0.0",
        "port": "3306",
        "dbname": "pytest"
    }

    mocker.patch("get_secret_string.get_secret_string", return_value=expected)

    secret_value: dict = get_secret_string()
    assert secret_value == expected
=============================================================== short test summary info ===============================================================
FAILED test_get_secret_string.py::test_get_secret_string - botocore.exceptions.NoCredentialsError: Unable to locate credentials
================================================================== 1 failed in 3.62s ==================================================================

NoCredentialsError が出た。ということは GetSecretsValue API が呼ばれたということになりそうだ。この理由はまだ分かっていなくて、理解したタイミングで別エントリに記載しようと思う。

motoでget_secret_valueをモックする

ようやく本題。Credential を指定しない状態でテストを実行し、PASS することをゴールとする。 moto のドキュメントから、 get_secret_value のモックはできそうということが分かった。

docs.getmoto.org

ではテストコードを実装していく。mock_secretsmanager decorator を利用すればモックできそうだ。

# test_get_secret_string.py
from get_secret_string import get_secret_string
from moto import mock_secretsmanager

@mock_secretsmanager
def test_get_secret_string(mocker):
    expected: dict = {
        "username": "admin",
        "password": "FLsr(uZoI@@Z+Aa?",
        "engine": "mysql",
        "host": "0.0.0.0",
        "port": "3306",
        "dbname": "pytest"
    }

    mocker.patch("get_secret_string.get_secret_string", return_value=expected)

    secret_value: dict = get_secret_string()
    assert secret_value == expected
$ pytest
中略
E           botocore.errorfactory.ResourceNotFoundException: An error occurred (ResourceNotFoundException) when calling the GetSecretValue operation: Secrets Manager can't find the specified secret.

/Users/enokawa/.anyenv/envs/pyenv/versions/3.9.12/lib/python3.9/site-packages/botocore/client.py:708: ResourceNotFoundException
=============================================================== short test summary info ===============================================================
FAILED test_get_secret_string.py::test_get_secret_string - botocore.errorfactory.ResourceNotFoundException: An error occurred (ResourceNotFoundExcep...
================================================================== 1 failed in 0.33s ==================================================================

ResourceNotFoundException となった。事前に moto のなか(?)で Secrets を作成しておく必要がありそうだ。CreateSecret API を呼び出して、Secrets を作成する Fixture を用意する。

boto3.amazonaws.com

# test_get_secret_string.py
import json
import boto3
import pytest

from get_secret_string import get_secret_string
from moto import mock_secretsmanager

@pytest.fixture(scope="function", autouse=False)
def create_secret():
    def _create_secret(secret_name: str, secret_value: str):
      client = boto3.client('secretsmanager', region_name="ap-northeast-1")
      client.create_secret(Name=secret_name,SecretString=secret_value)
      
    return _create_secret


@mock_secretsmanager
def test_get_secret_string(create_secret, mocker):
    expected: dict = {
        "username": "admin",
        "password": "FLsr(uZoI@@Z+Aa?",
        "engine": "mysql",
        "host": "0.0.0.0",
        "port": "3306",
        "dbname": "pytest"
    }

    create_secret(secret_name="sample/pytest", secret_value=json.dumps(expected))

    mocker.patch("get_secret_string.get_secret_string", return_value=expected)

    secret_value: dict = get_secret_string()
    assert secret_value == expected
$ pytest
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.12, pytest-7.1.3, pluggy-1.0.0
rootdir: /Users/enokawa/workspace/github.com/enokawa/pytest-sandbox/secrets_manager
collected 1 item

test_get_secret_string.py .                                                                                                                     [100%]

================================================================== 1 passed in 0.24s ==================================================================

テストが PASS することを確認できた。

おわりに

ステップバイステップで pytest と moto を利用して Secrets Manager をテストした。 恥ずかしながら、(pytest-)mock が内部で何をしているのかであったり、実関数を呼び出しているのかどうかであったり、moto が何をしているのかがちゃんと理解できていない。

このエントリを切り口として、理解できていない部分を実装して、理解を深めていきたい。

サンプルコードは以下。

github.com

2023年3月

育児

2 歳 4 ヶ月になった。

  • 落ちた食パンが食べたくて朝からギャン泣き
    • あんぱんをあげるとニンマリ
  • 園で公園にいったときほしい車のおもちゃを先にとられた
    • じーっと見て違うおもちゃを選んだらしいすごい
  • あんぱん大好き
    • あげすぎに注意せねば..
  • 歯医者で歯の写真とるのにギャン泣き
    • 大人 3 人でおさえつけ(すまんな...)
  • 歯医者がんばったからケーキ屋さんでマドレーヌ買った
    • 頑張って家まで運んでくれた
    • お昼に食べれなくてギャン泣き(すまんな...)
    • おやつに美味しくいただきました
  • 久々の吐き戻し
    • 咳がひどくて吐いた
  • お風呂嫌がる
    • パパのお膝の上に乗る
    • いままでは立ってた
    • シャンプー流す時に嫌がる
  • 無印のローテーブル買ってテンションアゲ
    • 今まではダイニングテーブルとハイチェアだった
    • ついでにハイチェアの股のガードも外した
      • 自分で登るようになった
  • 久々のシチュー喜んで食べてくれた
  • 自分で目薬をさす
    • 目を >< にして目頭に
  • 歯ブラシ口に咥えたまま歩く
    • ママがダメって大きめの声で言ったらギャン泣き
  • お風呂場で無言で髪切ったら怖がった
    • ごめんよ..
    • 髪伸びてるし床屋でも嫌がるしどうしたものか
  • 「きゅうきゅうしゃ どこ?」が言えた!
  • 手洗った後にキレイアピールする
    • 両手を伸ばして手の甲をママにみせる
  • かんしゃくの嵐
    • 麦茶をコップに入れたくて暴れる
    • おもちゃで思うように遊べず暴れる
    • お風呂に入りたくなくて暴れる
    • YouTube で観たい動画が伝えられず暴れる
  • ごみ収集車のトミカを買った
  • はたらく車のパズルが上手になった
    • 親の手助けなしではめる

今月もいろいろと大変だった。。癇癪が増えた気がする。ママパパにうまく気持ちを伝えられなくて暴れる感じ。こちらも汲み取れるよう努力していきたい。 あと保育園に入園してちょうど 1 年が経った。園でお友達や先生とともに色々なことを経験することで、息子氏はとても成長したと日々感じる。園の先生方は園児のことを一番に考えていて、真剣に向き合っていて、積極的に研修を行っていて本当にありがたい。時々悲しいニュースが流れたりするけど、そのようなことが起きない未来を願うばかりである。

あと、幼児の虫歯に関するツイートが流れてきてはっとさせられた。あんぱんや甘いものをたまに与えたりしているので、今後は息子氏のことを考えて甘いものは一切与えないようにしたい。最低でも 3 歳までは。

仕事

  • とあるサービスをリリースした
  • 特に大きなトラブルも起きておらずホッとしている
  • 反省点も多いので今後に活かしていく
  • がっつり振り返り会もした

その他

  • Web API: The Good Parts を読んだ
    • ざっくりとだけど美しい API をどう設計すればよいかがクリアになった
    • HTTP の標準に則ろう

www.oreilly.co.jp

2023年2月

育児

2 歳 3 ヶ月になった。

  • 園だより最高だった
  • りんごを口の中で咀嚼して吸って吐く
    • そして吐いたカスを手で握って口の中へ..エンドレス
  • 「こんにちは」が言えるようになった
    • 「こんちゃー」
  • 鼻水すってギャン泣き
  • 小児科でちょこまか動き回る
  • パパ小児科で風邪をもらう
    • 約 1 週間ダウンしてた..もう若くない
  • バレンタインでチョコマドレーヌをあげた
    • めっちゃおいしそうに笑顔で頬張っていた
  • 園で「おさかな いないねぇ」と言っていたらしい
    • はじめての 2 語!
  • ブロックのおもちゃ買った
    • # の形とかあるやつ
    • ニューブロックというやつ
    • 色々つくれてパパママの方が楽しんでる
  • 園で公園にいって息子氏あわせて3 人で並んでハイハイしたらしい
    • 3 両のハイハイ電車
  • パパのマネをする
    • はみがき、ドライヤー、手洗い、顔の保湿
  • スーパーでミスド買って頑張って家まで歩いて運んだ
    • 3 時のおやつで美味しくいただきました
    • しかも普通サイズのドーナツ
    • ちっちゃい D ポップ?も買ってたけど普通サイズが食べたいらしい

仕事が忙しいタイミングで、小児科で風邪をもらった。いつもは 1,2 日で回復するが今回は 3 日もダウンしてしまった。もう若くないので体調管理はちゃんとしたいところ。園から届く連絡帳がいつも楽しみで、今月も「あぁ息子氏がんばったなー偉いなー」という気持ちにたくさんなった。

仕事

  • 新プロジェクトでガッツリ API つくった
    • TDD が染み付いてきた
    • API の設計というかドキュメント作成がまだネック
  • インフラ構築も好きにやらせてもらった
    • CloudFront + S3 の構成で OAC 利用
    • Aurora Serverless v2 よい
  • 多忙だったが楽しかった