CloudFormationでFn::ImportValue組み込み関数を使う(VPC,EC2の作成)

technologies

こんにちは株式会社ベンジャミンの木村直紀です!

すっかり暖かくなり、春を感じる今日この頃です。2022年のRe:Inventに参加してから、周りのAWSエンジニアに感化され、資格取得に燃えており、現在AWS資格のAWS DevOps Professional取得に向けて勉強しております!

今回、AWS DevOps Professionalの勉強によく出てくるCloudFormationの理解のため、Fn::ImportValue組み込み関数を使ってリソースの構築をしてみましたので、その利点や記述方法について見ていければと思います!

Fn::ImportValue組み込み関数とは?

そもそもFn::ImportValue組み込み関数とは何なのでしょうか?

AWS公式ページには以下のように記載があります。

Fn::ImportValue 組み込み関数は、別のスタックによってエクスポートされた出力の値を返します。 この関数は通常、クロススタック参照を作成するために使用されます。

Fn::ImportValue – AWS CloudFormation

Fn::ImportValue組み込み関数を使えば、作成したスタックの値を別のスタックでも使えるようになり、リソースごとにファイルを分けて記述できるようになるようです。

それではVPC、EC2の作成を例にFn::ImportValue組み込み関数を使わない記述、使う記述を両方見て何が違うのかを見ていきたいと思います。

Fn::ImportValue組み込み関数を使わずにVPC、EC2を作成する

ファイル名:vpc.yaml

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  TestVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: "TestVPC"

  TestPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref TestVPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: "TestPublicSubnet"

  TestPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref TestVPC
      CidrBlock: "10.0.2.0/24"
      AvailabilityZone: ap-northeast-1c
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: "TestPrivateSubnet"

  TestInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: "TestInternetGateway"

  TestGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref TestVPC
      InternetGatewayId: !Ref TestInternetGateway

  TestRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref TestVPC
      Tags:
        - Key: Name
          Value: "TestRouteTable"

  TestPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: TestGatewayAttachment
    Properties:
      RouteTableId: !Ref TestRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref TestInternetGateway

  TestPrivateRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref TestRouteTable
      SubnetId: !Ref TestPrivateSubnet

  TestPublicRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref TestRouteTable
      SubnetId: !Ref TestPublicSubnet

  TestVpcInstance:
  Type: AWS::EC2::Instance
  Properties:
    ImageId: ami-02a2700d37baeef8b
    InstanceType: t2.micro
    KeyName: testkey
    NetworkInterfaces:
      - AssociatePublicIpAddress: true
        DeleteOnTermination: true
        DeviceIndex: 0
        SubnetId: !Ref TestPublicSubnet
        GroupSet:
          - !Ref TestSecurityGroup

  TestSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow SSH access from anywhere
      VpcId: !Ref TestVPC
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22

※上記の記述はあくまでサンプルです。Fn::ImportValue組み込み関数の動きを確認するために用意したものなので、ご使用の際はサンプルだと言うことをご理解の上お願いいたします。

※TestPublicSubnetやTestPrivateSubnetのプロパティにあるMapPublicIpOnLaunchは、PublicIPを自動でつけるかどうかをture,falseで表しております。

※GatewayAttachmentリソースはInternetGatewayをVPCにアタッチするために記載しております。

作成しているリソースはVPC、PublicSubnet、PrivateSubnet、InternetGateway、RouteTable、EC2(パブリック)、SecurityGroupになります。今回、作成しているリソースはEC2作成に最低限必要なリソースを用意しました。VPCやEC2の細かい記述については触れませんが、上記のEC2を作成するための記述を別のファイルに移動したら、どんな記述が必要かを確認していきたいと思います。

Fn::ImportValue組み込み関数を使ってVPC、EC2を作成する

まずはEC2を作成する記述を別のファイルに移動した後、vpc.yamlの中身をどう変えないといけないかを見ていきます。

ファイル名:vpc.yaml

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  TestVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: "TestVPC"

  TestPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref TestVPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: "TestPublicSubnet"

  TestPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref TestVPC
      CidrBlock: "10.0.2.0/24"
      AvailabilityZone: ap-northeast-1c
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: "TestPrivateSubnet"

  TestInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: "TestInternetGateway"

  TestGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref TestVPC
      InternetGatewayId: !Ref TestInternetGateway

  TestRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref TestVPC
      Tags:
        - Key: Name
          Value: "TestRouteTable"

  TestPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: TestGatewayAttachment
    Properties:
      RouteTableId: !Ref TestRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref TestInternetGateway

  TestPrivateRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref TestRouteTable
      SubnetId: !Ref TestPrivateSubnet

  TestPublicRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref TestRouteTable
      SubnetId: !Ref TestPublicSubnet

  # TestVpcInstance:
  # Type: AWS::EC2::Instance
  # Properties:
  #   ImageId: ami-02a2700d37baeef8b
  #   InstanceType: t2.micro
  #   KeyName: testkey
  #   NetworkInterfaces:
  #     - AssociatePublicIpAddress: true
  #       DeleteOnTermination: true
  #       DeviceIndex: 0
  #       SubnetId: !Ref TestPublicSubnet
  #       GroupSet:
  #         - !Ref TestSecurityGroup

  TestSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow SSH access from anywhere
      VpcId: !Ref TestVPC
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22

