Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

Photoshopスクリプト(ExtendScript)を書いてみた

こんにちは! アカツキゲームス クライアントエンジニアのSuです。
この記事は Akatsuki Advent Calendar 2024 11日目の記事です。
昨日の boke0 さんの踊り文字についての記事は面白かったです。一歩一歩 → 一歩々々という書き方は初めて知りました。とても勉強になりました!25日の解決編も楽しみですね!

はじめに

自分は学生時代からエディターの拡張に興味があり、Blenderエディターの関連研究も少し関わりました。現在もチーム内のクリエイターさん達が効率的に作業できるように改善活動を日々頑張っています。

今回はデザイナーさんからちょっと複雑な自動化要望があったので、Photoshopスクリプトを書いてみました。本記事はPhotoshopスクリプトを書くための環境構築、デバッガー、実際に書くの流れを紹介したいと思います。

Photoshop の自動化

Photoshopの中にすでに自動化機能があります。簡単かつ重複的な操作ならアクション、バッチ、変数・データセット機能でカバーできると思いますが、もし操作が条件によって分岐したい場合、スクリプトを書くには一つの手と思います。

ここの「スクリプト」というのは、Adobe社が開発したスクリプト言語および関連ツールキット ExtendScript です。ベースは  ECMAScript 3 なので JavaScript や ActionScript に似ている。Photoshop、InDesign、After Effects などのアプリケーション内のツールにアクセスしてプロジェクトをバッチ処理することができます。

ExtendScript IntelliSense

テキストエディターさえあれば書けますが、快適に書ける方法を紹介します。
公式拡張機能があるので、エディターは Visual Studio Code がおすすめです。

自動補完が大事なのでまずは IntelliSenese ですね!こちらの動画を参考しました。

www.youtube.com

下記の ExtendScript の TypeScript をプロジェクトフォルダに入れて、jsconfig.json ファイルを設定し、IntelliSenese を実現する方法です。

github.com

ExtendScript デバッガー

現在、Apple Sillicon ネイティブでデバッガーを実行できないため、Intel 版の Visual Studio Code をインストールして Rosetta で起動する必要があります。

Mac → zip「Intel chip」の方をダウンロードし、展開した実行ファイルを Visual Studio Code(Intel) にリネームして実行します。

code.visualstudio.com

次は「ExtendScript Debegger」という拡張機能をインストールします。現時点最新の v2.0.3 は自分の環境で問題なく動きました。

VSCode のデバッグボタンをクリックします。launch.jsonを作成します。

ExtendScriptを選択します。

 launch.json が作成されて、左のパネルに「Launch Script in ExtendScript Engine」の選択肢があればそれに切り替えして、これで設定完了です!

では実際にコードにブレイクポイントをセットします。

ここではウィンドウを出すコードの2行目の2の左をクリックすると、ブレイクポイントをつけます。

さっきのデバッグパネル「Launch Script in ExtendScript Engine」左の実行ボタンを押して、使用するPhotoshopを選んでコードが実行されます。

ちゃんとブレイクポイントに止まりました!

ウォッチ式も使えますね。便利です!

実行し続けるなら下記のようなダイアログが出てきます。

楽しく ExtendScript を書ける環境セットアップは以上です!

Github repo

今回の記事で使ったコードをGithubにまとめました。よかったらどうぞ!

github.com

最後に

この記事読んだ君の役に立てればいいですね〜

明日は周さんのAI Chatと関連する内容です。楽しみですね!

アカツキでは一緒に働くエンジニアを募集しています。
カジュアル面談もやっていますので、気軽にご応募ください。

games.aktsk.jp

新卒・中途メンバーとアクティブブックダイアログを通じてカルチャーを紡ぐ

この記事はAkatsuki Games Advent Calendar 2024の8日目の記事です。


はじめに

新卒4年目を迎えようとしている、クライアントエンジニア田﨑です。今年の4月から、新卒社員と中途入社の社員(計3名)を対象にアクティブブックダイアログ(ABD)を実施してきました。12月の今、3冊の本を読み終え、改めてこの活動が組織にもたらした効果を振り返っています。


始めたきっかけ

私たちの組織は、現在大きな変化の中にあります。そんな中でも、自分が心から大切だと思う会社の文化をしっかりと残したい。アカツキ / アカツキゲームスでは、有志のメンバーが主体的にABDを運営しており、その取り組みに刺激を受けました。このABDは、単に知識を共有するだけでなく、新卒や中途入社の方々に「こんな素晴らしい文化があるんだ!」と感じてもらい、共に実践するきっかけを作る場にしたいという思いで始めました。


