話題の記事

【AWS】CloudFormationの作成ノウハウをまとめた社内向け資料を公開してみる

2013.10.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは植木和樹です。5月にクラスメソッドに入社してから毎日CloudFormationと戯れる日々を過ごして来ました。クラスメソッドのAWSエンジニアにとってCloudFormationが必須技術になりつつあるので、これまでに溜め込んだノウハウ社内向け資料としてまとめてみました。せっかくなのでこれからCloudFormationを始める方向けに公開してみようと思います。

スタックに関するノウハウ

スタック内リソースの「破棄」タイミングを合わせる

CloudFormationというとEC2やRDSをバッチ的に「構築」する機能に着目しがちです。しかし当然のことながら一度作ったスタックをDELETEすることもあります。その際に削除して欲しくないリソースもまとめて消されてしまうことを想定しておきましょう。

たとえばEC2を作成したスタックの中に、EIP(グローバルIP)が含まれていたらどうでしょう。さらにこのEC2がメールサーバー用途で、割り当てたEIPでメールの送信制限解除を申請していたとします。スタックを削除して作りなおすと、これまでのEIPは破棄され新しいEIP(異なるアドレス)が割り当てられます。つまり申請をやり直すことになってしまいます。スタックを削除した際に、消えて欲しくないリソースは同じテンプレートに含めないようにしましょう。

これを回避するためには、一部のリソースはマネージメントコンソールから作るか、AWS CLIを用いたシェルスクリプトを用意しましょう。もしくはEIP作成用のテンプレートを別に作成し、EC2作成用スタックとは別スタックに分けるという方法もあります。

消えてしまっては困るリソースにはEIPの他、IAMユーザーがあります。IAMユーザー(厳密にいうとAWS::IAM::AccessKey)を作りなおした場合、これまでのアクセスキーが使えなくなることに注意しましょう。

リソース作成に関するノウハウ

セキュリティグループの相互参照を行う

たとえばWebサーバー用途のEC2からRDSへ接続する場合、WebサーバーはRDSとのみ通信を許可する(Outbound)、RDSはWebサーバーからのみの接続を許可する(Inbound)という設定をしたい場合があります。その際、以下のようなテンプレートを記述するとWebサーバー用セキュリティグループとRDS用セキュリティグループの作成が鶏と卵の関係になり「circular dependencies」(循環参照)というエラーでスタックを作成することができません。

  "Resources" : {
    "WebServerSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "SecurityGroup web-grp",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp",
            "FromPort" : "80",
            "ToPort" : "80",
            "SourceSecurityGroupId" : { "CidrIp" : "0.0.0.0/0" } }
        ],
        "SecurityGroupEgress" : [
          { "IpProtocol" : "tcp",
            "FromPort" : "3306",
            "ToPort" : "3306",
            "SourceSecurityGroupId" : { "Ref" : "RDSSecurityGroup" } }
        ]
      }
    },
    "RDSSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "SecurityGroup rds-grp",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp",
            "FromPort" : "3306",
            "ToPort" : "3306",
            "SourceSecurityGroupId" : { "Ref" : "WebServerSecurityGroup" } }
        ]
      }
    }
  }

この場合はAWS::EC2::SecurityGroupリソースと、AWS::EC2::SecurityGroupEngressを分けて作成することで実現することができます。まずはAWS::EC2::SecurityGroupで「入れ物」を作ってからAWS::EC2::SecurityGroupEngressで「中身」を詰めるわけです。

  "Resources" : {
    "WebServerSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "SecurityGroup web-grp",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp",
            "FromPort" : "80",
            "ToPort" : "80",
            "CidrIp" : "0.0.0.0/0" }       
        ]
      }
    },
    "WebServerSecurityGroupEgress" : {
      "Type" : "AWS::EC2::SecurityGroupEgress",
      "Properties" : {
          "GroupId" : { "Ref" : "WebServerSecurityGroup" },
          "IpProtocol" : "tcp",
          "FromPort" : "3306",
          "ToPort" : "3306",
          "SourceSecurityGroupId" : { "Ref" : "RDSSecurityGroup" }
      }
    }, 

    "RDSSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "SecurityGroup rds-grp",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp",
            "FromPort" : "3306",
            "ToPort" : "3306",
            "SourceSecurityGroupId" : { "Ref" : "WebServerSecurityGroup" } }
        ]
      }
    }
  }

Java インターフェース的なセキュリティグループの使い方

