Amplify Gen 2 における pipeline-deploy コマンドがどのようにして実行環境を CI か判別しているのか調べた


  • Amplify Gen 2 には pipeline-deploy というデプロイ用のコマンドがある
  • ローカル環境では実行できないコマンドらしく、どのようにして実行環境がローカルか CI かを判別しているのか気になったので調べた

pipeline-deploy コマンドとは?

CLI commands - React - AWS Amplify Gen 2 Documentation
https://docs.amplify.aws/react/reference/cli-commands/#npx-ampx-pipeline-deploy

npx ampx pipeline-deploy
Deploys the Amplify project in a CI/CD pipeline for a specified Amplify app and branch.

  • Amplify Gen 2 が提供しているコマンドの 1つ
  • Amplify Gen 2 で定義されたリソースを実際の AWS 環境にデプロイしてくれる

具体的に以下のような指定で使用する

npx ampx pipeline-deploy --branch $BRANCH_NAME --app-id $AWS_APP_ID
  • 一方で、このコマンドをユーザーが直接ターミナルに打ち込む機会は恐らく無く、これは Amplify Hosting のビルドで使用されるコマンド
  • 適当な例だが、 Amplify Gen 2 を使用した場合、 Amplify Hosting 側では以下のようなビルド設定が生成される

    version: 1
    applications:
    - backend:
      phases:
        build:
          commands:
            - npm ci --cache .npm --prefer-offline
            - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
  • このように Amplify Hosting のビルド環境で実行され、実際のデプロイを行うというもの

ローカル環境で pipeline-deploy コマンドを実行したらどうなるの?

気になったので実行してみた

% BRANCH_NAME=main # 実際に存在するブランチを指定
% AWS_APP_ID=hogehogehoge # 実際に作成した Amplify Hosting App ID を指定
% npx ampx pipeline-deploy --branch $BRANCH_NAME --app-id $AWS_APP_ID

ampx pipeline-deploy

Command to deploy backends in a custom CI/CD pipeline. This command is not inten
ded to be used locally.

オプション:
  --debug            Print debug logs to the console         [真偽] [デフォルト: false]
  --help             ヘルプを表示                                                 [真偽]
  --branch           Name of the git branch being deployed            [文字列] [必須]
  --app-id           The app id of the target Amplify app             [文字列] [必須]
  --outputs-out-dir  A path to directory where amplify_outputs is written. If no
                     t provided defaults to current process working directory.
                                                                           [文字列]
  --outputs-version  Version of the configuration. Version 0 represents classic
                     amplify-cli config file amplify-configuration and 1 represe
                     nts newer config file amplify_outputs
                                [文字列] [選択してください: "0", "1", "1.1"] [デフォルト: "1.1"]
  --outputs-format   amplify_outputs file format
                    [文字列] [選択してください: "mjs", "json", "json-mobile", "ts", "dart"]

RunningPipelineDeployNotInCiError: It looks like this command is being run outside of a CI/CD workflow.
  • RunningPipelineDeployNotInCiError が発生し、 CI/CD 環境で実行してねという感じでエラーが発生した
  • さて、どのようにしてコマンドの実行環境が CI/CD 環境であることを識別しているのだろうか?