ABDとは?

ABD(アクティブブックダイアログ)とは、本を分担して読み、それぞれが要約を準備して共有する読書会の手法です。以下がその基本的な流れです:

  1. 分担読書
    本を章ごとに分担し、各自がその部分を読み込みます。

  2. 要約作成
    担当部分を簡潔に要約し、他のメンバーにもわかりやすく説明できるよう準備します。

  3. 発表と対話
    要約を順に発表し、その後グループ全体で対話を行います。対話の中で他者の視点を知り、自分の理解を深めることができます。

ABDの大きな特徴は、読書を個人の活動に留めず、チームでの対話を通じて学びを共有し、広げられる点です。単なる知識の共有にとどまらず、チーム全体の価値観や文化形成にも役立つ方法です。


これまで読んだ3冊と得た学び

1. Team Geek(4月〜6月)

www.oreilly.co.jp

  • 内容のポイント
    優れたチームを作るためのコラボレーション、信頼、リーダーシップの重要性を解説した一冊です。

  • 経緯と学び
    新卒・中途の新規JOINメンバーがいち早く会社のカルチャーを理解できるようにと、私から提案しました。特に、Team Geekの中で言及される「HRT(謙虚・尊敬・信頼)」は、私たちアカツキ / アカツキゲームスのコアバリューでもあり、会社の公式サイトにも掲載しています。 参加者からは、「共感できる部分が多く、読んでよかった」という声が多く寄せられました。また、私自身もリーダーシップを発揮するための重要な原則を改めて学び直す機会になりました。

aktsk.jp


2. エンタメビジネス全史(7月〜9月)

bookplus.nikkei.com

  • 内容のポイント
    エンターテインメント業界の進化を俯瞰し、革新を生み出す要因を探る一冊。

  • 経緯と学び
    「エンタメ業界で働くなら、業界全体を俯瞰的に理解しておくべき」という考えから、メンバー全員で本書を選定しました。ゲーム業界の歴史や市場動向を学べただけでなく、他のエンタメ業界からも多くの学びを得ることができました。また、業界を超えて共通する成功要因に気づくことで、視野が広がりました。

    読書後、メンバー一同が「世界に誇る素晴らしいエンタメを届けたい」という強いモチベーションを共有できたことが、この本を通じた大きな成果となりました。


3. ルールズオブプログラミング(10月〜12月)

www.oreilly.co.jp

  • 内容のポイント
    プログラミングを効率的に行うための実践的なルールや哲学を紹介した一冊。

  • 経緯と学び
    技術的なスキルを学びたいという新卒メンバーの期待から選定しました。

現在読み進めているところです。技術的なスキルを学びたいという新卒メンバーの熱意に私自身が刺激を受けると共に、中途メンバーや私から現場で起きた「あるある」を伝える機会になっています。


ABDを通じて感じた変化

  1. 文化への共感が深まった
    各本のテーマを通じて、会社の大切な価値観や文化を共有することで、新卒・中途の垣根を越えた共感が生まれました。それぞれのバックグラウンドが異なるからこそ、多様な視点が交わり、共通のカルチャーを再確認する場となりました。

  2. 対話がもたらす新しい気づき
    異なる経験や視点を持つメンバー同士のディスカッションを通じて、「自分一人では気づけなかったこと」に多く出会うことができました。特に、バラバラなチームで構成されているからこそ、各チームの知見を共有する貴重な場にもなっています。また、新卒メンバーのキャリア相談や、個人的な悩みの共有といった、より人間的なつながりを育む機会も生まれました。

  3. カルチャーの実践が始まった
    読書会で培った価値観をもとに、メンバー自身が文化的な活動を率先して行う主体へと成長しています。最近では、社内カンファレンス「Akatsuki Dev Meet Up」の運営を主体的に担うなど、新たなカルチャーづくりに貢献する動きが広がっています。


今後の展望

ABDを通じて、「読書を超えた文化の共有と実践」ができることを実感しました。来年も新しい本をテーマに活動を継続し、さらに深い対話と実践の場を作っていきたいと考えています。


最後に

