kentarokentaro

Artifact RegistryにNode.jsのライブラリを作成する【前編】

2025-12-01

はじめに

こんにちは。SRE チーム所属の kentaro です。

業務を進めていく中でpnpmで管理しているモノレポのNode.jsのライブラリを、チーム向けに公開するということを行いました。

その中で色々と躓いた点などがあったため、私と同じようにチーム内でArtifact Registryでnpm用のプライベートリポジトリを作ろうと思っている方に向けて記録を残しておきます。

この記事の趣旨は下記の通りです。

  1. 作成したライブラリをArtifact Registryにnpm用のプライベートリポジトリに登録
  2. Next.js側で作成したライブラリをinstallして利用
  3. 最後にNext.jsをDockerでビルドしてコンテナ化を行いDocker用のArtifact Registryのプライベートリポジトリに登録

書いていると長くなってしまったため、前編と後編に分けます。

前編では上記の1の項目までを取り扱います。

そのためこの記事ではまずGoogle CloudのPrivate Artifact Registry上にnpm用のプライベートのリポジトリを作り、そこに作成したライブラリを登録するまでの流れを行います。

前提条件

前提条件としてここでは下記のような条件でライブラリの登録からCI/CDの整備、及びライブラリの登録を行います。

  • RegistryはGoogle CloudのPrivateのArtifact Registryを利用
  • CI/CDにはGitHub Actionsを利用
  • 作成するライブラリのパッケージマネージャにはpnpmを利用

Node.jsとpnpmのバージョンは下記の通りです。

  • Node.js version 22.13.0
  • pnpm version 10.0.0

またビルドツールとしてviteを利用します。

viteは下記の通り6.2.2のものを利用します。

  • vite 6.2.2

主な流れ

流れとしては下記の流れで行います。

  • Google Cloud Artifact Registryにnpm用のプライベートのリポジトリを作成
  • 作成したコードをArtifact Registryへ登録するためにサービスアカウントの作成
  • pnpmを用いてモノレポでコードを作成
  • 作成したコードをnpm用のプライベートリポジトリへ登録

Google Cloud Artifact Registryにnpm用のプライベートのリポジトリを作成

まずはgcloudコマンドを利用してArtifact Registry上にプライベートのリポジトリを作成します。

# それぞれ適宜置き換えてください
gcloud artifacts repositories create ${REPOSITORY_NAME} \
    --repository-format=npm \
    --location=asia-northeast1 \
    --description="This is a test registry." \
    --project=${PROJECT_ID}

参考:https://docs.cloud.google.com/artifact-registry/docs/repositories/create-repos?hl=ja

これでnpm用のプライベートリポジトリの作成周りは完了です。

作成したコードをArtifact Registryへ登録するためにサービスアカウントの作成

作成ができたら今度はCI/CDで利用するサービスアカウントを作成します。

下記のgcloudコマンドで作成します。

# それぞれ適宜置き換えてください
gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
  --description="DESCRIPTION" \
  --display-name="DISPLAY_NAME"

参考:https://docs.cloud.google.com/iam/docs/service-accounts-create?hl=ja#gcloud

作成するCI/CDのワークフロー上でArtifact Registryのプライベートリポジトリへの書き込み権限を付与してあげます。

# それぞれ適宜置き換えてください
gcloud projects add-iam-policy-binding ${PROJECT} \
   --member=serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \
   --role=roles/artifactregistry.writer

下記のような表示が出ますが、今回はテストなのでNoneを選択しましょう。

The policy contains bindings with conditions, so specifying a condition is required when adding a binding. Please specify a condition.:

これでサービスアカウントの作成と設定は完了です。

pnpmを用いてモノレポでコードを作成

今度は作成したプライベートリポジトリにpnpmのライブラリを登録するためのテスト用のコードを作成します。

まずはモノレポを作りたいディレクトリに移動して、下記のコマンドを用いてpnpmでpackage.jsonを作成します。

pnpm init

次にpnpm-workspace.yamlを作成します。

touch pnpm-workspace.yaml

下記のようにpackagesを指定します。

packages:
  - 'packages/*'

packages配下に適当にディレクトリを切って、今回のnpm用のプライベートリポジトリに登録する自作のライブラリを追加します。

packages/test-lib/

作成したパッケージに移動して、package.jsonを作成します。

cd packages/test-lib
pnpm init

package.jsonは下記のようにします。

下記の@hogehogeの箇所は自由に設定してください。

ただしメジャーなものと被らないように注意しましょう。

