はじめに
AWS CodePipelineのソースステージを、個人用GitHubリポジトリから組織用GitHubリポジトリへ一括移行する機会がありました。数十本のパイプラインが対象で、手作業では現実的でないため、シェルスクリプトで自動化しました。
この記事では、移行の手順に沿って各スクリプトが何をしているかを解説します。各パイプラインの変更対象は以下の2つだけです。
| パラメータ | 変更前 | 変更後 |
|---|---|---|
| ConnectionArn | 個人用CodeStar Connection ARN | 組織用CodeStar Connection ARN |
| FullRepositoryId | old-account-name/repo |
new-org-name/repo |
実行環境はAWS CloudShell(Amazon Linux, Bash)です。スクリプト全体は以下のリポジトリで公開しています。
https://github.com/keiichi-kyoei/script-codepipeline
目次
- 全体の流れ
- Phase 1: 環境変数の設定 — setup.sh
- Phase 1: パイプライン一覧の取得 — get_pipeline_list.sh
- Phase 2: バックアップの取得 — backup_pipelines.sh
- [Phase 2: バックアップの検証 — verify_backup.sh
- Phase 3: 変更用JSONの作成 — create_updated_json.sh
- Phase 3: 変更内容の差分確認 — verify_changes.sh
- Phase 3: パイプラインの一括更新 — execute_update.sh
- Phase 4: 更新後の設定確認 — verify_settings.sh
- Phase 5: 実行テスト — test_pipeline_execution.sh
- まとめ
1. 全体の流れ
移行は5つのPhaseで構成しています。
ざっくり説明すると
①現状のPipelineの設定をJSONファイルでバックアップ保存
②JSONファイルを更新
③更新されたJSONファイルを適用
という具合です。
CodePipelineの場合、「特定のパラメータを変更する」という操作が不可なので、わざわざ上記のような手順で作業を行っています。
Phase 1: 準備
source setup.sh → 環境変数の設定・作業ディレクトリの作成
bash get_pipeline_list.sh → 対象パイプライン一覧の取得
Phase 2: バックアップ
bash backup_pipelines.sh → 全パイプライン設定のJSON取得
bash verify_backup.sh → バックアップ件数の検証
Phase 3: 変更実施
bash create_updated_json.sh → 変更済みJSONの作成
bash verify_changes.sh → 変更前後の差分確認
bash execute_update.sh → パイプライン一括更新
Phase 4: 動作検証
bash verify_settings.sh → 更新後の設定値確認
bash test_pipeline_execution.sh → サンプルパイプラインの実行テスト
Phase 5: 完了
bash create_summary.sh → 結果サマリ(Markdown)の作成
CloudShell上に作成する作業ディレクトリは以下の構成です。
~/pipeline-migration/{YYYYMMDD}/
backup/ : 変更前パイプライン設定JSON
updated/ : 変更後JSON(update-pipeline入力用)
logs/ : 各種ログ・結果CSV・サマリ
verify/ : 検証用ファイル
2. Phase 1: 環境変数の設定 — setup.sh
スクリプト: setup_prod.sh
最初に環境変数を設定し、後続の全スクリプトから参照できるようにします。
source setup.sh # ← bash ではなく source で実行する
なぜ source なのか
sourceはスクリプトを現在のシェルプロセス内で実行します。これにより、スクリプト内でexportした変数がそのまま残ります。bashで実行すると子プロセスが起動されるため、スクリプト終了と同時に変数が消えてしまいます。
当方はスクリプト全然だめですので、当然この罠にはまりました。
今回は環境変数の設定を行うこのスクリプトだけsourceで実行し、残りは全てbashで実行する設計にしています。
スクリプトの中身(抜粋)
# 作業日付をコマンド置換で取得
WORK_DATE=$(date +%Y%m%d)
# ブレース展開で4つのサブディレクトリを一括作成
WORK_DIR=~/pipeline-migration/${WORK_DATE}
mkdir -p ${WORK_DIR}/{backup,updated,logs,verify}
# 子プロセスにも引き継ぐため export で環境変数を定義
export NEW_CONNECTION_ARN="arn:aws:codeconnections:ap-northeast-1:123456789012:connection/xxxxxxxx-..."
export NEW_ORG_NAME="new-org-name"
export OLD_ACCOUNT_NAME="old-account-name"
export TARGET_ENV="prod"
export WORK_DATE=${WORK_DATE}
export WORK_DIR=${WORK_DIR}
ポイント:
$(date +%Y%m%d)— コマンドの実行結果を変数に代入する{backup,updated,logs,verify}— カンマ区切りの各要素に展開され、mkdirを1行で書けるexport— 変数を環境変数にする。exportしないとシェル変数(現在のシェルのみ)にしかならず、bashで起動した後続スクリプトから参照できない
3. Phase 1: パイプライン一覧の取得 — get_pipeline_list.sh
スクリプト: get_pipeline_list.sh
AWSアカウント内の全パイプラインを取得し、一覧ファイルを作成します。
なお、パイプラインのうち、本変更を適用したくないパイプラインは除外しています。
Step 1: AWS CLIで全パイプライン名を取得
aws codepipeline list-pipelines --query 'pipelines[*].name' --output text | tr '\t' '\n' > pipeline_list_all.txt
この1行の中に3つの処理が入っています。順に追ってみましょう。
① AWS CLIでパイプライン一覧を取得
aws codepipeline list-pipelines --query 'pipelines[*].name' --output text
--query 'pipelines[*].name'
→ レスポンスJSONからnameフィールドだけを抽出する(JMESPath式)
--output text
→ JSON形式ではなくプレーンテキストで出力する
出力: pipeline-a pipeline-b pipeline-c ...(タブ区切りの1行)
② tr でタブを改行に変換
| tr '\t' '\n'
変換前: pipeline-a pipeline-b pipeline-c
変換後: pipeline-a
pipeline-b
pipeline-c
③ ファイルに保存
> pipeline_list_all.txt
tr は translate(変換)の略で、文字単位の置換を行うコマンドです。
Step 2: 除外リストを配列で定義する
EXCLUDE_PIPELINES=(
"prod-s3-event-service-a-website-pipeline"
"prod-s3-event-service-b-website-pipeline"
"prod-s3-event-service-c-website-pipeline"
)
( ) で囲むとBashの配列になります。「除外したいパイプライン名を入れた箱」をイメージしてください。
箱の中身:
[0] → "prod-s3-event-service-a-website-pipeline"
[1] → "prod-s3-event-service-b-website-pipeline"
[2] → "prod-s3-event-service-c-website-pipeline"
Step 3: 配列から | 区切りの正規表現パターンを作る
EXCLUDE_PATTERN=$(printf "|%s" "${EXCLUDE_PIPELINES[@]}")
分解して見ていきます。
printf "|%s" とは?
printf printf は第1引数の「フォーマット文字列」をテンプレートとして、残りの引数を埋め込んで出力するコマンドです。
例
printf "Hello %s, you are %d years old\n" "Alice" 30
# => Hello Alice, you are 30 years old
第一引数:"Hello %s, you are %d years old\n"
第二引数:"Alice"
第三引数:30
|%s — 「ここに文字列(string)を埋め込む」という指定子。後ろの引数から順番に対応し、この場合は "Alice" が入ります。
%d「ここに整数(decimal)を埋め込む」という指定子。この場合は 30 が入ります。
\n — 改行を意味するエスケープシーケンスです。printf は echo と違い、末尾に自動で改行を入れないため、明示的に書く必要があります。
では手順のスクリプトに戻ります。
EXCLUDE_PATTERN=$(printf "|%s" "${EXCLUDE_PIPELINES[@]}")
"${EXCLUDE_PIPELINES[@]}"は配列の全要素を個別の引数として取り出す書き方です。
つまり以下と同じです。
printf "|%s" "prod-s3-event-service-a-website-pipeline" "prod-s3-event-service-b-website-pipeline" "prod-s3-event-service-c-website-pipeline"
全部つながって、EXCLUDE_PATTERN にはこう入ります。
|prod-s3-event-service-a-website-pipeline|prod-s3-event-service-b-website-pipeline|prod-s3-event-service-c-website-pipeline
Step 4: 先頭の余分な | を取り除く
EXCLUDE_PATTERN="${EXCLUDE_PATTERN:1}" # 先頭の | を除去
${変数:1} は「1文字目から末尾まで」を切り出します(0始まり)。先頭の | が邪魔なので削除しています。
変換前: |prod-s3-event-service-a-...|prod-s3-event-service-b-...|prod-s3-event-service-c-...
↑ これが邪魔
変換後: prod-s3-event-service-a-...|prod-s3-event-service-b-...|prod-s3-event-service-c-...
この結果は正規表現のOR条件(A または B または C)として使えます。
Step 5: 一覧ファイルから除外する
grep -vE "^(${EXCLUDE_PATTERN})$" pipeline_list_all.txt > pipeline_list_bulk.txt
変数を展開すると、実際にはこう実行されます。
grep -vE "^(prod-s3-event-service-a-website-pipeline|prod-s3-event-service-b-website-pipeline|prod-s3-event-service-c-website-pipeline)$" pipeline_list_all.txt > pipeline_list_bulk.txt
各部分の意味:
| 要素 | 意味 |
|---|---|
-v |
マッチした行を除外する(通常のgrepと逆) |
-E |
拡張正規表現を有効にする |
^ |
行の先頭 |
$ |
行の末尾 |
(A\|B\|C) |
A または B または C に一致 |
^ と $ で挟むことで行全体の完全一致になります。これがないと、たとえば service-a が別のパイプライン名の一部にもマッチしてしまう恐れがあります。
全体の流れ図:
pipeline_list_all.txt(全50本)
├── prod-api-gateway-pipeline
├── prod-lambda-function-pipeline
├── prod-s3-event-service-a-website-pipeline ← 除外
├── prod-s3-event-service-b-website-pipeline ← 除外
├── prod-s3-event-service-c-website-pipeline ← 除外
├── prod-frontend-pipeline
└── ...(残り)
↓ grep -vE で3本を除外
pipeline_list_bulk.txt(47本)
├── prod-api-gateway-pipeline
├── prod-lambda-function-pipeline
├── prod-frontend-pipeline
└── ...(残り)
除外対象が増えても配列に1行追加するだけで済むのが利点です。
4. Phase 2: バックアップの取得 — backup_pipelines.sh
スクリプト: backup_pipelines.sh
変更前の状態を保全するため、全パイプラインの設定JSONを取得して個別ファイルに保存します。
while read PIPELINE_NAME; do
echo "バックアップ中: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
aws codepipeline get-pipeline --name "${PIPELINE_NAME}" \
> "${BACKUP_DIR}/${PIPELINE_NAME}.json" 2>> ${LOG_FILE}
if [ $? -eq 0 ]; then
echo " 成功: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
else
echo " 失敗: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
fi
done < ${PIPELINE_LIST}
このスクリプトで使っている主な構文を整理します。
while read ループ
done — 標準出力(正常なJSON)をバックアップファイルへ
2>>— 標準エラー出力(エラーメッセージ)をログファイルへ追記
2 はファイル記述子番号(0=標準入力, 1=標準出力, 2=標準エラー出力)です。
終了コード $?
$? は直前のコマンドの終了コードを保持する特殊変数です。0 なら成功、それ以外は失敗を意味します。-eq は数値比較演算子(equal)です。
5. Phase 2: バックアップの検証 — verify_backup.sh
スクリプト: verify_backup.sh
バックアップ件数とパイプライン一覧の件数が一致するかを確認します。
BACKUP_COUNT=$(ls -1 ${BACKUP_DIR}/*.json | wc -l)
TARGET_COUNT=$(wc -l < ${PIPELINE_LIST})
if [ "${BACKUP_COUNT}" -eq "${TARGET_COUNT}" ]; then
echo "OK: バックアップ件数一致 (${BACKUP_COUNT}件)"
else
echo "NG: バックアップ件数不一致 (バックアップ: ${BACKUP_COUNT}, 対象: ${TARGET_COUNT})"
echo "作業を中断し、原因を調査してください"
exit 1
fi
バックアップ取得件数と想定している実行件数を比較し、一致しているかどうかチェックします。
6. Phase 3: 変更用JSONの作成 — create_updated_json.sh
スクリプト: create_updated_json.sh
バックアップしたJSONの ConnectionArn と FullRepositoryId を書き換えた新しいJSONを作成します。今回の移行作業の核となるスクリプトです。
for BACKUP_FILE in ${BACKUP_DIR}/*.json; do
PIPELINE_NAME=$(basename ${BACKUP_FILE} .json)
OUTPUT_FILE="${UPDATED_DIR}/${PIPELINE_NAME}.json"
echo "処理中: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
jq --arg new_conn "${NEW_CONNECTION_ARN}" \
--arg new_org "${NEW_ORG_NAME}" \
--arg old_account "${OLD_ACCOUNT_NAME}" \
'del(.metadata) |
.pipeline.stages[0].actions[0].configuration.ConnectionArn = $new_conn |
.pipeline.stages[0].actions[0].configuration.FullRepositoryId =
(.pipeline.stages[0].actions[0].configuration.FullRepositoryId |
sub($old_account; $new_org))' \
"${BACKUP_FILE}" > "${OUTPUT_FILE}"
for ループとグロブ展開
${BACKUP_DIR}/*.json はグロブ展開で、ディレクトリ内の全.jsonファイルに展開されます。
basename でパイプライン名を取り出す
PIPELINE_NAME=$(basename ${BACKUP_FILE} .json)
# /home/user/.../backup/my-pipeline.json → my-pipeline
basename はパスからファイル名部分だけを抽出します。第2引数で拡張子を指定するとそれも除去されます。
jq によるJSONの加工
今回のスクリプトで最も重要な部分です。jq の機能を分解して見ていきます。
--arg でシェル変数をjqに渡す
jq --arg new_conn "${NEW_CONNECTION_ARN}" \
--arg new_org "${NEW_ORG_NAME}" \
--arg old_account "${OLD_ACCOUNT_NAME}" \
--arg 名前 値 で、jqフィルタ内で $名前 として参照できます。シェル変数を直接フィルタに埋め込むとクォートの問題が起きやすいため、--arg を使うのが安全です。
del(.metadata) で不要フィールドを削除
'del(.metadata) |
.pipeline.stages[0].actions[0].configuration.ConnectionArn = $new_conn |
.pipeline.stages[0].actions[0].configuration.FullRepositoryId =(.pipeline.stages[0].actions[0].configuration.FullRepositoryId |
sub($old_account; $new_org))' \
"${BACKUP_FILE}" > "${OUTPUT_FILE}"
get-pipeline のレスポンスには .metadata(最終更新日時など)が含まれますが、update-pipeline の入力には不要です。del() で除去しています。
パイプ演算子 | によるフィルタの連鎖
del(.metadata) | .field1 = $val1 | .field2 = $val2
jq の | はシェルのパイプと似た概念で、「metadataを削除 → ConnectionArnを書き換え → FullRepositoryIdを書き換え」と処理を連鎖させています。
sub() で文字列の一部を置換
sub($old_account; $new_org)
sub(正規表現; 置換文字列) で最初のマッチを置換します。old-account-name/repo → new-org-name/repo のように、組織名部分だけを差し替えています。jq では引数の区切りが ;(セミコロン)であることに注意してください。
7. Phase 3: 変更内容の差分確認 — verify_changes.sh
スクリプト: verify_changes.sh
変更前(backup)と変更後(updated)のJSONから該当フィールドを抽出して横並びで表示し、想定通りの変更になっているか目視確認します。
for UPDATED_FILE in ${UPDATED_DIR}/*.json; do
PIPELINE_NAME=$(basename ${UPDATED_FILE} .json)
BACKUP_FILE="${BACKUP_DIR}/${PIPELINE_NAME}.json"
echo "--- ${PIPELINE_NAME} ---"
OLD_CONN=$(jq -r '.pipeline.stages[0].actions[0].configuration.ConnectionArn' ${BACKUP_FILE})
NEW_CONN=$(jq -r '.pipeline.stages[0].actions[0].configuration.ConnectionArn' ${UPDATED_FILE})
echo "ConnectionArn: ${OLD_CONN} -> ${NEW_CONN}"
OLD_REPO=$(jq -r '.pipeline.stages[0].actions[0].configuration.FullRepositoryId' ${BACKUP_FILE})
NEW_REPO=$(jq -r '.pipeline.stages[0].actions[0].configuration.FullRepositoryId' ${UPDATED_FILE})
echo "FullRepositoryId: ${OLD_REPO} -> ${NEW_REPO}"
echo ""
done | tee ${PHASE_DIR}/logs/diff_check_${WORK_DATE}.log
jq -r
-r を付けると出力のダブルクォートが除去されます。"arn:aws:..." ではなく arn:aws:... と出力されるので、シェル変数に格納する際に便利です。
ループ末尾の | tee
for ... done | tee ファイル のように、ループブロック全体の出力をパイプで tee に渡しています。ループ内の個々の echo に | tee -a を書く必要がなく、出力をまとめて画面表示+ファイル保存できます。
8. Phase 3: パイプラインの一括更新 — execute_update.sh
スクリプト: execute_update.sh
いよいよ変更を適用します。変更用JSONを使って update-pipeline を一括実行します。
echo "pipeline_name,status,timestamp" > ${RESULT_FILE}
echo "=== 一括変更用 パイプライン更新開始: $(date) ===" | tee -a ${LOG_FILE}
for UPDATED_FILE in ${UPDATED_DIR}/*.json; do
PIPELINE_NAME=$(basename ${UPDATED_FILE} .json)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "更新中: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
aws codepipeline update-pipeline \
--cli-input-json file://${UPDATED_FILE} \
>> ${LOG_FILE} 2>&1
if [ $? -eq 0 ]; then
echo " 成功: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
echo "${PIPELINE_NAME},SUCCESS,${TIMESTAMP}" >> ${RESULT_FILE}
else
echo " 失敗: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
echo "${PIPELINE_NAME},FAILED,${TIMESTAMP}" >> ${RESULT_FILE}
fi
sleep 1
done
echo "=== 一括変更用 パイプライン更新完了: $(date) ===" | tee -a ${LOG_FILE}
echo ""
echo "=== 結果サマリ ==="
echo "成功: $(grep -c SUCCESS ${RESULT_FILE})件"
echo "失敗: $(grep -c FAILED ${RESULT_FILE})件"
file:// プロトコルでJSONを渡す
aws codepipeline update-pipeline --cli-input-json file://${UPDATED_FILE}
--cli-input-json には file://パス でファイルの内容を渡せます。JSONを引数に直接書くと文字数制限やエスケープの問題が起きますが、ファイル経由なら安全です。
sleep 1 によるAPI制限回避
AWSのAPIにはレート制限(スロットリング)があります。連続してAPI呼び出しを行うとリクエストが拒否される可能性があるため、ループ内で sleep 1 を挟んで呼び出し間隔を確保しています。
CSV形式での結果記録
echo "pipeline_name,status,timestamp" > ${RESULT_FILE} で先頭行にヘッダを書き、ループ内で各パイプラインの結果を echo "${PIPELINE_NAME},SUCCESS,${TIMESTAMP}" >> ${RESULT_FILE} で追記します。後からPhase 5のサマリ作成や grep -c SUCCESS で成功件数を集計するのに使います。
9. Phase 4: 更新後の設定確認 — verify_settings.sh
スクリプト: verify_settings.sh
更新後にAWSから設定を再取得し、期待値と一致しているかを確認します。
while read PIPELINE_NAME; do
echo "確認中: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
aws codepipeline get-pipeline --name "${PIPELINE_NAME}" \
> "${VERIFY_DIR}/${PIPELINE_NAME}_current.json" 2>> ${LOG_FILE}
CURRENT_CONN=$(jq -r '.pipeline.stages[0].actions[0].configuration.ConnectionArn' \
"${VERIFY_DIR}/${PIPELINE_NAME}_current.json")
if [ "${CURRENT_CONN}" = "${NEW_CONNECTION_ARN}" ]; then
echo " ConnectionArn: OK" | tee -a ${LOG_FILE}
else
echo " ConnectionArn: NG (期待値と不一致)" | tee -a ${LOG_FILE}
fi
CURRENT_REPO=$(jq -r '.pipeline.stages[0].actions[0].configuration.FullRepositoryId' \
"${VERIFY_DIR}/${PIPELINE_NAME}_current.json")
if [[ "${CURRENT_REPO}" == "${NEW_ORG_NAME}/"* ]]; then
echo " FullRepositoryId: OK (${CURRENT_REPO})" | tee -a ${LOG_FILE}
else
echo " FullRepositoryId: NG (${CURRENT_REPO})" | tee -a ${LOG_FILE}
fi
done < ${PIPELINE_LIST}
[ ] と [[ ]] の使い分け
このスクリプトでは2種類のテスト構文が使われています。
| 構文 | 使い方 | 特徴 |
|---|---|---|
[ "${CURRENT_CONN}" = "${NEW_CONNECTION_ARN}" ] |
完全一致の文字列比較 | POSIX互換。全てのシェルで動く |
[[ "${CURRENT_REPO}" == "${NEW_ORG_NAME}/"* ]] |
パターンマッチ(前方一致) | Bash拡張。* をワイルドカードとして使える |
ConnectionArnは完全一致で確認すればよいため [ ] を使い、FullRepositoryIdは new-org-name/ で始まるかどうかの前方一致で確認するため [[ ]] を使っています。
:::note warn
[[ ]] でパターンマッチする場合、* はクォートの外に出す必要があります。"${NEW_ORG_NAME}/"* はOKですが、"${NEW_ORG_NAME}/*" と書くと * がリテラル文字として扱われ、パターンマッチになりません。
:::
10. Phase 4: 実行テスト — test_pipeline_execution.sh
スクリプト: test_pipeline_execution.sh
代表的なパイプラインを選んで実際に実行し、新しい接続先から正常にソースを取得できるか確認します。
for PIPELINE_NAME in "${TEST_PIPELINES[@]}"; do
echo "実行開始: ${PIPELINE_NAME}" | tee -a ${LOG_FILE}
EXECUTION_ID=$(aws codepipeline start-pipeline-execution \
--name "${PIPELINE_NAME}" \
--query 'pipelineExecutionId' \
--output text)
echo " ExecutionId: ${EXECUTION_ID}" | tee -a ${LOG_FILE}
for i in {1..20}; do
sleep 30
STATUS=$(aws codepipeline get-pipeline-execution \
--pipeline-name "${PIPELINE_NAME}" \
--pipeline-execution-id "${EXECUTION_ID}" \
--query 'pipelineExecution.status' \
--output text)
echo " 状態確認 (${i}/20): ${STATUS}" | tee -a ${LOG_FILE}
if [ "${STATUS}" = "Succeeded" ]; then
echo " 結果: 成功" | tee -a ${LOG_FILE}
break
elif [ "${STATUS}" = "Failed" ]; then
echo " 結果: 失敗 - 詳細確認が必要" | tee -a ${LOG_FILE}
break
fi
done
echo "" | tee -a ${LOG_FILE}
done
ポーリングによる完了待ち
パイプラインの実行は非同期のため、完了を待つポーリング処理が必要です。for i in {1..20} で最大20回(30秒 × 20 = 10分)のループを回し、Succeeded または Failed になったら break で抜けています。
11. まとめ
スクリプトの全体はリポジトリで公開しています。この記事が、AWS運用の自動化やシェルスクリプトの理解の参考になれば幸いです。