今年のABDで感じたのは、文化は「伝えるだけでなく、一緒に作り上げるもの」だということです。組織の変化が激しい中でも、共通の価値観を持つ仲間が増えることで、会社としての軸がより強固になっていくはずです。この取り組みが、組織全体の未来を支える一歩となればと思います。

リスクベースドテストの使い所が少しだけ分かった話

この記事は Akatsuki Games Advent Calendar 2024 6日目の記事です。

昨日は NaoyaKohda さんの 自宅の消費電力を可視化してみた でした。 モジュールを使えば個人でもスマートメーターから値を引っ張ってこれるんですね。

naoyakohda.hatenablog.com

はじめに

アカツキゲームスQAエンジニアの山﨑 @tomo_tk11 です。ゲーム開発のプロジェクトで QA 効率化をミッションに掲げ活動しています。最近リスクベースドテストを実践してみたんですが、このテスト戦略の使い所やメリットが少しだけ分かった気がしたので書き留めておこうと思います。

目次

リスクベースドテストとは

JSTQB AL のテストマネージャシラバスには次のように書かれています。

リスクベースドテストでは、プロダクト品質リスクを使用してテスト条件を選択し、それらの条件に合わせてテスト工数を割り当て、作成されたテストケースを優先度付けする。リスクベースドテストには、収集するドキュメントにおける重要性の種類と度合いによるバリエーション、および適用する公式度合いによってさまざまな技法が存在する。また、リスクベースドテストには、明示的または暗黙的に、テストにより品質リスクのレベル全体を引き下げる、特にリスクレベルを受け入れ可能なレベルにまで引き下げるという目的がある。

ISTQBテスト技術者資格制度 Advanced Level シラバス 日本語版 テストマネージャ Version2012.J04 | p.24

リスクベースドテスト、JSTQB シラバスを読んだときは言葉として理解はできるものの、なんだかとても重厚な戦略だなと感じていました。リスクを扱うテストなので仕方がない面もあるのかなと思います。とは言っても重たい…。使い方が想像できない…。

また、リスクベースドテスト戦略は4つのプロセスで成り立っています。

リスクベースドテストは、次の 4 つの主な活動で構成される。
• リスク識別
• リスクアセスメント
• リスク軽減
• リスクマネジメント

ISTQBテスト技術者資格制度 Advanced Level シラバス 日本語版 テストマネージャ Version2012.J04 | p.24

特に、リスクベースドテストでどのようなテストを実施すべきかは、リスクマネジメントを除く3つのプロセスで検討されると思います。し、識別…?アセスメント…?難しそうです。

そして、リスクベースドテスト戦略の中で取られるテスト手法には公式で重い技法として、ハザード分析、エクスポージャーコスト、故障モード影響解析、フォールトツリー解析*1が例に挙げられています。シラバスのリスクベースドテストのページに書かれていることを実直にやろうとするととても大変そうです。

リスクベースドテストの使い所

テスト戦略は多いほうがいい

プロジェクトチームで話をしていたときに、「この機能、次のバージョンに入れたいんだけど、テストに時間がかかるらしくて難しいらしい。なんとかならないか?」という意見を聞きました。所属しているプロジェクトでは、テスト分析、設計を経て、比較的ローレベルなテストケースを実装し、テスト実行する、というテストプロセスです。通常、機能追加を行う際はそのバージョン開発開始時点で計画されます。リリースまでのマイルストーンに間に合うように開発するため、テスト工数の問題は発生しません。ただ、「この機能入れたいんだよね〜。」の要望は計画外のところからやってくるのが常です。機能入れたいんだよね、を提案する方のモチベーションはユーザのためであることがほとんどです。この機能を入れることでプロダクトがもっと良くなる、ここでユーザに提供できないと機会損失が生まれてしまう、だから入れたいんだ!という思いなはずです。その意思を尊重したい気持ちはありますが、いかんせんテストプロセスが重たく、「テスト工数が足りません…。」と返答せざるを得ません。この状態はなんとか解決したいです。

ここで注目すべきは、チームとして持ち合わせているテスト戦略が1つしかない、という点です。なのでテスト工数が少ない戦略を増やすことで解決できそうです。

過不足ないテストを実現するテスト戦略

