notebook

都内でWEB系エンジニアやってます。

Cloudformationで設定ファイルを分割する

AWSでの新規環境構築時など便利なCloudformation

一度テンプレートを作ってしまえば同じような環境はサクッと作れてとても便利ですね

最初にcloudformationを使ったときは分割できることは知っていたものの納期などの兼ね合いがあってとりあえず1ファイルのテンプレートで頑張る、みたいなことをやっていました

1ファイルで頑張ることもできそうではあるのですが意図しないところでトラブルが起きたり、うっかり手動で変更してしまったところが戻されてしまったりとかまぁ地獄を見そうなことは想像に難しくないかなと思います

なので以前使っていたときは初期の構築時の一発のみといった感じで使っていました

ファイルを分割してスタックを細かくわけることで

  • スタックの変更による影響範囲を狭めることができる
  • ファイルが分離されていることでなんのテンプレートについての設定なのかわかるようになる

といったメリットがありそうです

運用していくなら必須そうですね

今回はサンプルのテンプレートを分割していきたいと思います

一般的なWebサービス用のテンプレート

今回の構成は下記

  • VPC
    • SecurityGroup
    • Subnet
  • RDS
  • Redis

PVCとRDSなどのミドルウェアを分離させてみます

実際に運用するならEC2、AutoScalingGroup、etc... も必要ですが参照をしてみることを目的としてやってみます

分割するには

参考

AWS CloudFormation の更新 – YAML、クロススタック参照、簡略化された置換 | Amazon Web Services ブログ aws.amazon.com

分割するには下記作業が必要そうです

  • 参照される側のテンプレートでOutputsを定義して他スタックから参照可能にする
  • 参照する側のテンプレートで!ImportValue or Fn::ImportValue 関数をつかって参照する

Export

Outputs:で他スタックから参照可能にする

Outputs:
  SubBackend16C:
    Description: Subnet For Middleware, range:10.0.16.0/24, az: c
    Value: !Ref backend16C
    Export:
      Name: !Sub "${ResourceName}-SubBackend16C"

余談ですがDescriptionは日本語で入力すると文字化けするようで使えませんでした.....

Import

Fn::ImportValue:もしくは短縮形の!ImportValueで他スタックがOutputした情報を読み込む

関数をネストする場合は短縮形は使えないようです

Fn::ImportValue !Sub "${ResourceName}-SubBackend16C"

1枚のテンプレートからの分割

ということで分割です

Parametersでプロジェクト名(demo)を渡してそれを接頭辞としてスタックを作成していきます

  • demo-vpc
  • demo-db
  • demo-redis

参照先の指定も接頭辞(demo)をつけるようにします

分割前のファイルは下記です

cloudformation-cross-stack-ref/monolithic-template.yml at master · swfz/cloudformation-cross-stack-ref

VPC

VPCの部分を切り出します

cloudformation-cross-stack-ref/vpc-template.yml at master · swfz/cloudformation-cross-stack-ref

  • 一部抜粋
Parameters:
  ProjectNameParameter:
    Type: String
    Default: 'Sample'
    Description: Project name for tag
.....
.....
.....
Resources:
  myVPC:
    Type: AWS::EC2::VPC
    Properties:
.....
.....
.....
  middleware19A:
    Type: "AWS::EC2::Subnet"
    Properties:
      MapPublicIpOnLaunch: true
.....
.....
.....
  middleware20C:
    Type: "AWS::EC2::Subnet"
    Properties:
      MapPublicIpOnLaunch: true
.....
.....
.....
  defaultSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Default SecurityGroup
