本記事は 夏休みクラウド自由研究 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 テンプレート作成ライフをお祈りしています!