課題
- 以下の要件を満たすテンプレートを記述して下さい。また、テンプレートはコンポーネント毎に分割して下さい。
- 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台起動する
解説
ここから解説編です。 テンプレートの分割について、 ・どういった観点で行うべきなのか ・どんなテクニックがあるのか
という観点から解説していきます。
ライフサイクルによって整理する
まずは、テンプレートの分割はどういった観点から行うべきなのか、です。
ずばり! CloudFormationのテンプレートは、ライフサイクル毎に分けましょう。 ライフサイクルとは、作成・削除のタイミングが同じであることを指します。 今回の場合、 ・VPC、サブネット、インターネットゲートウェイなどのNW系 ・セキュリティグループ、EC2
に分けるのが良いでしょう。
では、テンプレートを分割してみましょう。
- network.yaml
AWSTemplateFormatVersion: "2010-09-09" Parameters: VpcCidr: Type: String SubnetCidr: Type: String 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
- ec2.yaml
AWSTemplateFormatVersion: "2010-09-09" Parameters: VpcId: Type: AWS::EC2::VPC::Id SubnetId: Type: AWS::EC2::Subnet::Id 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: 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 SubnetId GroupSet: - !Ref SecurityGroup Tags: - Key: Name Value: !Ref InstanceName
ec2.yamlのパラメータで「AWS::EC2::VPC::Id」と「AWS::EC2::Subnet::Id」を使ってあげれば、プルダウンでそれぞれのIDを選べるようになります。 これでnetworkとec2を分割出来ました。
パラメータを参照する方法を知る
ところで、VpcIdはシステム全体で1つのものを使う事が多いですよね。 これを都度入力するのはちょっと面倒です。 実は、CloudFormationにはスタック間でパラメータを参照する方法が用意されています。
-
- クロススタック参照
- ダイナミック参照
- クロススタック参照 CloudFormationのOutputsセクションを上手く使う事で、スタック間でパラメータ参照できます。 使い方はまず、参照元のテンプレートにOutputsセクションを記述します。
参照元: vpc.yamlAWSTemplateFormatVersion: "2010-09-09" Parameters: VpcCidr: Type: String Resources: Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidr Outputs: VpcId: Value: !Ref Vpc Export: Name: network-VpcId
次に、参照先で、Fn::ImportValueを使って、Export -> Nameに設定した値を引数に取ります。
参照先: ec2.yamlAWSTemplateFormatVersion: "2010-09-09" Parameters: VpcId: Type: AWS::EC2::VPC::Id Resources: SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: VpcId: !ImportValue network-VpcId # <--ココです 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
このように、VPCのような共有リソースのIDに対してOutputsセクションを定義、Exportで命名しておく事で、他のスタックから容易に参照できるようになります。 しかし便利な反面、注意事項もあります。 Exportした値が他のスタックから参照されている場合、元のリソースの置き換えが出来なくなります。 例えばCloudFormationでS3を作成し、他のスタックからバケット名を参照している場合、S3用スタックを削除できません。 場合によっては運用が止まる危険性もあるので、よく計画した上で利用しましょう。
- ダイナミック参照 スタック間のパラメータ参照方法その2は、SSMパラメータストアを経由して参照する方法です。 使い方は、まず参照元のテンプレートでリソースを作成後、パラメータストアへ格納します。
参照元: vpc.yamlAWSTemplateFormatVersion: "2010-09-09" Parameters: VpcCidr: Type: String Resources: Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidr VpcId: Type: AWS::SSM::Parameter Properties: Description: 'VpcId' Name: 'VpcId' Type: String Value: !Ref Vpc
そして、参照先でパラメータ名を指定するだけです。 ※ 書き方: ‘{{resolve:ssm:パラメータ名}}’
参照先: ec2.yamlAWSTemplateFormatVersion: "2010-09-09" Parameters: VpcId: Type: AWS::EC2::VPC::Id Resources: SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: VpcId: '{{resolve:ssm:VpcId}}' # <--ココです 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
こちらはSSMパラメータストアが間に挟まる分、スタックの更新に影響を与えません。 個人的には、ExportするよりSSMを使う方が好きだったりします。
以上がスタックを跨いだパラメータの参照方法です。
テンプレート分割のメリット
テンプレートを分割すると何がいいのか? 以下の2つが挙げられます。
-
- 可読性を上げられる
- 再利用が可能になる
- 可読性を上げられる こちらは最初に軽くお話した通り、テンプレートは長くなるとどんどん読みづらくなります。 ライフサイクル毎に分割することで、ある程度長さを抑えられるようになります。
- 再利用が可能になる こちらが一番大きなメリットです。 スタックを作成する際、再利用可能なテンプレートをモジュールとして用意しておくことで、整合性の担保や、コード量の抑制を実現できます。
例えば、以下のように構成することで、root.yamlを修正するだけで、EC2やRDSを増やせるようになります。
これを実際に運用するには、AWS CLIと併用するなどひと手間必要なのですが、それはまた別の機会にご説明します。
テンプレートの分割については以上です。 ここまででCloudFormationの基本的な書き方の勉強は終了です。
「ゼロから始めるInfrastructure as Code」は次回で最後です。 最終回ではCloudFormationのベストプラクティスについて総括します。 最後までお楽しみに! 以上、エンジニアの濱田でした!