TerraformでTailscaleの構築をしてみる

はじめに

QualiArts - Qiita Advent Calendar 2025 - Qiita、22日目担当の鈴木光です。

今回、いわゆるマネージドWireGuardであるTailscaleを構築する機会があったため、備忘録として残しておきます。

TailscaleVPNを簡単に構築・管理できるサービスです。この記事ではTerraformを使用してTailscaleのアクセス制御ポリシーを管理する方法と、ロールベースのアクセス制御を実装した事例を紹介します。

Tailscale Provider

Tailscaleでは公式より Tailscale Providertailscale/tailscale)が提供されているので、こちらを使ってリソース管理を行います。

出典: Terraform Registry - tailscale/tailscale

実装の概要

Tailscale Grantsポリシー

この実装では、TailscaleのGrantsポリシーを使用しています。Grantsは、従来のACLから移行された新しいアクセス制御方式です。

出典: Tailscale公式ドキュメント - Migrate from ACLs to grants

ロールベースアクセス制御の仕組み

以下の4つの要素で構成されています

  1. ロール定義: 各ロールがアクセス可能なプロジェクトタグを定義
  2. ユーザーアサイ: 各ユーザーに1つのロールを割り当て
  3. グループ生成: ロールごとにカスタムグループを自動生成
  4. タグオーナー設定: プロジェクトタグを管理する権限を持つグループを定義

実装詳細

main.tfの構成

main.tfファイルは以下の構造になっています。

1. Locals定義

locals {
  # ロール定義: プロジェクトタグを持つロール
  roles = {
    "admin"              = ["projectA", "projectB"]
    "projectA-developer" = ["projectA"]
    "projectB-developer" = ["projectB"]
  }

  # ユーザーとロールのアサイン(1ユーザーに1ロール)
  user_role_assignments = {
    "user1@example.com" = "admin"
    "user2@example.com" = "admin"
    "user3@example.com" = "projectA-developer"
    "user4@example.com" = "projectB-developer"
  }
}

2. 動的なグループ生成

Terraformのfor式を使用して、ロールごとにユーザーをグループ化します。

role_users = {
  for role_name, _ in local.roles :
  role_name => [
    for email, role in local.user_role_assignments :
    email if role == role_name
  ]
}

3. タグオーナーのマッピング

Tailscaleでは、タグオーナー(tagOwners)がタグをマシンに割り当てる権限を持ちます。この実装では、すべてのプロジェクトタグをgroup:adminが所有するように設定しています。

# タグオーナーのマッピングを作成
# プロジェクトタグはadminグループが所有
tag_owners = {
  for project_tag in local.all_project_tags :
  "tag:${project_tag}" => ["group:admin"]
}

この設定により、group:adminのメンバーのみが、プロジェクトタグ(例: tag:projectAtag:projectB)をマシンに割り当てることができます。

タグオーナーの概念は、Tailscaleのアクセス制御において重要な役割を果たします。タグをマシンに割り当てる権限を制限することで、意図しないタグの割り当てを防ぎ、アクセス制御ポリシーの整合性を保つことができます。

4. タグとロールのマッピング(tag_to_roles)

各プロジェクトタグに対して、アクセス可能なロール(admin以外)をマッピングするtag_to_rolesを定義しています。

# タグごとにアクセス可能なロール(admin以外)をマッピング
tag_to_roles = {
  for tag in local.all_project_tags :
  tag => [
    for role_name, tags in local.roles :
    role_name if contains(tags, tag) && role_name != "admin"
  ]
}

各プロジェクトタグに対して、そのタグにアクセス権限を持つロール(adminを除く)を自動的に特定できます。例えば、projectAタグにはprojectA-developerロールがマッピングされます。

tag_to_rolesは、後述するGrantsルールの生成時に使用され、各タグに対して適切なロールのユーザーがアクセスできるようにします。adminロールは常に全タグにアクセスできるため、このマッピングからは除外されています。

5. Tailscale ACLリソース

tailscale_aclリソースを使用して、Grantsポリシーを定義します。

resource "tailscale_acl" "this" {
  acl = jsonencode({
    groups = {
      for role_name, users in local.role_users :
      "group:${role_name}" => users
    }
    
    tagOwners = local.tag_owners
    
    grants = concat(
      # ExitNodeへのアクセスルール
      [...],
      # プロジェクトタグへのアクセスルール
      [...]
    )
  })
}

アクセス制御ルール

実装では、以下の2種類のアクセスルールを定義しています。

1. ExitNodeアクセスルール

プロジェクトごとに、ExitNode経由でのインターネットアクセスとプライベートIPレンジへのアクセスを許可します。

[
  for tag in local.all_project_tags : {
    src = concat(["group:admin"], [for role in local.tag_to_roles[tag] : "group:${role}"])
    dst = concat(["autogroup:internet"], local.private_ip_ranges)
    via = ["tag:${tag}"]
    ip  = ["*"]
  }
]

2. プロジェクトタグへのアクセスルール

各プロジェクトタグに対して、該当するロールのユーザーがアクセスできるようにします。

[
  for tag in local.all_project_tags : {
    src = concat(["group:admin"], [for role in local.tag_to_roles[tag] : "group:${role}"])
    dst = ["tag:${tag}"]
    ip  = ["*"]
  }
]

SSHルール

管理者グループ(group:admin)のメンバーが、自身のマシンに対してSSHアクセスできるように設定されています。

ssh = [
  {
    action = "accept"
    src    = ["group:admin"]
    dst    = ["autogroup:self"]
    users  = ["autogroup:nonroot", "root"]
  }
]

自動承認設定

プライベートIPレンジのルートとExitNodeの自動承認を設定しています。

autoApprovers = {
  routes = {
    for ip_range in local.private_ip_ranges :
    ip_range => [for tag in local.all_project_tags : "tag:${tag}"]
  }
  exitNode = [for tag in local.all_project_tags : "tag:${tag}"]
}

まとめ

Terraformを使用してTailscaleのアクセス制御を管理することで、IaCの原則に従った運用が可能になります。 また、ロールベースアクセス制御を実装することでユーザー管理が簡潔になり、スケーラブルなアクセス制御が実現できるようになりました。

この実装にはTailscale Premiumプラン以上が必要ですが、カスタムグループ機能を活用することで柔軟で保守性の高いアクセス制御システムを構築できるため、皆様の参考になれば幸いです。

【AWS】Aurora DSQLの負荷試験をしてみた【OpenTelemetry】

はじめに

QualiArts - Qiita Advent Calendar 2024 - Qiita、14日目担当の鈴木光です。アドカレ遅刻して申し訳ありません……

2024年12月03日、AWS re:Invent 2024にてAmazon Aurora DSQLが発表されました。Aurora DSQLは待望のAWSマネージドないわゆるNewSQLに分類されるデータベースです。製品サイトより抜粋してその特徴を簡単に紹介します(回し者ではないです!)