ここでリスクベースドテストの話に戻ってくるのですが、リスクベースドテストの本質は「やらないテストを決められること」にあると思っています。本来テストというものはリスクを回避、軽減、認知するための手段である、という考え方をしています(個人意見)。リスクベースドテストを知ったときも、リスクを基にテストを考えるのって普通じゃ?くらいに思っていました。多分それは間違いで、リスクに対して他のテストよりも向き合っているのがリスクベースドテストなのかなと感じています。時間が無限にある中では洗い出されたリスクに対して対策を講じれば良いと思いますが、時間が限られているのが普通です。切羽詰まっているときだってあります。

そのときに効果を発揮するのがリスクベースドテストです。洗い出されたリスクの中で優先度を設定し、限られた時間の中で対策したいリスクだけを選択する。選択したリスクに対してテストスコープを決め、そのリスクを軽減するための過不足ないテストを計画する。そうすることで、普段のテストプロセスよりも少ない時間で品質を保証することが可能です。

つまり、リスクベースドテスト戦略の使い所は「限られたテストコストの中で機能を追加した上で品質を保証し、なんとしてでもプロダクトをリリースしたいとき」です。

リスクベースドテストの使い方

早さを求められるのに、重厚である

冒頭で、リスクベースドテストは重厚である、という話をしました。リスクについて考えるのでどうしても時間をかけて行うことを想像してしまいます。実直にやろうとすると時間がかかるのです。一方で使い所は、限られたテストコストの中で機能追加してなんとしてでもリリースしたいとき、とも言いました。この矛盾を解決しなければ、使い所は分かっても使えません。

軽量化する

軽量な技法は、より公式な技法と同様に可能性および影響の要因に対する重み付けを使用して(たとえば、テストの精緻さの度合いなどに応じて)、ビジネスリスクまたはテクニカルリスクを強調できる。ただし、より公式な技法とは異なり、軽量な技法には次の特徴がある。
• 可能性と影響の 2 つの要因のみを使用する。
• シンプルで定性的な判定と尺度を使用する。
これらのアプローチは、軽量という特性により、柔軟性と広い範囲のドメインへの適用性、あらゆる経験とスキルを持つチーム(非技術要員および若手要員を含む)へのアクセシビリティを持つ。

ISTQBテスト技術者資格制度 Advanced Level シラバス 日本語版 テストマネージャ Version2012.J04 | p.28

JSTQB では公式で重い技法の対比で軽量な技法が紹介されています。重いならば軽くしましょう。リスク識別(洗い出し)、リスク判定(優先度付け)、リスク軽減(過不足ないテスト計画)、このプロセスにかかる時間をどれだけ早くできるか、がリスクベースドテストを実務に活かすポイントだと思います。

軽量なリスクベースドテストの良い所

やらないテストを決めることができる

これは既に上で話しましたね。リスク判定を行うことで過不足ないテストを計画することができます。それにより、不可能だったリリースが可能になります。

やらないテストの理由を関係者とともに考えることができる

「この機能入れたいんだよね〜。」という話とセットで言われるのが、「この機能を入れることによるリスクある?」です。リスクまで洗い出された状態で機能追加の提案を受けることは少ないんじゃないかなと思っています。その提案を受けるタイミングはたいてい切羽詰まっている状況なはずだからです。しかしながら、テストを計画する立場としてはリスクは考えなければいけません。Hotfix であれば不具合修正したコードの影響範囲を確認する必要があります。この作業を QA だけで行うのは大変です。リスクベースドテストを用いることでエンジニアなどの関係者と話し合いのもとリスクを洗い出し、優先度までつけることができます。助かりますね。

やらないテストの理由をステークホルダーに説明できる

関係者と話し合って決めた「やらないテスト」なので、そこにはきっとロジカルな理由が存在します。それを使ってリリース判断するプロダクトオーナーやプロジェクトリーダーに説明することで、やらないことへの正当性が生まれます。つまり、「やらない」ということをチーム全体で合意できたことになります。リスクベースドテストはやらないテストを決めて合意形成するまでの一連のプロセスを含んでいます。これにより、コミュニケーションコストがぐっと下がると思います。

軽量なリスクベースドテストの悩ましい所

使い所が限定される

影響範囲の少ない Hotfix 程度であればリスクの洗い出し、優先度付けまでスピーディーに進めることができます。実際私が行ったときは2時間くらいでテスト計画まで終えることができました。しかし、大きな機能に対してや複数の機能に対してリスクベースドテストを適用しようとすると途端に重たくなるはずです。

