【テクニカル・上級編】Composerの「Custom Repositories」詳細解説:独自のパッケージサーバーを構築して社内ライブラリをセキュアに管理する – ビルド・パッケージ管理ツール生産性向上バイブル

Composerの神髄:静的JSONカスタムリポジトリによる社内パッケージ管理の極限最適化

開発組織がスケールし、マイクロサービス化やドメイン駆動設計(DDD)によるコードベースの分割が進むにつれて、避けて通れない課題が「共通ライブラリのバージョン管理とセキュアな配信」だ。

世の中には Private Packagist や Satis といった素晴らしいツールが存在する。しかし、Satisのために専用のビルドサーバーを立てて常時稼働させたり、外部の有償SaaSに機密性の高い社内コードのメタデータを預けたりすることが、すべてのインフラ要件に合致するわけではない。特に、閉域網(オンプレミス環境や厳格なVPC内)において、可用性が高く、運用のオーバーヘッドが限りなくゼロに近いシステムを構築することは、DevOpsエンジニアリングの真骨頂である。

今回は、VCS(Git等)の直接参照によるパフォーマンス劣化や、動的なサーバーアプリケーションを排除し、「AWS S3(またはGCS/MinIO)+静的JSON+CI/CDによる完全自動生成」を用いた、最も軽量かつ強靭なComposerカスタムリポジトリの構築手法を解説する。

Composerの内部アーキテクチャ、依存関係解決(Dependency Resolution)のアルゴリズム特性、そしてCI/CDパイプラインを巻き込んだ自動化の全貌を紐解く。

—

1. 内部アーキテクチャの理解:Composerはリポジトリをどう読んでいるのか?

なぜ「静的JSONファイル」だけでComposerは高度なバージョン解決ができるのか。その答えは、Composerのクライアントサイド・アーキテクチャにある。

Composerが `composer require` や `composer update` を実行した際、内部では次のようなシーケンスでデータが処理されている。

1. リポジトリ定義の読み込み:`composer.json` の `repositories` キーからエンドポイント(URL)を取得する。
2. packages.jsonのフェッチ:Composerは、リポジトリのルートにある `packages.json`(または定義されたJSON構造)をHTTP GETで取得する。
3. 依存関係グラフの構築(SATソルバー):取得した全パッケージのメタデータ(バージョン、依存関係、配布アーカイブのURL、ハッシュ値)をメモリ上に展開し、SAT(Boolean Satisfiability Problem)ソルバーを用いて、競合のない最適なバージョン組み合わせを計算する。
4. ZIP/TARGZのダウンロードと展開:決定したバージョンのアーカイブをダウンロードし、`vendor/` ディレクトリへ配置する。

つまり、Composerにとって「リポジトリ」とは、動的なプログラムである必要は一切ない。「特定のスキーマに準拠したJSONファイルを返す静的ストレージ」であれば、何百万リクエストをさばくCDNであっても、社内のローカルストレージであっても、完全に同等に機能するのだ。

—

2. アーキテクチャ設計:静的JSONリポジトリの全体像

今回構築するアーキテクチャは以下の通り極めてシンプルだ。

[社内ライブラリリポジトリ (Git)]
│
▼ (GitHub Actions / GitLab CI)
[ビルド・バリデーション・JSON生成]
│
▼ (同期・アップロード)
[オブジェクトストレージ (AWS S3 / MinIO)] ──(HTTPS)──> [各開発者のPC / 本番CI]

この方式のメリットは計り知れない:

  • ゼロ・ランタイムコスト:PHPプロセスやデータベースが不要。S3のストレージ費用とわずかな転送量だけで稼働する。
  • 無限のスケールと高可用性:CDN(CloudFront等)の背後に置けば、世界中のどの拠点からでもミリ秒単位でメタデータを取得可能。
  • 強力なセキュリティ:IAMポリシーやIP制限、Basic認証(S3の場合は署名付きURLやCloudFront Functions等)により、社外への流出を完全にブロック。

