生涯未熟

生涯未熟

プログラミングをちょこちょこと。

TechBrew in 東京 〜SRE大集合!信頼性を高める取り組み〜 に登壇しました

気付いたら2024年が始まって3ヶ月記事を書いていなかったことに気付いたので、慌てて書いております。

ということで、2/14にFindyさんが主催の勉強会に登壇させていただきました。

findy.connpass.com

今回はSRE × セルフ・アウェアネスという題材でお話させていただきました。

speakerdeck.com

登壇のお話を頂いたときに「何の話をしようかな・・・」と考えたのですが、自作k8sコントローラを作った話とセルフ・アウェアネスの話と悩んだ末に、後者の話にしました。
正直、前者の話をしてもk8sを扱ってない人からするとピンと来ない話でしたし、もう少し汎用性のある話をしたかったというお気持ちで選択。

さて、今回のセルフ・アウェアネスの話ですが、そもそものきっかけはSREConでの「The Secret Weapon for a Successful SRE Career」というセッションを聞いたことでした。

www.usenix.org

これは、SREを実践するうえで起こる他者とのコミュニケーションなどを円滑に進めるためにセルフ・アウェアネスが有効ですよねという内容なのですが、SREの話題は技術的な話か、非技術的な話だと組織形成などの話がほぼでこのセッションのように「ソフトスキル」を軸にした話を聞いたのは初めてでちょっと衝撃を受けました。
登壇の中でも触れましたが、海外だと日本よりSREポジションにとってのソフトスキルの必要性が言及されており、改めて自分の中でソフトスキルを磨くことへのモチベーションが高まりました。

こういった海外でのSRE × ソフトスキルの話題の例の一つとして、Red HatでSREをやっている方のインタビューpodcastを貼っておきます。transcriptもあるので翻訳して読んでみるとよいです。この中でSREについて「It really boils down to the soft skills, it boils down to culture, it boils down to people. (結局のところ、ソフトスキル・文化・そして人に尽きる)」と仰られていて、非常に共感しました。

changelog.com

このセッションでセルフ・アウェアネスという概念を知りましたが、半年以上折に触れては内省を試してきた結果、自分にとってはかなり有用でした。人とコミュニケーションを取った後などによく「あぁ、なんか変なこと言っちゃったかな?」とか「あれは言わなくてもよかったかな・・・」とか心がウッとなることがあるのですが、適切に内省を行い「何がそう思わせるのか?」「やってしまったとして何がフォローとして出来ただろうか?」など少しは前向きな振り返りが出来るようになり、ポジティブな気持ちを持続できることが増えました。

エンジニアは精神的に病んでしまう方が多くなりがちな職業ではあるので、それを予防するためのセルフ・アウェアネスとしても非常に有効かと思います。こういった本も出ているので、良かったらお試しを。

あと、今回の登壇の裏テーマは「SRE × ソフトスキルの話が界隈にもう少し増えること」だったので、今後そのようなお話が増えるといいなと願っております🙏

2023の振り返り

今年ももう年の瀬ですね。30歳を越えてからというもの、年々時間の経つのが早く感じられて恐ろしくある今日此の頃でございます。

というわけで今年も一ヶ月毎に振り返ってみましょう。

1月

いきなり病気からスタートだったのですが、年末年始コロナにかかってダウンしておりました。いやー、コロナに初めて罹ったのですが辛いながらも 扁桃腺炎 > コロナ という感じでした。あえて分類するとすれば瞬発的な辛さは扁桃腺炎がダントツで、持続的な辛さはコロナで喉の痛みといった症状は一緒だけれども痛みの度合いは扁桃腺炎が段違いでしたね・・・コロナはコロナで、1月中は咳が止まらなくて生活のしづらさが目立ちました。

何はともあれいい年齢の人間になったので、病気には気を付けたいものです。

さて、1月の大きなトピックといえば年始早々のCircleCIのセキュリティインシデントですね。

circleci.com

弊チームでもご多分に漏れずCircleCIを利用してまして、シークレットの総取っ替えをする必要がありました。 病床についてる最中だったので、大丈夫かしら?と心配でSlackをチラチラ覗いていましたがチームメンバーがササッと対応してくれました。本当に頼りになるチーム・・・!

また、嬉しいことに新しいメンバーを迎えることができ、更にワイワイと楽しく・開発をより進めることのできるチームになったのもの1月のトピックでした。

この頃、自分はというとちまちまとフロントエンドのFragment Colocationの対応をしておりました。この背景としてはover-fetchingが目立つようになり、そこが原因でSSR時にLarge Page Dataで怒られることが増えたことが対応することになった起因です。
この対応についてはマネーフォワードさんのFragment Colocationの記事を大いに参考にさせてもらいました!先人の方々の残してくれている知識には感謝しかないですね🙏

zenn.dev

あとは新婚旅行として人生で初めて沖縄に行ってきました。あまりアウトドア・アクティビティが得意な夫婦ではないので、オフシーズンの安い時期な沖縄をチョイスしてのんびり色んなところを巡ってきました。
中でも由布島に行ってきたのですが、沖縄の中でも特に自然が満載で良いリフレッシュになりました。関東ではなかなか自然に囲まれる経験もできないですしね。あとは牛車で島間を往来するのも良い経験になりました。

※いませんでした・・・

2月

2月からはチームメンバーの発案でエンジニア会と呼ばれる2週間に一回のダッシュボードの観測や気になるエラーログの振り返り、エンジニアリング課題の共有などを行う定期会を開催するようになりました。今でも続いているのですが、作成したダッシュボードを活用する良い機会となり、もっと早くやっておけばよかったなぁと反省です。

インフラ関連で言うとGKEの負荷分散をコンテナネイティブの負荷分散に切り替えることをしました。切り替えの理由としてはサービスで少数ながら502が出ていて、調査したところInstance Groupを通したHealth Checkが70s間隔で、削除予定のnodeが存在した場合に最大70sは疎通ができていなくてもリクエストを送り続けていたため502が出ていました。
Instance Groupを通している場合でもHealth Checkの間隔自体は少なくするで対応できるものの、コンテナネイティブ負荷分散にすることでNGINXのgraceful shutdownも効くようになり、より502が出る可能性を低くすることが狙いでした。
結果、502が出ないようになり切り替えは成功でした✌

