AWS CloudFormation のテンプレートデバッグ時のスタック作成削除の繰り返しを AWS CLI で便利に

本記事は 夏休みクラウド自由研究 8/23付の記事です

こんにちは。AWS の作業を日々少しでも楽にしようと取り組んでいる兒玉です。

皆さん、AWS CloudFormation を使ってテンプレートを作成する際に、実行時エラーの修正を何度も行う際に、AWS Management Console を使って CloudFormation スタックへのパラメータの入力を何度も繰り返して行わないといけなくて、うんざりした経験はありませんか?

今回は、そんな際に CloudFormation のスタックを AWS CLI のコマンドを使って楽に何度も同じパラメータ入力をしなくても良いようにしよう、というちょっとした小技の紹介です。

なぜAWS CLI?

CloudFormation スタックの実行時は様々なパラメータを毎回 4 ステップ分入力する必要があります。

  1. テンプレートファイルを選択して、ローカルからアップロード
  2. スタック名を入力して、スタックのパラメータをパラメータ数分だけ入力
  3. タグを必要な数だけ入力、その他詳細オプション等あれば入力
  4. 入力した値を確認して送信

という手順になります。1回や2回くらいなら、「まぁ、手打ちでもいいか…」 となるのですが、テンプレートを作成中などで、テンプレートに不具合があってうまくスタックが作成完了しなかった場合には、ロールバックして、再度スタックを作成するために上記の4手順を行って… と繰り返していくと、テンプレート内の不具合がなかなか解消しなかった場合には段々とイライラしてきます…

何度も同じパラメータも同じで何度も実行するので、1回ちょっと手間をかけたら、あとはサッとスムーズに実行して、テンプレートの不具合修正に集中したいところです。

そこで、 CLI の出番です。

CLI なら、テキストエディタでコマンドを一回編集して貼り付ければ、あとは何度も同じ操作を実行する場合にはコマンド履歴からキーボード数回打鍵するだけで実行可能です!

やってみよう

早速、やってみましょう。

今回利用するのは以下の CloudFormationテンプレートです。VPCと、インターネットゲートウェイ、S3ゲートウェイエンドポイントを作るだけなのですが、パラメータの入力(4つ)を使ってタグ付けをしていて、しかも CreateDate は今日の日付を入れようという、スタック作成時に面倒なテンプレートです。

---
AWSTemplateFormatVersion: "2010-09-09"
Description: Create VPC*1, Subnet*2, InternetGateway*1, RouteTable*1 NACL*1 S3 GatewayEndpoint * 1

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Common Settings
        Parameters:
          - ProjectName
          - Environment
          - CostTagValue
          - CreateDate
      - Label:
          default: VPC Settings
        Parameters:
          - VPCCidr
          - PublicSubnetCidr1
          - PublicSubnetCidr2
          - PrivateSubnetCidr1
          - PrivateSubnetCidr2

Parameters:
  ProjectName:
    Description: Project Name
    Type: String
    Default: unnamed
  Environment:
    Description: Environment
    Type: String
    Default: sbx
    AllowedValues:
      - prd
      - dev
      - stg
      - sbx
  VPCCidr:
    Description: VPC IP Range
    Type: String
    Default: 10.0.0.0/16
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  PublicSubnetCidr1:
    Description: Public Subnet 1 IP Range
    Type: String
    Default: 10.0.1.0/24
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  PublicSubnetCidr2:
    Description: Public Subnet 2 IP Range
    Type: String
    Default: 10.0.2.0/24
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  PrivateSubnetCidr1:
    Description: Private Subnet 1 IP Range
    Type: String
    Default: 10.0.17.0/24
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  PrivateSubnetCidr2:
    Description: Private Subnet 2 IP Range
    Type: String
    Default: 10.0.18.0/24
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  CostTagValue:
    Description: Tag Value for Cost allocation tag.
    Type: String
    Default: nanashi
  CreateDate:
    Description: Create Date
    Type: String
    Default: 2023/09/15