rsyslogクライアントからrsyslogサーバーへの通信や、ZabbixエージェントからZabbixサーバーへの通信、td-agentからfluentdサーバーへの通信など、特定ノードからサーバーへの通信を許可したい場合があります。この時ノードがWeb、バッチ、メールなどそれぞれ異なるセキュリティグループに属するような場合、それらをすべてサーバー側のセキュリティグループに列挙するのは大変です。

この場合はIngressやEgressを指定しない「ガワ」だけのセキュリティグループを作成します。この「ガワ」からの通信を許可するセキュリティグループを作成してサーバーインスタンスに指定します。そしてクライアントノードには「ガワ」のセキュリティグループを割り当てます。Javaのインターフェースみたいなものでしょうか。EC2-VPCだと複数のセキュリティグループを割り当てることができるという機能を利用したテクニックですので、EC2-Classicでは使えません。

  "Resources" : {
    "RSyslogClientGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "SecurityGroup rsyslog-client-grp",
        "VpcId" : { "Ref" : "VpcId" }
      }
    },

    "RSyslogServerGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "SecurityGroup rsyslog-server-grp",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [
          { "IpProtocol" : "tcp",
            "FromPort" : "514",
            "ToPort" : "514",
            "SourceSecurityGroupId" : { "Ref" : "RSyslogClientGroup" } }
        ]
      }
    }
  }

20131003_cloudformation_002

Static-IPを指定するUpdate Stackには注意する

AWS::EC2::InstanceリソースはPrivateIpAddressプロパティーでプライベートIPアドレスを指定することができます。なにかしらの理由でIPアドレスを固定したい場合に使用するパラメーターです。

ただPrivateIpAddressプロパティを使う際には注意が必要です。たとえばインスタンスを作り直したいとしましょう。その場合には以下のような順序でインスタンスの入れ替えが行われます。

  • 新しいEC2インスタンスが作成される
  • 他のリソースで古いEC2への参照が新しいEC2を参照するよう置き換わる
  • 古いEC2インスタンスがTerminateされる

テンプレート内でプライベートIPアドレスを指定していた場合、新しいEC2インスタンスが作成されるタイミングでIPアドレスの重複が発生しEC2の作成に失敗します。

固定IPアドレスをENIに設定し、EC2インスタンスに付け替えることでこの問題を回避できるか試したのですが「Interface: [eni-xxxxxxxx] in use.」となりENIの付け替えができませんでした。

  "Resources" : {
    "AWSEC2NetworkInterface" : {
      "Type" : "AWS::EC2::NetworkInterface",  
      "Properties" : {
        "SubnetId" : "subnet-0c6db464", 
        "PrivateIpAddress" : "172.31.0.10"
      }
    },

    "AWSEC2Instance" : {
      "Type" : "AWS::EC2::Instance",  
      "Properties" : {
        "ImageId" : "ami-39b23d38",  
        "InstanceType" : "m1.small",
        "NetworkInterfaces" : [
          {
            "DeviceIndex" : "0", 
            "NetworkInterfaceId" : { "Ref" : "AWSEC2NetworkInterface" }
          }
        ]
      }
    }
  }

ひとまず現在稼働中のEC2インスタンスをTerminateしてからスタックをアップデートすることで、EC2インスタンスを作りなおすことができます。もっと良い方法があるかもしれません。

CFnで対応していないパラメーターがあるので注意

マネージメントコンソールで指定できるリソースの各種パラメーターのすべてがCloudFormationで指定できるわけではありません。例えばSQSのパラメーターはVisibilityTimeoutのみです。どのパラメーターが指定可能かは「Template Reference」を参照してください。

CFnで対応していないコンポーネントがある

パラメーターと同様、CloudFormationで対応していないリソースもあります。ElastiCacheをVPC内に作成することは(2013年10月2日現在)できません。

CloudFormationに対応しているリソースについても「Template Reference」を参照してください。

新しいコンポーネントやパラメーターがCloudFormationで対応するとAmazon Web Services ブログで発表されますので、マメにチェックおくと良いでしょう。

Amazon Web Services ブログ

2013/10/03 07:00訂正
VPC内のElastiCacheは2013/09/17のUpdateで対応していました。
AWS CloudFormation Introduces Support for Additional VPC Resource Types and Properties」)

EC2インスタンスにデフォルトのIAMロールはつけておく

