EC2の終了保護をシェルスクリプトで一括有効化してみた

EC2の終了保護をシェルスクリプトで一括有効化してみた

おはようございます。gio です。 AWSの運用を始めると次第に管理するEC2インスタンスが多くなると思います。 そんなときに作業ミスなど予期せぬ事象でEC2インスタンスを誤って終了してしまうのを防ぐため、終了保護を一括で有効化するシェルスクリプトをAIと壁打ちしながら作成しました。 「数十台起動してるけど、どれも無効にしてる」という場合に活用できると思います。
2026.10.15

はじめに

Amazon EC2 インスタンスを誤って終了 (Terminate) してしまった経験はないでしょうか。検証環境の片付け中に、本番用のインスタンスを選んでしまった、というのは意外とよくある話です。

EC2 には終了保護 (Termination Protection)という機能があり、有効にしておくと、コンソールや CLI、API からの終了操作がエラーになります。ただし設定はインスタンスごとに行う必要があるため、台数が増えると手作業では大変です。

そこで、AWS CLI を使って終了保護を一括で有効化するシェルスクリプトを作りました。作るにあたって、次の点を意識しています。

  • すでに終了保護が有効なインスタンスは、実行リストに入れない
  • Name タグの一部で対象を絞り込める
  • 実行前に対象のインスタンス ID 一覧を表示して、確認を取る
  • --dry-run で、変更せずに対象だけ確認できる

この記事では、スクリプトの内容と使い方、注意点を紹介します。

前提条件

  • AWS CLI v2 がインストール済みであること
  • 対象アカウントへの認証が済んでいること (aws configure、SSO、IAM ロールなど)
  • bash が使える環境 (macOS / Linux / WSL など)
  • 実行する IAM プリンシパルに、次の権限があること
    • ec2:DescribeInstances
    • ec2:DescribeInstanceAttribute
    • ec2:ModifyInstanceAttribute

終了保護とは

終了保護は、EC2 インスタンスの DisableApiTermination 属性として管理されています。この属性が true のインスタンスに対して終了操作を行うと、OperationNotPermitted エラーになります。

AWS CLI で 1 台ずつ操作する場合は、次のコマンドを使います。

# 有効化
aws ec2 modify-instance-attribute \
  --instance-id i-0123456789abcdef0 \
  --disable-api-termination

# 無効化
aws ec2 modify-instance-attribute \
  --instance-id i-0123456789abcdef0 \
  --no-disable-api-termination

# 現在の設定を確認
aws ec2 describe-instance-attribute \
  --instance-id i-0123456789abcdef0 \
  --attribute disableApiTermination

--disable-api-termination は「API による終了を無効にする」という意味で、終了保護を有効にするオプションです。名前が紛らわしいので注意してください。

マネジメントコンソールから有効化/無効化する場合は、画像のように、「インスタンスの状態」から「終了保護を変更」を選択することで変更できます。

スクリプトの全体像

処理は次の流れで進みます。

[対象リージョン] → [各リージョンのEC2のID取得] → [1台ずつ終了保護の状態確認(並列)]
                                                        ↓
                                          未設定のものだけ実行リストに保存
                                                        ↓
              [--dry-run なら表示のみで終了] → [確認プロンプト(y/N)] → [並列で有効化]
  1. 対象候補を集める
  2. すでに有効なものを除外して、実行リストを作る
  3. 実行リストだけを処理する

オプション

オプション 短縮形 内容 デフォルト
--dry-run -n 対象一覧の表示のみで、変更は行わない false(変更する)
--yes -y 実行前の確認 (y/N) をスキップする false(確認する)
--parallel N -p N 並列実行数 (1 以上の整数) 3
--regions LIST -r LIST 対象リージョン。スペースまたはカンマ区切り ap-northeast-1(東京のみ)
--help -h ヘルプを表示 -

オプションではない引数は、Name タグの一部として扱います。複数指定した場合は OR 条件です。