{
  "name": "@hogehoge/test-lib",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "engines": {
    "node": "22.13.0",
    "npm": "forbidden, use pnpm",
    "pnpm": ">=10",
    "yarn": "forbidden, use pnpm"
  },
  "scripts": {
    "dev": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module"
}

作成したら同階層内にindex.jsを作成し、下記のようします。

export function hello() {
  console.log('hello pnpm')
}

// 一旦テストで動かすために実行します。Artifact Registryに追加する時は削除しておきます。
hello()

上記が完了したらrootのpackage.jsonに下記を追加します。

"scripts": {
  "test-lib": "pnpm --filter test-lib"
},

追加したらrootで下記のコマンドを打つとhello pnpmと出るはずです。

pnpm test-lib dev

次にビルドできるようにviteをルートのパッケージに追加します。

下記のコマンドでviteをルートレベルで追加します。

pnpm add --workspace-root --save-dev --save-exact vite@6.2.2

追加できたらtest-lib配下にvite.config.jsファイルを作成します。

中身は下記のようにしましょう。

import { resolve } from "node:path";
import { defineConfig } from "vite";

export default defineConfig({
    build: {
        lib: {
            // entry point of library
            entry: resolve(__dirname, "index.js"),
            // name of library
            name: "test-lib",
            // filename
            fileName: (format, entryName) => `${entryName}.${format}.js`,
            // format
            formats: ["es", "cjs"],
        },
        rollupOptions: {
            output: {
                preserveModules: true,
            },
        },
        ssr: {
            target: "node", // you can use 'node' or 'webworker'
        },
    },
});

vite.config.jsについて

追加したvite.config.jsについて少し説明します。

build libについて

下記のlib指定は通常のウェブサイト用のビルドではなく、ライブラリ用のビルドモードで利用するという設定になっています。

build: {
  lib: {
    // ...
  },
}

entry point of libraryについて

下記の箇所については、ライブラリのエントリポイントとなるファイルがvite.config.jsの同階層内のindex.jsを指定するという意味です。

entry: resolve(__dirname, 'index.js')

エントリポイントとは、ライブラリを利用する時に一番最初に読み込まれるファイルを指しています。

filenameについて

filenameの箇所は出力されるファイル名を指定しています。

下記だと例えばindex.es.jsやindex.cjs.jsなどの形式に出力されるようにしています。

fileName: (format, entryName) => `${entryName}.${format}.js`

rollupOptionsについて

下記の設定はtrueにすることによって、元のファイル構造やインポート関係を維持したまま出力してくれるようになります。

viteを使用してビルドをすると、内部でutil.jsを利用していてもエントリポイントのファイルに纏められてしまいます。

ただrollupOptionsをtrueに設定することで、index.es.jsとutil.es.jsなどが単体で生成されるようになります。

rollupOptions: {
    output: {
        preserveModules: true,
    },
},

vite.config.jsを追加したらtest-lib配下のpackage.jsonのscriptsの箇所にbuildのコマンドを追加します。

"scripts": {
  "dev": "node index.js",
  "build": "vite build"
}

ルートディレクトリに戻り下記のコマンドを実行します。

pnpm test-lib build

するとdistディレクトリが生成されます。

作成したコードをnpm用のArtifact Registryのプライベートリポジトリへ登録

コードもできたところで作成したコードをGoogle CloudのArtifact Registryのプライベートリポジトリへ登録していきます。

ここでのステップとしては下記のステップで実行していきます。

  • Workload Identity Federationの作成と設定
  • GitHub Actions上でワークフローを作成し、Artifact Registryへ登録

Workload Identity Federationの作成と設定

GitHub ActionsでWorkload Identity Federationへ登録を行うには、下記のactionを利用します。

上記のactionの中にWorkload Identity Federationへサービスアカウントを利用してデプロイするための方法が記載されているため、こちらを参照して設定していきます。

すでにサービスアカウントの作成は行っているため、2. Create a Workload Identity Pool:から始めていきます。

下記のコマンドでWorkload Identity Poolを作成します。

gcloud iam workload-identity-pools create "github-test" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="Test GitHub Actions Pool"

下記のコマンドで上記で作成したWorkload Identity PoolのIDを取得します。

(これは後々のGitHub Actionsのワークフローで利用します。)

gcloud iam workload-identity-pools describe "github-test" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"

下記のコマンドでWorkload IdentityのProviderを作成します。

gcloud iam workload-identity-pools providers create-oidc "${REPOSITORY_NAME}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="github-test" \
  --display-name="My GitHub repo Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'" \
  --issuer-uri="https://token.actions.githubusercontent.com"

下記のコマンドで作成したサービスアカウントでWorkload Identity Poolで認証されるように権限を割り当てます。

gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPOSITORY_NAME}"