分散 SQL データベース – Amazon Aurora DSQL – AWS

  • あらゆるワークロードの需要に合わせて自動的にスケーリング: 読み取り・書き込み・コンピューティング・ストレージを個別に継続的に拡張し、パフォーマンスを維持しながらスケーリングのボトルネックを解消
  • アクティブ-アクティブ高可用性: 単一リージョンで 99.99%、マルチリージョンで 99.999% の可用性を実現するように設計
  • サーバーレスインフラストラクチャ: マイナー バージョン アップグレード・パッチ適用・セキュリティ更新などの更新を自動的に処理するため、メンテナンスのダウンタイムがない
  • PostgreSQL互換: わずかな構成変更で一般的な PostgreSQL ドライバーとツールをサポート
  • 分散アーキテクチャ: 単一障害点のないフォールト トレランスを内蔵
  • 高いセキュリティ: データベースの認証と承認のために AWS IAMとネイティブに統合され、すべての顧客データは非公開で保存時および転送時に常に暗号化

これまで、この分野ではGoogle CloudのSpannerが業界を席巻していました。SpannerのためにAWSからGoogle Cloudへ移行する事例もある中、この流れに一石を投じるソリューションになるのでしょうか

プレビュー期間は無料ということで、今回はAurora DSQLの負荷試験をしてみました

Aurora DSQLについて

AWSブログとAWS re:Invent 2024のYoutube動画はAurora DSQLを理解する上でとても参考になるので、是非ご覧ください

Amazon Aurora DSQL の紹介

Amazon Aurora DSQL の紹介 | Amazon Web Services ブログ

Amazon Aurora DSQL の同時実行制御

Amazon Aurora DSQL の同時実行制御 | Amazon Web Services ブログ

AWS re:Invent 2024 - Get started with Amazon Aurora DSQL (DAT424)

https://www.youtube.com/watch?v=9wx5qNUJdCE

AWS re:Invent 2024 - Deep dive into Amazon Aurora DSQL and its architecture (DAT427-NEW)

https://www.youtube.com/watch?v=huGmR_mi5dQ

サンプルアプリケーションの実装

ぶっちゃけアドカレに費やした時間は4割がアプリケーションの実装、5割がトレーシング環境の構築、1割で負荷試験となってしましました……

今回は簡単なブログサーバを作っています。"データベース本体のベンチマークを計測するツール" は多いと思うのですが、それをインフラが隠蔽され裏で無制限にスケールするAurora DSQLに打ったところでAWSのインフラの負荷試験になってしまうので、シナリオベースの負荷試験を行うためにサンプルアプリケーションとシナリオを実装しました。なんかいい感じのツールがあれば教えて下さい

実装

Goで実装しました APIサーバの負荷試験をするつもりはないので、HTTPサーバを起動せず擬似的にリクエストを送信してハンドラーの処理内容だけ呼び出しています。また、サーバを実装していた時代の名残も残っていますがあしからず……

こちらのソースコードGitHubにて公開しています

github.com

テーブル定義

CREATE TABLE IF NOT EXISTS public."users"
(
    "id"            varchar   NOT NULL,
    "name"          varchar   NOT NULL,
    "email"         varchar   NOT NULL UNIQUE,
    "password_hash" varchar   NOT NULL,
    "created_at"    timestamp NOT NULL,
    "updated_at"    timestamp NOT NULL,
    PRIMARY KEY ("id")
);

CREATE TABLE IF NOT EXISTS public."articles"
(
    "id"                   varchar   NOT NULL,
    "title"                varchar   NOT NULL,
    "body"                 text      NOT NULL,
    "user_id"              varchar   NOT NULL,
    "total_favorite_count" bigint    NOT NULL,
    "created_at"           timestamp NOT NULL,
    "updated_at"           timestamp NOT NULL,
    PRIMARY KEY ("id")
-- Aurora DSQLは外部キーをサポートしていない
--    FOREIGN KEY ("user_id") REFERENCES "users" ("id")
);

CREATE TABLE IF NOT EXISTS public."users_articles"
(
    "user_id"    varchar   NOT NULL,
    "article_id" varchar   NOT NULL,
    "created_at" timestamp NOT NULL,
    "updated_at" timestamp NOT NULL,
    PRIMARY KEY ("user_id", "article_id")
-- Aurora DSQLは外部キーをサポートしていない
--    FOREIGN KEY ("article_id") REFERENCES "articles" ("id"),
--    FOREIGN KEY ("user_id") REFERENCES "users" ("id")
);

エンドポイント一覧

8つのエンドポイントを実装しています。ユーザー登録API以外はミドルウェアにて認証のクエリが発行されることに注意してください

メソッド パス 処理内容
POST /user ユーザー登録を行う
GET /articles 直近投稿された記事を100件取得する。created_atにはインデックスを貼っておらず、テーブルスキャンが入る(はず)
GET /article/:article_id 記事詳細を取得する
POST /article 記事を投稿する
PATCH /article/:article_id 記事を編集する
DELETE /article/:article_id 記事を削除する
GET /favorite/articles お気に入り一覧を取得する。IN句の値にはサブクエリを使う
POST /favorite/article/:article_id 記事をお気に入り登録し、記事側のtotal_favorite_countをインクリメントする(ここが競合要素になる)

負荷試験シナリオ

シナリオとしては以下のような設定をしています

1. ユーザー登録
2. 90%の確率で記事作成し、以下を最大5回繰り返す
  2-1. 50%の確率で記事一覧を取得
  2-2. 50%の確率で記事詳細を取得
  2-3. 20%の確率で記事を更新
  2-4. 1%の確率で記事を削除
3. 50%の確率でお気に入り一覧を取得
4. 50%の確率でセットアップで用意された記事をお気に入り登録
  4-1. IDの配列からIDを受取り、お気に入り登録する
  4-2. 10%の確率で継続

負荷試験環境の構築

インフラのセットアップ

Aurora DSQLのシングルリージョンクラスターとマルチリージョンクラスターで検証してみました。シングルリージョンクラスターの作成方法は割愛します

まず us-east-1(バージニア北部) に対してクラスターを作成します。Linked Clusterとして us-west-2(オハイオ) 、ウィットネスリージョンとして us-west-2(オレゴン) を指定し、クラスターを起動します。設定項目は本当にこれだけです。Create clusterを押し、数分したらActiveになります 右上の Connect ボタンを押すと、ホスト名(エンドポイント)、ポート番号、データベース名、パスワード(認証トークン)を確認できます。パスワードは15分の期限があり、再取得が必要です。本来はそのあたりまで自動化するべきですが、今回は期限が切れたらここに取りに来るというスタンスで作業しています

次に us-east-1(バージニア北部) へEC2インスタンスを建てます。AmazonLinux、t2.xlargeインスタンスを適当に作成しました。SSHキーを登録し、セキュリティグループを設定してSSH出来るようにしておきます。インスタンスを起動したら負荷試験ツール兼サンプルアプリケーションをビルドしたバイナリをSCPコマンドで転送し、SSHしてpsqlDDLを流すのですが、SSHする際はリモートポートフォワーディングを行います。EC2にトレース環境を構築するのが面倒だったので、EC2で実行したトレースをローカルに送信しています。最後にバイナリを実行して負荷試験を行います