Outputs:
  SecurityGroupIds:
    Value: !Ref TestSecurityGroup
    Export:
      Name: TestVpcStack-TestSecurityGroup
  SubnetId:
    Value: !Ref TestPublicSubnet
    Export:
      Name: TestVpcStack-TestPublicSubnet

EC2作成部分の記述は別ファイルに移動したいため、TestVpcInstanceの記述はコメントアウトしました。

また、EC2作成の際に必要な値であるSecurityGroupのIDPublicSubnetのIDはvpc.yamlに残ります。これらはOptputsセクションExportに記述しております。このように記述することでEC2を作成するファイルではOutputからExportされた値を移動先のファイル(後で出てくるec2.yaml)で扱うことができます。

※ExportのNameはAWS アカウントごとに、リージョン内で一意である必要があります。

次にec2の作成ファイルを見ていきます。

ファイル名:ec2.yaml

AWSTemplateFormatVersion: 2010-09-09

Resources:
  TestVpcInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-02a2700d37baeef8b
      InstanceType: t2.micro
      KeyName: testkey
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          SubnetId: !ImportValue TestVpcStack-TestPublicSubnet
          GroupSet: [!ImportValue TestVpcStack-TestSecurityGroup]

※GroupSetのところは[ ]付きで記述しないと「Value of property GroupSet must be of type List of String」とエラー文が出ます。

先ほど同じファイルでRef!と記載されていたところが!ImportValueになっております。TestVpcStack-TestPublicSubnetやTestVpcStack-TestSecurityGroupは先ほどのvpc.yamlでExportしたNameを記述しています。

このようにFn::ImportValue組み込み関数を使うことで、リソースごとにファイルを分けて記述することが可能になりました。

AWS DevOps Professionalの資格勉強をしているとFn::ImportValue組み込み関数を問う問題で、「論理部分を別ファイルに記述するにはどうすればいいか?」や今回のような「VPCとEC2の記述を分けるにはどうすればいいか?」と聞かれますので、参考にしていただければと思います。

おまけ(つまずいたところ)

NetworkInterfacesを指定するときの設定


Resources:  
  TestVpcInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t2.micro
      ImageId: ami-0c3fd0f5d33134a76
      KeyName: testkey
      SecurityGroupIds: !Ref TestSecurityGroup
      SubnetId: Ref! TestPublicSubnet

NetworkInterfacesを指定していないときは上記のようにセキュリティグループはSecurityGroupIdsでSecurityGroupのIDを指定して記述します。

ただEC2に紐づけるネットワークインターフェースの設定を行いたいときは下記のように、パラメータを増やさなくてはなりません。このときエラーが起こった…

Resources:
  TestVpcInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0c55b159cbfafe1f0
      InstanceType: t2.micro
      KeyName: mykey
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          SubnetId: Ref! TestPublicSubnet
      SecurityGroupIds: !Ref TestSecurityGroup

上記のように、NetworkInterfacesを指定したときに、セキュリティグループを別で記述すると下記のようなエラーが出てきてしまいました。

Network interfaces and an instance-level security groups may not be specified on the same request

翻訳すると「ネットワーク インターフェイスとインスタンス レベルのセキュリティ グループを同じリクエストで指定することはできません」となります。つまり、セキュリティグループとネットワークインスタンスは同じパラメータとして横並びで記述できませんよと言うことなのです。

では、セキュリティグループを記述するにはどうすればいいのでしょうか?

それは、下記のようにNetworkInterfacesの値の一部として記述すればいいのです。

Resources:
  TestVpcInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0c55b159cbfafe1f0
      InstanceType: t2.micro
      KeyName: mykey
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          SubnetId: Ref! TestPublicSubnet
          GroupSet: !Ref TestSecurityGroup

同じEC2 を作成する記述でも、少し記述を変えるだけでエラーが出てくるのは少し厄介ですね…

これを読んでくれた方が、少しでも問題を解決していだければと思います。

関連記事一覧