私のプロジェクトチームでは使い所は「限られたテストコストの中で機能追加してなんとしてでもリリースしたいとき」に限っています。なので、追加される機能も自然と小さなものに限られるはずです。その場合は軽量化された戦略でスピーディーに意思決定すれば良さそうです。

逆に、大きな機能であれば検討する時間の余裕があるはずなので、リスクに向き合う時間も確保できると思います。より重い技法を使うこともできるはずです。

そもそもちゃんとしたリスクベースドテストなの?

公式に軽量化したリスクベースドテストというよりは、概念を理解したうえで実務に活かすにはどうすればいいか?から考えたので、オリジナルな要素が強い戦略になってしまいました。ただ、理論は実践するのが大切だと考えているので、理想を掲げるだけではなく今のプロジェクトに即した形にチューニングして実戦投入したことは今のところ後悔していません。価値を出すことが何よりも大事です。

まとめ

リスクベースドテストは重厚なテスト戦略ですが、軽量化することで「限られたテストコストの中で機能を追加した上で品質を保証し、なんとしてでもプロダクトをリリースしたいとき」に効果的に活用できます。

軽量化されたリスクベースドテストのメリットは以下の通りです:

  • やらないテストを決めることができる
  • やらないテストの理由を関係者とともに考えることができる
  • やらないテストの理由をステークホルダーに説明できる

ただし、大規模な機能や複数機能への適用は難しく、使用シーンは限定的です。理論を実践に落とし込む際は、プロジェクトの状況に合わせて柔軟にチューニングすることが重要です。

また、今回リスクベースドテストを実践するにあたりJSTQB シラバスの他に、ソフトウェアテストのセオリーという書籍も大変参考になりました。テストプロセスに関連する情報を網羅的に紹介していくれているのでかなりオススメです。

techplay.jp

UnityのAnimationMixerPlayableにはブレンドした時、片方が空クリップだったとしてもルートモーションのRotationだけ謎にWeightでLerpされるバグがある!

Akatsuki Games Advent Calendar 3日目の記事です。

AnimationMixerPlayableとは

早い話がAnimationControllerをスクリプトベースで作るAPIの一つです。

AnimationControllerはノーコードで設計可能なステートマシンであり、グラフベースで記述できる何やら便利そうな機能なのですが・・・

本格的なゲームのキャラクター制御などを行えるほど複雑なステートマシンを保守できるほど人類は賢くありません。

そうなってくるとスクリプトベースのアニメーション制御システムが必要になるわけですが、そこで使われるAPIがAnimationMixerPlayableやAnimationClipPlayableなどのPlayableAPIです。

さてこのAPIたちの使い方を解説しようとするとこのブログ記事の余白では到底足りないので、その解説はUnity公式リファレンスやChatGPTにお任せするとして、AnimationMixerPlayableを使う上で遭遇する致命的な不具合を解説します。

ブレンドの振る舞いがルートモーションのRotationだけ異なる

直感的な期待動作として、クリップAとクリップBをブレンドした時、片方にしか存在しないキーフレームはWeightが1で適用されて欲しいわけですが、以下のようなプログラムでテストしてみましょう。

public class TestAnimationPlayable : PlayableBehaviour
{
    private PlayableGraph _graph;
    private AnimationMixerPlayable _baseMixer;
    private AnimationClipPlayable[] _playables = new AnimationClipPlayable[2];

    public override void OnPlayableCreate(Playable playable)
    {
        base.OnPlayableCreate(playable);

        _graph = playable.GetGraph();
        _baseMixer = AnimationMixerPlayable.Create(_graph, 2);
        _baseMixer.SetInputWeight(0, 1f);
        _graph.Connect(_baseMixer, 0, playable, 0);
        
        playable.SetInputWeight(0, 1f);
    }
    
    public void Play(AnimationClip[] clips)
    {
        for (int i = 0; i < clips.Length; ++i)
        {
            _baseMixer.DisconnectInput(i);
            _playables[i] = new AnimationClipPlayable();
            _playables[i] = AnimationClipPlayable.Create(_graph, clips[i]);
            _playables[i].SetDuration(clips[i].length);
            _playables[i].SetApplyFootIK(false);
            _playables[i].SetApplyPlayableIK(false);
            _playables[i].SetSpeed(1f);
            _baseMixer.ConnectInput(i, _playables[i], 0);
            _baseMixer.SetInputWeight(i, 1.0f);
        }
    }
}