コマンドを以下に添付しましたので、よければ参考にしてください

$ GOOS=linux GOARCH=amd64 go build -o bin . # ソースコードから負荷試験ツールのバイナリをビルド
$ scp -i <SSHキー.pem> ./bin ec2-user@<EC2のPublicIPアドレス>:/home/ec2-user # バイナリをSCPコマンドで転送
$ ssh -i <SSHキー.pem> -R 4317:127.0.0.1:4317 ec2-user@<EC2のPublicIPアドレス> # リモートポートフォワーディングしつつSSH
$ sudo dnf update -y
$ sudo dnf install postgresql16
$ export PGSSLMODE=require
$ export PGHOST=<コンソールで取得したホスト名>
$ export PGPASSWORD=<コンソールで取得したパスワード>
$ psql --quiet --username admin --dbname postgres
$ postgres=> CREATE DATABASE blog;
$ postgres=> \c blog
$ postgres=> <ddl.sqlの内容を貼り付ける>
$ postgres=> exit
$ DB_HOST=$PGHOST DB_USER=admin DB_PASS=$PGPASSWORD DB_NAME=postgres OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 APP_DURATION=180 APP_USERS=10 APP_SPAWN_RATE=1 ./bin # 3分間、最大同時10ユーザー、1秒間に1ユーザーずつユーザーを追加するという設定で負荷試験を実行で負荷試験を実行

OpenTelemetry&Jaeger&Tempoによる計測とJaeger&Grafanaを使った可視化

このサンプルアプリケーションにはクエリを発行する部分にトレーサーを仕込んでいます。トレーシングデータはOpenTelemetry Collectorに集約され、Exporterとして登録しているJaegerTempoに送られます。これをJaeger UIやGrafanaを使って可視化しました

最初はJaguarSPMだけで完結するかと思ったのですが、レイテンシが95パーセンタイルでしか確認できなかったのでTempoと併用する形にしています。逆にTempo(Grafana)では各Traceを分布図の形でプロットする方法がわからなかったので、こちらで統一ともいきませんでした(ご存じの方教えて欲しいです)

ちなみにJaegerのストレージバックエンドとしてOpenSearchを使っています。本当はJaeger組み込みのローカルファイルストレージであるBadgerを使いたかったのですが、 SPAN_STORAGE_TYPE: badgerMETRICS_STORAGE_TYPE: prometheus を同時に設定するとエラーになってJaegerが起動しないという現象に遭遇したためOpenSearchを使うことにしました

負荷試験の実施と結果

APP_DURATION=180 APP_USERS=10 APP_SPAWN_RATE=1 ./bin # 3分間、最大同時10ユーザー、1秒間に1ユーザーずつユーザーを追加するという設定で負荷試験を実行

上記の通り負荷試験の設定が結構甘いのですが、やはりしっかりした環境ではないからかエラー率が上がってしまってこの数値に調整しました。このあたりのチューニングもしっかりしたいところですが、アドカレなのでご容赦を……

実行してみた所、発表通り楽観的同時実行制御を行っているようでコミット時に衝突してエラーということがよく起こりました。なのでトランザクションを貼る部分にリトライ機構を入れてみたところ、エラーでレスポンスが返るケースはほとんど無くなったのですが、その代わり内部でリトライ祭りになっているのか遅いトランザクションが頻発するようになりました。SpanのAttributesにトランザクション実行回数を加えて分析した所、やはり POST /favorite/article/:article_id APIが多かったです

シングルリージョンクラスタ

マルチリージョンクラスタ

結果

やはりマルチリージョンクラスターのほうがレイテンシが悪い傾向にありますね。特に50パーセンタイルでみると全然違うことがわかります。ただ色々出てきた値を見ている感じ、Spannerより速いという謳い文句は嘘ではなさそうなレイテンシをしていました

ちゃんと検証するなら……

  • マルチリージョンからの書き込み: マルチリージョンクラスターに対し、このアプリケーションを us-east-1(バージニア北部)us-west-2(オハイオ) から同時に負荷試験したほうがもっとそれっぽい検証になるのですが、競合要素の初期データ作成が面倒だったので今回はやっていません。本格的に検証する際はこういう部分も検証したいですね
  • 負荷試験環境・設定のチューニング: 単一サーバから行っているため、負荷試験の設定を上げるとエラー率が上がってDBコネクション数なども詰まっている印象がありました

まとめ

アドカレだしレイテンシを計測するくらいでネタになるだろうと思っていたら、結構沼ってしまって一番重要な部分が雑になった気もしなくもないですが、この週末で分散トレーシングについてはかなり身についた気がします。それだけでも良しとしましょう

とはいえちゃんとスケジュールは守らないとですね

【Unity】ゲーム開発の現場でなぜJenkinsが利用され続けるのか

はじめに

QualiArtsのカレンダー | Advent Calendar 2022 - Qiita、19日目担当の鈴木光です
私はバックエンドエンジニアですが、クライアントのビルド周りも携わるので今回はゲーム開発現場におけるJenkinsについてのお話をさせていただきます

Jenkinsについて

皆さんご存知の通り、Jenkinsは非常に汎用的なCI/CDのプラットフォームです。Jenkins自体はJavaで実装されたOSSのソフトウェアなので基本的に自前でホストする必要がありますが、それゆえに構築してしまえばベンダーロックインされることなく多くの現場で活用することが可能です
最近はGitHub Actionsを筆頭にCircle CI, Gitlab CI, AWS Code BuildやGoogle Cloud Buildなど様々なマネージドCIサービスが業界を席巻していますが、露出の少ないゲーム系のテックカンファレンスなどを見ると未だにJenkinsはよく登場しています。おじいちゃんはまだまだ現役なのです

Jenkinsの欠点

特にWeb系の方々からすると「まだJenkinsなんて使っているの?」という感想ではないでしょうか。かくいう自分もその一人で、入社した時はほうぼうでJenkinsが利用されていて驚いた記憶があります。なぜマネージドサービスを利用しないのかと
実際、QualiArtsのバックエンドエンジニアも以前はサーバアプリケーションのCI/CD基盤としてJenkinsを採用していましたが、最近ではほとんど使っていません。もっぱらGitHub ActionsかGoogle Cloud Buildで完結しています(一部CD基盤にArgoCDを利用していたりしますが、CI基盤としてはマネージドサービスがメインです)。この理由は皆様と同じでしょう、Jenkinsにはいくつか欠点があります

  • マネージドサービスではないため自前でマシンやソフトウェアを管理する必要がある
  • Jenkins自体がプラグインにとても依存する。プラグインのバージョンについていけなかったり、アップデート作業をするとJenkinsやジョブが壊れる。そして直近のアップデートができないのでその次のアップデートもできなくなり化石になっていくという悪循環
  • ジョブや設定がコード化されていない。コード化するプラグインを使ったとしてもやはりそこだけで全ては完結しない(そのプラグインのインストールは?)。さらには馴染みのないGroovy

