クラウド エンジニアブログ

[ゼロから始めるInfrastructure as Code] 第3回 テンプレートの汎用性を高める

2023年2月15日掲載

始めに

クラウドインテグレーション課 クラウドエンジニアの濱田です。 「ゼロから始めるInfrastructure as Code」、第3回目は、汎用性の高いテンプレートの書き方について勉強していきます。

前回は、組み込み関数を使ってリソースから値を参照する方法を学びました。 これで1枚のテンプレートで、依存関係のあるリソースを作れるようになりましたね。

ところで、これまでに作成したテンプレートで、どこか不便に感じたところはないですか? 例えば、以下の2つはどうでしょう?

  1. デプロイする度に、VPCとサブネットのCidrを修正する必要がある
  2. 東京リージョンのみでしか実行できない

1について、VPCとサブネットのCidrは場合によって変わりますよね。 前回のテンプレートだと、Cidrを都度書き直さなければいけません。

2について、もしかしたら同じテンプレートを、別のリージョンに流す事もあるかもしれません。 前回のテンプレートではサブネットで「ap-northeast-1」を指定しているので、他リージョンにデプロイする際、都度書き直す必要があります。

都度の修正は、ミスの危険性を上げることになります。

再利用可能なテンプレートを心がける

CloudFormationを利用することのメリットには、多くの場合以下の3つが挙げられます。

  • 整合性の担保
  • 作業リスクの削減
  • 作業の効率化

整合性の担保とは、現在の環境がテンプレート通りになっている事。 作業リスクの削減は、文字通り作業ミスを減らせる事。 作業の効率化は、構築時の工数を削減できる事です。

再利用可能なテンプレートを作成することは、テンプレートの修正ミスを減らす事に直結します。 また、環境に合わせてパラメータだけ変える設計にする事で、同じテンプレートで環境を作り分けることが出来、整合性の担保も実現できます。

Parametersセクションの活用

という訳で、デプロイの度に値だけ変えられるように実装していきましょう。 その為にはParametersセクションを利用します。 Parametersセクションは、テンプレートバージョンや、Resourcesセクションの仲間です。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:  # <-- New!!
  論理名:
    Type: データ型

Resources:

Resourceセクション内で「Ref」を使って論理名を呼ぶ事で、入力したパラメータを参照出来ます。

!Ref 論理名

データ型とは、どんな値を入れるかを予め指定しておくものです。 文字列、数値、配列、AWS固有パラメータ等いろいろありますが、まずは文字列(String)を覚えましょう。

Parametersの詳細は、公式リファレンスを参照して下さい。
※ 公式リファレンス: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html

実際の利用イメージを確認しましょう。 以下のテンプレートを作成します。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  VpcCidr:
    Type: String

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr

これをCloudFormationコンソールに流し込むと、以下のようにテキストボックスが表示され、値の入力を求められます。

このテキストボックスにパラメータを入力する事で、その値を使ったスタックを作成できるのです。

課題

では、本日の課題です。

  • 以下の要件を満たすテンプレートを記述して下さい。また、パラメータは可能な限りデプロイの度に指定できる形で実装して下さい。
    • 192.168.0.0/16のCidrを持つVPCを作成する
    • 作成したVPC内に、以下セグメントのサブネットを作成する
      • 192.168.0.0/24
    • Internet Gatewayを作成し、VPCにアタッチする
    • ルートテーブルを作成し、Subnetに関連付ける
    • ルートテーブルに0.0.0.0/0 -> IGWのルートを書く
    • EC2インスタンス用セキュリティグループを作成する
    • 作成したサブネット内に、EC2インスタンスを1台起動する

※ インスタンスについてはパラメータが山ほどあるので、一旦誰かのブログを参照してもよいです

解説

こちらです

本日の課題は、Parametersセクションを書いたり、新しいリソースを書いたり、盛りだくさんですね。 基本的な書き方はリファレンスを参照頂くとして、以下のポイントに絞って解説します。

  • Parametersセクションの書き方
  • 組み込み関数の活用法

Parametersセクションの書き方

まずはParametersセクションに書くものを列挙してみましょう。

  • ネットワーク系
    • VPCのCidr
    • SubnetのCidr
  • インスタンス系
    • インスタンス名
    • キーペア名
    • イメージID
    • インスタンスタイプ
    • ボリュームサイズ

このあたりですかね。 実は、この中のいくつかはTypeとPropertiesを活用すると便利に書けるので、その辺りの解説をしていきます。

  • KeyName 「Type: String」で書いても問題ないですが、AWS固有パラメータとして「AWS::EC2::KeyPair::KeyName」が用意されています。 これを使うと、パラメータ入力時に、今あるキーペアをプルダウンから選べるようになります。
    KeyName:
      Type: AWS::EC2::KeyPair::KeyName
    
  • ImageId これもStringでも問題ありません。 しかし、AMIは頻繁にアップデートされる為、都度最新版のIDを引っ張ってくるのって大変ですよね。 AWS固有パラメータの「AWS::SSM::Parameter::ValueAWS::EC2::Image::Id」を使う事で、SSMパラメータストアの値を参照できます。 Systems Managerのパラメータストアで、パブリックパラメータを確認すると、使えるパラメータが並んでいるのが分かります。今回は、Defaultプロパティを使って、以下AMI IDがデフォルトで表示されるようにしておきましょう。
    AmiId:
      Type: AWS::SSM::Parameter::Value
      Default: /aws/service/ami-windows-latest/Windows_Server-2022-Japanese-Full-Base
    
  • VolumeSize ボリュームサイズには「Type: Number」を指定する事で、数値ならではの制約をかけられるようになります。 例えば、インスタンスのボリュームサイズはOSによって最低値・最大値が決まっています。 MinValue / MaxValueプロパティを使うと、パラメータを制限範囲内に収める事が可能です。 今回は最小値だけ設定してみましょう。
    VolumeSize:
      Type: Number
      MinValue: 40
    