あとはMIXI Tech Conferenceに登壇することとなり、登壇資料を作成するのにめちゃくちゃ頑張ってました。

techcon.mixi.co.jp

大きな場での登壇というのが初でもあったので、ネタ出しに非常に苦労しました・・・

speakerdeck.com

結果として大きな場でも小さな場でもちゃんと準備さえしていれば何も変わらないなぁということが分かったので、何事も経験かなと登壇依頼を受けましたが良かったです😊

3月

この辺でめちゃくちゃbotからのアクセスが増えてきました。オートスケールが走るレベルで定期的にやってくるようになってきたので、こりゃどうにかせんといかんとCloud Armorを導入しました。
Cloud Armorでは事前構成WAFルールというものがあり、これを利用することで簡単にSQLインジェクションクロスサイトスクリプティングなどを防ぐことができます。感度などのレベルを調整する必要があるものの、これ幸いと利用しております。このおかげで今ではbotからの悪意のあるアクセスはほぼ防ぐことができました。

syossan.hateblo.jp

※こういうこともありました

GKEアップグレードやArgoCDのアップグレードなど定常的なアップグレードをこなしつつ、GCのイメージ管理をContainer RegistryからArtifact Registryへ移行しながらgcr-cleanerを使ったStorageの古いイメージの削除をやるなどコスト面に関する作業もちらほらとやっていました。

syossan.hateblo.jp

4月

またまた旅行の話なのですが、これまた人生で初めて金沢に行きました。今回は兼六園がメインターゲットで、これで日本三大庭園を全て回ることができました!

それともう一つのお目当てがナガノ展でして、夫婦揃ってちいかわが大好きで以前東京開催を申し込んで落選してしまったナガノ展の金沢開催が当選したので行ってきた次第です。これまた最高のコンテンツでした・・・!

仕事ではk8sのDeschedulerを試して引き続きコスト面を気にした対応をしたり、レイテンシーやエラー率をまとめたちゃんとしたダッシュボードをLooker Studioで作成したりしてました。
あとはGitHub Actionsを workflow_call を使って、一つのリポジトリで管理するようにしました。これは再利用のためなどの理由もありつつ、本番環境へのデプロイ時にGitHub Actionsなどのコミットがあると開発チームから「デプロイしても大丈夫ですか?」と確認の連絡が飛んできてしまうので、それを防ぐ意味合いもあります。

また仕組み作りではステージング環境のデータを本番環境と同期させたいよねという提案があり、これを実現させるためにゴニョゴニョ動いていたりしました。

syossan.hateblo.jp

5月

2023年6月に改正電気通信事業法が施行されるということで、ユーザー情報を外部に送信しているサービスは何か?などをまとめたりしていました。こういったオーダーが飛んできた時に優先度組み換えて対応しやすくする"遊撃部隊"というポジションを立ち上げておいて良かったと思いますね。

また、ありがたいことにFindyさんのこちらのイベントにも登壇させていただきました。

findy.connpass.com

speakerdeck.com

会社でのグレードが上がったこともあって、こういった外部登壇を増やしていこうと今年は登壇頑張りました。来年も機会を見つけてどこかで登壇したいなぁ・・・

プライベートではマハラージャンのライブで初めて野音に行ってきました!来年の10月に建て替えが始まるということでその前に行くことができて嬉しかったです。やっぱり野外でのライブはいいですねぇ。

6月

GDPR対応が突然降ってきたので対応しておりました。ちょうどCloud Armorを適用していたのでこれを使って解決することに。

syossan.hateblo.jp

あとはSlackのTwitter共有仕様変更に伴って、自前で動かしていたTwitterエゴサ結果のSlack通知が上手く動かなくなってしまったのでIFTTT Proを契約してTwitter AppletからSlackへ通知を飛ばすようにしました。こういうビジネス側の要求も後回しにせずに優先して対応することは心がけております。

インフラ面だとk8sの特定のPodのリソース状況によってevictさせる君を作ったりしました。

syossan.hateblo.jp

「実践入門 Kubernetesカスタムコントローラーへの道」も有り難く読ませていただいたのですが、本当にk8sは学べることが満載で沼ですね。もっとガッツリ触ってみたい・・・

7月