Jenkinsを利用していた時代もこのあたりを解決するためFabric(GitHub - fabric/fabric: Simple, Pythonic remote execution and deployment.)を使ったり、あまりJenkins側で複雑な設定をしなかったりと工夫していましたが、近年のマネージドサービスはこのあたりを解決してくれるため移行しています。またJenkinsは多岐にわたる機能を持ちますが、我々が行いたかったのは自動テストや単純なビルド・デプロイ作業であったため高度な機能が必要とされなかったというのもあるかもしれません

Unityのビルド

さて、QualiArtsでは主にUnityを用いてゲーム開発を行っており、バックエンドとは違ってクライアントでは引き続きJenkinsを利用しています。具体的にどのようなことがJenkinsによって自動化されているのかは1日目の記事(Unity開発の現場でJenkinsがしていることの紹介 | QualiArtsエンジニアブログ)を参照していただくとして、大別すると「アプリのビルド」と「アセットのビルド」の2つに分けられます。他にも細かいジョブはありますが、そちらは実験的にGitHub Actionsへ移行しています。逆に言うと、これらのジョブは移行することが出来ないわけです。なぜでしょうか?詳しく解説していきましょう

2種類の"巨大な"ビルドとキャッシュシステム

アプリのビルド

「アプリのビルド」から解説していきましょう。こちらは単純明快、クライアントのソースコードからアプリケーションをビルドし、ipaやapkといった実行ファイルを出力するジョブです。これがLinuxマシンで稼働する一般的なサーバアプリケーションであればJenkinsは不要だったでしょう。しかし、ここでビルドのターゲットになるのはiOSAndroidです。特にiOSのビルドにはmacOSを用いてしかビルドできないという制約があります。したがって、この時点でマネージドサービスであったとしてもかなりサービスを選びます。アプリケーションのビルドにはそこそこのマシンパワーも必要ですのでマネージドサービスにすると費用もかさみます。ビルドジョブには手動で行われるものの他にも1時間に1度など定期的に行われるビルドもあるため、1日のうちそこそこの時間マシンリソースが専有されているというのも従量課金制と相性が悪いです
このため、QualiArtsではMac Proを社内にホストしてJenkinsエージェントをインストール、クラウドVM上に構築されたJenkinsコントローラーと接続してジョブはMac Pro上で実行するという形式をとっています

VMMac ProにあるJenkins

アセットのビルド

Unityには画像や動画といったアセットを外部へ切り出すためにアセットバンドルというシステムがあります。Unityがアセットを読み込むためにプラットフォームごとの変換処理やアセットの圧縮などを事前に行い、アセットをメタデータと一緒に外へ切り出すことで、ランタイムで動くアプリケーションに対して動的にコンテンツを配布できるという仕組みです。Unityで開発された運用中のサービスにおいて、新しく追加されるガチャやDLCコンテンツなどがアプリ更新なしに行われているなら十中八九アセットバンドルを使っていることでしょう
このビルド処理にはかなりのマシンリソースが必要で、おろし金と呼ばれる弊社のMac Proを用いても数十分はかかる処理になっています。ただ実はこの処理、ビルドターゲットがiOSでもビルドマシンがmacOSである必要はないのでAWSのスポットインスタンスのようなLinuxVMを用いてコスト最適化しつつクラウド化することは可能です(過去にそのようなプロジェクトは実在しました)

JenkinsのメリットとUnityビルドにおけるキャッシュ

今までの話だけだと「Macが使えるマネージドサービスならコストを一定かければJenkinsじゃなくて良いの?」ということになるかもしれません。アセットのビルドなんて「macOSじゃくても良いならJenkinsである必要ある?」となることでしょう。しかし、なかなかそういうわけにもいかないのです
先程、Jenkinsのデメリットについてお話しました。逆に我々がJenkins使い続ける究極のメリットを紹介しましょう。それが"ジョブごとにワークスペースが維持される"というものです
一般的に、CIは毎回クリーンな環境で行われる方が良いとされています。そうしないと冪等性や再現性が担保されないためです。変に以前の状態が維持されている環境でCIを実行してしまったがために、想定していたプログラミング言語のバージョンやライブラリの依存関係がおかしくなって挙動が変わるということは容易に想像できます。ただ、これは毎回クリーンな環境で実行したCIが現実的な時間内で終了することが前提です。1テストするのに10時間かかっていたら開発も進みません。しかし、上記2つのビルドではそれが起こるのです
ここでアプリのビルドについて触れてみます。Unityには Library というディレクトリがあり、色々なキャッシュが詰まっています。これがあることでエンジニアは快適な開発体験を享受できます。 Library ディレクトリは数GBあり、Unityは Library ディレクトリ自体やその中のキャッシュデータが不足しているとそれを起動時などに生成しますが、数GBのキャッシュデータを生成するのですから当然時間がかかります。さらにUnityのプロジェクトは大きくなりがちです。VCSからチェックアウトするだけでもかなりの時間がかかります。さらにさらに、アプリのビルドに必要な埋め込みのアセットをSVN, Git LFS, Plastic SCM, Perforceといったアセット用VCSからチェックアウトすることも必要です。これらの処理を毎回するのでしょうか。帯域の問題もありますが、何より時間がかかります。開発中のアプリのビルドは速ければ速いほど検証やQAといったイテレーションを回せるのでビルド時間というのはとても重要な項目です
また、多くのマネージドCIサービスではジョブが並列に走ることが前提です。サービス上にキャッシュの仕組みが用意されているとはいえ、バージョン管理されたコードを並列にビルドしつつ、それを適切にキャッシングするのはなかなか骨が折れるのではないでしょうか。Jenkinsはエージェントごとに並列数も絞れますし、そもそもジョブはキューイングされるので直列のビルドです。以前のジョブで使われていたワークスペースはそのまま維持され、次のジョブでも利用されるため Library ディレクトリの生成やデータのチェックアウト処理は短時間で完了します
これがそのままアセットのビルドにも関わります。アセット用VCSからチェックアウトする必要があるといったことを先程お伝えしましたが、アプリ埋め込みのアセット量などたかが知れています。本当にヤバいのはアセットバンドルとして外部へ切り出されるリソースたちです。タイトルの規模や対応プラットフォームにもよりますが、例えばQualiArtsのIDOLY PRIDEではこれらは全てのプラットフォーム合わせてデータ量が数十GBあります。アセットバンドルは圧縮されているので生のアセットデータは100GB近くあったりします。この容量になるとマネージドサービスのキャッシュシステムの上限を超えたり、そもそもキャッシュへストア・リストアする作業自体にも時間がかかります。例えばGitHub Actionsでは actions/cache というワークフローを用いてデータのキャッシュを実現できますが、キャッシュのデータサイズには10GBの上限がありますし、ログを見る感じ内部的には恐らく分散ストレージに対してキャッシュデータをダウンロード・解凍・圧縮・アップロードといった処理をしています。"ジョブごとにワークスペースが維持される"というのはこの問題に対して非常に効果を発揮します。アセットバンドルのフルビルドは本来丸一日レベルのジョブですが、Jenkinsが持つこの特性のおかげで数十分で済んでいます。これがゲーム開発の現場で良くJenkinsが使われている理由です
(ちなみにGitHub Actionsのセルフホステッドランナーはワークスペースを維持しますが、ワークスペースがジョブごとではなくエージェントごとに維持されます。共有ディレクトリをマウントするなどすればジョブごとにすることも可能かもしれませんが、その工夫の維持管理にもコストがかかりますし、どちらにしろ後述する問題があることにより移行は厳しいかなという所感です)