—

3. 実装:最小かつ最強の `packages.json` 構造

静的リポジトリの中核となる `packages.json` の構造を定義する。
Satisなどのジェネレーターを使わずとも、自前のスクリプトやCIでこのJSONを生成・更新すれば、Composerは完全にそれを理解する。

以下は、社内認証基盤ライブラリ `acme/auth-sdk` を配信するための実用的な `packages.json` の例だ。

{
“packages”: {
“acme/auth-sdk”: {
“1.2.0”: {
“name”: “acme/auth-sdk”,
“version”: “1.2.0”,
“version_normalized”: “1.2.0.0”,
“type”: “library”,
“description”: “ACME Corp Unified Authentication SDK for Internal Microservices”,
“keywords”: [“auth”, “jwt”, “oauth2”, “internal”],
“homepage”: “https://git.internal.acme.com/libs/auth-sdk”,
“license”: [“proprietary”],
“authors”: [
{
“name”: “DevOps Core Team”,
“email”: “devops@acme.internal”
}
],
“require”: {
“php”: “^8.2”,
“ext-json”: “”,
“guzzlehttp/guzzle”: “^7.5”
},
“require-dev”: {
“phpunit/phpunit”: “^10.0”
},
“dist”: {
“url”: “https://repo.internal.acme.com/archives/acme/auth-sdk/acme-auth-sdk-v1.2.0.zip”,
“type”: “zip”,
“reference”: “v1.2.0”,
“shasum”: “e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855”
},
“source”: {
“url”: “https://git.internal.acme.com/libs/auth-sdk.git”,
“type”: “git”,
“reference”: “v1.2.0”
}
}
}
}
}

アーキテクチャの急所:`dist` と `source` の使い分け

  • `dist` (推奨):ビルド済みのZIPアーカイブへのリンク。ソースコードの履歴(`.git`)を含まないため、ダウンロードサイズが極小になり、CI/CDのフェッチスピードが劇的に向上する。
  • `source`:Gitリポジトリへの直接参照。開発時にローカルでデバッグしつつコミットをいじりたい場合などに有効だが、本番デプロイや通常のパッケージングでは `dist` を優先すべきだ。`shasum`(SHA-256またはSHA-1)を必ず付与することで、転送時の改ざん検知とComposer側のキャッシュ効率化が最大化される。

—

4. CI/CDパイプラインによる「完全自動化」の実装

手動で `packages.json` を書き換えるなど論外だ。社内ライブラリのGitリポジトリにタグ(例: `v1.2.0`)がプッシュされた瞬間、自動的にビルドが行われ、S3上のメタデータがアトミックに更新されるパイプラインを構築する。

以下は、GitHub Actions を用いた自動化ワークフローの完全な実装コードである。

name: Publish to Private Composer Repository

on:
push:
tags:

  • ‘v’ # セマンティックバージョニングのタグ(例: v1.2.0)がプッシュされた時のみ起動

jobs:
publish:
runs-on: ubuntu-latest

# セキュリティ担保のため、社内ネットワークからアクセス可能なランナー、
# またはOIDC等を用いたクラウド認証を設定する
permissions:
id-token: write
contents: read

steps:

  • name: Checkout Repository

uses: actions/checkout@v4

  • name: Setup PHP Environment

uses: shivammathur/setup-php@v2
with:
php-version: ‘8.2’
tools: composer:v2

  • name: Validate composer.json

run: composer validate –strict

  • name: Create Distribution ZIP Archive

