JINSテックブログ
🛠️

SSM Automationにアップデート作業させてみた

に公開

この投稿は、2025年JINSのアドベントカレンダー5日目の記事です。

アップデートしたい

社内にあるEC2マシンたち、なかなかアップデート大変だったりします。
手作業でアップデートと動作確認するの大変!という声がいろいろあったので、自動化して自分でやってみることにしました。

AWS Systems Manager Automation

いきなりですが結論です。
AWS Systems Manager Automationでアップデートを組んでみました。
以下ができたフロー。

なんじゃこりゃという感じだと思うので、順番にお伝えします。

前提

今回の前提としては以下です。

  • 自社AWSアカウント内に散らばっているEC2マシンみんなにアップデートかけたい
  • EC2マシンのLinux内部にはスクリプトなどの事前配置はしたくない
  • S3上にスクリプト置くにしても、S3用ロールをEC2マシンにいちいちアタッチしたくない
  • アップデートコマンド実行だけでなく、ALB切り離し、EC2の再起動も自動化する
  • 生成AIさんに書いてもらう

1. S3 Presigned URLの発行

アップデート前後で動作確認用コマンドを実行しますが、スクリプトをS3 Presigned URL経由でS3から取りに行きます。
aws:executeScriptで直接記述してもいいのですが、動作確認項目が多いとSSMドキュメントのjson定義ファイルで見づらいと思い、生成AIさんに提案してもらいました。

import boto3
import urllib.parse

def get_presigned_url(events, context):
  s3_client = boto3.client('s3')
  bucket = events['S3BucketName']
  key = events['KernelCheckScriptName']
  try:
    url = s3_client.generate_presigned_url(
      'get_object',
      Params={'Bucket': bucket, 'Key': key},
      ExpiresIn=300
    )
    print(f'Generated URL: {url}')
    return {'PresignedUrl': url}
  except Exception as e:
    print(f'Error generating URL: {e}')
    raise e

URL発行後に、aws:runCommand内でダウンロードしスクリプトを実行。
まずは事前動作確認を行います。
(同じスクリプトでアップデート後にも再度動作確認します。)

{
  "commands": [
    "#!/bin/bash",
    "set -e",
    "SCRIPT_NAME={{ KernelCheckScriptName }}",
    "URL='{{ GenerateKernelPresignedUrl.PresignedUrl }}'",
    "cd {{ WorkingDirectory }} || mkdir -p {{ WorkingDirectory }} && cd {{ WorkingDirectory }}",
    "echo 'Downloading script via Presigned URL...'",
    "curl -s -L $URL -o $SCRIPT_NAME",
    "if [ $? -ne 0 ]; then",
    "  echo 'Error downloading script using curl.'",
    "  exit 1",
    "fi",
    "chmod +x $SCRIPT_NAME",
    "./$SCRIPT_NAME '{{ ExpectedKernelVersion }}' pre"
  ]
}

今回はkernelアップデートしたいので、kernelのバージョン確認を記述してます。

# スクリプトの内容を一部抜粋

check_kernel_version() {
    print_header "1. Linuxカーネルバージョンの確認"
    local current_kernel
    current_kernel=$(uname -r)
    rpm -qa | grep kernel | sort | tee -a "$LOG_FILE"
    if [ "$MODE" == "pre" ]; then
        save_state "kernel_version_pre.txt" "uname -r"
        if [ -n "$EXPECTED_KERNEL_VERSION" ]; then
            if [[ "$current_kernel" == "$EXPECTED_KERNEL_VERSION"* ]]; then
                print_status "WARN" "現在のカーネルは既にターゲットバージョンです: $current_kernel"
            else
                print_status "OK" "アップグレード前のカーネルバージョンを記録しました: $current_kernel"
            fi
        else
            print_status "INFO" "マイナーアップデートモード。現在のカーネルバージョンを記録します: $current_kernel"
        fi
    else
        local pre_kernel_version
        if [ -f "$STATE_DIR/kernel_version_pre.txt" ]; then
            pre_kernel_version=$(cat "$STATE_DIR/kernel_version_pre.txt")
        else
            pre_kernel_version="不明 (preモードの実行ファイルが見つかりません)"
            print_status "FAIL" "アップグレード前のカーネルバージョンファイルが見つかりません。"
            return
        fi
        if [ -n "$EXPECTED_KERNEL_VERSION" ]; then
            if [[ "$current_kernel" == "$EXPECTED_KERNEL_VERSION"* ]]; then
                print_status "OK" "カーネルが期待通りにアップグレードされました: $current_kernel"
            else
                print_status "FAIL" "カーネルバージョンが期待と異なります。期待値(前方一致): $EXPECTED_KERNEL_VERSION, 現在値: $current_kernel, アップグレード前: $pre_kernel_version"
            fi
        else
            if [ "$current_kernel" != "$pre_kernel_version" ]; then
                print_status "OK" "カーネルが更新されました: $pre_kernel_version -> $current_kernel"
            else
                print_status "FAIL" "カーネルが更新されていません。現在値: $current_kernel"
            fi
        fi
    fi
}

