はじめに

みなさんは CloudFormation はお好きでしょうか?私は以前のエントリで以下のように書いていました。

CloudFormation、便利ですよね。インフラのコード化はインフラの状態の変化を可視化できるというたいへん素晴らしいメリットがあります。くわえてCloudFormationはスタック単位で作成・削除ができるため、WEBコンソールから手動で作成・削除するときに起こりうる「不要なリソースが残ったまま」や「必要なリソースを削除してしまう」というリスクが減ることで心理的・金銭的なコストが軽減される点がわたしは気に入っています。

cfn-initはUbuntuのどの起動システムにも対応しているのか?(其の一) | QUARTETCOM TECH BLOG

この思いに嘘偽りないのですが、実はだからといって CloudFormation が好きなわけではありません。むしろ「めんどくさいなー」と思っています。 いろいろ好きになれない理由はあるのですが、一番は「目的のリソースを構築しようとするときにどう書いていいかわかりづらい」という「根本的なところやないかい」というものです。

新しく触れるリソースを作るのにいきなり CloudFormation に手を出す方はいないと思います(いらっしゃったらごめんなさい)。私はまずはWebコンソールからゴニョゴニョしてリソースの設定やリソース間の関係性などをなんとなく把握してから CloudFormation のテンプレートに立ち向かっていました。この際ドキュメントを読むのですが「こんな名前の(設定|リソース)さっきは出てこなかったんですけどー」と思うことが多々あります。

これがつらい、私は。

特に日本語ドキュメントがこなれていないこともあり、四六時中 CloudFormation を触っているわけではない人間からすると脳みそ汗かきまくりなんですよね。

で、先日久しぶりに CloudFormation 使って作業することがあって、また動悸が始まったのですが、自分なりに工夫して「少し好きになれるかも」となったのでその方法を書いてみたいと思います。

要件

例として以下のような要件の環境を作ります。

作成する主なリソース

  • ECS
  • SQS
  • CloudWatch Alarms
  • CloudWatch Logs

動作

  1. SQS キューにメッセージがエンキューされる
  2. CloudWatch Alarm がアラーム状態になる
  3. アラームの状態によって ECS のサービスタスク数が増減する
  4. ECS のサービスは現在時刻を標準出力に echo するだけ。これを CloudWatch Logs に出力する

AWS Organizations でメンバーアカウントを作成

AWS Organizations は 公式サイト にあるように 複数の AWS アカウントへの一括請求が行えたり、ポリシーを集中管理できたりと何かと便利です。

各種リソースを仮に作る場合はまっさらなアカウントで試したいものです。そのために新たなアカウントを作るたびに請求情報を登録するのも面倒なのでこちらを利用します。

ここでは以下のメンバーアカウントを作っておきます。

  • naoyes001: Webコンソールでリソースが作成されるメンバーアカウント
  • naoyes002: CloudFormation でリソースが作成されるメンバーアカウント

Webコンソールでリソース作成

まずは メンバーアカウント naoyes001 にログインしてWebコンソールでリソースを作ります。

ECSからクラスターを選んで…という細かい手順は今回は割愛します。なお、名前をつけられるリソースについては基本的に test-*** という接頭辞をつけるようにしました。

たとえば以下は作成した CloudWatch Alarm test-alarm を利用して、 ECS Service に Auto Scaling の設定するところです。こんな感じでポチポチやっていきます。

ECS Webコンソール

各種リソースを作成した後、Webコンソールから SQS のキューにメッセージを追加すると、やがて ECS のサービス必要タスク数が増えて所望のタスクが稼働しました。 リソースは問題なく作られたようです。

CloudFormer

手本となるべきリソースは完成しました。これを基に CloudFormation の定義を構築していきます。

まずは CloudFormer を利用したいと思います。CloudFormer とは、以下の引用にあるとおりのツールです。

CloudFormer は、アカウントに既に存在する AWS リソースから AWS CloudFormation テンプレートを作成するテンプレート作成用のベータツールです。

CloudFormer (ベータ) を使用して既存の AWS リソースから AWS CloudFormation テンプレートを作成する - AWS CloudFormation

