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 のコードを確認する - 
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