こんにちは。インフラエンジニアの内山(@k4ri474)です。
弊社ではCloudFormationとOpsWorksを活用してインフラ構築をしています。
この両サービスではハマりどころが多く、中でもOpsWorksでインスタンスが複数のレイヤに所属する構成を構築した際にとても苦戦しました。
そこで今回は、私が上記構成でインフラ構築をした際に悩んだ点の解決法をTipsとして公開します。
目次
サービスの概要
CloudFormation
CloudFormationは、AWSリソースをJSON/YAMLで表現したテンプレートを元に、その構成を自動で構築してくれるサービスです。
本エントリでは、後述するOpsWorksのリソース宣言や設定項目のセットをCloudFormationのテンプレートを用いて紹介しています。
OpsWorks
OpsWorksは管理下のインスタンスに対してChefを実行することで構成管理を行うサービスです。
スタック、レイヤ、インスタンスという3要素でインスタンスを管理しており、以下のような条件で構成されます。
- インスタンスは1つ以上のレイヤに所属する
- レイヤはスタックに割り当てる
このため、上記3要素はStack > Layer > Instance
という階層のように表現されます。
なお、1つのスタックに複数のレイヤを割り当てることができ、インスタンスは複数のレイヤに所属することができます。
CloudFormationとOpsWorksについて詳しく知りたい方はこちらのエントリをご覧ください。
[Theme 1]インスタンス名の決定
前提条件
スタックのAdvanced optionsにあるHostname theme
が、デフォルトのLayer Dependent
であることを前提条件とします。
命名法則
OpsWorksで作成したインスタンスの名前(Nameキーの値)は、以下3つの要素から構成されます。
- 所属するスタックのNameプロパティの値
- インスタンスが所属するレイヤの中で最も先に作成されたレイヤのShortNameプロパティの値
- 2の値が同じインスタンスの中で、何番目に作成されたインスタンスなのかを表す数値
これら3要素を使い<stack_name> - <layer_shortname><number>
と命名されます。
ここで命名に利用されるレイヤは1つだけという制約があるのですが、1つのインスタンスは複数のレイヤに所属することが可能です。
この場合どのレイヤが優先されるかは、リソースとしてどのレイヤが先に作成されたかで決定されます。
例えば、以下のようなリソースがCloudFormationのテンプレートに記述されていたとします。
TestStack: Type: "AWS::OpsWorks::Stack" Properties: Name: "TestStack"
CommonLayer: Type: "AWS::OpsWorks::Layer" Properties: Shortname: "common" StackId: !Ref TestStack
WebLayer: Type: "AWS::OpsWorks::Layer" Properties: Shortname: "web" StackId: !Ref TestStack
WebFirst: Type: "AWS::OpsWorks::Instance" Properties: LayerIds: - !Ref CommonLayer - !Ref WebLayer StackId: !Ref TestStack
(※説明簡略化のため必須プロパティを省略しています)
上記4つのコードブロックでは、以下の状態を表しています。
- TestStackというスタックにCommonLayer・WebLayerという2つのレイヤがある
- WebFirstというインスタンスが両レイヤに所属している
上の例の場合、WebLayerがCommonLayerより先に作成されていた際のWebFirstの名前は、TestStack - web1
となります。
逆にCommonLayerの方が先に作成されていた場合は、WebFirstの名前はTestStack - common1
となります。
この時、CloudFormationのテンプレートで指定したWebFirst
は論理IDというリソースとして扱われ、各サービスでのインスタンス名表記には利用されないことに注意します。
順序が明確でない場合の挙動
複数レイヤを同じ変更セットで作成した場合
先述の通り、レイヤの作成順序によってインスタンス名が左右されます。
作成順序は適用順で決まるので、より先の変更セットに含まれているリソースが"先に作成された"と見なされます。
また、同じ変更セットで作成された複数のレイヤにインスタンスを所属させた場合、どちらのレイヤのShortNameが使われるかは未定です。
上の例の場合、同じ変更セットでCommonLayerとWebLayerを作成するとTestStack - web1
になるのかTestStack - common1
になるのかが把握できないということになります。
複数インスタンスを同じ変更セットで作成した場合
CloudFormationは可能な限り並列にAWSリソースを作成・更新しようとします。
そのため、同じ変更セットで同レイヤに所属するインスタンスを複数作成しようとする場合は、"何番目のインスタンスか"を表す数値が同一になります。
上の例の場合、インスタンスを2つ作成すると2つともTestStack - web1
となってしまいます。
論理IDをWebFirst・WebSecondのように設定していても上図のようになるので、戸惑いがちなポイントかと思います。
このように、レイヤ・インスタンスの作成順序が明確でないと運用上困ることがあるので、後述するDependsOn属性をリソースに付与してコントロールします。
順序のコントロール
DependsOn属性を付与することで、テンプレート上で明確に作成順序を分けることができます。この場合、変更セットを分ける必要はありません。
レイヤの作成順序を制御したコードは以下のようになります。
CommonLayer: Type: "AWS::OpsWorks::Layer" DependsOn: - WebLayer Properties: Shortname: common StackId: !Ref TestStack
WebLayer: Type: "AWS::OpsWorks::Layer" Properties: Shortname: web StackId: !Ref TestStack
WebFirst: Type: "AWS::OpsWorks::Instance" Properties: LayerIds: - !Ref CommonLayer - !Ref WebLayer StackId: !Ref TestStack
WebSecond: Type: "AWS::OpsWorks::Instance" DependsOn: - WebFirst Properties: LayerIds: - !Ref CommonLayer - !Ref WebLayer StackId: !Ref TestStack
CommonLayerにDependsOn属性を記述してWebLayerが先に作成されるように明示することで、この両レイヤの作成順序は常にWebLayerが先になります。
そのため、WebFirst・WebSecondインスタンスのNameキーの値にはweb
が利用されるようになります。
インスタンスにおいてもルールは同様です。
WebSecondにてDependsOn属性を記述してWebFirstに依存するように設定すると、同じ変更セットでインスタンスを作成しても以下のように命名されます。
- WebFirst :
TestStack - web1
- WebSecond :
TestStack - web2
このようにDependsOn属性を活用することで、コード上で順序関係を把握することができ、1つの変更セットで意図した通りに複数リソースを作成できます。
[Theme 2]ダミーセキュリティグループの利用
導入ケース
OpsWorksでは、全てのレイヤにセキュリティグループを割り当てることが必須となっています。
この制約がネックとなる場面を例示するため、弊社のテンプレートの設計を紹介します。
弊社では以下3種類のレイヤを作成しており、インスタンスは最大で3レイヤに所属しています。
- サービスにおける役割(ロールと呼ぶ)ごとの設定を記述したレイヤ
- 複数のロールにおける共通の設定を記述したレイヤ
- 全インスタンスにおける共通の設定を記述したレイヤ
この時、2と3のレイヤにおいては複数のロールに跨っていることを考慮してセキュリティグループのルールを考えねばなりません。
2のレイヤでアプリケーションサーバ用のレイヤとwebサーバ用のレイヤをくくり出していた場合、共通のインバウンド・アウトバウンドルールとして挙げられる選択肢はそう多くないと思います。
こういった場合に、ルールを持たないセキュリティグループが役に立ちます。
実装方法
セキュリティグループの作成時にインバウンド・アウトバウンドのルールは必須項目ではないため、ルールを持たないセキュリティグループを作成する事ができます。 CloudFormationのテンプレートでは以下のように記述します。
DummySecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: "dummy-SG" Tags: - Key: "Name" Value: "dummy-SG" VpcId: !Ref MyVPC
このようなルールを持たないセキュリティグループをレイヤに割り当てることで、CloudFormationの制約を守りながらサービスに影響を与えることなく構築ができます。
[Theme 3]レシピのマージ
注意点
マルチレイヤ構成をとる場合、CustomJsonに記述したレシピについて以下の注意点があります。
- インスタンスが複数レイヤに所属する場合、レイヤ間のCustomJsonはマージされる
- 同名の要素があった場合はどちらの要素が使われるか不定
そのため、以下のコードブロックの例だとCommonLayer・WebLayerの両レイヤに所属するインスタンスでは、Rubyのversionが2.5.0か2.2.5のどちらかとなります。
CommonLayer: Type: "AWS::OpsWorks::Layer" DependsOn: - WebLayer Properties: CustomRecipes: Setup: - 'ruby' CustomJson: | { "ruby": { "version": "2.5.0" } }
WebLayer: Type: "AWS::OpsWorks::Layer" Properties: CustomRecipes: Setup: - 'ruby' CustomJson: | { "ruby": { "version": "2.2.5" } }
スタック全体のレイヤ数が増え、インスタンスが3レイヤ以上に所属するような状態だと、意図せず発生してしまう問題です。
問題発見のアプローチ
OpsWorksによって作成された全てのインスタンスには、AWS OpsWorks スタック エージェントがインストールされます。
OpsWorksスタックエージェントのCLIで用意されたコマンドの1つであるget_json
を用いると、上のコードブロックのようにChefで実行されたJSONを確認することができます。
$ sudo opsworks-agent-cli get_json { "ruby": { "version": "2.2.5" }, "run_list": [ ] } $
CloudFormationを利用するとテンプレート上で全てのリソースを把握できますが、実際のインスタンスでの適用状況を確認することも重要となります。
まとめ
CloudFormationとOpsWorksを使って複数レイヤ構成を構築する際のTipsを紹介しました。実現したいインフラの構成が違えば直面する問題も異なってくるかと思いますが、この記事が参考になりましたら幸いです。
VASILYではモダンな手法を取り入れながら柔軟にインフラ構築をしていきたいエンジニアを募集しています。興味のある方は以下のリンクからご応募ください。