${REPOSITORY_NAME}についてはuser_nameまたはorg_name/repo_nameなどになっている場合は、user_name or org_name/repo_nameを指定します。

${WORKLOAD_IDENTITY_POOL_ID}には下記で取得したIDを指定します。

gcloud iam workload-identity-pools describe "github-test" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"

またWorkload Identity Federationを通してトークンを発行できるようにroles/iam.serviceAccountTokenCreatorをサービスアカウントに割り当てておきます。

# それぞれ適宜置き換えてください
gcloud projects add-iam-policy-binding ${PROJECT} \
   --member=serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \
   --role=roles/iam.serviceAccountTokenCreator

よりセキュアな方法としてrepositoryやrepository_ownerなどのname属性を利用するのではなく、一意な値を利用する方法が作成方法の中で紹介されています。

今回はデモ的な意味合いで作成するためスルーしますが、実際の利用では上記を利用するようにした方が良いでしょう。

これでWorkload Identity Federationの作成と設定は完了です。

GitHub Actions上でワークフローを作成し、Artifact Registryのプライベートリポジトリへ登録

諸々設定できたため、今回作成したライブラリを実際にArtifact Registryのプライベートリポジトリへ登録していきます。

GitHub Actionsは下記を利用します。

$で始まる各変数は適宜置き換えてください。

また下記のアクションはトリガーされるイベントにpushイベントを利用していますが、必要に応じて適宜置き換えてください。

name: register pnpm library to artifact registry