Resources:
  # Create VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-vpc
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}

  # Create InternetGateway & VPC Attach
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-igw
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Create Public RouteTable & Setting Routing
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-public-rtb1
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  PublicRoute1:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # Create Public Subnet *2
  # Public 1
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [0, !GetAZs ""]
      CidrBlock: !Ref PublicSubnetCidr1
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-public-subnet1
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  PublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable1
  # Public 2
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [1, !GetAZs ""]
      CidrBlock: !Ref PublicSubnetCidr2
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-public-subnet2
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}

  PublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable1

  # Create Private Subnet *2
  # Private 1
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [0, !GetAZs ""]
      CidrBlock: !Ref PrivateSubnetCidr1
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-private-subnet1
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-private-rtb1
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  PrivateSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable1

  # Private 2
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [1, !GetAZs ""]
      CidrBlock: !Ref PrivateSubnetCidr2
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-private-subnet2
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}

  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-private-rtb2
        - Key: Environment
          Value: !Sub ${Environment}
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  PrivateSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable2

  # Create Network ACL
  # Public NACL
  PublicNetworkACL1:
    Type: AWS::EC2::NetworkAcl
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-public-nacl1
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  NetworkACLEntryPublicIngress1:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      CidrBlock: "0.0.0.0/0"
      Egress: false
      NetworkAclId: !Ref PublicNetworkACL1
      Protocol: -1
      RuleAction: "allow"
      RuleNumber: 100
  NetworkACLEntryPublicEgress1:
    Type: "AWS::EC2::NetworkAclEntry"
    Properties:
      CidrBlock: "0.0.0.0/0"
      Egress: true
      NetworkAclId: !Ref PublicNetworkACL1
      Protocol: -1
      RuleAction: "allow"
      RuleNumber: 100
  # Private NACL
  PrivateNetworkACL1:
    Type: AWS::EC2::NetworkAcl
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-${Environment}-Private-nacl1
        - Key: Cost
          Value: !Sub ${CostTagValue}
        - Key: CreateDate
          Value: !Sub ${CreateDate}
  NetworkACLEntryPrivateIngress1:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      CidrBlock: "0.0.0.0/0"
      Egress: false
      NetworkAclId: !Ref PrivateNetworkACL1
      Protocol: -1
      RuleAction: "allow"
      RuleNumber: 100
  NetworkACLEntryPrivateEgress1:
    Type: "AWS::EC2::NetworkAclEntry"
    Properties:
      CidrBlock: "0.0.0.0/0"
      Egress: true
      NetworkAclId: !Ref PrivateNetworkACL1
      Protocol: -1
      RuleAction: "allow"
      RuleNumber: 100
  # VPC Endpoint for S3 gateway Endpoint
  # prod-region-starport-layer-bucket is ECR used S3 bucket for Docker image download from ECR.
  # https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-minimum-s3-perms
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - s3:*
            Resource:
              - !Sub arn:aws:s3:::*${AWS::Region}.amazon.com/*
              - !Sub arn:aws:s3:::*${AWS::Region}.amazon.com/
          - Effect: Allow
            Principal: '*'
            Action:
              - s3:GetObject
            Resource:
              - !Sub arn:aws:s3:::prod-${AWS::Region}-starport-layer-bucket/*
      RouteTableIds:
        - !Ref PublicRouteTable1
        - !Ref PrivateRouteTable1
        - !Ref PrivateRouteTable2
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcId: !Ref VPC
      VpcEndpointType: Gateway

  # NetworkACL Association
  PublicNetworkACLAssocation1:
    Type: AWS::EC2::SubnetNetworkAclAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      NetworkAclId: !Ref PublicNetworkACL1
  PublicNetworkACLAssocation2:
    Type: AWS::EC2::SubnetNetworkAclAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      NetworkAclId: !Ref PublicNetworkACL1
  PrivateNetworkACLAssocation1:
    Type: AWS::EC2::SubnetNetworkAclAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      NetworkAclId: !Ref PrivateNetworkACL1
  PrivateNetworkACLAssocation2:
    Type: AWS::EC2::SubnetNetworkAclAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      NetworkAclId: !Ref PrivateNetworkACL1

Outputs:
  ExportVPC:
    Value: !Ref VPC
    Export:
      Name: !Sub ${ProjectName}-${Environment}-VPC
  ExportVPCCidr:
    Value: !Ref VPCCidr
    Export:
      Name: !Sub ${ProjectName}-${Environment}-VPCCidr
  ExportPublicSubnet1:
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub ${ProjectName}-${Environment}-PublicSubnet1
  ExportPublicSubnet2:
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub ${ProjectName}-${Environment}-PublicSubnet2
  ExportPrivateSubnet1:
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub ${ProjectName}-${Environment}-PrivateSubnet1
  ExportPrivateSubnet2:
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub ${ProjectName}-${Environment}-PrivateSubnet2
  ExportPrivateRouteTable1:
    Value: !Ref PrivateRouteTable1
    Export:
      Name: !Sub ${ProjectName}-${Environment}-PrivateRouteTable1
  ExportPrivateRouteTable2:
    Value: !Ref PrivateRouteTable2
    Export:
      Name: !Sub ${ProjectName}-${Environment}-PrivateRouteTable2
  ExportS3GatewayEndpoint:
    Value: !Ref S3Endpoint
    Export:
      Name: !Sub ${ProjectName}-${Environment}-S3GatewayEndpoint

今回は AWS のアクセスキー設定等が不要で便利な AWS CloudShell 環境(Amazon linux 2023でした)から実行します。

[cloudshell-user@ip-10-130-56-54 kodama-th-cloundformation-cli]$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Amazon Linux 2023.5.20240708"

上記のCloudFormationテンプレートを、vpc.yaml として保存して、カレントディレクトリに配置しておきます。

[cloudshell-user@ip-10-130-56-54 kodama-th-cloundformation-cli]$ 
[cloudshell-user@ip-10-130-56-54 kodama-th-cloundformation-cli]$ ls -la
total 20
drwxr-xr-x.  2 cloudshell-user cloudshell-user  4096 Aug 21 14:51 .
drwxr-xr-x. 21 cloudshell-user cloudshell-user  4096 Aug 21 14:51 ..
-rw-r--r--.  1 cloudshell-user cloudshell-user 11885 Aug 21 14:51 vpc.yaml

この状態で、以下コマンドをのCLIをテキストエディタなどで編集したあと、コマンドラインにはりつけて実行します。

STACK_NAME=<作成する CloudFormationスタックの名前 kodama-th-sample-stackname> 
TEMPLATE_FILE=<CloudFormation テンプレートファイルの名前> 
TIMEOUT_IN_MINUTES=<スタックのタイムアウト時間(分)> 
PROJECT_NAME=<作成するリソースに付与する Project タグ名>
COST=<作成するリソースに付与する Cost タグ名>
Environment=<デプロイするリソースの環境, prd, dev, stg, sbx のいずれか>
aws cloudformation create-stack \
  --stack-name ${STACK_NAME} \
  --template-body "file://${TEMPLATE_FILE}" \
  --parameters \
  ParameterKey="ProjectName",ParameterValue="${PROJECT_NAME}" \
  ParameterKey="Environment",ParameterValue="${Environment}" \
  ParameterKey="CostTagValue",ParameterValue="${COST}" \
  --tags \
  Key=Cost,Value="${COST}" \
  Key=Project,Value="${PROJECT_NAME}" \
  Key=Environment,Value="${Environment}" \
  Key=Name,Value=${STACK_NAME} \
  Key=CreateDate,Value=$(date '+%Y/%m/%d') \
  --timeout-in-minutes="${TIMEOUT_IN_MINUTES}" \
  --capabilities CAPABILITY_NAMED_IAM

今日の日付を CreateDate タグにつけているのですが、これを Linux の date コマンドを利用して自動的にちゃんと実行した日付になるようにしています。

赤字の部分は、必要に応じて書き直すのですが、今回は、

STACK_NAME=kodama-th-sample-stackname
TEMPLATE_FILE=vpc.yaml
TIMEOUT_IN_MINUTES=10
PROJECT_NAME=kodama-th-cloudformation-cli
COST="j.kodama"
Environment=sbx

としました。

実際に貼り付けて実行すると、以下のようになりました。

“StackID”: “arn:aws:cloudformation:~” の応答が返ってくればスタックが作成されて実行が開始されています。

Managemend Console の CloudFormation スタックの画面を見ると、無事作成されています!

削除したい場合

スタックでAWSリソースの作成に失敗したりして、削除したい場合は、簡単です。
すでにスタックは環境変数STACK_NAMEとして登録済みなので、以下を貼り付けるだけで実行できます。

aws cloudformation delete-stack \
  --stack-name "${STACK_NAME}"

Management Consoleでスタック名を「削除済み」で検索すると、削除されていることがわかります。

あとは、これを理想のCloudFormation テンプレートに仕上がるまで繰り返していきます。
GUIでは 4ページ分必要だったパラメータ入力が、Linux Shell のコマンド履歴から矢印キーを数回 + Enter キーで実行できるようになります!

終わりに

いかがでしょうか? これで CloudFormation テンプレートを作ろうとしているけど、デバッグ時の入力作業の繰り返しがうんざりしてやめてしまった方でも「もう一度トライしてみよう!」と思えませんか?

皆様の良き CloudFormation テンプレート作成ライフをお祈りしています!

著者について

ビデオゲームと昼寝と現実逃避をこよなく愛するシステムエンジニア。 好きなAWSサービスは AWS Lambda 。 マネジメントサービスであることだったり、pay-as-you-go の極致であったり、簡単に使えたり、でも使いこなすには奥が深かったりと、AWSの良さと楽しさを最大限に含まれるサービスだと思っています!

兒玉純をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSクラウド
シェアする
タイトルとURLをコピーしました