また、環境変数 DRY_RUN / YES / PARALLEL / REGIONS でもデフォルト値を指定できます。優先順位は オプション > 環境変数 > スクリプト内のデフォルト です。

スクリプト全文

AI と壁打ちしながら enable-termination-protection.sh として作成しました。
コードの解説は最後に補足として記載しております。

#!/usr/bin/env bash
# enable-termination-protection.sh
# EC2インスタンスの終了保護(DisableApiTermination)を未設定のものだけ一括で有効化する。
# 詳細は -h / --help を参照。
set -euo pipefail
export AWS_PAGER=""

usage() {
  cat <<'EOF'
使い方:
  enable-termination-protection.sh [オプション] [名前の一部...]

引数:
  名前の一部...        Nameタグに指定文字列を含むインスタンスだけを対象にする
                       (複数指定はOR条件。大文字小文字は区別される)
                       省略時は、終了保護が未設定の全インスタンスが対象

オプション:
  -n, --dry-run        対象一覧の表示のみで、変更は行わない
  -y, --yes            実行前の確認(y/N)をスキップする
  -p, --parallel N     並列実行数(デフォルト: 3)
  -r, --regions LIST   対象リージョン。スペースまたはカンマ区切り
                       (デフォルト: ap-northeast-1 = 東京のみ)
  -h, --help           このヘルプを表示

例:
  enable-termination-protection.sh --dry-run
  enable-termination-protection.sh web batch
  enable-termination-protection.sh -r ap-northeast-1,us-east-1 -p 10 web
  enable-termination-protection.sh -y -r ap-northeast-1

補足:
  環境変数 DRY_RUN / YES / PARALLEL / REGIONS でもデフォルトを指定できる。
  両方指定した場合は、オプションが優先される。
  Nameが "-" で始まる場合は、"--" のあとに指定する。
EOF
}

die() {
  echo "エラー: $*" >&2
  echo "ヘルプ: $0 --help" >&2
  exit 1
}

# デフォルト値(環境変数があればそれを使う。DRY_RUNのデフォルトは false)
PARALLEL="${PARALLEL:-3}"
DRY_RUN="${DRY_RUN:-false}"
YES="${YES:-false}"
REGIONS="${REGIONS:-ap-northeast-1}"   # デフォルトは東京リージョンのみ
NAMES=()

# オプション解析(オプション指定は環境変数より優先)
while [[ $# -gt 0 ]]; do
  case "$1" in
    -n|--dry-run)  DRY_RUN=true; shift ;;
    -y|--yes)      YES=true; shift ;;
    -p|--parallel) [[ $# -ge 2 ]] || die "$1 には値が必要です"
                   PARALLEL="$2"; shift 2 ;;
    -r|--regions)  [[ $# -ge 2 ]] || die "$1 には値が必要です"
                   REGIONS="$2"; shift 2 ;;
    -h|--help)     usage; exit 0 ;;
    --)            shift; NAMES+=("$@"); break ;;
    -*)            die "不明なオプション: $1" ;;
    *)             NAMES+=("$1"); shift ;;
  esac
done

[[ "$PARALLEL" =~ ^[1-9][0-9]*$ ]] || die "--parallel には1以上の整数を指定してください: ${PARALLEL}"

# カンマ区切りはスペース区切りに変換(for ループで1つずつ処理するため)
REGIONS="${REGIONS//,/ }"
[[ -n "${REGIONS// /}" ]] || die "--regions にはリージョン名を指定してください"