on:
  push:
    branches:
      - feature/**

permissions: {}
jobs:
  register:
    permissions:
      contents: read
      id-token: write
    env:
      COREPACK_DEFAULT_TO_LATEST: 0
      REPOSITORY_NAME: $ARTIFACT_REGISTRY_REPOSITORY_NAME
    runs-on: ubuntu-latest
    timeout-minutes: 30
    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Authentication with Google Cloud
        id: google-auth
        uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: $WORKLOAD_IDENTITY_PROVIDER
          service_account: $SERVICE_ACCOUNT
          token_format: access_token

      - name: Generate registry path
        shell: bash
        id: registry-path
        env:
          RP_PROJECT: $GOOGLE_CLOUD_PROJECT_ID
          RP_LOCATION: asia-northeast1
        run: |
          echo "registry_path=${{ env.RP_LOCATION }}-npm.pkg.dev/${{ env.RP_PROJECT }}/${{ env.REPOSITORY_NAME }}/" >> $GITHUB_OUTPUT

      - name: Enable corepack
        run: corepack enable

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version-file: $NODE_VERSION_FILE # rootのpackage.jsonを指定します
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile=true

      - name: Build
        env:
          PACKAGE_NAME: test-lib
        run: pnpm ${{ env.PACKAGE_NAME }} build

      - name: Create .npmrc
        working-directory: &package_working_directory $WORKING_DIRECTORY # test-libまでのディレクトリを指定します
        env:
          REGISTRY_URL: 'https://${{ steps.registry-path.outputs.registry_path }}'
          REGISTRY_NAME: ${{ steps.registry-path.outputs.registry_path }}
        run: |
          touch .npmrc
          cat <<EOF > .npmrc
            @${{ env.REPOSITORY_NAME }}:registry=${{ env.REGISTRY_URL }}
            //${{ env.REGISTRY_NAME }}:_authToken="${{ steps.google-auth.outputs.access_token }}"
            //${{ env.REGISTRY_NAME }}:always-auth=true
          EOF

      - name: Extract version tag from github.ref
        id: version-tag
        env:
          GIT_REF: ${{ github.ref }}
        run: echo "value=$(echo ${{ env.GIT_REF }} | awk -F'/' '{print $NF}')" >> $GITHUB_OUTPUT

      - name: Update package.json version
        working-directory: *package_working_directory
        run: |
          if [[ ${{ steps.version-tag.outputs.value }} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git config --local user.name "github-actions[bot]"
            npm version ${{ steps.version-tag.outputs.value }}
          else
            echo "No version found. Skipping update of package.json version..."
          fi

      - name: Publish Package
        working-directory: *package_working_directory
        run: |
          if [[ ${{ steps.version-tag.outputs.value }} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            npm publish --scope=${{ env.REPOSITORY_NAME }} --tag latest
          else
            npm publish --scope=${{ env.REPOSITORY_NAME }} --tag alpha
          fi

上記のGitHub Actionのコードについて解説します。

COREPACK_DEFAULT_TO_LATESTについて

COREPACK_DEFAULT_TO_LATESTを0にしているのは、上記のワークフローを実行している時に下記のエラーを回避するためです。

Cannot find matching keyid:

これは下記issueの回答を見るに、npmレジストリ上のパッケージに対して署名するキーのローテーションが発生し、pnpm, npmではキーがローテーションされているが、CI環境ではキャッシュが利用されそのローテーションされたキーと不一致が発生するようです。

https://github.com/nodejs/corepack/issues/612#issuecomment-2746125395

COREPACK_DEFAULT_TO_LATEST=0にすることで、指定したパッケージマネージャの最新バージョンものを見に行かないように設定ができます。

COREPACK_DEFAULT_TO_LATEST can be set to 0 in order to instruct Corepack not to lookup on the remote registry for the latest version of the selected package manager, and to not update the Last Known Good version when it downloads a new version of the same major line.

つまりCOREPACK_DEFAULT_TO_LATEST を 0 に設定すると、Corepackに対して以下の2つのような動作を行うようになります。

  • (例:現行のpnpmがv10.0.0でv10.1.0が利用可能だとしても)その最新バージョン(v10.1.0)を探すために、リモートレジストリへ確認しに行かないようにする。

  • (仮に)同じメジャーラインの新しいバージョンをダウンロードしたとしても、「最終正常起動バージョン(LKG)」の記録を更新しないようにする。

Corepackについて

CorepackはNode.jsのデフォルトインストールで配布される、パッケージマネージャのバージョンを管理するツールです。

今回利用しているNode.jsのv22.13.0でも利用できるため、利用するようにしました。

Node.js: v22.13.0

そのためワークフロー内でもactions/setup-nodeを利用するようにしています。

Authentication with Google Cloudについて

Authentication with Google Cloudでは、下記のようにaccess_tokenを指定しています。

- name: Authentication with Google Cloud
  id: google-auth
  uses: google-github-actions/auth@v3
  with:
    workload_identity_provider: $WORKLOAD_IDENTITY_PROVIDER
    service_account: $SERVICE_ACCOUNT
    token_format: access_token

これによりワークフローの中で生成されたトークンを利用することができます。

下記のPrerequisitesにあるように、.gitignoreや.dockerignoreにgha-creds-*.jsonを無視するように設定を入れましょう。

Prerequisites

Create .npmrcについて

これは下記pnpmの公式に記載があるように、Artifact Registryの認証を突破するための処理です。

:_authToken

Extract version tag from github.refとUpdate package.json versionについて

実際にパッケージをリリースする時はバージョンタグを切って、リリースを行うことがほとんどだと思います。

そのため下記のワークフローでバージョンタグからバージョンを取得します。

- name: Extract version tag from github.ref
  id: version-tag
  env:
    GIT_REF: ${{ github.ref }}
  run: echo "value=$(echo ${{ env.GIT_REF }} | awk -F'/' '{print $NF}')" >> $GITHUB_OUTPUT

バージョンを取得したら、下記のstepで取得したバージョンをpackage.jsonの箇所にセットしてバージョンタグを切ったらそのバージョンが自動的にpackage.jsonに反映されるようにしています。

  - name: Update package.json version
    working-directory: *package_working_directory
    run: |
      if [[ ${{ steps.version-tag.outputs.value }} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
        git config --local user.name "github-actions[bot]"
        npm version ${{ steps.version-tag.outputs.value }}
      else
        echo "No version found. Skipping 'Update package.json version' step."
      fi

上記の41898282+github-actions[bot]@users.noreply.github.comのアドレスについては、公式のドキュメントで明確に記載はありませんが、どうやらgithub-actionsのbot用のものらしいです。

Publish Packageについて

Publish Packageの箇所でlatestとalphaを用意しています。

alpha版は主に動作確認用、latestは正式リリース時に付与するようにしました。

動作確認をする時は、v0.0.0-alphaなどでタグを切ってalpha版をリリースして動作確認をするようにしています。

まとめ

今回はpnpmで管理されたライブラリをGoogle CloudのPrivate Artifact RegistryへGitHub Actionsを利用して登録するところまでを紹介しました。

次回はNext.jsで今回登録したライブラリをインストールして利用、またライブラリをインストールしたNext.jsをDockerでビルドしてコンテナ化し、Artifact Registryのプライベートリポジトリへ登録するところまでを行います。

また、弊社 Belong では一緒に働く仲間を募集しています。

興味がある方は エンジニアリングチーム紹介ページ をご覧いただけると幸いです。

No table of contents available for this content