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

[ゼロから始める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
    

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

関連コラム

このコラムに関連する製品

このコラムに関連する
導入事例

このコラムに関連する
セミナーアーカイブ動画

AWS Ambassador Presents トレンド技術を語り尽くせ! 第1回 「Infrastructure as Code」

SHARE
シェアシェア ポストポスト
AWS Ambassador Presents トレンド技術を語り尽くせ! 第1回 「Infrastructure as Code」
SHARE
ポスト シェア