.....
.....
.....
Outputs:
  VPCId:
    Description: 'VPC ID'
    Value: !Ref myVPC
    Export:
      Name: !Sub "${ProjectNameParameter}-VPC"
  MiddleWare19A:
    Description: 'Subnet For Middleware,range: 10.0.19.0/24, az: a'
    Value: !Ref middleware19A
    Export:
      Name: !Sub "${ProjectNameParameter}-MiddleWare19A"
  MiddleWare20C:
    Description: 'Subnet For Middleware, range: 10.0.20.0/24, az: c'
    Value: !Ref middleware20C
    Export:
      Name: !Sub "${ProjectNameParameter}-MiddleWare20C"
  DefaultSecurityGroup:
    Description: 'Default Security Group'
    Value: !Ref defaultSecurityGroup
    Export:
      Name: !Sub "${ProjectNameParameter}-DefaultSecurityGroup"

これを実行すると出力の項目でOutputsで定義した値が出てきます

f:id:swfz:20180725035629p:plain

RDS

RDSの部分を切り出します

  • 一部抜粋
Parameters:
  ProjectNameParameter:
    Type: String
    Default: 'Sample'
    Description: Project name for tag
.....
.....
.....
Resources:
  RDSSubnetGroup:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupDescription: !Sub ${ProjectNameParameter} RDS subnet group
      SubnetIds:
        - {"Fn::ImportValue": !Sub "${ProjectNameParameter}-MiddleWare19A"}
        - {"Fn::ImportValue": !Sub "${ProjectNameParameter}-MiddleWare20C"}
.....
.....
.....
  RDSSecurityByEC2SecurityGroup:
    Type: "AWS::RDS::DBSecurityGroup"
    Properties:
      GroupDescription: "Ingress for Amazon EC2 security group"
      EC2VpcId: {"Fn::ImportValue": !Sub "${ProjectNameParameter}-VPC"}
.....
.....
.....
  RDSInstance:
    Type: "AWS::RDS::DBInstance"
    Properties:
      VPCSecurityGroups:
        - {"Fn::ImportValue": !Sub "${ProjectNameParameter}-DefaultSecurityGroup"}
.....
.....
.....

Redis

Redisの部分を切り出します

cloudformation-cross-stack-ref/redis-template.yml at master · swfz/cloudformation-cross-stack-ref

実行

VPC,db,redisの順に実行して作成します

./bin/execute.sh vpc create
./bin/execute.sh db create
./bin/execute.sh redis create

シェルスクリプトにまとめてしまってますが実際に叩いているコマンドは

aws cloudformation create-stack \
    --stack-name demo-vpc \
    --template-body file://$(pwd)/vpc-template.yml \
    --parameters \
    ParameterKey=AZ1Parameter,ParameterValue=ap-northeast-1a \
    ParameterKey=AZ2Parameter,ParameterValue=ap-northeast-1c \
    ParameterKey=ProjectNameParameter,ParameterValue=Demo
  • db
aws cloudformation create-stack \
    --stack-name demo-db \
    --template-body file://$(pwd)/db-template.yml \
    --parameters \
    ParameterKey=ProjectNameParameter,ParameterValue=Demo \
    ParameterKey=RDSInstanceTypeParameter,ParameterValue=db.t2.small \
    ParameterKey=RDSUserParameter,ParameterValue=root \
    ParameterKey=RDSPasswordParameter,ParameterValue=aaaabbbbcccc
  • redis
aws cloudformation create-stack \
    --stack-name demo-redis \
    --template-body file://$(pwd)/redis-template.yml \
    --parameters \
    ParameterKey=AZ1Parameter,ParameterValue=ap-northeast-1a \
    ParameterKey=ProjectNameParameter,ParameterValue=Demo \
    ParameterKey=RedisInstanceTypeParameter,ParameterValue=cache.t2.micro

こんな感じです

無事、VPC内にRDS、Redisが作成されたことを確認しました

まとめ

RDSとVPCの部分だけですがクロススタック参照を使うことでテンプレートファイルを分割することができました

やってみるまでは面倒だなーと思っていたのですがやってみたらとても簡単ですね

インフラの構成をすべてcloudformationで賄うってなかなか大変そうだなとは思うもののやはりコードで管理できるのは便利だと思うので頑張っていきたいなと思います