ワークスペースが維持されることでキャッシュになる

ビルド時間以外のメリット

実はビルド時間以外にもJenkinsにはメリットがあります。それは従量課金でないことと、UIがわかりやすいというものです
我々の現場ではJenkinsのジョブを実行するユーザーがエンジニアであるとは限りません。クリエイターやプランナーなど多様な職種のユーザーがJenkinsのボタンをポチっと押します。仮に素晴らしいキャッシュシステムを構築し、Jenkinsから脱却できるぞと意気込みながらGitHub Actionsに完全移行した場合は、次にこの問題が立ちはだかることでしょう。そう、Jenkinsはユーザー数によってお金がかかるということはありませんが、GitHubはユーザー数による従量課金制なのです。QualiArtsが所属するCyberAgentグループは全社的にGitHub Enterprise Cloudを導入していますが、料金表を見るとユーザーあたり21ドルかかります。しかもこれが日割りだとは思えません。最低でも月ごと、あるいは年ごとになるでしょう。ゲーム開発の現場は人員の流動性も激しく、開発人数も多いです。ソーシャルゲームの開発であっても100人を超える現場は珍しくありません。当然全員がJenkinsを扱うわけではありませんが、必要な人に絞ったとしても利用するなら2週間のインターン生や3ヶ月のヘルプで来ていた人のためにGitHubのアカウントを1ヶ月分・1年分契約したりする必要があるということになります。仮にJenkinsの頃と比べ20人追加でGitHubアカウントが必要になり、それが年契約だとすると5040ドルです。追加でかかるにはちょっと看過できない金額ですね
加えて、Jenkinsは(あまり役に立ちませんが)ジョブの残り時間、進捗やキュー状況などが直感的なUIで表示されます。GitHub Actionsを利用したことがある方ならわかると思いますが、エンジニアにはわかりやすくても非エンジニアにはちょっとわかりにくい作りをしているのではないでしょうか?定期実行されたジョブの確認方法や手動実行するためにはどのボタンを押せば良いかなどの操作方法のドキュメントを書いたり、問い合わせの対応をしたりして工数が発生するならなんのためにGitHub Actionsへ移行したのかわかりません

JenkinsにおけるダッシュボードのUI
GitHub Actionsにおける手動トリガーのUI

まとめ

今回はゲーム開発の現場でJenkinsが利用され続ける理由について解説しました。近年では色々な記事やカンファレンスでマネージドCIサービスのことが話され、Jenkinsから脱却したというテーマを聞くことも珍しくありません。ゲーム系でもなんとかしてJenkinsを脱却したという他社さんの取り組みの記事を見ることが増えてきましたが、やはりマネージドMacマシンのスペック不足やキャッシュのための巨大なディスクをネットワークマウントした際に起きる実行速度の問題など課題が残っているという話もされています。バックエンドの人間としてはあまりJenkinsをホストしたくないものですが、現状のゲーム開発現場においてJenkinsは非常に素晴らしいソリューションです。とはいえGitHub Actionsが最適な部分もあるので今後もJenkinsと併用していきたいと思います。もしこのあたりの課題がまるっと解決された、いい感じのCIソリューションがあれば教えていただけると幸いです

【Golang】ジェネリクスのメリデメを比較してみる【1.18】

はじめに

Calendar for QualiArts | Advent Calendar 2021 - Qiita20日目担当の鈴木光です
今回はGo言語 1.18について導入されるジェネリクスについてのお話をさせていただきます

Go言語 1.18 Betaの公開

先日Go言語 1.18のBetaが公開され、ついにジェネリクスが本格的に使えるようになってきたと話題になりました
go.dev

Go言語は言語仕様にジェネリクスが入っていなかったため、多くの人々はそれを自動生成で賄ってきました
そこでジェネリクスが入ることでどのように実際の開発が変わるのかを見ていきたいと思うのですが、ジェネリクスとは何かや具体的な記法などは多くの方が詳しく解説してくださっているので、今回は具体的にどう変わるのかということに対してスポットを当てていこうと思います

Go言語におけるジェネリクス

とはいえGo言語しか知らない方向けに簡単に説明させていただきます ジェネリクスとは型パラメータを用いて抽象的なコードを実装することで、特定の型に依存することなく型安全性を維持しながら汎用的なコードを実装する手法のことです
例えばJava言語にはArrayListというリストを表現するための可変長配列のクラスがあります
これは

public class ArrayList<E>

として定義されており、実際には

List list = new ArrayList<String>();

のように使います。ArrayListはListインタフェースを実装しており、型に関わらずadd, remove, sortなどListインタフェースに定義されたあらゆる操作を行うことが可能です
Go言語ではこれを組み込み関数と気合で解決してきましたが、ジェネリクスによって気合が不要になり人類は楽をできるというわけです

実際にGo言語でジェネリクスを使ってみたい方はdevブランチモードのGo Playgroundで簡単に試せます
go.dev

また、こちらがジェネリクスチュートリアルになります
go.dev

従来の手法による実装

前置きはこのくらいにして実際にどうなるのかを見ていきましょう。ただその前に従来の自動生成による実装を考えてみます
まず皆さんは何をソースに自動生成を行っているでしょうか。私のプロジェクトでは数千ファイルをProtocol Buffersによる定義から出力しているのですが、これはマイナーな手法だと思うので今回はソースをGo言語の構造体で定義しているとしましょう。実運用を考えるとGORMのようなORMのModelからリフレクションなどを用いて情報を取得し、関連コードを生成するのが一般的でしょうか

type User struct {
  ID           uint
  Name         string
}

type Users []*User

この構造体に対して以下のようなMap, Filter, Findのようなコレクション操作を行いたい場合、どのような実装があるかを考えてみます

func (u Users) Map (f func(u *User) Users) {}

func (u Users) Filter (f func(u *User) bool) {}

func (u Users) Find (f func(u *User) *User) {}

色々手法はあると思いますが一例を上げてみます

  1. User構造体とUsers型の定義をuser.goに定義する
  2. 同一パッケージ内にUsers型を拡張するメソッドを定義したuser_gen.goのようなファイルを生成する

都度実装しても良いのですがそれは面倒です。しかしGo言語にはジェネリクスがありません。そこでメソッドによる構造体拡張を用いることで全てのModel構造体に対してコレクションメソッドを自動生成するという手法です

問題点

さて、この手法にはいくつか問題点があります
まずは自動生成の手間です。新しいテーブルを追加するたび、あるいはコレクション操作が必要になるたびにこの自動生成を実行しなければなりません
次に自動生成コードのメンテナンスです。自動生成するためのスクリプト、テンプレートをメンテナンスしていかなければなりません。ジェネリクスを用いた場合でも当然そのコードはメンテナンスしなければならないのですが、自動生成の場合一つのツールのメンテナンスとなります。メンテナンスコストはジェネリクスのものよりは重くなりがちです