引用元に記載のあるとおり、すべてのリソースがサポートされているわけではありません。また利用に際してはツールが動くためのEC2インスタンスが起動しますので料金が発生することも念頭に置いておく必要があります。

さて、ここでは naoyes001 におけるリソースを CloudFormer で解析していきます。

まずは CloudFormer ツールの構築です。

CloudFormation_スタック_1 CloudFormation_スタック_2 CloudFormation_スタック_3

Webアクセス時のユーザネームとパスワードを忘れないようにします。なお、このツールリソース自体が CloudFormation で作成されています。

構築が完了したらWebアクセスするための URL が発行されますのでブラウザでアクセスします。テンプレート作成のウィザードが表示されますのでこれに従って操作を進めます。

CloudFormer 1

リージョンを選んで Create Template をクリック

CloudFormer 2

テンプレートに記載される Description を記入して、CloudFormer が提示してくれるリソースをフィルタするための文字列を任意で入力します。今回は test を入力しています。

CloudFormer 3

DNS 関係のリソースはありませんので提示されません。

CloudFormer 5

SQS のキューが提示されました。チェックを入れておきます。

CloudFormer 6

CloudWatch のアラームも提示されていますのでチェックを入れておきます。

CloudFormer 7

チェックを入れたふたつのリソースについて、テンプレート定義を吐き出してくれました。S3 の指定のバケットに登録しておくこともできるようですが、とりあえず私は手元のテンプレートファイルにコピペしておきました。

---
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  queuetestqueue:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds: '0'
      MaximumMessageSize: '262144'
      MessageRetentionPeriod: '345600'
      ReceiveMessageWaitTimeSeconds: '0'
      VisibilityTimeout: '30'
  alarmtestalarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ActionsEnabled: 'true'
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: '1'
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Period: '300'
      Statistic: Average
      Threshold: '0.0'
      AlarmActions:
      - arn:aws:autoscaling:ap-northeast-1:********:scalingPolicy:**********:resource/ecs/service/test-cluster/test-service:policyName/ScalePolicy
      Dimensions:
      - Name: QueueName
        Value: test-queue
Description: test-ecs-app

aws-cli

次に、 CloudFormer で足りない部分を補っていきます。

ここでは先ほど CloudFormer が吐いてくれた AWS::CloudWatch::Alarm が参照している AlarmActions にあたる部分の定義を作成していきます。(なお、この際に必要な AWS::ECS::Cluster および AWS::ECS::Service は既に記述済みの前提で進めていきます。)

このアラームが ALARM 以外の状態から ALARM 状態に遷移するときに実行されるアクションのリスト。各アクションを Amazon リソースネーム (ARN) として指定します。アラームの作成および指定可能なアクションの詳細については、Amazon CloudWatch API リファレンス の「PutMetricAlarm」および Amazon CloudWatch ユーザーガイド の「Amazon CloudWatch アラームの作成」を参照してください。

AWS::CloudWatch::Alarm - AWS CloudFormation

ドキュメントを読むと上記のような記載がありました。これだけだと正直私はぜんぜんピンときません。しかし、 CloudFormer の吐いてくれた定義上に arn:aws:autoscaling:ap-northeast-1:********:scalingPolicy:**********:resource/ecs/service/test-cluster/test-service:policyName/ScalePolicy とあるので見当をつけて試行錯誤してみます。

ここで、AWS リソースおよびプロパティタイプのリファレンス - AWS CloudFormation を見てそれっぽいリソースをあたってみます。 そのものズバリ Auto Scaling というリソースがあるようです。 aws-cli で確認してみましょう。

$ aws autoscaling describe-policies --profile naoyes001
{
    "ScalingPolicies": []
}

空配列が返ってきました。これは見当が外れたようです。

もう一度リソースの一覧を眺めてみると、似たようなもので Application Auto Scaling というリソースがあるようです。これも aws-cli で確認してみましょう。