Playに渡されたアニメーションクリップリストを全部Weight1でブレンドするシンプルなPlayableBehaviourです。(雑なコードなのでリストは2個しか受け付けてなさそうです)

さて、このPlayableBehaviourにこんな感じでクリップを渡してみます。

public class TestComponent : MonoBehaviour
{
    private Animator _animator;
    
    private PlayableGraph _graph;
    private TestAnimationPlayable _animationPlayable;

    [SerializeField]
    private AnimationClip _clip;

    private void Awake()
    {
        _animator = GetComponent<Animator>();
        _animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
        _animator.updateMode = AnimatorUpdateMode.Normal;
        _animator.applyRootMotion = true;
        
        _graph = PlayableGraph.Create(_animator.gameObject.name + ".Animation");
        _graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);

        var playable = ScriptPlayable<TestAnimationPlayable>.Create(_graph, new TestAnimationPlayable(), 1);
        _animationPlayable = playable.GetBehaviour();
        var output = AnimationPlayableOutput.Create(_graph, "Output", _animator);
        output.SetSourcePlayable(playable);
    }

    public void Play()
    {
        _animationPlayable.Play(new [] { _clip, new AnimationClip() });
        _graph.Play();
    }

    private void OnDestroy()
    {
        _graph.Destroy();
    }
}

PlayableGraphを生成し、AnimationPlayableOutputでAnimatorに出力先を設定 Play関数でシリアライズフィールドで受け取ったクリップと空のクリップを先ほどのPlayableBehaviourに渡しています。

つまりこれはあるアセットのクリップと空のクリップをブレンドするだけの特に意味のないテスト用コンポーネントです。

まず、検証用に作成したアニメーションクリップを上記のシステムを使わずに単体で再生してみましょう。

www.youtube.com

くるっと90度回転しながら直進するアニメーションです。

さて、このアニメーションクリップを上記のシステムを使い、すなわち空のクリップとブレンドして再生してみましょう

youtu.be

うおおおどこいくねーん!!!

これ、Positionの変化量は変わってないんですが、Rotationの変化量だけ謎にLerpされているんですよね。今回の場合両方のクリップをWeight1でブレンドしたので、「無」とLerpした結果、変化量が半分になってます。Rotationだけですね。

これ何が問題が起こるかというと、普通のモーションとフェイシャルモーションなどをブレンド再生した時に、普通のモーションのルートモーションのRotationだけおかしくなるわけです。 AvatarMaskをフェイシャル用とボディ用と分けて管理すれば解決するかもしれませんが、弊社ではその方式をとっていませんでした。

また、AnimationLayerMixierPlayableを使って加算アニメーションとして再生してもこの問題は起こりませんが、加算アニメーションをうまく使うためには0F目のキーの状態がモデルの参照Poseと一致している必要があります。弊社ではフェイシャルアニメーションがこの制約を満たしていないためやはり採用できず。

この問題はすでにUnity社に報告済みで、なんと 2021/2022/6000すべてのバージョンで現在再現します。

Unity Issue Tracker - Root GameObject behaves differently when blending animations with and without keyframes and using AnimationPlayable

プロダクトではとんでもねぇワークアラウンドを使ってこの問題を回避しましたが、皆さんはちゃんとAvatarMaskを使いましょう! では。

Elixir から Workload Identity 認証で Google Cloud のサービスアカウントを利用する

本記事は Akatsuki Games Advent Calendar の2日目の記事です。

Elixir の Google Cloud サービスアカウント認証

アカツキゲームスの一部のゲームタイトルでは Elixir をサーバー開発言語として利用しています。

BigQuery 等のサービスを利用する際、Google Cloud のユーザー認証が必要になりますが、GitHub Actions などの CI からサービスにアクセスするような場合、Workload Identity を利用してサービスアカウントの認証が行えるように構成するのがベストプラクティスとされています。これは、秘密鍵の管理が不要になりセキュリティの向上が図れるためです。

参考: Workload Identity Federation  |  IAM Documentation  |  Google Cloud

さて、Elixir からサービスアカウントの認証を行う場合には、 Goth というライブラリが利用されることが多く、 Google API を呼び出すためのライブラリ GoogleApis の README でも Goth を使用してトークンを取得する手順が記載されています。

github.com

この Goth ですが、現在未リリースではあるものの、 master ブランチでは Workload Identity を利用可能にする Pull Request がマージされています。本記事ではこれを利用してみたので、その方法を紹介します。

