AWS CloudFormation で UserData を用いて Linux インスタンスを構築する際の TIPS

テンプレートの例、挙動、TIPS などを雑多にまとめた。

前提

  • Lin インスタンスは RHEL

テンプレート

Resources:
  Instance1:
    Type: AWS::EC2::Instance
    Properties: 
      ……
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash -xe

          # ★ここに ShellScript を書く

          # 制約
          # - root ユーザーで実行される(sudo 不要)
          # - インタラクティブは受け付けない
          #   X) yum install unzip
          #   O) yes | yum install unzip

          # 変数の書き方
          # - CloudFormation系は ${} で囲む
          #     !Ref Vpc1 → ${Vpc1}
          #     ${AWS::Region}
          # - ShellScript の変数は ${!VarName} のように ! でエスケープ
          #     target_vpc="`cat vpc-id.txt`"
          #     aws ec2 describe-instances --filters "Name=vpc-id,Values=${!target_vpc}" ...

          # デバッグ
          # - 動作ログは以下みたいだが, メタ情報すぎて役に立たない
          #   /var/log/cloud-init.log
          # - UserData に書いたスクリは以下に保存されてる
          #   /var/lib/cloud/instances/(instance-id)/user-data.txt

ドキュメント

挙動

UserData に書いたスクリはいつ実行されるか?

Ans: CREATE COMPLETE から少しタイムラグがある

  • 1: 管理コンソール上で Lin インスタンスが CREATE COMPLETE した
  • 2: Lin インスタンスに SSH ログインできるようになった
  • 3: UserData に書いたスクリが実行された

1, 2, 3 は同時ではなく この順番で実行にタイムラグがある。感覚では 1 の後に 2 が可能になるまで数十秒、2 の後に 3 が実行されるまでに 10 秒。

なので素早く操作してると「あれ?UserData実行されてないんじゃん?」 → 「あ、実行されてた」なんてことになってちょっと焦る。

UserData に書いたスクリが実行失敗したらステータスはどうなる?

Ans: CREATE COMPLETE のまま。

管理コンソール上からは UserData スクリの実行成否は見えない

たぶんだけど、そもそもレイヤーが違う感じ。以下のようになってる?

  • Lin インスタンスが生成されたら CREATE COMPLETE
  • その後で、
  • UserData を走らせる
    • この結果は管理コンソールには出さないよ

UserData 部分を編集して再アップ(スタック更新)したら再実行どうなる?

Ans: されません。

リファレンス AWS::EC2::Instance - AWS CloudFormation によると、

Update requires: Some interruptions

とある。Some interruptions(中断を伴わない更新)とは リソースの使用を中断することなく、またリソースの物理 ID を変更することなく、リソースを更新する もの。一方で、UserData は「新しくインスタンスをつくるときに一度だけ実行される」もの。

よって再実行はされない。

※リソース更新として何が更新されるかはよくわからん。 /var/lib/cloud/instances/(instance-id)/user-data.txt あたりが置き換わったりしてる?

TIPS - 書き方

あらかじめつくっといたスクリを実行させたいのですが?

Ans: どっかにアップしたものをゲットしてくる。

たとえば S3 にアップしておいて、curl で取ってくるとか。

curl "https://(BucketName).s3-(RegionName).amazonaws.com/(UploadedScriptName).sh" -o "(YourFavoriteName).sh"
chmod 775 (YourFavoriteName).sh
./(YourFavoriteName).sh

ただし S3 のアクセス権が面倒か。適切にアクセス権つけて IAM つける必要がある。

私は今回はちょっとしたお試しだったので以下のようにした。

  • スクリプトは全部パブリックにする
  • バケットは……
    • 使うときはパブリックにする
    • 使わないときはパブリックを全部切る

ガバガバなので好ましくはない。たとえば URL がばれたら誰でも見れちゃうし、悪意ある人が DDoS で get を仕掛けたら(たとえば何百万回とダウンロードするとか)課金地獄になってしまうかもしれない。

AWS CLI をセットアップしたいのですが?

Ans: 愚直に。

AWS_REGION="ap-northeast-1"
curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
python get-pip.py
pip install awscli
aws configure set region ${AWS_REGION}

自身が所属する VPC の ID など AWS リソース情報がほしいのですが?

Ans: AWS CLI の describes 系コマンドで頑張る。

たとえば以下は「指定タグがついた VPC」の ID を表示する。

$ aws ec2 describe-vpcs --filters "Name=tag-key,Values=${TagKeyForMyParentVpc}" --query "Vpcs[].VpcId[]"
[
    "vpc-XXXXXXXXXXXXXXXXX"
]

上記をもう少し詳しく言うと、TagKeyForMyParentVpc タグのついた VPC 情報 response を引っ張ってきて、その response.vpcs.0.VpcId を見ている。クエリは --query "Vpcs[].VpcId でも良いかも?

ちなみに上記は jq でパースすれば楽に取り出せる。

$ aws ec2 describe-vpcs --filters "Name=tag-key,Values=${TagKeyForMyParentVpc}" --query "Vpcs[].VpcId[]" | jq -r .[0]
vpc-XXXXXXXXXXXXXXXXX

参考: describe-vpcs — AWS CLI 1.16.281 Command Reference

JSON をパースしたいのですが?

Ans: jq を使う。RHEL ではパッケージがないので自力でバイナリを get してきて配置する。

yes | yum install wget
wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64
chmod 775 ./jq
cp jq /usr/bin

参考: How to install jq on RHEL6.5 - Server Fault

TIPS - UserData を素早く試行するには?

まず ShellScript 部分はインスタンス上で一通り動作確認まで持っていく。問題はその後、UserData に書き写してから動作成功に持っていくところだろう。

やり方1: 全部入りテンプレ

愚直に行くと「インスタンス」と「インスタンスに必要な VPC やら IAM やら」を全部つくるテンプレに UserData を書いてスタック作成することになる。仮に 全部入りテンプレ と呼ぼう。

全部入りテンプレは言うまでもなく時間がかかる。最小構成でも VPC x1、Subnet x1~2、RouteTable x1~2、InternetGateway x1、NAT x1、SecurityGroup x1、Instance x1 くらいはつくると思う。この規模だと スタック作成に 5 分、削除にも 5 分くらいかかる感触。つまり一度の試行で 10 分くらいかかる。

なのでもう少し楽をする。

やり方3: 注入テンプレ

注入テンプレ と名付けた方法を使う。といっても、単純で、

  • 1: 「インスタンスに必要な環境をつくるテンプレ A 」で環境をつくる
  • 2: 「A の環境にインスタンスをつくるテンプレ B」でインスタンスをつくる
  • 試行するときは B だけをスタック作成&削除する

こうする。これなら全部入り(A+B)とは違い、インスタンス(B)だけが作成&削除の対象になる。作成に 1 分、削除に 1 分くらいで済む

※IAM もつくっちゃうと 2 分くらいなるけど……

ちなみにインスタンス作成時には「サブネットの ID」が必要なので、以下のようにすると良いか。

  • テンプレ A の Output で、サブネットの ID を表示する
  • テンプレ B には「テンプレ A のサブネット ID」を受け付けるパラメーターを設置する

おわりに

サーバー設定の自動化として UserData を使う方法についてまとめた。AWS リソースと比べるとトライアルアンドエラーしづらいが、このレイヤーも自動化したいなら頑張るしかない。少しずつ頑張っていくとしよう。

他にも AWS::CloudFormation::Init などの手段があるみたいなので追々試したい。