本記事は 夏休みクラウド自由研究 8/23付の記事です。 |
こんにちは。AWS の作業を日々少しでも楽にしようと取り組んでいる兒玉です。
皆さん、AWS CloudFormation を使ってテンプレートを作成する際に、実行時エラーの修正を何度も行う際に、AWS Management Console を使って CloudFormation スタックへのパラメータの入力を何度も繰り返して行わないといけなくて、うんざりした経験はありませんか?
今回は、そんな際に CloudFormation のスタックを AWS CLI のコマンドを使って楽に何度も同じパラメータ入力をしなくても良いようにしよう、というちょっとした小技の紹介です。
なぜAWS CLI?
CloudFormation スタックの実行時は様々なパラメータを毎回 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 テンプレート作成ライフをお祈りしています!





