AWS CDK での EC2 インスタンス構築で cfn-signal を使って user data 実行完了を待機してみた

2024.04.14

こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。

CloudFormation の cfn-signal スクリプト を使うと、スタックデプロイによる EC2 インスタンスの起動時に ALB のヘルスチェックなどの完了結果が CloudFormation に送信されるのを待機させ、失敗シグナルが送信された場合はデプロイをロールバックさせることができます。

ここで user data(ユーザーデータ) とは、EC2 インスタンス起動時に実行できるスクリプトのことです。このスクリプトの実行完了を待機させたい場合にも cfn-signal を使うことができます。

今回は、AWS CDKでの EC2 インスタンス構築で cfn-signal を使ってユーザーデータ実行完了を待機する実装を試してみました。

やってみた

cfn-signal を使ってユーザーデータ実行完了を待機する

EC2 インスタンスのデプロイ時に cfn-signal を使ってユーザーデータ実行完了を待機する CDK コード(TypeScript)です。

lib/cdk-sample-stack.ts

import { aws_ec2, Stack, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // VPC を作成
    const vpc = new aws_ec2.Vpc(this, 'Vpc');

    // インスタンスを作成
    const instance = new aws_ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: aws_ec2.InstanceType.of(
        aws_ec2.InstanceClass.T2,
        aws_ec2.InstanceSize.MICRO
      ),
      machineImage: aws_ec2.MachineImage.latestAmazonLinux2023(),
      resourceSignalTimeout: Duration.seconds(300), // cfn-signal を300秒待機
    });

    // ユーザーデータとして 60 秒待機コマンドを追加
    instance.userData.addCommands('sleep 60');

    // ユーザーデータに cfn-signal コマンドを追加
    instance.userData.addSignalOnExitCommand(instance);
  }
}

  • Instance Construct の resourceSignalTimeout プロパティに cfn-signal の最大待機時間を設定します。ここでは 300 秒です。
  • addCommands() を使うとユーザーデータのコマンドをインスタンスに設定できます。ここでは 60 秒待機コマンドを追加しています。
  • addSignalOnExitCommand() を使うとユーザーデータに cfn-signal コマンドを追加できます。

上記の CDK コードをデプロイすると、インスタンスの起動時に成功シグナルが受信されたことが確認できます。また最大待機時間 300 秒に対してデプロイ時間は 2 分弱となっており、ユーザーデータの実行が完了次第、成功シグナルが送信され、リソースの作成が完了していることが確認できます。

Received SUCCESS signal with UniqueId i-0082078e2ea77eeed

起動されたインスタンスのユーザーデータは次のようになっています。addSignalOnExitCommand により、スクリプト冒頭に cfn-signal を送信するexitTrap シェル関数が追加されて、trap exitTrap EXIT によりスクリプト実行終了時に exitTrap が実行されるようになっています。

#!/bin/bash
function exitTrap(){
exitCode=$?
/opt/aws/bin/cfn-signal --stack CdkSampleStack --resource InstanceC1063A87 --region ap-northeast-1 -e $exitCode || echo 'Failed to send Cloudformation Signal'
}
trap exitTrap EXIT
sleep 60

cfn-signal を待機せず、また送信もしない場合

CloudFormation 側で cfn-signal の待機をせず、またユーザーデータ側で送信もしない場合を試してみます。

lib/cdk-sample-stack.ts

import { aws_ec2, Stack, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // VPC を作成
    const vpc = new aws_ec2.Vpc(this, 'Vpc');

    // インスタンスを作成
    const instance = new aws_ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: aws_ec2.InstanceType.of(
        aws_ec2.InstanceClass.T2,
        aws_ec2.InstanceSize.MICRO
      ),
      machineImage: aws_ec2.MachineImage.latestAmazonLinux2023(),
      // resourceSignalTimeout: Duration.seconds(300), // cfn-signal を300秒待機
    });

    // ユーザーデータとして 300 秒待機コマンドを追加
    instance.userData.addCommands('sleep 300');

    // ユーザーデータに cfn-signal コマンドを追加
    // instance.userData.addSignalOnExitCommand(instance);
  }
}

ユーザーデータの実行完了に関わらず CloudFormatino 上のリソース作成が完了することを確認するため、ユーザーデータには 300 秒という長い時間を待機するコマンドを追加しています。

上記の CDK コードをデプロイすると、CloudFormation 上ではインスタンスの作成は 40 秒弱で完了しています。ユーザーデータの実行完了を待機しない動作となりました。

起動されたインスタンスのユーザーデータは次のようになっています。addCommands により追加したコマンドのみが設定されています。

#!/bin/bash
sleep 60

cfn-signal を待機するが送信しない場合(デプロイ失敗する)

CloudFormation 側で cfn-signal の待機をするが、ユーザーデータ側では送信をしない場合を試してみます。

lib/cdk-sample-stack.ts

import { aws_ec2, Stack, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // VPC を作成
    const vpc = new aws_ec2.Vpc(this, 'Vpc');

    // インスタンスを作成
    const instance = new aws_ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: aws_ec2.InstanceType.of(
        aws_ec2.InstanceClass.T2,
        aws_ec2.InstanceSize.MICRO
      ),
      machineImage: aws_ec2.MachineImage.latestAmazonLinux2023(),
      resourceSignalTimeout: Duration.seconds(100), // cfn-signal を100秒待機
    });

    // インスタンスにユーザーデータを追加
    instance.userData.addCommands('sleep 60'); // 実行時間が60秒のコマンドを追加

    // // ユーザーデータに cfn-signal コマンドを追加
    // instance.userData.addSignalOnExitCommand(instance);
  }
}

待機時間内に cfn-signal が送信されない場合、リソース作成が失敗し、CDK デプロイが失敗します。

Failed to receive 1 resource signal(s) within the specified duration

おわりに

AWS CDK での EC2 インスタンス構築で cfn-signal を使ってユーザーデータ実行完了を待機する実装を試してみました。

cfn-signal とユーザーデータは、個人的には初めて触った機能だったのですが、最近取得した AWS Pro 資格の受験勉強で得た知識が役に立ちました。

以上