rainを導入してコマンドラインベースのCloudFormationをより簡単に、より使いやすく

2021.09.01

今日のひとこと:止まない雨はない

はじめに

コマンドラインベースのCloudFormationの管理を便利にするrainを触り倒していきます。

rainとは

rainとは、AWS CloudFormationテンプレート(以下、テンプレート)やスタックを操作するためのコマンドラインツールです。

rainの特徴

rainの特徴は、既存のテンプレートを活用できることだと思っています。Terraform、Pulumi、AWS CDKなど、様々なソリューションがありますが、どれもIaCを再コーディングする必要があります。既存の資産を有効活用するならrainはもってこいのツールです。

rainのセットアップ

まずはrainのインストールして利用できるようにしてきます。筆者の環境はMacOS(Catalina10.15.7)です。Windowsにもインストールして利用できますがここでは割愛します。

brew insatll rain でインストールできます。
rain -v でバージョンが表示されればインストールOKです。

% rain -v 
rain v1.2.0 darwin/amd64

rainは .aws/config.aws/credentials を参照して実行します。 rain info を実行するとcredentialsから認証情報をできます。

% rain info 
Account:  012345678910
Region:   ap-northeast-1
Identity: arn:aws:iam::012345678910:user/iamusername

他のprofileはAWS CLI同様に、 -p, --profile オプションを指定して実行できます。AssumeRoleで複数のAWSアカウントを操作するユーザーとしては嬉しいです。

% rain ls --profile profile-name

rainの基本操作

ls(スタック表示)

rainはCloudFormationサービスに特化したツールです。まずは以下のコマンドでスタックの一覧を表示します。

% rain ls 

CloudFormation stacks in ap-northeast-1:
  stack-name1: CREATE_COMPLETE
  stack-name2: CREATE_COMPLETE
CloudFormation stacks in us-east-1:
  stack-name1: CREATE_COMPLETE

Shellを使い慣れている人には直感的にわかりやすいコマンドです。AWS CLIで同じ内容を表示しようとするとオプション含めて

% aws cloudformation describe-stacks --query 'Stacks[*].[StackName,StackStatus]' --output text

を入力する必要があります。

rain ls stack-name1 とすると指定したスタックのみ表示できます。-a, --allオプションを指定するとスタックの詳細が表示されます。 outputsを表示してくれるところがめっちゃ良きです。

% rain ls stack-name1

Stack stack-name1: CREATE_COMPLETE
  Outputs:
    AssumeRole: arn:aws:iam::012345678910:role/AssumeRole # AssumeRoleArn


% rain ls -a stack-name1

Stack stack-name1: CREATE_COMPLETE
  Parameters:
    ExternalID: e3c2d49e-903e-xxxx-xxxx-e68bbb592da9
  Resources:
    stack-name1: CREATE_COMPLETE
      stack-name1
  Outputs:
    stack-name1: arn:aws:iam::012345678910:role/stack-name1 # stack-name1Arn

fmt(テンプレートのフォーマット)

まずはスタックを作成してみます。ローカル環境にあるテンプレートを指定してスタックを作成できます。以下のサンプルテンプレートを用意しました。

AWSTemplateFormatVersion: "2010-09-09"
Description: Network Template.

Parameters:
  Env:
    Type: String
  vpcCidr:
    Type: String

