AWS CloudFormationで使える4種類のヘルパースクリプトについて使い方と機能をまとめてみた

cfn-init/cfn-hup/cfn-get-metadata/cfn-signal の4つのヘルパースクリプトについて調べてみました。
2020.07.05

CloudFormationのヘルパースクリプトは スタック内のEC2インスタンスの構築・変更等を便利にする機能を提供しています。

ヘルパースクリプトは全部で以下の4種類がありますが、 それぞれ実際にはどのように記述する必要があり、またどんな動作をするのか、確かめてみました。

cfn-init: リソースメタデータの取得と解釈、パッケージのインストール、ファイルの作成、およびサービスの開始で使用します。
cfn-signal: CreationPolicy または WaitCondition でシグナルを送信するために使用し、前提となるリソースやアプリケーションの準備ができたときに、スタックの他のリソースを同期できるようにします。
cfn-get-metadata: 特定のキーへのリソースまたはパスのメタデータを取得するために使用します。
cfn-hup: メタデータへの更新を確認し、変更が検出されたときにカスタムフックを実行するために使用します。

CloudFormation ヘルパースクリプトリファレンス - AWS CloudFormation

やってみた

検証用テンプレート(基本)

以下のCloudFormationテンプレートを基本に進めていきます。 Amazon Linux2のまっさらなEC2インスタンスを1台と、セキュリティグループを作成しています。 まだヘルパースクリプトは利用していません。

template.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: Cfn Helper Script Sample
Parameters:
  VpcId:
    Type: 'AWS::EC2::VPC::Id'
    Description: Your default VPC Id
  SubnetId:
    Type: 'AWS::EC2::Subnet::Id'
    Description: SubnetId in your default VPC
  KeyName:
    Type: 'AWS::EC2::KeyPair::KeyName'
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
  FromIpAddress:
    Type: String
    Description: The IP address range that can be used to SSH and HTTP to the EC2 instances
    Default: 0.0.0.0/0
  AmiId:
    Description: AMI Id
    Type: String
    Default: ami-0a1c2ec61571737db
Resources:
  ServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref AmiId
      InstanceType: t3.micro
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      KeyName: !Ref KeyName
      SubnetId : !Ref SubnetId
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Enable SSH access and HTTP access on the inbound port
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref FromIpAddress
        -
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref FromIpAddress
      VpcId: !Ref VpcId

cfn-init

リソースメタデータの取得と解釈、パッケージのインストール、ファイルの作成、およびサービスの開始で使用します。

cfn-initは、CloudFormationリソースのメタデータ AWS::CloudFormation::Init セクションに記述されたコンフィグを取得し その記述のとおりにEC2インスタンス内でパッケージのインストールやファイルの作成、サービスの開始などを実行するヘルパースクリプトです。

リファレンスを参照しつつ、試しに書いてみました。

  1. インスタンスのメタデータ内 AWS::CloudFormation::Init セクションに以下のコンフィグを記述。(リファレンス)
    • httpdのインストール
    • /var/www/html/index.html の配置
    • httpdの起動設定
  2. インスタンスのUserData内でcfn-init スクリプトを実行するよう指定。 これがメタデータに記されたコンフィグを読み取り、処理してくれます。(リファレンス)

template.yml

Resources:
  ServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple web app
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
          files:
            /var/www/html/index.html:
              content: !Sub |
                <p>Hello!</p>
              mode: '000644'
              owner: root
              group: root
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
    Properties:
      ImageId: !Ref AmiId
      InstanceType: t3.micro
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      KeyName: !Ref KeyName
      SubnetId : !Ref SubnetId
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          /opt/aws/bin/cfn-init -v \
          --stack ${AWS::StackName} \
          --resource ServerInstance \
          --region ${AWS::Region}
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'

テンプレートファイル全体はこちら(デプロイ時には東京リージョンの、デフォルトVPCを指定してください)

このCloudFormationテンプレートから起動したインスタンスは メタデータで指定したとおりにhttpdが起動しており、 テンプレート内で作成したindex.htmlがブラウザから確認できました!

2020/7/7 追記: 今はSSMのState Managerを利用する方法が推奨されているようです。
CloudFormation で cfn-init に代えて State Manager を利用する方法とその利点 | Amazon Web Services ブログ

cfn-hup

メタデータへの更新を確認し、変更が検出されたときにカスタムフックを実行するために使用します。

cfn-hupは、CloudFormationリソースのメタデータの変更を検知して ユーザの指定した動作を実行するために待ち受けるデーモンです。

事前設定

リファレンスを参考にcfn-hupデーモンの設定ファイルを作成し、 インスタンス内でcfn-hupデーモンが常時起動するように設定します。 メタデータの更新を1分間隔で確認し、変更が検出されたらcfn-initを実行する設定を記述しています。

template.yml

Resources:
  ServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple web app
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
          files:
            /var/www/html/index.html:
              content: !Sub |
                <p>Hello!</p>
              mode: '000644'
              owner: root
              group: root
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackName}
                region=${AWS::Region}
                interval=1
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.ServerInstance.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -v \
                  --stack  ${AWS::StackName} \
                  --resource ServerInstance \
                  --region ${AWS::Region}
                runas=root
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf

テンプレートファイル全体はこちら(ユーザーデータを実行する必要があるため、新規にスタックを作り直してください)

動作確認

CloudFormationテンプレート内のメタデータに変更をかけてみて、実際にcfn-hupが動くことを確かめます。 試しにメタデータ内で作成したindex.htmlファイルの中身を更新してみました。

template.yml

files:
            /var/www/html/index.html:
              content: !Sub |
                <p>Welcome!!!!</p>

UpdateStackを実行します。

変更はメタデータの内容のみで特にインスタンスの置換や再起動などは発生しませんが、 暫く待ってみるとcfn-hupによりcfn-auto-reloader-hookがトリガーされたようで 無事コンテンツが更新されたことが確認できました。

cfn-get-metadata

特定のキーへのリソースまたはパスのメタデータを取得するために使用します。

メタデータの情報を取得するためのスクリプトです。 先程cfn-hupの検証のため起動したEC2インスタンス内で cfn-get-metadataを実行してみたところ、 以下のようにCloudFormationリソースのメタデータの内容が出力されました。(リファレンス)

[ec2-user@ip-172-31-20-XX ~]$ sudo /opt/aws/bin/cfn-get-metadata \
&gt; --stack cfn-helper-scripts2 \
&gt; --resource ServerInstance \
&gt; --region ap-northeast-1
{
    "Comment": "Install a simple web app",
    "AWS::CloudFormation::Init": {
        "config": {
            "files": {
                "/etc/cfn/cfn-hup.conf": {
                    "owner": "root",
                    "content": "[main]\nstack=cfn-helper-scripts-2\nregion=ap-northeast-1\ninterval=1\n",
                    "group": "root",
                    "mode": "000400"
                },
                "/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
                    "content": "[cfn-auto-reloader-hook]\ntriggers=post.update\npath=Resources.ServerInstance.Metadata.AWS::CloudFormation::Init\naction=/opt/aws/bin/cfn-init -v \\\n  --stack  cfn-helper-scripts-2 \\\n  --resource ServerInstance \\\n  --region ap-northeast-1\nrunas=root\n"
                },
                "/var/www/html/index.html": {
                    "owner": "root",
                    "content": "&lt;p&gt;Welcome!!!!&lt;/p&gt;\n",
                    "group": "root",
                    "mode": "000644"
                }
            },
            "services": {
                "sysvinit": {
                    "httpd": {
                        "ensureRunning": "true",
                        "enabled": "true"
                    },
                    "cfn-hup": {
                        "files": [
                            "/etc/cfn/cfn-hup.conf",
                            "/etc/cfn/hooks.d/cfn-auto-reloader.conf"
                        ],
                        "ensureRunning": "true",
                        "enabled": "true"
                    }
                }
            },
            "packages": {
                "yum": {
                    "httpd": []
                }
            }
        }
    }
}

cfn-signal

CreationPolicy または WaitCondition でシグナルを送信するために使用し、前提となるリソースやアプリケーションの準備ができたときに、スタックの他のリソースを同期できるようにします。

cfn-signalは、 EC2リソースが正常に作成/更新されたかどうかを示すシグナルをCloudFormation に送信するスクリプトです。 シグナルはCloudFormationの CreationPolicy,UpdatePolicy 属性などで使われています。

こちらの動作を確かめるために、テンプレートに以下の設定を加えてみます。

  1. EC2インスタンスリソースの CreationPolicy を指定し、タイムアウトをめちゃくちゃシビア(10秒)に設定。 (リファレンス)
  2. インスタンスのUserData内でcfn-init スクリプトの成否をシグナルとしてスタックに送信するよう設定。 (リファレンス)
Resources:
  ServerInstance:
    Type: AWS::EC2::Instance
    CreationPolicy:
      ResourceSignal:
        Count: 1
        Timeout: PT10S
    Metadata:
      Comment: Install a simple web app
UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          /opt/aws/bin/cfn-init -v \
          --stack ${AWS::StackName} \
          --resource ServerInstance \
          --region ${AWS::Region}

          # signal the status from cfn-init
          /opt/aws/bin/cfn-signal -e $? \
          --stack ${AWS::StackName} \
          --resource ServerInstance \
          --region ${AWS::Region}
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'

テンプレート全体はこちら(CreationPolicyの判定を動かすため、スタックは更新ではなく新規に作り直してください)

これでリソースの作成開始から10秒以内にCloudFormationにシグナルが飛ばない場合にはスタックがロールバックされるはず…。

実際に動かしてみたところ、指定した期間内にシグナルを受信しなかったためタイムアウトが発生し リソースの作成は失敗扱いとなりました。

なお、タイムアウトの10秒から10分間に伸ばしてみたところ 作成から1分弱でシグナルの受信を検知した旨のメッセージが表示され、リソースの作成が完了していました。

使いみちとしては、AutoScalingグループ内のEC2インスタンスの更新時などに 新インスタンスのデプロイに失敗した(=成功シグナルを時間内に受信できなかった)場合はインスタンスを更新せずにロールバックする、などがありそうです。

まとめ

以上、CloudFormationの4種類のヘルパースクリプトをそれぞれ使ってみました。

何らかのお役に立てれば幸いです。