id: archive
run: |
# タグ名からプレフィックス(v)を除いたバージョン文字列を取得 (例: 1.2.0)
VERSION=${GITHUB_REF#refs/tags/v}
PACKAGE_NAME=$(jq -r .name composer.json)
SAFE_NAME=$(echo $PACKAGE_NAME | tr ‘/’ ‘-‘)

ZIP_NAME=”${SAFE_NAME}-v${VERSION}.zip”

# 開発用ファイルやテスト、.gitを除外したクリーンなZIPを作成
git archive –format=zip –prefix=”${SAFE_NAME}-v${VERSION}/” HEAD > “$ZIP_NAME”

# Composerメタデータ用のSHA-1/SHA-256ハッシュを計算
SHASUM=$(sha256sum “$ZIP_NAME” | awk ‘{print $1}’)

echo “version=$VERSION” >> $GITHUB_OUTPUT
echo “zip_name=$ZIP_NAME” >> $GITHUB_OUTPUT
echo “shasum=$SHASUM” >> $GITHUB_OUTPUT
echo “package_name=$PACKAGE_NAME” >> $GITHUB_OUTPUT

  • name: Configure AWS Credentials (OIDC)

uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GithubActionsComposerPublisherRole
aws-region: ap-northeast-1

  • name: Sync Artifacts and Update Repository Metadata

env:
VERSION: ${{ steps.archive.outputs.version }}
ZIP_NAME: ${{ steps.archive.outputs.zip_name }}
SHASUM: ${{ steps.archive.outputs.shasum }}
PACKAGE_NAME: ${{ steps.archive.outputs.package_name }}
S3_BUCKET: s3://repo.internal.acme.com
run: |
# 1. 生成したZIPアーカイブをS3の所定のパスへアップロード
aws s3 cp “$ZIP_NAME” “$S3_BUCKET/archives/$PACKAGE_NAME/$ZIP_NAME” –storage-class STANDARD_IA

# 2. S3から現在の packages.json を取得(存在しない場合は初期テンプレートを作成)
if aws s3 ls “$S3_BUCKET/packages.json”; then
aws s3 cp “$S3_BUCKET/packages.json” packages.json
else
echo ‘{“packages”:{}}’ > packages.json
fi

# 3. jqコマンドを用いて、packages.json に新しいバージョンのメタデータを安全にマージ・挿入
# 既存のパッケージツリーを維持しつつ、該当バージョンのオブジェクトを正確にアペンドする
jq –arg pkg “$PACKAGE_NAME” \
–arg ver “$VERSION” \
–arg url “https://repo.internal.acme.com/archives/$PACKAGE_NAME/$ZIP_NAME” \
–arg sha “$SHASUM” \
–arg ref “v$VERSION” \
‘.packages[$pkg][$ver] = {
name: $pkg,
version: $ver,
version_normalized: ($ver + “.0”),
type: “library”,
dist: {
url: $url,
type: “zip”,
reference: $ref,
shasum: $sha
},
source: {
url: “https://git.internal.acme.com/libs/” + ($pkg | split(“/”)[1]) + “.git”,
type: “git”,
reference: $ref
}
}’ packages.json > packages_updated.json

mv packages_updated.json packages.json

# 4. 更新された packages.json をS3へアトミックにアップロード
aws s3 cp packages.json “$S3_BUCKET/packages.json” –cache-control “max-age=60”

このパイプラインによって、開発者は `git tag v1.2.0 && git push origin v1.2.0` を実行するだけで、インフラ側の手を一切煩わせることなく、社内リポジトリへのパッケージ配信が完全に完了する。

—

5. 消費者側(利用プロジェクト)の設定と最適化ハック

社内リポジトリが構築できたら、実際にそのパッケージを利用するアプリケーション側の設定を行う。ここでも、パフォーマンスとセキュリティを極限まで高めるためのプロの知見を導入する。

アプリケーションの `composer.json` に以下のようにカスタムリポジトリを定義する。

{
“repositories”: [
{
“type”: “composer”,
“url”: “https://repo.internal.acme.com”
},
{
“packagist.org”: false
}
]
}

1. パフォーマンスを劇的に改善する「`packagist.org: false`」の効能

多くの開発者がやりがちなアンチパターンが、社内リポジトリを追加した際にデフォルトの Packagist (packagist.org) をそのまま有効にしておくことだ。
Composerは依存関係を解決する際、登録されているすべてのリポジトリに対してパッケージの存在確認(HTTPリクエスト)を行う性質がある。

もしアプリケーションが公開パッケージしか使っていなくても、社内カスタムリポジトリが追加されていると、Composerは「この社内リポジトリの中に、あの公開パッケージ(例えば `monolog/monolog`)が存在しないか?」を探しにいくため、無駄なHTTP通信が発生し、依存関係解決のスピードが数秒〜数十秒遅延する。

`”packagist.org”: false`(または必要な公開リポジトリ以外を排除するフィルタリング)を適切に行うことで、無駄なネットワークI/Oを根絶し、SATソルバーの計算時間を最小化する。

2. 閉域網・オンプレミスにおける認証の最適化

S3を直接公開したくない場合、CloudFront+Lambda@Edge(またはCloudFront Functions)によるBasic認証やIPアドレス制限をかけることが多い。
Composerからプライベートリポジトリへ認証情報を渡すには、環境変数またはグローバルの `auth.json` を利用する。

CI環境や開発者のローカルで、HTTP Basic認証のトークンを安全に設定する
composer config –global http-basic.repo.internal.acme.com __token__ “YOUR_SECURE_API_TOKEN_OR_BASIC_HASH”

これにより、コードベースにハードコードすることなく、セキュアにプライベートリポジトリへのアクセスが可能になる。

—

6. 現場のトラブルシューティングと運用知見

最後に、この静的リポジトリ運用において現場で直面しがちな罠と、その回避策(DevOpsの知見)を共有する。

トラブル1:Composerが古いキャッシュを参照し続けて新しいバージョンを見つけられない

  • 原因:Composerはパフォーマンス最適化のために、ダウンロードした `packages.json` やZIPのメタデータをローカル(`~/.cache/composer`)に強くキャッシュする。そのため、S3側のJSONが更新されても、開発者のローカル環境で古いバージョンがキャッシュされ続けることがある。
  • 解決策:

CIやローカルで強制的にキャッシュをクリアして最新化するコマンドを定常運用に組み込む。

composer clear-cache
# または特定のパッケージのみキャッシュをパージ
composer update acme/auth-sdk –lock

また、S3上の `packages.json` のレスポンスヘッダーに適切な `–cache-control “max-age=60″`(1分程度の短いキャッシュ)を設定し、CDN側で古いメタデータが永続化されないよう設計するのが鉄則だ。

トラブル2:複数人が同時にタグを切った際のマージ競合

  • 原因:GitHub Actionsなどが同時に走った場合、S3上の `packages.json` の読み込みから書き込みまでの間に競合(Race Condition)が発生し、片方のパッケージ情報が上書きされて消える恐れがある。
  • 解決策:

大規模な組織や複数チームが並行して頻繁にライブラリをリリースする場合は、S3の単一JSONファイルを直接書き換える方式ではなく、S3の `packages/%package%.json` のように、パッケージごとにJSONファイルを分割するマルチファイル形式(Composerの高度なリポジトリ仕様)を採用するか、CI側でS3の楽観的ロック(Versioning & If-Match)を実装するべきである。
初期〜中規模のフェーズであれば、今回紹介したアトミックなjqマージで十分実用に耐えうるが、組織のスケールに合わせてファイル分割方式への移行をロードマップに入れておこう。

—

総括

SatisやPrivate Packagistという「既製品」に頼らずとも、Git、GitHub Actions、そしてAWS S3という、どのインフラエンジニアにとっても馴染み深いプリミティブな道具を組み合わせるだけで、極めて堅牢で、メンテナンスフリー、かつ最高速のプライベートComposerリポジトリを構築できる。

ツールに依存するのではなく、「Composerというクライアントが何を欲し、どう動いているのか」という低レイヤの仕様を完全に掌握することこそが、真のDevOpsエンジニアリングであり、荒波のようなシステム変更にも揺るぎない開発基盤を支える最大の武器となる。

タイトルとURLをコピーしました