ジェネリクスを導入することによるメリット・デメリット

メリット

ジェネリクスを導入することでその抽象コードのメンテナンスだけで済みますが、型解決のためコンパイル速度は15%ほど遅くなります

Because of changes in the compiler related to supporting generics, the Go 1.18 compile speed can be roughly 15% slower than the Go 1.17 compile speed. The execution time of the compiled code is not affected. We intend to improve the speed of the compiler in Go 1.19.
https://go.dev/blog/go1.18beta1 より

しかし、これは実際には問題とならないケースもあるでしょう。なぜなら大量のコードを生成していた場合、そのコードを読み込むための時間がかかるためです。リフレクションが存在する限り未参照のコードを削除するというようなことはできませんし、そもそも未参照というのを解決するためにもファイルの読み込みが必要だからです
また、大量のファイルが生成されることによるデメリットはいくつか考えられます。セキュリティソフトの起動フックになったり、そもそも自動生成自体がとても長い時間かかったり、IDEのインデックス再構築を待つ時間があったりとコードのメンテナンス以外のデメリットも存在します

デメリット

コンパイル速度についてもそうですが、ジェネリクスによる実装は複雑になりがちだからです。自動生成コードには全てが記述されていますし、Protocol Buffersなどを使う場合はそもそもProtocol Buffers側でそのような抽象化がサポートされいないため、そのあたりの自動生成コードはそのまま使えます
また、自動生成にしかできないこともあります。 sqlboiler が良い例ですが、リレーションされているコードを静的に呼び出すことなど、モノによって実装があるなしが変わるようなものには当然ジェネリクスは使えません。入力された型パラメータによって戻り値が変化するような実装を現在のジェネリクス仕様では表現することができないためです
さらに、専用の関数を直接呼び出すほうが早いということもありえます。以下のコードをご覧ください。なお、実行はできませんのでご了承ください

go.dev

実行してみたい方は 1.18 beta1系 のDockerコンテナがDocker Hubにアップロードされているので簡単に試すことが可能です。今回は golang:1.18beta1-alpine3.15 イメージを用いてベンチマークを取ってみました。私のPCでの実行結果は次のようなものになっています

/app # go test -bench . -benchmem
goos: linux
goarch: amd64
pkg: example.com
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkCast-8         11541190          102.3 ns/op          35 B/op         2 allocs/op
BenchmarkGenerics-8     11655067          109.7 ns/op          35 B/op         2 allocs/op
PASS
ok      example.com 2.687s
/app # 

ほぼ変わりませんが、若干ジェネリクス用の実装のほうが遅いことがわかります。重要なのは直接変換関数を呼び出しているか、動的に値の型を取得してtype switchしているかです。型ではなく値の型ということがキモです。本来は型情報だけで最適なswitchへ変換するようコンパイル時に判定できそうですが、現状は cannot use type switch on type parameter value というエラーが出ており、この記法はまだ未対応なのでこのような結果となっています。これについては下記issueのとおり最適化を待つしか無いようです

https://github.com/golang/go/issues/45380#issuecomment-851580844

まとめ

今回はジェネリクスが導入されることによるメリット、デメリットを比較してみました。この春から始まるプロジェクトについては最初からジェネリクスありきの設計にすることでより柔軟なコードを実装することが出来ると思います。しかし世の中には既に運用中だったり開発中だったり色々なフェーズのプロジェクトが存在し、ジェネリクスを導入するかどうかは一つの大きな決断になると思います。この記事がそのような人々やプロジェクトの参考になれば幸いです

【Golang】自動生成漏れをCIで検知する【GitHub Actions】

はじめに

QualiArts Advent Calendar 2020 - Qiita、7日目担当の鈴木光です
今回はGo言語とGitHub Actionsについてのお話をさせていただきます

自動生成

Go言語は言語仕様上、自動生成によるコード実装が多い言語です
例えば

  • protobufから生成したコード
  • ORMのコード
  • テストで使うmockコード
  • 何かを元に生成したenumの実装
  • APIドキュメント
  • 単純なCRUD API
  • etc ...

自動生成したコードは普通にgit commitしないといけません
ただ、そのうち自動生成コマンド自体の数が増えてくると、コミットの中に漏れが発生することがあります
もちろん、これらが漏れていたからといってアプリケーションが必ずしも動作しないわけではありません
テスト対象によってはmockコードがなくても通る場合はありますし、そもそもORMなどは漏れること自体があまりないでしょう
しかし、自動生成を忘れた人以外の人がそれらを生成した際に、自分の変更範囲外のものまで生成されるとちょっとアレじゃないでしょうか?
今回はその漏れを見逃さないようにGitHub Actionsを用いてチェックする方法をご紹介します

実装

今回は次の3つについて、GitHub Actions上で自動生成が漏れていないかチェックする実装を行っていきます

  • ORMのコード&単純なCRUD API
  • テストで使うmockコード
  • APIドキュメント

また、基本的に自動生成が漏れていないかチェックするだけなら

  1. コードを生成
  2. git diff --quietでチェック(diffがある場合は終了コードが1になる)

だけで済みます

ORMのコード&単純なCRUD API

現在sqlboilerという既存DBから実装コードを生成するタイプのORMを利用しています
Go言語だとやはりgormをよく見かけますが、sqlboilerはタイプセーフなコードを生成できてパフォーマンスも良いのでお気に入りです
加えてマスターデータを操作する単純なCRUD APIを自動で生成しており、カラム追加時などにちょくちょく忘れてしまうのでチェックしたいと思います(むしろこちらがメイン)
また、DBマイグレーションのツールとしてdbmateを使っています。非常に使い勝手が良いのでぜひ使ってみてください

name: Check autogen db code
on:
  pull_request:
    branches:
      - master
      - 'feature/**'
    paths:
      - 'db/migrations/*' # マイグレーションファイルに変更があった時だけ実行
jobs:
  check-autogen-db:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:5.7
        ports:
          - 3306:3306
        env:
          MYSQL_ROOT_PASSWORD: root
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Setup golang
        uses: actions/setup-go@v2
        with:
          go-version: 1.14.10
      - name: Cache Go Modules
        uses: actions/cache@v2.1.3
        id: cache
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-
      - name: Download Modules
        if: steps.cache.outputs.cache-hit != 'true'
        run: go mod download
      - name: Migration database
        uses: docker://amacneil/dbmate:v1.10.0
        env:
          DATABASE_URL: mysql://root:root@mysql:3306/hoge
        with:
          args: --wait --no-dump-schema up
      - name: Setup tools
        run: GO111MODULE=off go get github.com/volatiletech/sqlboiler github.com/volatiletech/sqlboiler/drivers/sqlboiler-mysql
      - name: Generate entity
        run: sqlboiler mysql --wipe
      - name: Check diff
        run: git add . && git diff --cached --quiet
      - name: Show diff if failure
        if: ${{ failure() }}
        run: git diff --cached
      - name: Generate master api
        run: make gen-masterapi # マスターデータ操作APIを自動生成する独自実装
      - name: Check diff
        run: git add . && git diff --cached --quiet
      - name: Show diff if failure
        if: ${{ failure() }}
        run: git diff --cached

