CloudFormationの中のEC2のユーザーデータでシェル変数を使用する

CloudFormationの中でユーザーデータでシェル変数を取り扱う時にちょっと詰まったのでその備忘録です。 組み込み関数Fn::Subの変数は最初の中括弧の後に感嘆符 (!) を追加することでエスケープできます。 (例:${!Literal})
2019.06.11

CloudFormationでEC2のユーザーデータをごりっごりに書きたいんじゃ…!

そんな気分の時はございませんか?


ユーザーデータとは、EC2起動時に実行されるシェルスクリプトです。

Linux インスタンスでの起動時のコマンドの実行 - Amazon Elastic Compute Cloud

起動時にホスト名、時刻同期、文字コード、タイムゾーンの設定をスクリプト化することでEC2の構築作業を省力化できます。 弊社ブログで具体的な方法を紹介しています。

[AWS]CloudFormationを使いこなして早く帰るTips5選 | DevelopersIO

そんな中、ユーザーデータでシェル変数を取り扱う時にちょっと詰まったのでその備忘録です。

CloudFormationでユーザーデータを書く

ユーザーデータをEC2に直接定義するとマネジメントコンソールで確認できないため、今回はLaunchTemplate(起動テンプレート)を利用します。

CloudFormationでユーザーデータを定義したLaunchTemplateを書くとこんな感じです。 ホームフォルダによくわかんないファイルを作っていますが、とりあえず流してください。

---
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  SampleLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: sample-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: |
            #!/bin/bash

            echo INSTANCE_ID > /home/ec2-user/instance-id.txt

CloudFormationでユーザーデータを記述する場合、Base64エンコードされてなければいけません。なので組み込み関数の Fn::Base64 を使用しています。

AWS::EC2::Instance - AWS CloudFormation

Fn::Base64 - AWS CloudFormation

組み込み関数Fn::Subを使用する

ユーザーデータの中でCloudFormationのパラメーターを使いたいことがよくあります。

ホスト名をパラメーターで指定できるようにしてテンプレートを使いまわしたいとか、そういうケースです。

そういう場合は、組み込み関数Fn::Subを使用します。

Fn::Sub - AWS CloudFormation

ファイル名をパラメータ指定できるように変更するとこんな感じです。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  InstanceIdFileName:
    Type: String
    Default: instance-id.txt

Resources:
  SampleLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: sample-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              echo INSTANCE_ID > /home/ec2-user/${FileName}
            - {
                FileName: !Ref InstanceIdFileName
              }

ユーザーデータの中でシェル変数を使用する

さて、ここからが本題です。

Fn::Subの変数は ${MyVarName} といった表記をしているんですが、完全にシェルの変数参照と表記が被っています。

なのでcurlコマンド使ってEC2のメタデータからインスタンスIDとってきて変数に格納、後の行で変数参照しようと思って以下の様な書き方をすると、 Unresolved resource dependencies [INSTANCE_ID] ってvalidateで怒られます。

---
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  InstanceIdFileName:
    Type: String
    Default: instance-id.txt

Resources:
  SampleLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: sample-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

              echo ${INSTANCE_ID} > /home/ec2-user/${FileName}
            - {
                FileName: !Ref InstanceIdFileName
              }

どうしたものかと悩んでいたんですが、よく見るとエスケープの方法がドキュメントに記載されていました。

Fn::Sub - AWS CloudFormation

ドル記号と中括弧 (${}) をそのまま書き込むには、最初の中括弧の後に感嘆符 (!) を追加します (${!Literal} など)。AWS CloudFormation では、このテキストは ${Literal} のようになります。

と、いうわけでドキュメントに記載されている通りエスケープしてやれば、シェル変数もガンガン使えます。

---
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  InstanceIdFileName:
    Type: String
    Default: instance-id.txt

Resources:
  SampleLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: sample-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

              echo ${!INSTANCE_ID} > /home/ec2-user/${FileName}
            - {
                FileName: !Ref InstanceIdFileName
              }

実際にCloudFormationスタックを作成して、マネジメントコンソールでユーザーデータの内容を確認してみると、ファイル名だけCloudFormationのパラメーターに置換されているのに対して、エスケープした ${INSTANCE_ID} はそのまま残せています。

このLaunchTemplateを元にしてEC2を立ててSSH接続してみると、想定したとおりホームフォルダにインスタンスIDが記載されたファイルができていることがわかります。

おわりに

これでユーザーデータに変数山盛りにしたCloudFormationテンプレートがガンガン書けますね!!

他の人が読めるテンプレートになるよう、ほどほどにしておきましょう。

おまけ

検証に使用したEC2付きのCloudFormationテンプレートを記載しておきます。 イメージID,キーペア名,セキュリティグループID,サブネットIDは適切に設定してください。

---
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  InstanceIdFileName:
    Type: String
    Default: instance-id.txt
  Ec2InstanceType:
    Type: String
    Default: t2.micro
  Ec2ImageId:
    Type: AWS::EC2::Image::Id
  Ec2KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
  Ec2SecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
  Ec2SubnetId:
    Type: AWS::EC2::Subnet::Id
    
Resources:
  SampleLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: sample-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

              echo ${!INSTANCE_ID} > /home/ec2-user/${FileName}
            - {
                FileName: !Ref InstanceIdFileName
              }
  SampleInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref Ec2ImageId
      InstanceType: !Ref Ec2InstanceType
      KeyName: !Ref Ec2KeyPairName
      SecurityGroupIds:
        - !Ref Ec2SecurityGroupId
      SubnetId: !Ref Ec2SubnetId
      Tags:
        -
          Key: Name
          Value: sample-ec2
      LaunchTemplate:
        LaunchTemplateId: !Ref SampleLaunchTemplate
        Version: !GetAtt SampleLaunchTemplate.LatestVersionNumber