Workload Identity の設定

今回は GitHub Actions から利用したかったので、「他の ID プロバイダとの Workload Identity 連携を構成する」の手順で設定を行いました。

Workload Identity Pool を作成し、そこにプロバイダとして GitHub を追加します。外部 IdP としては OIDC を利用します。そのほか設定に必要な「発行元(URL)」等の情報は GitHub のドキュメントを参照しながら進めます。
Google Cloud Platform での OpenID Connect の構成 - GitHub Docs

属性マッピングには 「OpenID Connect を使ったセキュリティ強化について - GitHub Docs」の「Understanding the OIDC token」にある値が使用でき、これを利用して認証が行えるレポジトリやワークフローを制限することができます。
ここでは一例として、以下のようにマッピングとアクセス制限を設定しました。

加えて利用したいサービスアカウントに対して、プールの画面にある「アクセスを許可」→「サービス アカウントの権限借用を使用してアクセス権を付与する」から利用できるように設定を行います。この時も「プリンシパルの選択」で「フィルタに一致するIDのみ」を選択し、条件として先ほどのマッピングを利用して、例えば attribute.repository が 「org名/repository名」であるなどといった制限を指定しておくことが推奨されています。

最後に、プールの「接続済みサービス アカウント」から認証情報の構成が記述されたファイルをダウンロードします。この時に、「OIDC ID tokenのパス」というものを入力する必要がありますが、これは後の Actions でトークンの受け渡しに使うファイルパスを記入します。(以下では仮に /tmp/token としますが、あまり /tmp は使わないほうがいいかもしれません。)

ダウンロードした構成ファイルのパスを環境変数 GOOGLE_APPLICATION_CREDENTIALS で渡すか、その中身のテキストを GOOGLE_APPLICATION_CREDENTIALS_JSON で渡すことで、Goth が Workload Identity を利用できるように設定されます。

GitHub Actions でのトークン取得

OIDC ID トークンは、GitHub Actions で permission として id-token: write を指定すると有効化される環境変数を使って、以下のようにcurlで取得できます。
取得したトークンをファイルとして上記で指定した「OIDC ID tokenのパス」に配置して、 Goth を利用した Elixir アプリケーションを呼び出すことで、このトークンをサービスアカウントのトークンと交換して認証が可能になります。

name: goth-test
on:
  push:

permissions:
  id-token: write
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    container: hexpm/elixir:1.17.3-erlang-27.0.1-alpine-3.20.3
    steps:
      - name: Package Install
        run: |
          apk update
          apk add bash tar curl git openssh jq
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Get Id Token
        run: |
          AUDIENCE=$(echo "$GOOGLE_APPLICATION_CREDENTIALS_JSON" | jq -r .audience)
          curl -sf -m 10 -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
               "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$AUDIENCE" | jq -r .value > /tmp/token
      - name: Execute application
         run: ... # Elixir アプリケーションを呼び出す

他のscopeを利用する場合

Google Cloud のサービスを利用する場合には上記で大丈夫なのですが、Google Drive 等の他のサービスを利用したい場合には scope の指定が必要になります。
残念ながら現状の Goth では Workload Identity 用の scope がデフォルトのGCP用に固定されてしまっていました。

そこで、scope を指定可能にするための Pull Request を出しました。
Pull Request #178: Support the scopes option in workload identity
これを利用すると、例えば以下のようにアプリケーションを初期化することで指定したスコープのトークンを取得できるようになります。

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    credentials =
      "GOOGLE_APPLICATION_CREDENTIALS_JSON"
      |> System.fetch_env!()
      |> Jason.decode!()

    source = {:workload_identity, credentials, 
      scopes: [
         "https://www.googleapis.com/auth/spreadsheets",
         "https://www.googleapis.com/auth/drive"
       ]}

    children = [
      {Goth, name: MyApp.Goth, source: source}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end
  token = Goth.fetch!(MyApp.Goth)

まとめ

本記事では Elixir で Workload Identity を利用して Google Cloud のサービスアカウントの認証を行う方法を紹介しました。
Goth の Workload Identity 機能はまだtrunkに入ったばかりで、他にも curl を使わずに直接指定URLからトークンを取得する機能の Pull Request などが提案されているようです。*1 今後の進展が気になります。

*1:多分私のPull Requestとコンフリクトしてしまいそうですが…