# describe-instances に渡すフィルタ(AWS側で絞り込むので転送量も減る)
#   終了済み(terminated)のインスタンスは常に除外
FILTERS=("Name=instance-state-name,Values=pending,running,stopping,stopped")
if [[ ${#NAMES[@]} -gt 0 ]]; then
  # 引数 a b → "*a*,*b*" (Nameタグの部分一致・OR条件)
  NAME_VALUES=$(printf '*%s*,' "${NAMES[@]}")
  FILTERS+=("Name=tag:Name,Values=${NAME_VALUES%,}")
  echo "検索条件: Nameタグに「${NAMES[*]}」を含むインスタンス(複数指定はOR)"
else
  echo "検索条件: なし(終了保護が未設定の全インスタンス)"
fi
echo "設定: dry-run=${DRY_RUN} yes=${YES} parallel=${PARALLEL} regions=${REGIONS}"

# 終了保護が未設定なら "region instance-id" を出力
check() {
  local v
  v=$(aws ec2 describe-instance-attribute --region "$1" --instance-id "$2" \
        --attribute disableApiTermination \
        --query 'DisableApiTermination.Value' --output text) \
    || { echo "CHECK_ERROR $1 $2" >&2; return 0; }
  [[ "$v" == "False" ]] && echo "$1 $2"
  return 0
}

# 終了保護を有効化
enable() {
  if aws ec2 modify-instance-attribute --region "$1" --instance-id "$2" --disable-api-termination; then
    echo "OK $1 $2"
  else
    echo "NG $1 $2" >&2
  fi
}
export -f check enable

TARGETS="$(mktemp)"
trap 'rm -f "$TARGETS"' EXIT

# 1) 対象候補の取得と、終了保護が未設定かの確認
#    describe-instances はリージョンごとに1回、確認(check)はインスタンスごとに1回
#    終了済み(terminated)は除外、Spotは終了保護非対応のため除外
for r in $REGIONS; do
  aws ec2 describe-instances --region "$r" \
    --filters "${FILTERS[@]}" \
    --query "Reservations[].Instances[?InstanceLifecycle!='spot'].InstanceId[]" \
    --output text | tr '\t' '\n' | sed "/^$/d; s/^/$r /"
done | xargs -P "$PARALLEL" -L1 bash -c 'check "$@"' _ > "$TARGETS"

# 2) 実行リスト(=未設定のものだけ)
COUNT=$(wc -l < "$TARGETS" | tr -d ' ')
echo "終了保護が未設定のインスタンス: ${COUNT} 件"
cat "$TARGETS"

[[ "$COUNT" -eq 0 ]] && { echo "対象なし。終了します。"; exit 0; }
[[ "$DRY_RUN" == "true" ]] && { echo "[DRY_RUN] 変更は行いません。"; exit 0; }

# 3) 実行前の確認(--yes の場合はスキップ)
if [[ "$YES" != "true" ]]; then
  IDS=$(awk '{print $2}' "$TARGETS" | paste -sd, - | sed 's/,/, /g')
  echo
  echo "以下のインスタンスID一覧を保護しますか?"
  echo "  ${IDS}"
  # 標準入力が端末でない(EOF等)場合は空文字になり、中止扱いになる
  read -r -p "終了保護を有効化します。よろしいですか? [y/N]: " ANS || ANS=""
  if [[ ! "$ANS" =~ ^[Yy]([Ee][Ss])?$ ]]; then
    echo "中止しました。変更は行っていません。"
    exit 0
  fi
fi

# 4) 有効化(並列実行)
xargs -P "$PARALLEL" -L1 bash -c 'enable "$@"' _ < "$TARGETS"

使い方

実際にマネジメントコンソールでも状態を見ながら、使い方を確認します。
現在画像のように "gio","test1","test2" という EC2 が 3 台あるとします。終了保護はインスタンスが停止中でも有効化できます。

また、終了保護が無効になっています(他の EC2 も同様ですが省略)。

AWS CLI を使える環境ならどこでも問題ありませんが、今回は AWS CloudShell で実行したいと思います。enable-termination-protection.sh を AWS CloudShell へアップロードしてください。手順は省略します。

以降は AWS CloudShell 内でコマンドを実行してください。

まず、シェルスクリプトの実行前に実行権限を付けます。

chmod +x enable-termination-protection.sh

ここからシェルスクリプトを実行します。

1. まず dry-run で対象を確認する

./enable-termination-protection.sh --dry-run

変更は行わず、終了保護が未設定のインスタンスの一覧だけを表示します。

有効にしたい3台のインスタンスが表示されました。

2. 終了保護を有効化する

./enable-termination-protection.sh

対象のインスタンス ID 一覧が表示され、[y/N] の確認が出ます。y を入力すると有効化が始まります。

実際にマネジメントコンソールを見ると、有効になっていることを確認できます。
gio 以外の EC2 インスタンスも同様に有効になっております。

3. Name タグで絞り込む

実際の運用で全ての EC2 インスタンスを有効化することは少ないと思います。
コマンドライン引数に Name タグの一部を入力すると、指定した Name を含む EC2 インスタンスのみを対象にできます。
複数入力できるので、今回の場合では test と gio を指定すれば同じような動作を実施できます。

./enable-termination-protection.sh test gio

実行例の画像を貼りますが、下の画像は既に有効化済みのため対象なしとなっています。

その他の細かな使い方は以下のとおりです。

4. リージョンや並列数を指定する

# 東京とバージニア北部を対象に、並列数20で、Nameに web を含むものだけ
./enable-termination-protection.sh -r ap-northeast-1,us-east-1 -p 20 web

# 確認をスキップして実行(自動化用)
./enable-termination-protection.sh -y -r ap-northeast-1

5. エラーをファイルに残す

CHECK_ERROR や NG は標準エラー出力に出ます。ログに残したい場合は、次のようにします。

./enable-termination-protection.sh 2> error.log

実行結果のイメージ

実際の結果の画像を載せていますが、テキストでも記載しておきます。
以下は出力のイメージです (インスタンス ID は架空のものです)。

検索条件: Nameタグに「web」を含むインスタンス(複数指定はOR)
設定: dry-run=false yes=false parallel=3 regions=ap-northeast-1
終了保護が未設定のインスタンス: 2 件
ap-northeast-1 i-0aaaaaaaaaaaaaaaa
ap-northeast-1 i-0cccccccccccccccc

以下のインスタンスID一覧を保護しますか?
  i-0aaaaaaaaaaaaaaaa, i-0cccccccccccccccc
終了保護を有効化します。よろしいですか? [y/N]: y
OK ap-northeast-1 i-0aaaaaaaaaaaaaaaa
OK ap-northeast-1 i-0cccccccccccccccc

並列で実行しているため、OK の表示順は毎回同じとは限りません。

注意点

「確認してから設定」は、API 呼び出しが増える

modify-instance-attribute は何度実行しても結果が変わらない (冪等な) 操作です。そのため、確認なしで全インスタンスに実行すれば、API 呼び出しはインスタンス数の分だけで済みます。

今回のスクリプトは、確認 (check) で 1 回、変更で 1 回と、未設定のインスタンスには最大 2 回呼び出します。それでも確認を入れているのは、すでに有効なインスタンスに対する不要な変更操作を、実行リストの時点で除外したいためです。呼び出し回数を最優先にする場合は、確認を省く構成も検討してください。

終了保護で防げないケースがある

終了保護は、API 経由の終了操作を防ぐ機能です。次のようなケースでは、終了保護を有効にしていてもインスタンスが終了する可能性があります。

  • Auto Scaling グループによるスケールインや、ヘルスチェック失敗による置き換え
    • ASG 側で「インスタンスのスケールイン保護」を設定する必要があります
  • OS からのシャットダウン操作で、かつ InstanceInitiatedShutdownBehavior が terminate の場合
  • スポットインスタンスの中断 (そもそも終了保護を設定できないため、スクリプトでも対象外にしています)

停止保護は別の機能

終了保護とは別に、停止保護 (Stop Protection)という機能もあります。インスタンスの停止を防ぎたい場合は、DisableApiStop 属性を別途設定する必要があります。

削除するときは先に無効化する

終了保護をかけたインスタンスを削除する場合は、先に保護を外す必要があります。
以下のコマンドやマネジメントコンソールで終了保護を無効化してください。

aws ec2 modify-instance-attribute \
  --instance-id i-0123456789abcdef0 \
  --no-disable-api-termination

CloudFormation のスタック削除や Terraform の destroy で削除する場合も、終了保護が有効だと失敗します。

YES=true を常用しない

確認プロンプトをスキップする --yes や環境変数 YES=true は、シェルの設定ファイル (~/.bashrc など) に書かないでください。常に確認が飛ばされ、意図しないインスタンスに終了保護がかかる原因になります。なお、スクリプトの設定を環境変数で変えたい場合も、設定ファイルに書く必要はありません。コマンドの前に PARALLEL=5 ./enable-termination-protection.sh のように付ければ、そのコマンド 1 回だけに有効です。

補足:コードの解説

シェルスクリプト初心者向けの解説を記載しております。
各コマンドの内容に疑問点がある際にご活用ください。

安全装置と初期設定

set -euo pipefail
export AWS_PAGER=""

set -euo pipefail は、スクリプトの事故を防ぐ定番の設定です。

オプション 意味
-e コマンドが失敗したら、その時点でスクリプトを終了する
-u 未定義の変数を使ったらエラーにする (typo の検出)
-o pipefail A | B | C のパイプの途中で失敗しても、エラーとして扱う

AWS_PAGER="" は、AWS CLI v2 のページャー(less のような表示) を無効にします。スクリプトの途中で入力待ちになるのを防ぎます。

デフォルト値とオプション解析

PARALLEL="${PARALLEL:-3}"
DRY_RUN="${DRY_RUN:-false}"
YES="${YES:-false}"
REGIONS="${REGIONS:-ap-northeast-1}"

${変数名:-デフォルト値} は、「変数が設定されていればその値、未設定ならデフォルト値を使う」という書き方です。この時点で環境変数が反映され、そのあとのオプション解析で、指定された値に上書きされます。これが「オプション > 環境変数 > デフォルト」という優先順位の仕組みです。

while [[ $# -gt 0 ]]; do
  case "$1" in
    -n|--dry-run)  DRY_RUN=true; shift ;;
    -p|--parallel) [[ $# -ge 2 ]] || die "$1 には値が必要です"
                   PARALLEL="$2"; shift 2 ;;
    ...
    *)             NAMES+=("$1"); shift ;;
  esac
done
部分 意味
while [[ $# -gt 0 ]] 引数が残っている間、先頭から 1 つずつ処理する ($# は引数の個数)
case "$1" in 先頭の引数がどのオプションかで分岐する。| は「どちらでも」の意味
shift 先頭の引数を 1 つ捨てて、残りを前に詰める
shift 2 値を取るオプション (-p 20 など) で、オプション名と値の 2 つを捨てる
NAMES+=("$1") オプションではない引数 (Name タグの一部) を配列に追加する
--) -- 以降は、すべて Name として扱う
-*) 上のどれにも当てはまらず - で始まるものは、不明なオプションとしてエラーにする

getopts は短い形式 (-n) にしか対応していないため、--dry-run のような長い形式も使えるように case で自前の解析にしています。

並列数は [[ "$PARALLEL" =~ ^[1-9][0-9]*$ ]] で、1 以上の整数かどうかを確認しています。abc や 0 はエラーになります。

Name タグでの絞り込み

FILTERS=("Name=instance-state-name,Values=pending,running,stopping,stopped")
if [[ ${#NAMES[@]} -gt 0 ]]; then
  NAME_VALUES=$(printf '*%s*,' "${NAMES[@]}")
  FILTERS+=("Name=tag:Name,Values=${NAME_VALUES%,}")
fi

web batch のように 2 つ指定すると、*web*,*batch* という文字列が作られます。これを describe-instances の --filters に渡すと、AWS 側で「Name タグに web または batch を含む」インスタンスだけに絞り込まれます。絞り込みを AWS 側で行うので、転送量も減ります。

${NAME_VALUES%,} は、変数の末尾にあるカンマを 1 つ取り除く書き方です。printf が各要素の末尾にカンマを付けるため、最後のカンマを削っています。

関数 check と enable

check() {
  local v
  v=$(aws ec2 describe-instance-attribute --region "$1" --instance-id "$2" \
        --attribute disableApiTermination \
        --query 'DisableApiTermination.Value' --output text) \
    || { echo "CHECK_ERROR $1 $2" >&2; return 0; }
  [[ "$v" == "False" ]] && echo "$1 $2"
  return 0
}

check は、終了保護の値が False(未設定) のインスタンスだけ、リージョン インスタンスID の形式で出力します。すでに True のものは何も出力しないので、実行リストに入りません。これが「有効なものは対象外にする」仕組みです。

describe-instances の結果には終了保護の状態が含まれず、describe-instance-attribute は 1 インスタンスずつしか呼べません。そのため、状態確認はインスタンスごとに 1 回の API 呼び出しが必要です。

export -f check enable

あとで使う xargs は、新しい bash を起動して関数を実行します。export -f をしないと、その bash は関数を知らず、command not found になります。

並列実行 xargs -P

done | xargs -P "$PARALLEL" -L1 bash -c 'check "$@"' _ > "$TARGETS"
部分 意味
-P 3 最大 3 個を同時に実行する
-L1 入力の 1 行ごとに 1 回コマンドを実行する
bash -c '...' 新しい bash で文字列をコマンドとして実行する
check "$@" 受け取った引数 (リージョンと ID) を check に渡す
_ bash -c では最初の引数が $0 になるため置くダミー

入力が ap-northeast-1 i-0123... なら、空白で分割されて $1=ap-northeast-1、$2=i-0123... として渡されます。

並列化の目的は、API 呼び出しの回数を減らすことではなく、全体の実行時間を短くすることです。並列度を上げすぎると、API のレート制限 (スロットリング) に引っかかる可能性があります。Throttling や RequestLimitExceeded が出た場合は、--parallel を下げてください。

実行前の確認

IDS=$(awk '{print $2}' "$TARGETS" | paste -sd, - | sed 's/,/, /g')
read -r -p "終了保護を有効化します。よろしいですか? [y/N]: " ANS || ANS=""
if [[ ! "$ANS" =~ ^[Yy]([Ee][Ss])?$ ]]; then
  echo "中止しました。変更は行っていません。"
  exit 0
fi

実行リストの 2 列目 (インスタンス ID) だけを取り出して、カンマ区切りで表示します。y または yes 以外の入力、そして標準入力が端末でない場合 (|| ANS="") は、中止として扱います。cron などで自動実行するときは --yes を付けてください。

まとめ

EC2 の終了保護を、AWS CLI とシェルスクリプトで一括有効化する方法を紹介しました。

  • すでに有効なインスタンスは実行リストに入れず、未設定のものだけを処理する
  • --dry-run と確認プロンプトで、誤って意図しないインスタンスを変更するのを防ぐ
  • 終了保護は API 経由の終了のみを防ぐため、ASG などは別途対策が必要

本番環境のインスタンスには、誤操作対策として終了保護を有効にしておくことをおすすめします。この記事が、どなたかの参考になれば幸いです。

参考

クラスメソッドオペレーションズ株式会社について

クラスメソッドグループのオペレーション企業です。

運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AI をフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。

当社は様々な職種でメンバーを募集しています。

「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 採用サイト をぜひご覧ください。※2026 年 1 月 アノテーション㈱から社名変更しました。

この記事をシェアする

関連記事