テストで使うmockコード

お次はmockコードの生成です
現在テストライブラリとしてアサーションtestify、mock生成にmockeryを利用しています
自動生成コードなど一部のファイルをテスト対象から外しているケースでそこへ変更があった際に生成を忘れがちです(そもそも生成対象からはずせよっていう話でもありますがw)

name: Check autogen mock code
on:
  pull_request:
    branches:
      - master
      - 'feature/**'
    paths:
      - 'pkg/**'
jobs:
  check-autogen-mock:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Generate mock
        run: rm -rf mock && docker run -v ${PWD}:/src -w /src vektra/mockery:latest --all --dir=./pkg --keeptree --outpkg=mock --output=mock --disable-version-string # dockerコマンドも使える
      - name: Check diff
        run: git add . && git diff --cached --quiet
      - name: Show diff if failure
        if: ${{ failure() }}
        run: git diff --cached

APIドキュメント

最後はAPIドキュメントです
前提として現在のプロジェクトではコードに埋め込んだアノテーションからSwagger Specを生成する手法をとっており、go-swaggerを利用しています
GitHubリポジトリにSwagger Specを置く運用をしているのですが、APIのリクエスト・レスポンスを更新した時に生成を忘れてしまうなどはよくあるユースケースです
そんな時はGitHub ActionsにSwaggerの生成とドキュメントのコミットまでやってもらいましょう
同一リポジトリ内にコミットするなら非常に簡単なのですが、今回はドキュメント専用の別リポジトリに対してコミットしたかったのでdeploy keysを利用します
こちらのリポジトリにはシークレット情報としてDOCS_REPO_DEPLOY_KEYという名前で秘密鍵を設定します。ドキュメント側のリポジトリには公開鍵を登録しておきましょう

name: Generate Swagger Spec
on:
  push:
    branches:
      - master
    paths:
      - 'pkg/presentation/http/router.go'
      - 'pkg/presentation/http/request/**'
      - 'pkg/presentation/http/response/**'
jobs:
  generate-swagger:
    runs-on: ubuntu-latest
    env:
      DOCKER_WORKDIR: /github/workspace
      SPEC_PATH: spec/swagger.yaml
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Swagger specを生成
        uses: docker://quay.io/goswagger/swagger:latest
        with:
          args: generate spec -m -o ${{env.DOCKER_WORKDIR}}/${{env.SPEC_PATH}}
      - name: deploy keyをセットアップ
        env:
          DOCS_REPO_DEPLOY_KEY: ${{secrets.DOCS_REPO_DEPLOY_KEY}}
        run: |
          echo "$DOCS_REPO_DEPLOY_KEY" > ~/deploy_key.pem
          chmod 600 ~/deploy_key.pem
      - name: diffがあればdocs用リポジトリにcommit&push
        env:
          GIT_SSH_COMMAND: ssh -i ~/deploy_key.pem -o StrictHostKeyChecking=no -F /dev/null
          DOCS_REPO: hoge-server-docs
        run: |
          git clone git@github.com:hikyaru-suzuki/$DOCS_REPO.git ../$DOCS_REPO
          cd ../$DOCS_REPO
          mkdir -p ./spec
          rm -f ./${{env.SPEC_PATH}}
          cp $GITHUB_WORKSPACE/${{env.SPEC_PATH}} ./${{env.SPEC_PATH}}
          git add ./${{env.SPEC_PATH}}
          result=0
          $(git diff --cached --quiet) || result=$?
          if [ $result -ne 0 ]; then
            git config --local user.email "action@github.com"
            git config --local user.name "GitHub Action"
            git commit -m "update: swagger spec"
            git push origin master
          fi

まとめ

小さなことですが今回のCI設定で楽になりました
個別の作業はそれぞれ1分もかからないものですが、自動生成の確認は毎回しないといけないので地味にストレスになっていたのかもしれません
自動生成自体はGo言語に限ったことではないですし、何かの参考になれば幸いです

参考

PHPの各種フレームワークにおけるDIについて

qiita.com

QualiArts Advent Calendar 2019、2日目担当の鈴木光です。
タイトルの通り、今回はPHPの各種フレームワークにおけるDIについて書いていこうと思います。

Dependency Injection(DI)

あるクラスが依存するオブジェクトをコンストラクタ、メソッド、セッターなどの引数から注入することでクラスから依存関係を減らすという手法です。テストや仕様変更に強くなるという特徴があり、10年以上前から用いられてきたデザインパターンとなります。
ちなみにDIの説明のためにこちらの記事をとても参考にさせていただきました。

やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something

DIとは

DI自体はそんなに難しいことではありません。以下のようにコンストラクタなど対して依存するオブジェクトを注入すればそれはDIパターンを適用している(DIしている)と言えます。

<?php

class UserController
{
    private $userService;

    public function __construct(UserServiceInterface $userService)
    {
        $this->userService = $userService;
    }
}

DIコンテナ

DIコンテナとはDIする際に利用するインスタンスが登録されたオブジェクトです。このDIコンテナの仕様はPSR(PHP Standards Recommendations = PHP標準勧告)のPSR-11で定義されており、Psr\Container\ContainerInterfaceを継承することで簡単に実装することが可能です。
DIコンテナを使わない場合はnew UserController(new UserService())とするところを、new UserController($container[UserService::class])のようになります。このDIコンテナにインスタンス生成のロジックを集約しようという手法です。
ここで注意していただきたいのはDIするのにDIコンテナは必須ではないというところです。なので次の話に繋がります。
ちなみにDIコンテナの実装をサポートするライブラリとして以下のようなものになります。

サービスロケータ

DIコンテナを使うとこういうことも出来てしまいます。

<?php

class UserController
{
    private $userService;

    public function __construct(ContainerInterface $container)
    {
        $this->userService = $container[UserService::class];
    }
}

これがサービスロケータと呼ばれます。アプリケーションは$containerを通じて任意のインスタンスを取り出すことが出来ます。これの問題点はUserControllerがUserServiceではなくDIコンテナに依存していることです。従ってサービスロケータでは依存するオブジェクトが格段に増えています。
しかしUserControllerでは本当にDIコンテナに登録されている全オブジェクトが必要なのでしょうか。この場合だとUserServiceだけで大丈夫のはずです。しかし注入する側はそんなこと知りません。なので中身を知らない場合は完全に実装されたDIコンテナを注入する必要があります。これはテストの実行速度や保守性に関わってきます。 このことからサービスロケータはアンチパターンとされています。

PHP: The Right Way

PHP各種フレームワークにおけるDI

Symfony