$ aws application-autoscaling describe-scaling-policies --service-namespace ecs --profile naoyes001
{
    "ScalingPolicies": [
        {
            "PolicyName": "ScalePolicy", 
            "ScalableDimension": "ecs:service:DesiredCount", 
            "ResourceId": "service/test-cluster/test-service", 
            "CreationTime": 1564132644.053, 
            "StepScalingPolicyConfiguration": {
                "MetricAggregationType": "Average", 
                "Cooldown": 300, 
                "StepAdjustments": [
                    {
                        "ScalingAdjustment": 0, 
                        "MetricIntervalLowerBound": 0.0, 
                        "MetricIntervalUpperBound": 1.0
                    }, 
                    {
                        "ScalingAdjustment": 1, 
                        "MetricIntervalLowerBound": 1.0, 
                        "MetricIntervalUpperBound": 2.0
                    }, 
                    {
                        "ScalingAdjustment": 2, 
                        "MetricIntervalLowerBound": 2.0
                    }
                ], 
                "AdjustmentType": "ExactCapacity"
            }, 
            "PolicyARN": "arn:aws:autoscaling:ap-northeast-1:********:scalingPolicy:**********:resource/ecs/service/test-cluster/test-service:policyName/ScalePolicy", 
            "PolicyType": "StepScaling", 
            "Alarms": [
                {
                    "AlarmName": "test-alarm", 
                    "AlarmARN": "arn:aws:cloudwatch:ap-northeast-1:********:alarm:test-alarm"
                }
            ], 
            "ServiceNamespace": "ecs"
        }
    ]
}

手作業でWebコンソールにて設定した内容らしいものが返ってきました。

どうやら AWS::ApplicationAutoScaling::ScalingPolicy というリソースが必要そうだということがわかりました。

このドキュメントと先ほどの aws-cli の戻り値を照らし合わせながら記述していきます。 CloudFormation で定義すべきプロパティと aws-cli の戻り値がけっこう整合しているので値を埋めていくだけです。簡単ですね。

ResourceId : Required: Conditional.ScalingTargetId プロパティまたは ResourceId、ScalableDimension、そして ServiceNamespace プロパティのいずれかを指定する必要があります。ResourceId、ScalableDimension、そして ServiceNamespace プロパティを指定した場合、ScalingTargetId プロパティを指定しないでください。 :

AWS::ApplicationAutoScaling::ScalingPolicy - AWS CloudFormation

ドキュメントには上記のように記載されていて、ResourceId, ScalableDimension, ServiceNamespace がすべて記述されていれば ScalingTargetId は不要とのことです。さきほどの aws-cli の戻り値を見ても、ScalingTargetId という値はありませんでしたしこれで良さそうですね。

TestAppAutoscalingScalingPolicy:
  Type : AWS::ApplicationAutoScaling::ScalingPolicy
  Properties:
    PolicyName: 'ScalePolicy'
    PolicyType: StepScaling
    ResourceId:
      !Join
      - '/'
      - - 'service'
        - !Ref TestCluster
        - !GetAtt TestService.Name
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs
    StepScalingPolicyConfiguration:
      AdjustmentType: ExactCapacity
      Cooldown: 300
      MetricAggregationType: Average
      StepAdjustments:
        - ScalingAdjustment: 0
          MetricIntervalLowerBound: 0
          MetricIntervalUpperBound: 1
        - ScalingAdjustment: 1
          MetricIntervalLowerBound: 1
          MetricIntervalUpperBound: 2
        - ScalingAdjustment: 2
          MetricIntervalLowerBound: 2

さっそく適用してみます。ふたつ作ったメンバーアカウントのうちこの時点でまっさらな naoyes002 に適用してみます。

$ aws cloudformation create-stack --stack-name test-ecs-app --template-body file://$(pwd)/template.yaml --capabilities CAPABILITY_NAMED_IAM --profile naoyes002

メンバーアカウント naoyes002 のWebコンソールなどで CloudFormation のスタック作成の進捗を確認すると、以下のようなエラーメッセージと共に異常終了していることがわかりました。

No scalable target registered for service namespace: ecs, resource ID: service/test-ecs-app-TestCluster-***********/test-ecs-app-TestService-*********, scalable dimension: ecs:service:DesiredCount (Service: AWSApplicationAutoScaling; Status Code: 400; Error Code: ObjectNotFoundException; Request ID: ****-****-****-****)