組み込み関数の活用方法

前回触れた組み込み関数は、値を参照するものでした。 今回は、文字列を操作するタイプの組み込み関数を使ってみましょう。

  • Fn::Sub これは、いわゆる「置換処理」を行う関数です。 Refで参照できるパラメータを、文字列の中に埋め込む時に使えます。 例えば「InstanceName: hmddev」というパラメータがあったとします。 以下のように記述する事で、”hmddev_SecurityGroup”という文字列を作ることができます。

    !Sub ‘${InstanceName}_SecurityGroup’

    これでインスタンス毎にセキュリティグループ名を生成できますね。

  • Fn::Select こちらはちょっとプログラミングっぽい組み込み関数です。 これを使うことで、「配列」というデータ型の中から任意の値を参照できます。 例えば、[hamada, ishihara]というような配列から任意の値を参照するには、以下のように記述します。
    # hamadaを参照する場合
    !Select
      - 0
      - [hamada, ishihara, hida]
    
    # ishiharaを参照する場合
    !Select
      - 1
      - [hamada, ishihara, hida]
    

    これを用いて、ちょっと合わせ技を。 「Fn::GetAZs」という組み込み関数で、使えるAzの配列を取得できます。

    Fn::GetAZs ap-northeast-1
      -> [ap-northeast-1a, ap-northeast-1c, ap-northeast-1d]
    

    ここから1つの値を参照するにはFn::Selectを使えばいいので、

    !Select
      - 0
      - Fn::GetAZs ap-northeast-1
    

    これで、サブネットを作成する時、「都度使える東京リージョンのAzを取得して、1つを選択する」というロジックを実現できます! ・・・もう一つだけ言わせて下さい。 AWSは、よく使うAWS系のパラメータをRefで参照できるようにしています。
    ※ 疑似パラメータ: https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html
    リージョンも「AWS::Region」で参照出来ます。 以下のように書くことで、リージョンへの依存を無くせます。

    !Select
      - 0
      - Fn::GetAZs !Ref AWS::Region
    

    今度こそ完璧です!

    解答例

    AWSTemplateFormatVersion: "2010-09-09"
    Parameters:
      VpcCidr:
        Type: String
      
      SubnetCidr:
        Type: String
    
      KeyName:
        Type: AWS::EC2::KeyPair::KeyName
    
      InstanceName:
        Type: String
    
      ImageId:
        Type: AWS::SSM::Parameter::Value
        Default: "/aws/service/ami-windows-latest/Windows_Server-2022-Japanese-Full-Base"
    
      InstanceType:
        Type: String
    
      VolumeSize:
        Type: Number
        MinValue: 60
    
    Resources:
      Vpc:
        Type: AWS::EC2::VPC
        Properties:
          CidrBlock: !Ref VpcCidr
    
      InternetGW:
        Type: AWS::EC2::InternetGateway
    
      AttachGateway:
        Type: AWS::EC2::VPCGatewayAttachment
        Properties:
          VpcId: !Ref Vpc
          InternetGatewayId: !Ref InternetGW
    
      Subnet:
        Type: AWS::EC2::Subnet
        Properties:
          AvailabilityZone: !Select
            - 0
            - Fn::GetAZs: !Ref 'AWS::Region'
          VpcId: !Ref Vpc
          CidrBlock: !Ref SubnetCidr
    
      RouteTable:
        Type: AWS::EC2::RouteTable
        Properties:
          VpcId: !Ref Vpc
    
      Route:
        DependsOn: AttachGateway
        Type: AWS::EC2::Route
        Properties:
          RouteTableId: !Ref RouteTable
          DestinationCidrBlock: 0.0.0.0/0
          GatewayId: !Ref InternetGW
    
      AssociateRouteTable:
        Type: AWS::EC2::SubnetRouteTableAssociation
        Properties:
          SubnetId: !Ref Subnet
          RouteTableId: !Ref RouteTable
    
      SecurityGroup:
        Type: AWS::EC2::SecurityGroup
        Properties:
          VpcId: !Ref Vpc
          GroupName: !Sub '${InstanceName}_SecurityGroup'
          GroupDescription: Enable Private access
          SecurityGroupIngress:
            - CidrIp: 10.0.0.0/8
              IpProtocol: -1
            - CidrIp: 172.16.0.0/12
              IpProtocol: -1
            - CidrIp: 192.168.0.0/16
              IpProtocol: -1
          SecurityGroupEgress:
            - CidrIp: 0.0.0.0/0
              IpProtocol: -1
              
      EC2:
        Type: AWS::EC2::Instance
        Properties:
          ImageId: !Ref ImageId
          InstanceType: !Ref InstanceType
          KeyName: !Ref KeyName
          BlockDeviceMappings: 
            - DeviceName: '/dev/sda1'
              Ebs:
                VolumeType: gp3
                VolumeSize: !Ref VolumeSize
                DeleteOnTermination: true
          NetworkInterfaces:
            - AssociatePublicIpAddress: false
              DeviceIndex: '0'
              SubnetId: !Ref Subnet
              GroupSet: 
                - !Ref SecurityGroup
          Tags:
            -
              Key: Name
              Value: !Ref InstanceName
    

かなり立派なスクリプトになってきましたね。 次回はテンプレートの分割について勉強していきます。 お楽しみに! 以上、エンジニアの濱田でした!

[ゼロから始めるInfrastructure as Code] 第3回 テンプレートの汎用性を高める

SHARE
シェアシェア ポストポスト
[ゼロから始めるInfrastructure as Code] 第3回 テンプレートの汎用性を高める
SHARE
ポスト シェア