Resources:
  igw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Env}-igw

  igwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref igw
      VpcId: !Ref vpc1

  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref vpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-vpc1

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: "AWS::Region"
      CidrBlock: !Select [0, !Cidr [!GetAtt vpc1.CidrBlock, 2, 8]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-public-subnet-1

Outputs:
  vpc1:
    Value: !Ref vpc1

rainは、cfn-formatによる標準フォーマットでデプロイされます。手元のテンプレートと差分をなくす為、デプロイ前にテンプレートをフォーマットします。

% rain fmt ./template.yml
AWSTemplateFormatVersion: "2010-09-09"

Description: Network Template.

Parameters:
  Env:
    Type: String

  vpcCidr:
    Type: String

Resources:
  igw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Env}-igw

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref igw
      VpcId: !Ref vpc1

  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref vpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-vpc1

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [0, !Cidr [!GetAtt vpc1.CidrBlock, 2, 8]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-public-subnet-1

Outputs:
  vpc1:
    Value: !Ref vpc1

オプションを指定しない場合は、標準出力です。 -w, --write オプションを指定してテンプレートを上書きできます。

% rain fmt -w ./template.yml

deploy(ローカル環境にあるテンプレートからスタック展開)

fmtしたローカルにあるテンプレートをデプロイしてきます。 --params でParametersに代入できます。key1=Value,key2=Value,key3=Value... ファイルの指定は、ファイルパスです。(file://~ではありません)

% rain deploy ./template.yml stack-name1 --params Env=dev,vpcCidr="10.10.10.0/16"

rain needs to create an S3 bucket called 'rain-artifacts-0123456789010-ap-northeast-1'. Continue? (Y/n) Y
CloudFormation will make the following changes:
Stack stack-name1:
  + AWS::EC2::VPC vpc1
Do you wish to continue? (Y/n) Y
Deploying template 'stack-name1.yml' as stack 'stack-name1' in ap-northeast-1.
Stack stack-name1: CREATE_COMPLETE
  Outputs:
    vpc1: vpc-08c7ec1ca037ac008
Successfully deployed stack-name1

deployの流れは以下のとおりです。

  1. rain用S3バケットのチェック
  2. ChangeSet
  3. スタックのデプロイ

deployには、パッケージ化したテンプレートのアーティファクトの格納先としてrain用のS3バケットの有無がチェックされます。バケットがない場合は、 rain needs to create an S3 bucket called 'rain-artifacts-0123456789010-ap-northeast-1'. Continue? (Y/n) が出力されるのでS3バケットを作成します。ChangeSetを確認し、スタックをデプロイします。 Successfully deployed stack-name1 が表示されればデプロイの成功です。
なお、(Y/n)の入力は、Shellでもよく見かける -y, --yes オプションを指定すれば省略されます。

スタックの作成またはアップデートが失敗すると、エラー対象のLogicalIDと理由が出力されます。template.ymlのPublicSubnet1を一部を修正してデプロイしてみます。

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
-        - 0
+        - 3
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [0, !Cidr [!GetAtt vpc1.CidrBlock, 2, 8]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-public-subnet-1
% rain deploy -y ./template.yml stack-name1 --params Env="dev",vpcCidr="10.10.0.0/16"

Deploying template 'template.yml' as stack 'stack-name1' in ap-northeast-1.
Stack stack-name1: UPDATE_ROLLBACK_COMPLETE
  Outputs:
    vpc1: vpc-0ab94716b91314657
Messages:
  - PublicSubnet1 - subnet-003e0bd623f3a0a12: Template error: Fn::Select  cannot select nonexistent value at index 3
failed deploying stack 'stack-name1'

デプロイエラーのLogicalIDだけ表示されるので確認しやすいです。ちなみに「3.スタックのデプロイ」で新規デプロイの場合は、スタックの削除までやってくれます。

logs(スタックのイベントログを出力)

スタックのイベント履歴を確認できます。主にのエラーイベント(UPDATE_FAILEDなど)を出力します。

% rain logs stack-name1

No interesting log messages to display. To see everything, use the --all flag

上記にも出力されている通り、エラーイベントなどがなければ出力されません。すべてのイベント履歴を表示するには、 -a, --all オプションを指定します。

% rain logs stack-name1 --all

Aug 31 12:30:23 stack-name1/vpc1 (AWS::EC2::VPC) CREATE_COMPLETE
Aug 31 12:30:07 stack-name1/vpc1 (AWS::EC2::VPC) CREATE_IN_PROGRESS "Resource creation Initiated"
Aug 31 12:30:06 stack-name1/vpc1 (AWS::EC2::VPC) CREATE_IN_PROGRESS
~

rm(スタックの削除)

デプロイしたスタックを削除します。

% rain rm stack-name1

Stack stack-name1: CREATE_COMPLETE
Are you sure you want to delete this stack? (y/N) y
Successfully deleted stack 'stack-name1'

(Y/n)の入力は、deployコマンドと同様に -y, --yes オプションを指定すれば省略されます。

cat(スタックのテンプレート取得)

デプロイされているスタックのテンプレートを出力します。デフォルトフォーマットされた形式で出力されます。

% rain cat stack-name1
AWSTemplateFormatVersion: "2010-09-09"

Description: Network Template.

Parameters:
  Env:
    Type: String

  vpcCidr:
    Type: String

Resources:
  igw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Env}-igw

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref igw
      VpcId: !Ref vpc1

  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref vpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-vpc1

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [0, !Cidr [!GetAtt vpc1.CidrBlock, 2, 8]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-public-subnet-1

Outputs:
  vpc1:
    Value: !Ref vpc1

フォーマットせずにスタックからテンプレートを出力する場合は、-u, --unformatted オプションを指定します。

diff(テンプレートの比較)

ローカル環境にあるテンプレートを比較します。セクション(Resources,Parameters,Outputsなど)やLogicalID、Propertiesの差分を確認できます。

  • セクション差分(Outputsなし)
% rain diff template.yml template1.yml
(-) Outputs: {...}
  • LogicalID差分(ResourcesセクションのPublicSbunet1なし)
% rain diff template.yml template1.yml
(|) Resources:
(-)   PublicSubnet1: {...}
  • Propeties差分(Tagsプロパティなし)
% rain diff template.yml template1.yml
(|) Resources:
(|)   PublicSubnet1:
(|)     Properties:
(-)       Tags: [...]
  • Propeties値差分
% rain diff template.yml template1.yml
(|) Resources:
(|)   PublicSubnet1:
(|)     Properties:
(|)       Tags:
(|)         [0]:
(|)           Value:
(>)             Fn::Sub: ${Env}-public-subnet-2

catおよびdiffコマンドの用途は、スタックのテンプレートとローカル環境にある修正したテンプレートの差分チェックというところでしょうか。

tree(リソース間依存関係の確認)

テンプレート内の各リソースの依存関係(DependsOn)を確認できます。

% rain tree template.yml 
Resources:
  igw:
    DependsOn:
      Parameters:
        - Env
  vpc1:
    DependsOn:
      Parameters:
        - Env
        - vpcCidr
  MyVPCGatewayAttachment:
    DependsOn:
      Resources:
        - igw
        - vpc1
  PublicSubnet1:
    DependsOn:
      Parameters:
        - AWS::Region
        - Env
      Resources:
        - vpc1
Outputs:
  vpc1:
    DependsOn:
      Resources:
        - vpc1

igw(LogicalID))だと、ParametersのEnvが依存関係にあります。MyVPCGatewayAttachment(LogicalID)では、Resourcesのigw、vpc1が依存関係にあります。このような形で確認できるのは、めっちゃ良きです。

build(テンプレートの作成)

テンプレートを作成するには、AWSドキュメントを見ながらコーディングしていく必要があります。必須のプロパティだったり書き方を理解するのに時間がかかるものですが、 rain build であっという間にテンプレートが生成されます。template.ymlと同様のリソースを指定してrainでテンプレートを生成してみます。

% rain build "AWS::EC2::InternetGateway" "AWS::EC2::VPCGatewayAttachment" "AWS::EC2::VPC" "AWS::EC2::Subnet"
AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Resources:
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: CHANGEME
          Value: CHANGEME

  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AssignIpv6AddressOnCreation: false # Optional
      AvailabilityZone: CHANGEME # Optional
      CidrBlock: CHANGEME
      Ipv6CidrBlock: CHANGEME # Optional
      MapPublicIpOnLaunch: false # Optional
      OutpostArn: CHANGEME # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: CHANGEME

  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: CHANGEME
      EnableDnsHostnames: false # Optional
      EnableDnsSupport: false # Optional
      InstanceTenancy: CHANGEME # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: CHANGEME # Optional
      VpcId: CHANGEME
      VpnGatewayId: CHANGEME # Optional

Outputs:
  MySubnetAvailabilityZone:
    Value: !GetAtt MySubnet.AvailabilityZone

  MySubnetIpv6CidrBlocks:
    Value: !GetAtt MySubnet.Ipv6CidrBlocks

  MySubnetNetworkAclAssociationId:
    Value: !GetAtt MySubnet.NetworkAclAssociationId

  MySubnetOutpostArn:
    Value: !GetAtt MySubnet.OutpostArn

  MySubnetVpcId:
    Value: !GetAtt MySubnet.VpcId

  MyVPCCidrBlock:
    Value: !GetAtt MyVPC.CidrBlock

  MyVPCCidrBlockAssociations:
    Value: !GetAtt MyVPC.CidrBlockAssociations

  MyVPCDefaultNetworkAcl:
    Value: !GetAtt MyVPC.DefaultNetworkAcl

  MyVPCDefaultSecurityGroup:
    Value: !GetAtt MyVPC.DefaultSecurityGroup

  MyVPCIpv6CidrBlocks:
    Value: !GetAtt MyVPC.Ipv6CidrBlocks

リソースタイプを指定するだけであっという間にテンプレートが生成されます。 CHANGEME (# Optional無し)は必須のプロパティ、 CHANGEME # Optional は任意のプロパティです。必要に応じて値を設定します。
なお、最低限のプロパティだけ生成する場合は、-b, --bare オプションを指定します。

% rain build -b "AWS::EC2::InternetGateway" "AWS::EC2::VPCGatewayAttachment" "AWS::EC2::VPC" "AWS::EC2::Subnet"AWSTemplateFormatVersion:
"2010-09-09"

Description: Template generated by rain

Resources:
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties: {}

  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: CHANGEME
      VpcId: CHANGEME

  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: CHANGEME

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: CHANGEME

また、リソースタイプは複数指定も可能です。以下、AWS::EC2::Subnet を複数指定して生成できます。

% rain build -b "AWS::EC2::InternetGateway" "AWS::EC2::VPCGatewayAttachment" "AWS::EC2::VPC" "AWS::EC2::Subnet" "AWS::EC2::Subnet" "AWS::EC2::Subnet" "AWS::EC2::Subnet"
AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Resources:
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties: {}

  MySubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: CHANGEME
      VpcId: CHANGEME

  MySubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: CHANGEME
      VpcId: CHANGEME

  MySubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: CHANGEME
      VpcId: CHANGEME

  MySubnet4:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: CHANGEME
      VpcId: CHANGEME

  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: CHANGEME

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: CHANGEME

ちなみに -l, --list で指定できるリソースタイプを確認できます。v1.2.0だと 740 のリソースタイプが指定できます。
buildコマンドでベースのテンプレートを生成して、ParametersやOutputsセクションなど追加コーディングしていきましょう。

ファーストインプレッションを終えて

Shellに慣れている人間には直感的に扱いやすいです。コマンドオプションも多くなく、簡単な教育で現場に浸透させやすいツールだと感じます。
面倒な部分として -p, --profile オプションを指定するとrainコマンドの都度、MFA Tokenの入力が求められます。
ソースコードを見ると WithAssumeRoleCredentialOptionsによって以前の一時資格情報が上書きされる為です。
stsやdirenvなどでprofileオプションを指定しない方法が扱いやすいです。

個人的にはコマンドラインによるCloudFormataion管理で、rainの右に出るものは現時点でないと思っています。次回は、rainを使ったテンプレートのCI/CDパイプラインの実装を検証していきたいと思います。