実装コード

  • Amplify Gen 2 の CLI は aws-amplify/amplify-backend というリポジトリでコードが管理されている
  • pipeline-deploy コマンドの実装箇所はこの辺
  • 確認した時点での最新バージョン 1.2.5 のコードを確認する

    • https://github.com/aws-amplify/amplify-backend/blob/%40aws-amplify/backend-cli%401.2.5/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.ts

      import _isCI from 'is-ci';
      ...
      /**
      * Creates top level entry point for deploy command.
      */
      constructor(
      private readonly clientConfigGenerator: ClientConfigGeneratorAdapter,
      private readonly backendDeployer: BackendDeployer,
      private readonly isCiEnvironment: typeof _isCI = _isCI
      ) {
      this.command = 'pipeline-deploy';
      this.describe =
      'Command to deploy backends in a custom CI/CD pipeline. This command is not intended to be used locally.';
      }
      ...
      handler = async (
      args: ArgumentsCamelCase<PipelineDeployCommandOptions>
      ): Promise<void> => {
      if (!this.isCiEnvironment) {
      throw new AmplifyUserError('RunningPipelineDeployNotInCiError', {
      message:
        'It looks like this command is being run outside of a CI/CD workflow.',
      resolution: `To deploy locally use ${format.normalizeAmpxCommand(
        'sandbox'
      )} instead.`,
      });
      }
  • !this.isCiEnvironment というそれっぽいコードを発見

  • 具体的にどのような判定条件を持っているかは実装が見当たらず、そもそも is-ci というパッケージが提供している様子

is-ci というパッケージは何なのか

  • 普通に npm で公開されているパッケージ
  • ソースコードも GitHub で公開されている
  • その名の通り、実行環境が CI 環境かを判定するパッケージらしい
  • 確認時点最新バージョンの v3.0.1 のコードを見てみる

is-ci/index.js at v3.0.1 · watson/is-ci
https://github.com/watson/is-ci/blob/v3.0.1/index.js

'use strict'

module.exports = require('ci-info').isCI
  • うーん、シンプル!
  • たらい回しっぽいが、 ci-info というパッケージがあり、そこから isCI という機能を持ってきてるっぽい
  • is-ci は利便性的に is-ci というパッケージ名で提供したいから用意されたパッケージなのかな

ci-info というパッケージは何なのか

  • これも普通に npm で公開されているパッケージ
  • ソースコードも GitHub で公開されている
  • CI 環境の詳細を取得するパッケージ
  • 早速最新版の v4.0.0 のコードを見てみる

    • https://github.com/watson/ci-info/blob/v4.0.0/index.js

      const vendors = require('./vendors.json')
      
      const env = process.env
      
      ...
      
      vendors.forEach(function (vendor) {
      const envs = Array.isArray(vendor.env) ? vendor.env : [vendor.env]
      const isCI = envs.every(function (obj) {
      return checkEnv(obj)
      })
      
      exports[vendor.constant] = isCI
      
      if (!isCI) {
      return
      }
      
      ...
      
      exports.isCI = !!(
      env.CI !== 'false' && // Bypass all checks if CI env is explicitly set to 'false'
      (env.BUILD_ID || // Jenkins, Cloudbees
      env.BUILD_NUMBER || // Jenkins, TeamCity
      env.CI || // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari
      env.CI_APP_ID || // Appflow
      env.CI_BUILD_ID || // Appflow
      env.CI_BUILD_NUMBER || // Appflow
      env.CI_NAME || // Codeship and others
      env.CONTINUOUS_INTEGRATION || // Travis CI, Cirrus CI
      env.RUN_ID || // TaskCluster, dsari
      exports.name ||
      false)
      )
      
      ...
      
      function checkEnv (obj) {
      // "env": "CIRRUS"
      if (typeof obj === 'string') return !!env[obj]
      
      // "env": { "env": "NODE", "includes": "/app/.heroku/node/bin/node" }
      if ('env' in obj) {
      // Currently there are no other types, uncomment when there are
      // if ('includes' in obj) {
      return env[obj.env] && env[obj.env].includes(obj.includes)
      // }
      }
      if ('any' in obj) {
      return obj.any.some(function (k) {
      return !!env[k]
      })
      }
      return Object.keys(obj).every(function (k) {
      return env[k] === obj[k]
      })
      }
      
  • vendors.json に CI サービスで使用される環境変数データが定義されている

  • それらのベンダー環境変数ごとに checkEnv 関数で検証している

    • 実行環境の環境変数にベンダー環境変数が含まれているか
    • 含まれている場合の、更に細かい条件
      • ベンダー環境変数のデータとして includes が定義されていたら、その値が設定されているか
        • 例: { "env": "NODE", "includes": "/app/.heroku/node/bin/node" }
      • ベンダー環境変数に any が定義されている場合 any のいずれかの値が設定されているか
        • 例: { "any": ["ghprbPullId", "CHANGE_ID"] }
      • ベンダー環境変数にその他のプロパティが定義されている場合、各キーの値が一致しているか
        • 例: { "GITHUB_EVENT_NAME": "pull_request" }
  • それとは別に、シンプルに所定の環境変数が存在しているかをチェックしている

    • CI, BUILD_ID, RUN_ID など

自分でそれっぽい環境変数を設定して CI 判定を騙せるか

  • とりあえず CI という環境変数が設定されていれば良さそう
  • ひとまず is-ci で試す
  • 環境変数 CI を設定して終了コードを見れば良い

これで良さそう

% npx is-ci
% echo $?  
1
% CI=true npx is-ci
% echo $?
0
  • pipeline-deploy コマンドを騙してみる

    % BRANCH_NAME=main # 実際に存在するブランチを指定
    % AWS_APP_ID=hogehogehoge # 実際に作成した Amplify Hosting App ID を指定
    % CI=true npx ampx pipeline-deploy --branch $BRANCH_NAME --app-id $AWS_APP_ID
    [Warning at /amplify-dj7ei5fv7s9yo-main-branch-3428837f4a/data/amplifyData/GraphQLAPI] @predictions is deprecated. This functionality will be removed in the next major release.
    ...
    ✨  Synthesis time: 0.03s
    
    amplify-dj7ei5fv7s9yo-main-branch-3428837f4a:  start: Building 424267a4966cdd641ee920b25b5cdc78c95d4efb78a52ba6ad792da75ca746c7:current_account-current_region
    ...
    amplify-dj7ei5fv7s9yo-main-branch-3428837f4a: deploying... [1/1]
    amplify-dj7ei5fv7s9yo-main-branch-3428837f4a: creating CloudFormation changeset...
    amplify-dj7ei5fv7s9yo-main-branch-3428837f4a-auth179371D7-35XW14TVQV4P | 0/5 | 10:36:25 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack          | amplify-dj7ei5fv7s9yo-main-branch-3428837f4a-auth179371D7-35XW14TVQV4P User Initiated
    ...
    
    ✅  amplify-dj7ei5fv7s9yo-main-branch-3428837f4a
    
    ✨  Deployment time: 114.04s
    
    Outputs:
    amplify-dj7ei5fv7s9yo-main-branch-3428837f4a.allowUnauthenticatedIdentities = true
    ...
    ✨  Total time: 114.06s
    
    
    File written: amplify_outputs.json
  • CloudFormation のデプロイがキックされた!

  • CI=true で無事騙すことが出来た

結論

  • pipeline-deploy コマンドは is-ci, ci-info パッケージを使用して、実行環境が CI であるかを判定している
  • コマンド実行時に CI=true を指定することで、判定を騙してローカルで実行することも出来る

試した環境

% sw_vers        
ProductName:            macOS
ProductVersion:         14.6.1
BuildVersion:           23G93
% npx ampx --version
1.2.5