みんな大好きSymfonyです。PHPフレームワークなどで最初に行うcomposer installを眺めていたらだいたい出てきます。
Symfonyではサービスコンテナを利用します。これはサービスクラスが登録されたDIコンテナです。
また、Symfony ComponentとしてSymfony以外のアプリケーションではsymfony/dependency-injectionコンポーネントとして利用出来ます。Symfonyアプリケーションでは前述のサービスコンテナという仕組みを利用します。使い方は次のとおりです。

  1. config/services.yamlで設定ファイルを書く(自動構築されたプロジェクトならデフォルトで書かれている)
  2. コンストラクタやメソッドの引数にサービスコンテナに登録されたインタフェースの型で変数を定義

以上です。これだけでインスタンスがオブジェクトへ注入されます。まるで魔法のようですが、割と他のフレームワークやライブラリでも同じようなことが出来るようになっています。実際はリフレクションを使用して型を検出し定義ファイルと突き合わせて注入しています。
自動で依存関係を作成し注入することをAutowireやAutowireingと呼び、型を検出して注入することをPHPでは型宣言のことをタイプヒントと呼ぶことから、これをタイプヒントやタイプヒンティング(typehint or typehinting)と呼びます。リフレクションを使用するとパフォーマンスが低下するため、複雑なDIライブラリでは事前コンパイルをするなどしてパフォーマンスを向上させているようです。ちなみにアノテーションで注入するライブラリもあります。

例:PHP-DI 6: turning into a compiled container for maximum performances - The Dependency Injection Container for humans

Zend Framework

こちらもみんな大好きZend。composer installで見ることが多いですね。
ZendではPSR-11準拠のDIコンテナを利用します。また、InstanceManager :: addTypePreference()というインスタンスメソッドを呼び出して設定することでAutowiringも可能となります。さらにZend\Di Definitionを定義してリフレクションにかかる時間を短縮することも出来ます。

Laravel

最近流行りのLaravelです。Laravelでもサービスコンテナを利用します。こちらは注入する方式としてサービスプロバイダを利用します。使い方は次のとおりです

  • app/Providers/配下に新規にServiceProviderを実装したクラスを作る or デフォルトで用意されているAppServiceProviderに実装を記述
  • 新規でProviderを作成した場合、config/app.phpのprovidersマップに対象クラスをclassキーワード(::class)でクラス名解決を行い注入するサービスを設定

wiringする仕組みとしてサービスプロバイダを使っているのですね。これによりAutowiringが実現します。また、サービスプロバイダの中でエイリアスとオブジェクトを結合(登録)するわけですが、それにもいろいろな結合方法があります。

  • \Illuminate\Contracts\Foundation\Application::bind()の第2引数のクロージャでnewして結合
  • \Illuminate\Contracts\Foundation\Application::singleton()を使ってシングルトンとして結合
  • \Illuminate\Contracts\Foundation\Application::instance()でnewしたインスタンスを結合
  • \Illuminate\Contracts\Foundation\Application::when()->needs()->give()でプリミティブな値を結合

ちなみにLaravelでは依存オブジェクトを注入しなくても、オブジェクトのメソッドをFacadeという仕組みでクラスメソッドとして呼び出すことができます(なんかDIの意味が薄れてきますが)。

CakePHP

RailsPHPクローンとして一世を風靡したCakePHPさんです。
CakePHPではサービスロケータとして依存解決を行います。このDIコンテナ流れの中、頑なにCakePHPはサービスロケータで生きています。ちなみにまだかなり先ですが4.1のロードマップにPSR-11準拠のDIコンテナの実験的サポートとありますので気長に待っていればそのうち来るかもしれません。

4.1 Roadmap · cakephp/cakephp Wiki · GitHub

例えばCakePHPではORMのテーブルクラスを取得するために次のように実装します。

  • TableRegistry::getTableLocator()->get('Articles');
  • use Cake\ORM\Locator\LocatorAwareTrait;したクラスで$this->getTableLocator()->get('Articles');

CodeIgniter

Phalconが出るまで最速の地位にあったCodeIgniterではフレームワーク側でDIの仕組みは用意されていません。したがって自身で実装する必要があります。

Slim

PHPのマイクロフレームワークです。SlimにはビルドインでPimpleベースのDIコンテナが実装されており、依存解決はこちらを利用します。
SlimのDIコンテナは非常にシンプルな実装で、結合も単純明快です。わかりやすいのでここにも記します。

<?php

$container = new \Slim\Container;
$app = new \Slim\App($container);
$container = $app->getContainer();
$container['myService'] = function ($container) {
    $myService = new MyService();
    return $myService;
};
$app->get('/foo', function ($req, $res, $args) {
    $myService = $this->get('myService');

    return $res;
});

また、\Slim\App::get()などのクロージャ中で$this->myService;のように暗黙的にサービスを取得できます。また、サービスが登録されているかどうかは$this->has('myService')として確認できます。

Yii 2

Yiiにも独自のDIコンテナが存在します。そしてYiiはDIコンテナ上にサービスロケータを実装されており、アプリケーションはそのコンストラクタやメソッドに対して直接オブジェクトを注入するかコンテナを注入することでそのどちらで依存解決を行うか選ぶことが出来ます。
コンテナへの登録はyii\di\Container::set()で行うこと出来ます。複数のサービスを登録するにはyii\di\Container:: setSingletons()yii\di\Container:: setDefinitions()を利用します。ちなみにアプリケーション構成ファイルを用いて設定することも出来ます。

Phalcon

ハイパフォーマンスなフルスタックフレームワークであるPhalconではPhalcon\DiというDIコンテナが実装されています。このインスタンスに対して$di->set()で登録を行い、$di->getService("logger")でコンテナから取り出します。シングルトンとして登録したい場合はPhalcon\Di::set()の第3引数にtrueを設定することでシングルトンになります。

まとめ

いろいろなフレームワークにおけるDIを見てきました。最初はただ依存するオブジェクトを外出ししてnewするタイミングで注入するだけだったのが、フレームワークやライブラリ上にアプリケーションが構築されることを前提とすることでリフレクションやアノテーションを用いて注入されることが多いです。
最近、弊社でもGo言語の機運が高まってきており、Goではそのような注入ではなく自動生成ツールを用いて注入するコードを生成してそれを使ったりします(https://github.com/google/wire)。
自動的に注入されるのは確かに便利ですがこれは宣言的ではありません。最近のPHP7系やPython3.5系のように明示的な型宣言(typehint)、Goのようにコードを積極的に生成してフレームワークはあまり用いず読めばわかるをモットーに保守性を上げていくことが今後の流れなのかもしれません。

Immutableなデータベースの紹介

はじめに

これはCyberAgent 19新卒 エンジニア Advent Calendar 2018の2日目の記事です。

adventar.org 

 これが初エントリーなので自己紹介を。19卒で株式会社CyberAgentに入社する予定の鈴木光です。現在は株式会社CAmotionというスタートアップでサーバサイドエンジニアとして働いています。別のアルバイト先では設計からフロントまでやってます。

技術的にはPHPRDB設計がメインで最近はVue.jsやGoもやっています。ER図大好き。

趣味ではUnityやUE4でゲームを作ったり、MMDで動画作ったり色々やってます。ちなみにお菓子作りが好きでシフォンケーキを焼くのが得意です。

続きを読む