訳がわかりませんね。No scalable target registered とあるので、文面からするとscalable target なるリソースが足りないようです。

application-autoscaling コマンドには describe-scalable-targets というサブコマンドがあるので、

$ aws application-autoscaling help
 :
 :
       o describe-scalable-targets
 :

これを使って naoyes001 のリソースを確認してみましょう。

$ aws application-autoscaling describe-scalable-targets --service-namespace ecs --profile naoyes001
{
    "ScalableTargets": [
        {
            "ScalableDimension": "ecs:service:DesiredCount",
            "ResourceId": "service/test-cluster/test-service",
            "RoleARN": "arn:aws:iam::********:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService",
            "CreationTime": 1564129660.911,
            "MinCapacity": 0,
            "ServiceNamespace": "ecs",
            "MaxCapacity": 2
        }
    ]
}

さきほどの AWS::ApplicationAutoScaling::ScalingPolicy における

    ResourceId:
      !Join
      - '/'
      - - 'service'
        - !Ref TestCluster
        - !GetAtt TestService.Name
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs

の箇所と似ていますね。

ResourceId, ScalableDimension, ServiceNamespace がすべて記述されていれば ScalingTargetId は不要とのことです。さきほどの aws-cli の戻り値を見ても、ScalingTargetId という値はありませんでしたしこれで良さそうですね。

と先述しましたが、 No scalable target registered と言われてしまっているので、ここは ScalingTargetId を定義する方向に方針転換してみます。

公式ドキュメントを見てみましょう。AWS::ApplicationAutoScaling::ScalableTarget - AWS CloudFormation

aws application-autoscaling describe-scalable-targets --service-namespace ecs --profile naoyes001 での戻り値を参考に埋められそうです。なお IAMロール IamEcsAppAutoscalingRole の定義についてはここでは割愛します。

TestAppAutoscalingScalableTarget:
  Type: AWS::ApplicationAutoScaling::ScalableTarget
  Properties:
    MaxCapacity: 2
    MinCapacity: 0
    ResourceId:
      !Join
      - '/'
      - - 'service'
        - !Ref TestCluster
        - !GetAtt TestService.Name
    RoleARN: !GetAtt IamEcsAppAutoscalingRole.Arn
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs

AWS::ApplicationAutoScaling::ScalableTarget を定義し、

TestAppAutoscalingScalingPolicy:
  Type : AWS::ApplicationAutoScaling::ScalingPolicy
  Properties:
    PolicyName: 'ScalePolicy'
    PolicyType: StepScaling
-   ResourceId:
-     !Join
-     - '/'
-     - - 'service'
-       - !Ref TestCluster
-       - !GetAtt TestService.Name
-   ScalableDimension: ecs:service:DesiredCount
-   ServiceNamespace: ecs
+   ScalingTargetId: !Ref TestAppAutoscalingScalableTarget
    StepScalingPolicyConfiguration:
      AdjustmentType: ExactCapacity
      Cooldown: 300
      MetricAggregationType: Average
      StepAdjustments:
        - ScalingAdjustment: 0
          MetricIntervalLowerBound: 0
          MetricIntervalUpperBound: 1
        - ScalingAdjustment: 1
          MetricIntervalLowerBound: 1
          MetricIntervalUpperBound: 2
        - ScalingAdjustment: 2
          MetricIntervalLowerBound: 2

AWS::ApplicationAutoScaling::ScalingPolicy から参照するように変更しました。

以上で完成です。さきほどの異常終了したスタックを削除して再度スタックを作成すると無事に完了しました。

おわりに

Webコンソールは偉大です。直感的ですし面倒なリソース作成は裏でやってくれます。なのでじゃんじゃん使うべきだと思います。

ただし持続可能な再現性を担保するのは難しい。そうしたときに CloudFormation は非常にありがたい存在です。

今回提示した手法が少しでも参考になればと思います。

成果物は https://github.com/naoyes/201908-qtb-cfn/blob/master/template.yaml として上げておきました。こちらも併せて参考にしていただければ。