CloudFormationを勉強したので聞いて欲しい


CloudFormationへようこそ

この記事の対象者は:
AWSは触ったことあるけどCloudFormationは初めての方向けです。

あんまり向いていない対象者:
AWSを触ったことがない人

この記事の内容

僕個人が結構つまづいたポイントをまとめて初学者の学習スピードを縮めてもらえたらなと思います。

まずCloudFormationとは?

CloudFormationから引用

AWS CloudFormation は、クラウド環境で AWS とサードパーティ製アプリケーションリソースのモデリングおよびプロビジョニングをする際の、共通的な手法を提供します。AWS CloudFormation では、プログラミング言語またはシンプルなテキストファイルを使用して、あらゆるリージョンとアカウントでアプリケーションに必要とされるすべてのリソースを、自動化された安全な方法でモデル化し、プロビジョニングできます。これは、AWS とサードパーティ製のリソースに真に単一のソースを与えます。

要するに

「AWSのインフラ構成をコード化しましょう」

これにつきます。

CloudFormationを始めるに当たって

1.構成図を考える

CloudFormationを始めるに当たって必要なものがあります。
それが既存のAWS構成です。

よくCloudFormationの記事でほとんどクラスメソッドさんの記事なのですが、

こういった記事によくあるこのAWSの構成図これがとても大事でした。

ちなみに僕は台本と読んでいます。

ユーザがなぜこの構成が欲しいのか?を検討することにより、実現できなかったとしても別の方法が考えられると思ったからです。

2.実際にマネジメントコンソールで作ってみる

最初は簡単なものからでいいと思います。

EC2のインスタンスタイプを変更するなりEBSのサイズを変更するなり、
S3のバケットポリシーを変更するなり、
単純な構成のリソースを作成していくことが必要になります。

3.テンプレートファイル化にトライしてみる

いろいろな記事を読んで試しましたが、最終的にはユーザーガイド
が一番いい具合に落ち着きました。

もちろん他にもいろいろ勉強しました。
個人的なことですが、udemyを結構信頼している部分もあるのでudemyで最初はyamlファイルの書き方なども勉強しました。
Udemy CloudFormation

4.最終的には試行錯誤

CloudFormationは大まかな構成を作成するまでマネジメントコンソールでエラーを出してくれます。そのエラー文を読んで、ドキュメントを読むと自ずと自分の構成のどこが間違っているのか?と示してくれます。

そしてスタックを実行すると実行途中にエラー理由も吐いてくれます。どの論理IDのところでエラーが吐いているのか教えてくれるので修正箇所の発見も早くなります。

Tips

さて上記の内容はCloudFormationでインフラ構築を実施するための大まかな手順を掲載しました。
ここでは上記の手順をサポートする話を書こうと思います。
(実際にはYamlファイルの書き方などになってきます)

Yamlファイルの構成

僕が個人的に最初に詰まったのがここです。
CloudFormationではいろいろ書いてあるのですが、まず基本的に3つの構成に分かれます。
1. 事前の値
2. 作成する内容
3. 外部スタックからImportするためのExportする内容

この3つになります

サンプルで説明していくと

sample.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: 
  NAT Gateway Create

# ------------------------------------------------------------#
# NAT Gatewayの作成
# ap-northeast-1aと1cにそれぞれPrivateRouteTableが存在している状態
# そのルートテーブルにEIP付きのNAT Gatewayを構築する
# ------------------------------------------------------------#

Parameters:
  PJPrefix:
    Description: Enter a prefix of this system.
    Type: String
    Default: "test"
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9][ -~]*"
    ConstraintDescription: must begin with a letter and contain only alphanumeric characters.


# ------------------------------------------------------------#
# NAT Gateway AZ:A
# ------------------------------------------------------------#                
# NATGatewayA Create
Resources:
  NATGatewayA:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt NATGatewayAEIP.AllocationId
      SubnetId:
       Fn::ImportValue:
        !Sub "${PJPrefix}-public-subnet-a"
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-natgw-a

# NATGateway For EIP Create
  NATGatewayAEIP:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: vpc

# PrivateRouteA Create
  PrivateRouteA:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId:
       Fn::ImportValue:
         !Sub "${PJPrefix}-private-route-a"
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NATGatewayA

# ------------------------------------------------------------#
# NAT Gateway AZ:C
# ------------------------------------------------------------#                
# NATGatewayC Create
  NATGatewayC:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt NATGatewayCEIP.AllocationId
      SubnetId: !Ref PublicSubnetC
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-natgw-c

# NATGateway For EIP Create
  NATGatewayCEIP:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: vpc