2. ALBから対象EC2マシンを切り離し

アップデートするEC2が複数ALBにぶら下がっている場合もあるので、順番に切り離します。

import boto3
def deregister_targets(events, context):
  elbv2_client = boto3.client('elbv2')
  instance_id = events['InstanceId']
  target_group_arns = events['TargetGroupArns']
  if not target_group_arns:
    print('No TargetGroupArns provided, skipping deregistration.')
    return {'Status': 'Skipped'}
  for tg_arn in target_group_arns:
    print(f'Deregistering instance {instance_id} from target group {tg_arn}')
    try:
      elbv2_client.deregister_targets(
        TargetGroupArn=tg_arn,
        Targets=[{'Id': instance_id}]
      )
    except Exception as e:
      print(f'Error deregistering from {tg_arn}: {e}')
      raise e
  return {'Status': 'Success'}

3. EBSスナップショットの取得

作業失敗時のためにスナップショットを取得します。

4. kernelアップデート

今回はkernelアップデートを行う形にしました。
この箇所はaws:runCommandで直接コマンドを実行しています。
アップデート後に、aws:changeInstanceStateでOS再起動させます。

{
  "commands": [
    "#!/bin/bash",
    "set -e",
    "echo '--- Starting Kernel Upgrade ---'",
    "TARGET_KERNEL='{{ ExpectedKernelVersion }}'",
    "if [ -n \"$TARGET_KERNEL\" ]; then",
    "  echo '--- Major Upgrade Mode for specific kernel ---'",
    "  kernel_major_minor=$(echo \"$TARGET_KERNEL\" | cut -d. -f1,2)",
    "  echo \"Enabling and installing kernel-${kernel_major_minor}...\"",
    "  sudo amazon-linux-extras install \"kernel-${kernel_major_minor}\" -y",
    "else",
    "  echo '--- Minor Upgrade Mode (yum update) ---'",
    "  sudo yum update -y kernel kernel-tools kernel-headers",
    "fi",
    "echo '--- Kernel Upgrade Commands Executed ---'"
  ]
}

5. 動作確認

1. S3 Presigned URLの発行で持ってきたスクリプトをもう一度実行して、アップデート後の動作確認します。

{
  "commands": [
    "#!/bin/bash",
    "set -e",
    "cd {{ WorkingDirectory }}",
    "./{{ KernelCheckScriptName }} '{{ ExpectedKernelVersion }}' post"
  ]
}

6. 作業結果承認後のALB再組み込み

aws:approveで一時停止し、アップデートがうまくいったかをいったん人間による承認手続きを入れてます。
別途人間の目でも確認したい内容あればこの間に実施する想定です。

承認すると以下のaws:executeScriptでALBに再組み込みされ、作業完了です。

import boto3
def register_targets(events, context):
  elbv2_client = boto3.client('elbv2')
  instance_id = events['InstanceId']
  target_group_arns = events['TargetGroupArns']
  if not target_group_arns:
    print('No TargetGroupArns provided, skipping registration.')
    return {'Status': 'Skipped'}
  for tg_arn in target_group_arns:
    print(f'Registering instance {instance_id} with target group {tg_arn}')
    try:
      elbv2_client.register_targets(
        TargetGroupArn=tg_arn,
        Targets=[{'Id': instance_id}]
      )
    except Exception as e:
      print(f'Error registering with {tg_arn}: {e}')
      raise e
  return {'Status': 'Success'}

ex. スナップショット切り戻し

今までの作業の中で異常があった場合は、3. EBSスナップショットの取得のスナップショットから戻し作業を行います。
これも戻し終わった後、aws:approveで承認プロセス挟んでALB再組み込みです。

ひとまず検証中…

いったん検証環境で動かしましたが、とりあえず問題なくアップデートできてるみたいです。
引き続き本番環境でも動かせれば普段のアップデート作業が省力化できるかも。

明日は小野寺さんの「ウォーターフォール20年から踏み出す、アジャイル開発の第一歩」の記事です。

JINSテックブログ
JINSテックブログ

Discussion