EC2に指定できるパラメーターのうち、いくつかはインスタンスを作り直さないと変更することができません。IamInstanceProfileの設定・変更はEC2インスタンスの再作成を伴います。

パラメーターの変更によってインスタンスが再作成されるかどうかは、ドキュメントの「Update requires」に記されています。これが"Replacement"だと変更に伴い、リソースの再作成が発生します。"no interruption"だと再作成はされません。

リソースの置き換えについての解説は「Updating AWS CloudFormation Stacks」を参照してください。

CFnで利用するAMIは消さないこと

自作のAMIをベースにEC2インスタンスを作成し、後日不要と判断してAMIを削除したとします。

しばらくしてスタックを更新しなければならなくなり、前述のようにEC2インスタンスの再作成が発生したとしましょう。何事もなくスタックの更新が行われれば問題はないのですが、途中でテンプレートの記述ミスがあり更新処理がロールバックされてしまったらどうなるでしょう。

ロールバック処理によってEC2のインスタンスも復元しようとするのですが、元となるAMIが消されていると、このロールバックにも失敗します。そうなると更新もできない、ロールバックもできない「手詰まり」になります。こうなるとスタックを一度削除し、再作成するしか方法がありません。

AMIなどテンプレートから参照しているリソース、特にCloudFormationを使わずに作成したリソースは無闇に削除しないよう気をつけてください。

パラメーターを指定したくない場合

IAM instance profileやPrivate IPアドレスなど、指定したいインスタンスとしたくないインスタンスがあったとします。テンプレート内では条件分岐ができませんが、IamInstanceProfileには空文字列を与えればデフォルト設定が適用されます。

"IamInstanceProfile" : ""

IamInstanceProfileの指定有無でテンプレートを分けなくて良いので便利ですね。

ただPrivateIpAddressに空文字列を与えたところ「invalid value for parameter address」となってしまいました。こちらは空文字列で初期設定にはならないようです。dhcpみたいに指定できれば良いのですが・・・

"PrivateIpAddress" : "dhcp"

空文字列を指定できるパラメーターとできないパラメーターについて、まとまった資料を見つけることはできませんでした。

EC2-Classic から EC2-VPC へのスタック更新による変更は行わない

CloudFormationを使ってEC2-Classic環境で作成したEC2インスタンスを、スタック更新によってEC2-VPCに変更するのはやめましょう。理由はEC2-ClassicとEC2-VPCでEC2に指定するパラメーターの書き方が大きく異なり複雑で失敗いやすいからです。たとえばEC2の属するセキュリティグループをEC2-Classicでは「セキュリティグループ」で指定するのですが、EC2-VPCでは「セキュリティグループID」で指定します。

セキュリティグループIDで指定しなければならないところを、誤ってセキュリティグループ名で指定しているとスタック更新に失敗します。そしてロールバック処理が行われるのですが、ClassicとVPCの情報が混在し不整合が発生してしまうようでロールバック処理が途中で失敗します。スタックを削除して作りなおすしかなくなりますので気をつけましょう。

EC2にENIをつけるとき、EC2の"SecurityGroup"、"SubnetId"、"SourceDestCheck"は指定できない

EC2にネットワークを指定する方法は、EC2のプロパティで指定する方法と、ENIを作成してEC2に割り当てる方法があります。

ENIを割り当てる際は、EC2に指定できていた"SecurityGroup"、"SubnetId"、"SourceDestCheck"が指定できなくなります。これらのパラメーターは本来ENIに紐づくパラメーターなので、明示的にENIを割り当てる場合にはEC2には指定できなくなります。

パラレル化によってDependsOnは必須

InternetGatewayとEIPを作成したスタックを削除する際にInternetGatewayリソースが削除できない問題が発生しています。どうも「並列スタック処理」に伴って依存関係の解決がうまくいかない場合があるようです。

この問題はAWS::EC2::EIPAWS::EC2::InternetGateway(またはAWS::EC2::VPCGatewayAttachment)リソースへのDependsOnを追加して明示的に依存関係を指定することで回避することができます。

AWS CloudFormation サンプルテンプレート」の「multi-tier-vpc.template」や「vpc_multiple_subnets.template」はこの修正が施されていますので参考にしてください。

テンプレート作成環境に関するノウハウ

エディタはEclipse + AWS Toolkitが便利