このあたりで恥ずかしながらOpenTelemetry(以下OTel)について初めて触れました。まだサービスには導入できていないのですが、OTel自体はめちゃくちゃ気になる技術領域なので遅々ながらも追いかけていきたいなという所存です。(日本語のOTel本とか出ないかな・・・

それとフロントエンドの依存ライブラリアップデートもやっていました。定期的にやってくる作業ではあるのですが、本当に影響範囲ややることが多いので一人でやるのはしんどいですね・・・
とはいえ泣き言も言ってられないので、やるしかないのですがある意味ついでによろしくないところも直すことが出来る良い機会と思ってモチベーションを無理やり上げてやっていました。

syossan.hateblo.jp

あと、かたいなかさんが呟いていたこちらに反応した結果、後のゆるSRE勉強会を開くきっかけになったのも7月のトピックでした。

ちょうどSREについての勉強会ってもっと初心者向けというかゆるっとしたものはないかな〜?と考えていたりしたので、喜んで一緒にやることにしました。声をかけていただいたかたいなかさんには感謝です🙏
以前のコミュニティ活動で色々とあり、運営に関わるかどうか悩んだのですがやることにした心境についてはまたどこかで一筆書こうかと思います。(しずかなインターネットあたりで・・・

SRE NEXT 2023のプロポーザル受付が始まったのもこの時期ですね。今年はMIXI Tech Conferenceにも出たことですし、カンファレンスに登壇したいな〜という気持ちがちょうど高まっていた時でしたので練りに練ってプロポーザルを送りました。LTとかだと何かしらのTipsを持って帰って頂こうという気軽な感じで登壇できますが、カンファレンスの場合はもっと体系的な何かを持って帰ってもらうことを考えなければいけないので難しさが段違いでした。

8月

こちらの勉強会に参加したりしました。

offers.connpass.com

syossan.hateblo.jp

個人的に職種というものはあまり意味がないと思っていて、ワンチームで何かしらの問題に立ち向かうべきではあると思うんですよね。ただ、便宜上それでは得手不得手によって特定の人に負担がかかったり、専属チームを作ることにより相乗効果を生むために職種というものを作るというのは納得しているような側面があります。
そういった自分の考えとしてもこの勉強会が言わんとしていることは非常に納得感があり、満足感のある勉強会でありました🙏

他にも

findy.connpass.com

kinto-technologies.connpass.com

などにも参加させていただきました!

あとはFour Keysの取得に向けてアレコレやりだしたのもこの時期ですね。7月にGitHubがメトリクス取得のGitHub Actionsを出したことで、かなりFour Keysが取りやすくなったのでは?と思い、長らくTODOリストに積んでいたのを進めることに。
タスク優先度が常にガチャガチャ変わっていく中で、まだダッシュボードに反映するところまではいけてないのですが来年こそはやり切りたいですね・・・!

そして待望のゆるSRE勉強会の第一回目が開催できました!

yuru-sre.connpass.com

ありがたいことに多くの人にご参加いただけました。今までにも技術コミュニティを運営していたことがあったのですが、その中でも第一回目からここまで盛り上がるのは初めてのことだったので、SREの皆様の注目度が改めて感じられました。

9月

社内でGitHub Copilot導入に向けての機運が高まり、どのようにやっていくべきか?の取りまとめをやったりしておりました。最終的に年の瀬である今週に導入することができたので、来年からはチームの生産性が3000倍くらいになることを期待しております。

またTerraformを使ったIaCをやっていかないとなぁというお気持ちを長らく持っていましたが、ようやく動き出すことができました。まだ全てのIaC化は完了してはいないのですが、来年こそは・・・!こういう時にせめてあと一人SREポジションの人がいればなぁと思ったりします。

あとはGraphQLの一部クエリの速度改善にも着手しておりました。graphql-rubyを使っているのですが、レスポンスサイズが大きいクエリについてはサッパリ性能がでません。以前から認識していた問題ではあったのですが、結果としてはgraphql-ruby自体に手を入れないと何ともならんなぁというあまり喜ばしくない結果となりました。graphql-rubyにPR送るモチベーションも特に無いので、こういう時に代替ライブラリがない言語はめちゃくちゃ困りますね。(Goだとどれだけレスポンスサイズが大きくなってもレイテンシ増大しないのに、なんでこのライブラリはこうなの?ってissue読んで何かがポッキリ折れた

末にはSRE NEXT 2023の登壇がありました。SRE NEXTについての想いは以下に書いたのですが、そんな場に立てることができて光栄でした。

syossan.hateblo.jp

syossan.hateblo.jp

今年の登壇で持てるネタは割りと放出した感があるので、来年またプロポーザル送るかどうかは悩ましいですが是非ともトライはしてみたいです。

10月

GKEのPodから外へのアクセスに対して出口IPを固定したいというリクエストが来たので、その対応にSquidを使ったプロキシサーバーを建てることで対応したりしていました。

syossan.hateblo.jp

あとびっくりした事態といえば、Bitriseのワークフローが一つ謎にスタックして数十時間動いているという事態がありました。カスタマーサポートに連絡することで請求自体は事なきを得ましたが、いやはや心臓がキュッとなりました。

syossan.hateblo.jp

10月といえばゆるSRE勉強会の第二回を開催しました。

yuru-sre.connpass.com

第二回も変わらず多くの方にご参加頂いて感謝しかないです。第一回から引き続いて参加された方も多く、満足度の高い勉強会になって良かったと運営視点では安堵したりしていました。

また、プライベートではオクトーバーフェストに行ったりしました。ドイツの地ビールは苦味がキリッと効いていて良いですね!また来年も行きたい🍺

11月

以前に少し進めていたGitHub Copilotの導入について、もっと急いで!という上からの圧力を感じられたので進めることに。他部署でいち早く進めている部署がありましたので、残していただいていた資料を元に円滑に導入を進めることができました🙏
GitHub Enterprise Cloudへの移行という珍しいタスクをこなすことができて貴重な経験となりました。

syossan.hateblo.jp

弊チームではE2EテストにMagicPodを利用しているのですが、Flutter対応されたということでそちらの対応を進めていました。とはいえ、やったことはBitriseのワークフローを少し弄ってMagicPod向けのアプリをビルドしてQAチームに配信するといったことですね。

あとはコストについて見直す必要があり、GCを中心としたコスト見直しをすることになりました。開発を優先してこういったところはかなりおざなりになっていたので、無駄なところを是正する良い機会でした。今では30%ほどコスト最適化することができ、色々と知見を貯めることができました。

syossan.hateblo.jp

12月

ついに今月まで来ましたが、GitHub Copilotの導入について引き続き進めていたり、GMailのメール送信者ガイドラインに則ってDMARC対応を進めたりしておりました。

www.nri-secure.co.jp

正直メール周りは門外漢だったので、キャッチアップにかなり苦労しました・・・メール周りムズカシイ・・・

CloudNative Days Tokyo 2023に参加させていただいたのも記憶に新しいです。ゆるSRE勉強会にも参加されていた方が何人か登壇されていたりしたので、すごいな〜と思いながら拝聴させていただきました。

event.cloudnativedays.jp

ゆるSRE勉強会の第三回目も今月開催しました!

yuru-sre.connpass.com

インフルエンザも流行っていたのでキャンセルされる方が多くいらっしゃいましたが、それでも多くの方に参加いただき盛り上がりました!毎回、会の終わりに次回の開催予定を公開していましたが、今回はそれをせずに次回の開催はお楽しみに・・・という感じにしております。裏で色々と企画はしておりますのでお楽しみに!

まとめ

今年も振り返ってみると色々やったような、まだ足りないような・・・一人でやっているということを言い訳にせず、もっと1日1日の密度を濃くしてガンガン進めていかないとなぁと周りの皆さんの活動を見ていたら気が引き締まる思いです。

何はともあれ、自分ももういい歳ではあるので体調には気を付けて来年も更に頑張っていきたいです🙏

本番DBとステージングDBのデータを同期させる

この記事はMIXI DEVELOPERS Advent Calendar 2023の18日目の記事です。

今回はやったことある人が多そうな本番DBのデータをステージングDBに同期させる仕組みを作ったお話になります。

前提のお話

何故作ったのか?ですが、データ数の問題から開発・ステージング環境では発生せず本番環境では発生するような表示上のバグが数は少ないですが存在し、そういった問題を早期に発見するために対応できませんか?と相談されたことがきっかけで同期の仕組みを作ることになりました。

また、ステージング環境ではQAチームによる自動テストが動いており、本番環境データが同期されていればより潜在的な問題の発見に役立ちそうですね。

ここから詳しい説明に入りますが、DBはCloud SQL for MySQL v5.7に準拠した内容となっております。

作る

さて、ここから作るフェーズに入っていくのですがもう少し作りたいモノのイメージを明確にしていきましょう。
「本番DBのデータをステージングDBに同期させる」、当たり前ですがこれは大前提ですね。他には「継続的に同期させる」、こちらも当たり前ですが本番環境のデータは常に変化していくものなので継続的にステージングDBに反映していく仕組みでないとダメですね。
あとは「ステージングDBに反映してはいけないデータは反映しない」、これはユーザーの個人情報などですね。Identity-Aware Proxyを有効化したり、Cloud Armorを利用したり悪意のあるアクセスはなるべく弾くようにはしていますが、開発・ステージング環境はどうしても標的にされやすいものです。リスクを減らすためにも、漏れるとまずいデータは同期させないようにします。また「ステージングDBから営業用デモデータを退避させる」という要件も存在しており、本番DBのデータをインポートする前にそのデータを退避させなければいけません。

最後に「できれば低コストで作りたい」、というお気持ちがありました。現在、SREとしてインフラのコスト面も管理しているのですが普段コスト削減などを承っている立場上、あまりコストが高くならないようにという思いからです。

では、これらを踏まえてどうやって作っていくか考えていきましょう。先程の要件をまとめるとこちらですね。

まず、「本番DBのデータをステージングDBに同期させる」ですが、これは本番DBをエクスポートしてステージングDBにインポートすれば達成できますね。次は「継続的に同期させる」Google Cloud(以下、GC)にはSchedulerなど定期的に実行する仕組みがあるのでここも問題はなさそうです。
「ステージングDBに反映してはいけないデータは反映しない」については、本番DBからステージングDBにインポートする間にデータを編集する必要性がありそうですね。Cloud SQLでエクスポートできる形式はSQL/CSVの2種類なのですが、どちらもエクスポートしたデータを編集するのは骨が折れそうなので、今回は本番DB → ステージングDBの間に本番レプリカDBを挟み込み、このDB上でデータ編集するのが良さそうです。

「ステージングDBから営業用デモデータを退避させる」は、ステージングDBの退避させる対象データを参照しながら本番レプリカDBに追加する、といった感じで出来そうです。こうやって処理した本番レプリカDBをまたエクスポートしてからステージングDBにインポートすれば目出度く同期の完了、という目論見になります。
最後に、「できれば低コストで作りたい」という要件は、必要な処理類をCloud Functionsに寄せれば安上がりにいけそうだったので今回はがっつり利用しています。あと、それぞれ作成したCloud Functionsを一つのワークフローとして管理するためにCloud Workflowsも利用します。こちらも料金体系を確認すると、GC内のリソースをステップとする場合には月5000回まで無料で実行できるとのことで、1日1回ほどの実行頻度だと十分無料分のみで回せそうでした。

再びまとめるとこうなりますね。

ではこれらを考慮に入れて作成開始していきますか。まず先に完成図がこちらになります。

この完成図を元に詳しく見ていきましょう。ちなみにですが作成に利用する言語はGoになりますのであしからず🙇‍♂

本番DBからエクスポート

エクスポートについては幸いなことにGoogleがサンプルコードを提供してくれていますので、有り難く一部使わせていただきましょう。

cloud.google.com

それでは完成したコードはこちらになります。

このコードを見ながら重要な箇所を解説していきます。

// アプリケーションデフォルトの資格情報を使用する http.Client を作成
hc, err := google.DefaultClient(ctx, sqladmin.CloudPlatformScope)
if err != nil {
  fmt.Println("Failed to create http.Client: ", err)
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

// Google Cloud SQL サービスを作成
service, err := sqladmin.New(hc)
if err != nil {
  fmt.Println("Failed to create SQL Admin Service: ", err)
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

ここではCloud SQLへの接続準備をしているところで、Application Default Credentials(ADC)を使用したClientを作成してからCloud SQL serviceを作成しています。ADCを利用するということでCloud Functionsで利用するサービスアカウントはCloud SQLに関する権限を付け忘れないよう注意です。

// エクスポート操作を実行
rb := &sqladmin.InstancesExportRequest{
  ExportContext: &sqladmin.ExportContext{
    Kind:      "sql#exportContext",
    FileType:  "SQL",
    Uri:       uriPath,
    Offload:   true, // サーバーレス エクスポートを有効にする
    Databases: []string{database},
  },
}

op, err := service.Instances.Export(project, instance, rb).Context(ctx).Do()
if err != nil {
  fmt.Println("Failed to export: ", err)
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

エクスポートを実行しているところですね。先程作成したCloud SQL serviceを利用してエクスポートするのですが、その前に InstancesExportRequest でどのようにエクスポートするのか?を設定しています。本番環境からエクスポートする、ということでサーバーレスエクスポートを有効化しております。これはエクスポートのために一時的にインスタンスを生成し、そこを通してエクスポートすることでパフォーマンスの確保やインスタンスへのオペレーションを阻害しないなどのメリットがあるため有効にしてあります。

cloud.google.com

// エクスポートオペレーションの状態を10s毎に取得
for {
  op, err = service.Operations.Get(project, op.Name).Do()
  if err != nil {
    fmt.Println("Operations.Get: ", err)
    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
    return
  }
  if op.Status != "PENDING" && op.Status != "RUNNING" {
    break
  }
  time.Sleep(10 * time.Second)
}

if op.Status != "DONE" || op.Error != nil {
  fmt.Println("Failed to export operation: ", op.Error)
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

最後にエクスポートの進捗を都度取得しておきます。 service.Operations.GetPENDING , RUNNING , DONE , あとこれは取得する必要性あるかな?と思うのですが SQL_OPERATION_STATUS_UNSPECIFIED というオペレーションの状態が不明の場合のレスポンスがそれぞれ取得できます。

これを実行することで本番DBからエクスポートされたSQLファイルがCloud Storageに格納されるわけですね。

本番レプリカDBにインポート

そんなエクスポートしたデータを本番レプリカDBにインポートしていきます。と、いきたいのですがその前に本番レプリカDBについてもう少し説明したいと思います。
個人情報を扱う上でのセキュリティリスクは説明しましたが、対策としてデータをマスキングしましょうというお話をさせていただきました。しかしながら、本番レプリカDBでエクスポートしてからマスキングするまでの間は低いながらもリスクが存在するため、リスクを軽減するための対策を別途行いましょう。

Cloud SQLインスタンスは通常パブリックIPアドレスが割り当てられますが、これを割り当てずにプライベートIPアドレスのみ割り当てるようにすることでセキュリティリスクを低減させることができますので、こちらをやっていきましょう。

こちらについては説明すると長くなりますので割愛いたしますが、以下のような記事など巷に解説してらっしゃる方がいますので参考にしてみてください。

qiita.com

では、そちらを踏まえながらコードの解説に移ります。

it := client.Bucket(bucketName).Objects(ctx, &storage.Query{
  Prefix: targetDir,
})

fmt.Printf("Files matching date: %s\n", date)

var objectName string
for {
  attrs, err := it.Next()
  if err == iterator.Done {
    break
  }
  if err != nil {
    fmt.Println("Failed to list objects: ", err)
    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
    break
  }

  // 本日日付を含むファイルがあればインポート対象とする
  if strings.Contains(attrs.Name, date) {
    objectName = attrs.Name
  }
}

if objectName == "" {
  fmt.Println("export sql file not found")
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

ここではCloud Storageへのアクセスを行っており、先程のエクスポートファイルを取得していますね。判定はファイル名に記述してある日付をもとにしています。
で、取得してきたファイルをインポートする前にファイル内で USE DATABASE が記述しているところを、本番DBのものからステージングDBのものへと変えておきましょう。

replacedContents := strings.ReplaceAll(string(b), "production", "staging")

writer := client.Bucket(bucketName).Object(objectName).NewWriter(ctx)
if _, err = writer.Write([]byte(replacedContents)); err != nil {
  fmt.Println("Failed write sql file")
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}
if err := writer.Close(); err != nil {
  fmt.Println("Failed close sql file")
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

あとはエクスポートの時と同じ要領でインポートオペレーションを実行しましょう。インポートするファイルの指定はCloud StorageのURIを指定すればOKです。

// Cloud SQLへのインポートリクエスト
importRequest := &sqladmin.InstancesImportRequest{
  ImportContext: &sqladmin.ImportContext{
    FileType: "SQL",
    Uri:      fmt.Sprintf("gs://%s/%s", bucketName, objectName),
    Database: dbName,
  },
}

// SQLファイルをインポート
op, err := sqlAdminService.Instances.Import(project, instanceName, importRequest).Do()
if err != nil {
  fmt.Println("Instances.Import: ", err)
  http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  return
}

データマスキング

やっとこさマスキングまで来ましたが、今までと違い実際のコードからかなり省略したものを使って説明させていただきます。

実際のコードではこれ以外にも細かい様々なマスク処理をしていますが、今回は簡単にメールアドレスと電話番号を * に置き換えるコードにしてあります。

var opts []cloudsqlconn.DialOption
opts = append(opts, cloudsqlconn.WithPrivateIP())
mysql.RegisterDialContext("cloudsqlconn",
  func(ctx context.Context, addr string) (net.Conn, error) {
    return d.Dial(ctx, fmt.Sprintf("%s:%s:%s", project, region, instance), opts...)
  }
)

uri := fmt.Sprintf("%s:@cloudsqlconn(localhost:3306)/%s?parseTime=true", user, database)

ここではCloud SQLのGoコネクタを使って接続しています。以下のGoogleのサンプルコードを参考に致しました。

cloud.google.com

// マスク処理を行うSQL文を生成し、実行
for table, columns := range maskTarget {
  for _, column := range columns {
    query := fmt.Sprintf("UPDATE %s SET %s = REPEAT('*', CHAR_LENGTH(%s))", table, column, column)
    _, err := db.Exec(query)
    if err != nil {
      fmt.Println("Failed to mask the database: ", err)
      http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
      return
    }
  }
}

肝心のマスク処理ですが、 UPDATE %s SET %s = REPEAT('*', CHAR_LENGTH(%s))", table, column, column を実行し、既存のデータを * に置き換えています。至極簡単ですね。ただし、重複が許容されていないカラムなどは既存データをシードとしたランダム文字列を生成するなど、もう少し考慮する必要があります。

ステージングDBの一部データ退避

ここもかなりプロダクトのドメインと紐付いていて実際のコードを使って説明することが難しいのですが、やっていることとしては

  • 本番レプリカDBとステージングDBに接続する
  • 退避させるデータをステージングDBから取得する
  • 取得したデータを本番レプリカDBに追加する

の3つになります。
テーブルによってはFOREIGN KEY制約があると思いますが、そういった場合には本番レプリカDBに追加したデータのIDを保持しておいて、ステージングDBから取得したデータに上書いてから本番レプリカDBに追加するなどの処理が必要になります。

本番レプリカDBからエクスポート/

そろそろ終わりが見えてきました。データマスキングや退避データを入れた本番レプリカDBをエクスポートして、ステージングDBにインポートします。こちらも今までやったこととほぼほぼ同じなのでコードは省略いたしますが、違う点としては

  • エクスポート後に本番レプリカDBをまっさらにする
  • インポート前にバックアップを取っておく

の2つですね。前者は説明は要らないと思いますが、後者はコードからやるのもいいですがステージング環境のインスタンス設定で定期バックアップさせておくのが楽です。もし厳密に直前のバックアップを取るといった場合には sqladminBackupRun などがありますのでこれを使って出来そうですね。(未検証

pkg.go.dev

Cloud WorkflowsでCloud Functionsを実行する

最後にCloud WorkflowsでCloud Functionsを定期実行するように設定しましょう。Cloud FunctionsやCloud Runにリクエストを飛ばす場合にはOIDCを利用することになります。

cloud.google.com

一応、エラーがあった際などどこまで処理が成功していたのか?などログから分かりやすいように、 call: sys.log を使ってログを出力しておくのをオススメします。そういったことを含めて、Workflowsのコードとしては一部を載せますが以下のような感じになります。

main:
    params: [input]
    steps:
        - sync-production-to-staging:
            try:
                steps:

                # 本番DBからSQLファイルをCloud Storageへエクスポート
                - exportProductionDBLog:
                    call: sys.log
                    args:
                        data: "=== exportProductionDB START ==="
                        severity: "INFO"
                - exportProductionDB:
                    call: http.get
                    args:
                        url: 'https://hoge.cloudfunctions.net/export-production-db'
                        auth:
                            type: OIDC
                            audience: 'https://hoge.cloudfunctions.net/export-production-db'
                    next: importProductionReplicaDBLog

                # エクスポートされた本番DBのSQLファイルを本番レプリカDBへインポート
                - importProductionReplicaDBLog:
                    call: sys.log
                    args:
                        data: "=== importProductionReplicaDB START ==="
                        severity: "INFO"
                    next: importProductionReplicaDB
                - importProductionReplicaDB:
                    call: http.get
                    args:
                        url: 'https://hoge.cloudfunctions.net/import-production-replica-db'
                        auth:
                            type: OIDC
                            audience: 'https://hoge.cloudfunctions.net/import-production-replica-db'

            except:
                as: e
                steps:
                    - known_errors:
                        switch:
                        - condition: ${not("HttpError" in e.tags)}
                          raise: "Connection problem"
                        - condition: ${e.code == 404}
                          raise: "URL wasn't found"
                        - condition: ${e.code == 403}
                          raise: "Authentication error"
                        - condition: ${e.code == 500}
                          raise: "Internal Server Error"
                    - unhandled_exception:
                        raise: ${e}

try-except でどのようなエラーで落ちたか?も出力するようにしておくと失敗時の調査に良いですね。あとは今まで作成したCloud Functionsも記載していけば完了です。

おわりに

本番DBとステージングDBの同期はよくある実装ですが、一つの例として今回取り上げさせていただきました。読んでくださった方の役に立つ情報があれば幸いです🙇‍♂

次は、@fanglangによる記事になります!

GKEの開発環境クラスタはとりあえず「使用率の最適化」やっとこう

GKE / k8s、まだまだ分からんことが多くて学び甲斐があるなぁとしみじみ思っている今日この頃。
今回はそんな中で掲題のコスト最適化のお話をします。

要約

  • 本番環境が載っていないクラスタは自動スケーリング プロファイルをoptimize-utilization(使用率の最適化)やっとくと良い
  • 特に短いスパンでのCronJobを動かしているとコスト改善の可能性大

前提

GKEを使ってサービス運用している時に気になるのはやはりコストですよね。私が関わっているサービスでもコストをもう少し下げようといった声が大きくなり、その中でも大きな割合が割かれているGKEに注目が集まりました。

こういう時、まず手を付けるのは開発環境周りですね。Develop, Staging環境などを一つのクラスタで管理しており、ここなら最悪ぶっ壊しても問題なく着手しやすいのでここからやっていきます。

やったこと

やったこととしてはめちゃくちゃ単純なのですが、

  • GKEクラスタの自動スケーリング プロファイルをbalancedからoptimize-utilization(使用率の最適化)に変更する
  • PDBを設定する

になります。

自動スケーリング プロファイルの変更

GKEクラスタの自動スケーリング プロファイルとは、クラスタ内ノードの削除に対する挙動に関する設定です。balancedとoptimize-utilizationの2種類があるのですが、ドキュメントを読むとoptimize-utilizationは

クラスタ内で余剰リソースを保持するよりも使用率の最適化を優先させます。
このプロファイルを有効にすると、クラスタ オートスケーラーはクラスタをより積極的にスケールダウンします。

とのことです。正直この説明だけでは具体的なところが分からず、しっくり来なかったので調べてみるとGoogleの方の登壇資料に以下のようなことが書かれていました。

ref: Google Kubernetes Engine (GKE) で実現する運用レスな世界

つまり、balancedは「10分毎にスケジュール、使用率50%以下のノードが削除対象」で、optimize-utilizationは「1分毎にスケジュール、使用率85%以下のノードが削除対象」となるようです。optimize-utilizationではより1ノードの凝集度が高まる、といったことがよくわかりますね。

スパイク発生時などに余剰リソースが求められる本番環境では使えませんが、開発環境では使えそうですね。

PDBを設定する

これはノードを削除するために必要な設定となります。設定しましょうで終わる話なのですが、一つハマったところとしてCronJobリソースでの扱いです。
CronJobでは成功/失敗のJobを残すことができ、 successfulJobsHistoryLimitfailedJobsHistoryLimit という設定値があります。ここに幾つのJobを残すか設定できるのですが、開発環境では成功履歴に関しては残す必要がなかったので successfulJobsHistoryLimit を0に設定しておりました。

こうした場合に、成功時のノードの様子を見てみると fluentbitgke-metrics-agent などのkube-systemが残っている状態でした。てっきり、これらがoptimize-utilizationを設定した時にノード削除してくれるものかと思ったらノードが残ったままに・・・
なんでじゃろ?と調べてみると、このkube-systemのnamespaceに配備されているものに関してもPDBを設定する必要がありました。

こちらのCloud Skills Boostにもkube-systemのPodに対してもPDBを設定しましょうと書かれており、完全に盲点でした。

www.cloudskillsboost.google

デフォルトでは、これらの Deployment のシステム Pod のほとんどは、クラスタ オートスケーラーによって完全にオフラインにされ再スケジュールされることを回避します。
このラボでは、kube-system Pod に Pod 停止予算を適用し、クラスタ オートスケーラーが Pod を別のノードに安全にリスケジュールできるようにします。こうすることで、クラスタをスケールダウンする余裕を確保できます。

ラボ内で解説されている中では以下のようなPDBを設定しています。

kubectl create poddisruptionbudget kube-dns-pdb --namespace=kube-system --selector k8s-app=kube-dns --max-unavailable 1
kubectl create poddisruptionbudget prometheus-pdb --namespace=kube-system --selector k8s-app=prometheus-to-sd --max-unavailable 1
kubectl create poddisruptionbudget kube-proxy-pdb --namespace=kube-system --selector component=kube-proxy --max-unavailable 1
kubectl create poddisruptionbudget metrics-agent-pdb --namespace=kube-system --selector k8s-app=gke-metrics-agent --max-unavailable 1
kubectl create poddisruptionbudget metrics-server-pdb --namespace=kube-system --selector k8s-app=metrics-server --max-unavailable 1
kubectl create poddisruptionbudget fluentd-pdb --namespace=kube-system --selector k8s-app=fluentd-gke --max-unavailable 1
kubectl create poddisruptionbudget backend-pdb --namespace=kube-system --selector k8s-app=glbc --max-unavailable 1
kubectl create poddisruptionbudget kube-dns-autoscaler-pdb --namespace=kube-system --selector k8s-app=kube-dns-autoscaler --max-unavailable 1
kubectl create poddisruptionbudget stackdriver-pdb --namespace=kube-system --selector app=stackdriver-metadata-agent --max-unavailable 1
kubectl create poddisruptionbudget event-pdb --namespace=kube-system --selector k8s-app=event-exporter --max-unavailable 1

これでCronJobが成功後に残ったノードも削除されるようになりました。

まとめ

単純なことを2つやっただけですが、結果コストは以下のように最適化されました。

具体的な数値は出せませんが、グラフだけを見ると約半分くらいに最適化されたことがわかりますね。

開発環境クラスタを持っている場合は、この設定をやっておくのが良いと思います✌

GitHubのorganization移行をやったお話

この記事はSRE Advent Calendar 2023の1日目の記事です。

今回は業務で「GitHubリポジトリをあるorganization(以下、org)から別のorgへ移行させる」ということをやったので、その時のお話をさせていただきます。

何をするのか?

もう少し、何を目標としていたのか?を具体的に書くと、現在リポジトリ群が所属しているorgからGitHub Enterprise Cloud(以下、GHEC)配下に作成したorgへ移行させる、ということをやろうとしたお話になります。

GHECに移行するモチベーションとしてはGitHub Copilot for Businessが利用できたり、SAML SSOを利用できたりするというメリットを受けることが出来るからですね。(開発者にとってはCopilotが理由として大きいかも)

移行前の状況

移行前は以下のような状況でした。

  • 自分が関わっていて移行対象となるGitHubリポジトリが10個
  • 共通のPAT(Personal Access Token)を払い出すbotアカウントあり
  • 外部の業務委託でお手伝いいただいている方のアカウントあり
  • orgに紐付いたGitHub Projectsが2つ、特定のリポジトリに紐付いたProjectsが1つ
  • CI/CDはGitHub Actions、CircleCI、ArgoCDなどを利用
  • その他、GitHubリポジトリと紐付いている自作のツール類やGoogle CloudのCloud Functionsなどが存在している
  • 移行先のorgはOktaを利用したSSOを設定

色々と移行に際して引っ掛かりそうなポイントが幾つかありそうですね。何となく察しがつく方もいらっしゃるかもしれませんが、後々躓きポイントも説明していきます。

作業開始、その前に

早速移行させるぞ〜〜〜・・・とはいかず、まずは影響を調べたりなどの事前調査からですね。移行先orgを作成したり、作ったorgをGHEC配下にしたりは今回は省略させていただきます。

事前調査もすべてを隈なく調べ切れるならいいのですが、それでもどこかヌケモレが発生することが考えられます。また、移行に際して締切日が設定されていたため、どこが破綻すると一番まずいか?を開発チームと話し、「サービスのデプロイ」「GitHub Projectsが使える」の2つが破綻するとまずいとのことで、そこは徹底して調査するようにしました。

調査

GHEC配下の移行先orgにリポジトリを移行すると一体何が起こるのでしょうか?ザザッと箇条書きにしてまとめてみます。

  • リポジトリURLが変わる
  • issue, PR, wikiなどの情報は移行先のリポジトリに受け継がれる
  • 移行先orgに改めてGitHub Appをインストールし直す必要あり
  • OktaでのSAML SSOが設定されているため、移行先orgに招待するユーザーはOktaアカウントを持っていることが必要
  • orgと紐づいているGitHub Projectを移行させる
  • Teamsもorgに紐づいているので新しく作成する

事前情報としてササッと調べた結果、このような感じで思った印象は「割と色々やらなきゃならんな」でした。とはいえ、設けられた締切日には十分間に合うんじゃないかな〜という感じでありました。

さて、次に移行対象となるGitHubリポジトリの洗い出し、デプロイのために使っていたCircleCIについての調査をやっていきます。リポジトリの洗い出しは特に問題はなかったのですが、CircleCIでは新しくorgを作成する必要があり、こちら新orgを作ってさえしまえば特に問題起きないだろうなと思いきや、作業後に躓きポイントがあったのでこちらも後ほど説明します。

CIといえばGitHub Actionsで、共通して利用するようなActionsは呼び出し元とは別リポジトリで管理していて、例えば以下のような感じでした。

ここの、 uses で指定している old-orgnew-org と移行先org名にしないといけません。

あとはスマートフォンアプリも開発していて、その中で利用しているBitriseも調査しました。こちらはBitrise側で管理しているリポジトリURLを変更する必要があり、詳細は以下のリンク先に書いております。

devcenter.bitrise.io

以前記事にした「開発環境の使用状況分かるくん」など、自作ツールでデプロイに紐づくところも移行することで影響が出ないか調査しました。いくつかのツールでbotアカウントが生成したPATを使っていましたので、botアカウントを移行先のorgに追加するのを忘れないようにすれば良さそうです。

syossan.hateblo.jp

ここまでがCI/CDなどデプロイに関する調査でした。次にGitHub Projectsについて調査しましょう。

リポジトリに紐づいているGitHub Projectsは特に何もせずともリポジトリ移行すればOKなのですが、orgに紐づいているGitHub Projectsはそうはいきません。
調査した結果、Project自体はorgを跨いでコピーすることが出来てビューやフィールド、ドラフトissueなどもコピーされるようです。

docs.github.com

ただし、リポジトリに作成されてProjectに紐づけているissueはコピーでは反映されず、カスタムフィールドを利用している場合はそちらもコピーされないので(弊チームではスクラムのポイントをissueに割り振るのに使用)、ここはスクリプトなりでGitHub APIを叩いて何とかする必要がありそうです。

一旦はここまでが事前に定義した必須条件であるデプロイとGitHub Projectsの調査になります。

この他にも

  • Cloud FunctionsでリポジトリURLを指定しているところの修正
  • Cloud Buildのトリガーである接続リポジトリの修正
  • ドキュメント類の修正
  • 移行後のSlack通知の再有効化
  • 外部の業務委託の方のOktaゲストアカウントの払い出し
  • botアカウントをoutside collaboratorとして追加
  • SSHキー、PATのSSO設定をする
  • 作業後のチームメンバーへの共有事項をまとめる

などの対応が必要という調査結果となりました。

一つ注意が必要なのは、今回諸々の事情からbotアカウントをoutside collaboratorで追加することになりましたが、本来であればGitHub Appを作成しそちらを使うべきであります。また、 Personal access tokens (classic) を使っている場合は特に気にすることはないのですが、 Fine-grained personal access tokens を使っている場合はoutside collaboratorではリポジトリなどに権限を持ったトークンを生成できませんので、そこはご注意ください。

github.com

作業開始

ここまでで必要な分は調査できましたので、事故が無いように誰も触らない休日に移行作業を開始しました。

リポジトリ移行

なにはともあれ、まずはリポジトリ移行を行いましょう。

docs.github.com

GitHubのDanger Zone触るのが初めてだったので、多少ビビりましたがこちらは特に問題なく完了。

botアカウントをoutside collaboratorとして追加

次に、botアカウントをoutside collaboratorとして追加しましょう。こちらは移行したリポジトリCollaborators and teams 設定からbotアカウントを追加すればOKです。

docs.github.com

移行後のSlack通知の再有効化

このあたりで忘れないようSlack通知を再有効化しておきました。Slack上で実行するコマンドとしてはこんな感じ。

/github subscribe new-org/hoge issues, pulls, commits, releases, deployments, reviews, comments

SSHキー、PATのSSO設定をする

これは個人アカウント、botアカウント、メンバー各々のアカウントのすべてで必要な作業なのですが、移行先orgがSSOを利用しているためGitHubに登録しているSSHキー、生成しているPATで Configure SSO というSSO設定が必要になります。

docs.github.com

Teamsを再作成

移行元orgで作成していたTeamsを移行先orgでも再び作成し、移行したリポジトリに追加したりしておきましょう。幸いにも弊チームでは扱うTeamsの数が数個だったので手作業で移行しましたが、Teamsの量が多い方は gh コマンドなどを駆使して自動化しましょう。

リポジトリURLに依存している箇所を修正

をガガッと移行先org名に修正しておきました。機械的にパパっと置換していきましょう。

RenovateのGitHub Appを再連携

ここで事前調査と違ったタスクが出てきました👀

これはGitHub Appを改めてインストールしていた際にRenovateだけ再連携の必要があり、やっておきました。連携後にコンソール画面からその後の動作をどうするか?を聞かれると思いますが、特に何もしない的な選択肢を選んでおくと良さそうです。

GitHub Projectsの移行

さて、やってきましたGitHub Projectsの移行の時間です。

こちらは事前調査から

  • Projectを移行先orgへコピー
  • 紐付けていたissueを再紐づけ

といった作業が必要なことは分かっていました。で、スクリプトが必要というお話でしたね。
幸いなことに、社内の他チームの方が組んでらっしゃったスクリプトを参考に、少し手直しをして作成しました。それがこちら。

やっていることは gh コマンドを使って、移行元orgのProjectから必要な情報であるissueやfield, custom fieldを移行先orgのProjectに移行しているといった感じですね。

スクリプトも用意できましたので、作業再開しましょう。まずはスクリプトのコメントにも書いてある通り、 gh コマンドで現在のProjectの状態を backup.json として保存しておきます。
その後、移行したいGitHub Projectsのメニューから Make a copy を選んで、移行先orgへコピーしましょう。

ドラフトissueもコピーしておきたいので、 Draft issues will be copied if selected は忘れず選択しておきます。

移行先のProjectが問題なくコピーされていることを確認したら、スクリプトを実行してissueの紐付けなどやっておきましょう。

で、調査段階の時に漏れていたことがProjectのIDを指定して実行する自作ツールが幾つかあり、これらのID修正もやっておきました。もし、Project IDを使ってゴニョゴニョしている方はお気をつけ下さい。

確認

最後にデプロイフローがきちんと動くかどうかの確認をして終了になります。

起こったこと

さて、ここまでやったことをザッと書いていきましたが、もちろん作業中・後に色々と問題も起こりました。ここでは起こった問題を書き残していきます。

CircleCIのorganizationに紐付いたContext variablesを移行し忘れていた

デプロイフローの確認時に発覚したのですが、デプロイ中にSlack通知を飛ばすところがあり、そこが動いておりませんでした。アレー?と思い調べてみると、CircleCIのorganizationに作成していたContext variablesに登録していたAPI Tokenを移行しなくてはいけないのをすっかり忘れておりました。うっかり。

CircleCIのProject, Pipelineなどが見れない

移行後、チームメンバーに触ってもらっている時に発覚。これについては原因自体は謎なのですが、CircleCIのProjectやPipelineが一切見れないという状況になっておりました。
云々かんぬん試してみたのですが、結論としてCircleCIの再ログインを行うと何故か再び見れるように。何故に🤔

Bitriseのworkflowが走らない

コードをコミットしたらBitriseのworkflowが走り、コードの静的解析をするのですがそこが全く動かず・・・
Bitriseのappの設定を確認すると、何故か GitHub Checks の項目がdisabledになっていたのでenabledにすることで解決。

リポジトリに紐付けたTeamの権限間違い

これは凡ミスなのですが、リポジトリに紐付けたTeamの権限が軒並みReadになっていてチームメンバーからご指摘が。申し訳ない・・・
ただ、ありがちなミスではあるのでご注意を。

まとめ

GitHub organizationの移行という中々体験出来ないことが体験でき、経験になりました。こういう作業は難度の割にトチると業務が滞るので結構神経使いますね😇
この記事が、今後organizationの移行する方の参考になれば幸いです。

次は @shu-kobさんによる「ブロックチェーンノード運用の苦労話」です!

shu-kob.hateblo.jp