# PrivateRouteC Create
  PrivateRouteC:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId:
       Fn::ImportValue:
         !Sub "${PJPrefix}-private-route-c"
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NATGatewayC

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  # NATGateway EIP
  NATGatewayAEIP:
    Value: !Ref NATGatewayAEIP
    Export:
      Name: !Sub "${PJPrefix}-natgw-a-eip"

  NATGatewayCEIP:
    Value: !Ref NATGatewayCEIP
    Export:
      Name: !Sub "${PJPrefix}-natgw-c-eip"

Yamlファイルでインデントが使われているのが見えると思いますが、この「インデント」が非常に重要になります。
このインデント次第でスタックが生成されないことが何十件とありました。

一番左にきているのが
1. Parameters
2. Resources
3. Outputs

の3つになります。
この3つの中で絶対に必要なのが
「Resources」
です。

これがないと何も作られません。

その次に
「Parameters」
です。
このパラメータ例えば今回は「PJPrefix」としています。
個人的にはプロジェクト名などに統一することでそのプロジェクトで横展開されていくものだなと思っています
(この部分自分で書いていてもうまく纏まっていないことはわかるのでぼんやりでいいです。)

そして最後に
「Outputs」
です。
これはCloudFormationで作成した値を他のスタックで使いたい時
例えばSubnetのIDとか。
SecurityGroupのIDもそうです。
今回ですと
事前にRouteTableを作成していてそれをOutputしていたので
この中でRouteTableIdを呼び出したかった
みたいな感じです。

sample.yaml
      RouteTableId:
       Fn::ImportValue:
         !Sub "${PJPrefix}-private-route-c"

今回ResourcesとかParametersとかOutputsとかいろいろ言いましたが
基本的には
「事前の値」
「作成物」
「出力値」

この3つで構成されることを覚えておけばあとはドキュメントをみても大丈夫です。
ということがこのドキュメントに書かれています
ドキュメント

Fn::ImportValueの使い方

特にAWSの構成がたくさん増えてくると厄介になるのが一つにまとめるとYAMLファイルの構成が長くなりカスタマイズが困難になることです。
(個人的には長いコードはあんまり好きじゃないです。ただファイル数が増えても管理が面倒なので個人的に落ち着くファイル数とコードの長さに設定しています。)

ドキュメント

基本的には

sample.yaml
      RouteTableId:
       Fn::ImportValue:
         !Sub "${PJPrefix}-private-route-c"

Fn::ImportValue:と書いて
その下の行にインデントを実施して呼び出したい値を持ってくるとなっています。

ただちょっといいなと思った使い方がこちら
ドキュメント

マッピングで Fn::Subのところです
次の例では、${Domain} 変数を Ref 関数の結果の値と置き換えるためにマッピングを使用します。

Name: !Sub
  - www.${Domain}
  - { Domain: !Ref RootDomainName }

これ!RefのところもImportValue使えます
*!Refは同じYamlファイルで作成したResourceの情報のこと
この場合はRootDomainNameというResourceを作成していてそのリソースの情報を再利用する話

実際に私が使った内容としてはEC2にS3のバケットの読み取りが可能になるためのIAMロールを作成する時に役にたちました

sample.yaml
S3AccessPolicies:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Action:
          - s3:ListAllMyBuckets
          - s3:AbortMultipartUpload
          - s3:RestoreObject
          - s3:GetBucketNotification
          - S3:GetBucketPolicy
          Resource: arn:aws:s3:::*
        - Effect: Allow
          Action:
          - s3:ListBucket
          - s3:GetBucketLocation
          - s3:PutBucketNotification
          Resource:
            Fn::Sub: 
              - arn:aws:s3:::${S3BucketName}
              - {
                  S3BucketName: { "Fn::ImportValue": !Sub "${PJPrefix}-s3" }
                }
        - Effect: Allow
          Action:
          - s3:PutObject
          - s3:GetObject
          Resource:
            Fn::Sub:
              - arn:aws:s3:::${S3BucketName}/*
              - {
                  S3BucketName: { "Fn::ImportValue": !Sub "${PJPrefix}-s3" }
                }

検索の仕方

基本的に今現在は
「AWS::IAM::ManagedPolicy」
という感じで
検索しています。

最初は構成内容から検索していたのですが、最終的にはドキュメントを読んだ方が早いことに気がつきました。

ただAWSのサービスがたくさんあるのでその中から探すのが大変だと思います。
その時は
「サービス名 CloudFormation」
と検索することをお勧めします。

以上です。

豊かな開発ライフを!!