CloudFormationのテンプレートをコーディングする時はEclipse + AWS Toolkitを使っています。AWS Toolkitの「Amazon CloudFormation Template Editor」は入力補完や構文ハイライト、アウトライン機能があって、長くなりがちなテンプレートを書きやすく読みやすくしてくれます。

一応JSONの構文チェックもあります。行末のカンマ漏れくらいの簡単なチェックしかしてくれませんがないよりは便利です。あくまで構文チェックで、リソースの依存関係が正しいかどうかまではチェックしてくれません。

AWS Toolkit for Eclipseについては「色々な言語・環境(計8言語12種類)でAWSを触ってみた」も参考にしてください。

ローカルでValidationしてからcreate-stackすると効率的

AWS Toolkitではリソースの依存関係チェックまではしてくれませんが、apitoolsに含まれるcfn-validate-templateコマンドでチェックすることができます。homebrewならaws-cfn-toolsをインストールしてください。

$ brew install aws-cfn-tools
$ export AWS_CLOUDFORMATION_HOME="/usr/local/Library/LinkedKegs/aws-cfn-tools/jars"
$ export AWS_CREDENTIAL_FILE="<Path to the credentials file>"
$ export AWS_CLOUDFORMATION_URL=https://cloudformation.ap-northeast-1.amazonaws.com
$ cfn-validate-template --template-file=my-stack.template

aws-apitools(Java版)ではなくawscli(Python版)を利用する場合は「awscli(Python版)でCloudFormationテンプレートをValidateする」も参考にしてください。

スタック作成用シェルスクリプトを利用すると効率的

スタックを作成、更新するのに毎回マネージメントコンソールで実行するのは面倒です。シェルで実行できるようにしておきましょう。

以下のような簡単なシェルを用意しておくと便利です。

$ ./create_stack.sh (作成)
$ ./create_stack.sh -u (更新)

create_stack.sh

#/bin/sh
CFN_CMD=cfn-create-stack
[ ! -z "$1" -a "$1" = "-u" ] && CFN_CMD=cfn-update-stack && shift

STACK_NAME=WEB-STACK
PARAMETERS=`cat << EOT
ServerName=web;
AZ=a;
ServerInstanceType=t1.micro;
IAMInstanceProfile=web-role;
KeyName=my_keyname;
EOT`
# Parameters end
PARAMETERS=`echo $PARAMETERS | sed 's/; /;/g'`

${CFN_CMD} ${STACK_NAME} --template-file=my-stack.template --parameters="${PARAMETERS}"

CloudFormationに欲しい機能など

以下はCloudFormationを触っていて、こんな機能があったら使いやすいのになぁと思ったものです。

欲しいPseudo Parameters/Intrinsic Functions

Pseudo Parameters(特殊パラメーター)やIntrinsic Functions(組み込み関数)がもっと充実するとパラメーター指定がもっと分かりやすくなるのになぁといつも思います。

  • AWSアカウントIDをとるPseudo Parameters
  • VPCのタグ(またはデフォルトVPCなら"default")からVPC IDを返すIntrinsic Functions
  • サブネット名(またはタグ)からサブネットIDを返すIntrinsic Functions
  • VPCやサブネットからCIDRを返してくれるIntrinsic Functions
  • セキュリティグループ名(またはタグ)からセキュリティグループIDを返すIntrinsic Functions

端的にいうとランダムで割り当てられる各種IDでなく、分かりやすい名前やタグからIDを返してくれる関数が欲しいわけです。

SNS通知をフィルタしたい

スタック作成時の「Advanced Options」で、スタック作成経過をSNSで通知させることができます。スタックが完了されたかどうか、Refreshボタンを連打しながら待たなくて良いように設定してみたのですが、スタック作成/更新時のすべてのイベントが通知されてしまいます。メールボックスが通知で溢れます。

リソースのTypeとStatusでフィルタしてAWS::CloudFormation::Stackのイベントだけ通知するようにし、スタックの更新が成功(失敗)した時だけお知らせできるようになってほしいです。

20131003_cloudformation_001

dry-run 機能がほしい

スタック作成完了間近に、ID指定ミスや依存関係ミスでロールバックされると萎えます。事前にdry-runか、せめて指定したID(サブネット、AMI、セキュリティグループ)の存在チェックができるとうれしいです。

まとめ

今回はCloudFormation使用に関する社内資料を公開してみました。多少でも今後の参考になればと思います。

ここに書いてあることで「もっと良いやり方あるよ!」という方はぜひ教えていただけるとうれしいです。