こんにちは。ダントーです。
CloudFormationスタックで管理していたサブネットが、スタックの更新・削除により意図せずスタック管理下から外れてしまって焦っている方向けの記事です。
他リソースが依存しているサブネットをスタックから削除しようとすると、スタックから消えないだけでなくスタック管理下から外れるといった現象に出くわしたので、その際の対応を備忘録として残します。
想定検索ワード:CloudFormation スタック サブネット 外れる
TL; DR;
- CloudFormationの既存スタックへのリソースインポート機能を使う
- スタック管理下から外れたサブネットを既存スタックにインポートします
- 本来デプロイするはずのテンプレートを使用して既存スタックを再度更新します
- 消失したリソースがある場合はこの手順も踏んだほうが良いです
では、いきましょう。
構成図とCloudFormationテンプレート
今回想定している構成です。
ポイントはサブネットの存在に依存するリソースが存在していること、つまりサブネット上にVPC Lambdaがデプロイされている点になります。
また、ネットワーク復旧確認用にVPC EndpointとS3を用意しています。
本題
まずは、今回使用するCloudFormationテンプレートを確認します。
テンプレートはNetwork.yamlとLambda.yamlの2種類です。
Network.yamlでは
- VPC
- ルートテーブル
- サブネット
- VPCエンドポイント(Gateway)
- パラメータストア(VPCID) ← Lambda.yamlにパラメータを渡すために作成
- パラメータストア(サブネットID) ← Lambda.yamlにパラメータを渡すために作成
これらを作成しています。
また、Lambda.yamlでは、
- IAMロール
- セキュリティグループ
- Lambda関数
- S3バケット ←ネットワーク疎通確認用
これらを作成しています。
ネットワークの疎通が取れているかを確認するために、Lambda関数内ではS3バケットにファイルをはき出すロジックを入れています。
①正常時、②サブネットをスタックに引き戻したとき
これら2つのケースでネットワークに問題がないことを確認します。
詳細は以下のテンプレートを確認してください。
AWSTemplateFormatVersion: "2010-09-09"
Description: Network
Parameters:
Prefix:
Description: Enter Prefix.
Type: String
Default: sample
Resources:
#---------------------------------------------------#
# VPC
#---------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub ${Prefix}-vpc
#---------------------------------------------------#
# RouteTable
#---------------------------------------------------#
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-rt
#---------------------------------------------------#
# Subnet1A and RouteTableAssociation
#---------------------------------------------------#
PrivateSubnet1A:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.16.0/20
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-subnet-1a
PrivateSubnet1ARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet1A
#---------------------------------------------------#
# S3 VPCEndpoint
#---------------------------------------------------#
RecoveryS3VPCEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
RouteTableIds:
- !Ref PrivateRouteTable
#---------------------------------------------------#
# Parameters Section
#---------------------------------------------------#
ParamRecoveryVPCId:
Type: AWS::SSM::Parameter
Properties:
Name: /Network/VPCId
Type: String
Value: !Ref VPC
ParamPrivateSubnet1AId:
Type: AWS::SSM::Parameter
Properties:
Name: /Network/PrivateSubnet1AId
Type: String
Value: !Ref PrivateSubnet1A
AWSTemplateFormatVersion: "2010-09-09"
Description: Lambda
Parameters:
Prefix:
Description: Enter Prefix.
Type: String
Default: sample
ParamVPCId:
Type: AWS::SSM::Parameter::Value<String>
Default: /Network/VPCId
ParamPrivate1ASubnetId:
Type: AWS::SSM::Parameter::Value<String>
Default: /Network/PrivateSubnet1AId
Resources:
#---------------------------------------------------#
# IAM Role for Lambda Function
#---------------------------------------------------#
LambdaFunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${Prefix}-lambda-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
- arn:aws:iam::aws:policy/AmazonS3FullAccess
#---------------------------------------------------#
# SecurityGroup for Lambda Function
#---------------------------------------------------#
LambdaSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for lambda
GroupName: !Sub ${Prefix}-lambda-sg
VpcId: !Ref ParamVPCId
LambdaSGEgressToANY:
Type: AWS::EC2::SecurityGroupEgress
Properties:
IpProtocol: -1
CidrIp: 0.0.0.0/0
GroupId: !Ref LambdaSG
#---------------------------------------------------#
# Lambda Function
#---------------------------------------------------#
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${Prefix}-function
Role: !GetAtt LambdaFunctionRole.Arn
Runtime: python3.10
Handler: index.lambda_handler
Code:
ZipFile: |
import json
import boto3
from datetime import datetime
def lambda_handler(event, context):
s3 = boto3.client('s3')
bucket_name = 'sample-check-network-status-bucket'
current_time = datetime.now().strftime('%Y%m%d_%H%M%S')
file_name = f'trial_{current_time}.txt'
file_content = 'Hello World!'
s3.put_object(Bucket=bucket_name, Key=file_name, Body=file_content)
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSG
SubnetIds:
- !Ref ParamPrivate1ASubnetId
Timeout: 10
#---------------------------------------------------#
# S3 Bucket for checking the network status
#---------------------------------------------------#
AssetsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${Prefix}-check-network-status-bucket
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
VersioningConfiguration:
Status: Suspended
状況を再現(スタックを作成する)
ここでは、CloudShellからスタック作成を実施します。
(破壊した環境にもよりますが、おそらく通常では開発環境等で復旧テスト→当該環境で復旧作業となると想定しています。その際に余計な環境差を出さないためにもCloudShellからの実行としています。)
2つのスタックには依存関係があるので、Network.yaml → Lambda.yamlの順でデプロイしていきます。
ファイルをCloudShellにアップロードした後、以下のコマンドを実行します。
# 1. ネットワークスタックを作成
aws cloudformation deploy \
--template-file Network.yaml \
--stack-name sample-network-stack
# 2. Lambdaスタックを作成
aws cloudformation deploy \
--template-file Lambda.yaml \
--stack-name sample-lambda-stack \
--capabilities CAPABILITY_NAMED_IAM
# 3. ネットワークスタックの管理下にあるリソースを確認
aws cloudformation describe-stack-resources \
--stack-name sample-network-stack \
--query "join(', ', StackResources[*].LogicalResourceId)" --output text \
--output text
3の実行結果は以下となります。
また、Lambda関数を実行してS3バケットにファイルが出力できることも確認しておきます。
# 1. S3バケット内のオブジェクト数を確認
# → 「Total Objects: 0」と表示される
aws s3 ls s3://sample-check-network-status-bucket \
--recursive \
--summarize
# 2. Lambda関数を実行
aws lambda invoke \
--function-name sample-function \
outputfile.txt
# 3. S3バケット内のオブジェクト数を確認
# → 「Total Objects: 1」と表示される
aws s3 ls s3://sample-check-network-status-bucket\
--recursive \
--summarize
ここまででスタックの作成はOKです。
状況を再現(スタック管理下からサブネットを外す)
では、ここからはサブネットをスタック管理下から外していきます。
Network.yaml内の記述を修正したDestructive-Network.yamlを用意して、既存のスタックを上書きしましょう。
Destructive-Network.yamlで作成予定のリソースは以下です。
- VPC
- ルートテーブル
サブネットを削除しているのがポイントです。
AWSTemplateFormatVersion: "2010-09-09"
Description: Network
Parameters:
Prefix:
Description: Enter Prefix.
Type: String
Default: sample
Resources:
#---------------------------------------------------#
# VPC
#---------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub ${Prefix}-vpc
#---------------------------------------------------#
# RouteTable
#---------------------------------------------------#
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-rt
ではこのテンプレートをCloudShellにアップロードして、デプロイしちゃいます。
aws cloudformation deploy \
--template-file Destructive-Network.yaml \
--stack-name sample-network-stack
1時間程度経過した後にコンソール画面を確認すると、サブネットの依存関係の問題で数回DELETE_FAILEDとなり、最終的にエラー文は出力されていはいるもののUPDATE_COMPLETEステータスとなっています。
# DELETE_FAILED時のエラー文
Resource handler returned message: "The subnet 'subnet-0dxxxxxxxxxxxxx25' has dependencies and cannot be deleted.
# UPDATE_COMPLETE時の文
Update successful. One or more resources could not be deleted.
ここで、ネットワークスタック管理下にあったサブネットの状況を確認します。
# 1. ネットワークスタックの管理下にあるリソースを確認
aws cloudformation describe-stack-resources \
--stack-name sample-network-stack \
--query "join(', ', StackResources[*].LogicalResourceId)" --output text \
--output text
# 2. Nameを指定して、既存のサブネットから対象のサブネットの情報を表示
aws ec2 describe-subnets \
--query "Subnets[?Tags[?Key=='Name' && Value=='sample-private-subnet-1a']]"
1の実行結果は以下です。
サブネットはスタック管理下にはいないことが伺えます。
また一方で、2の結果からはサブネットは削除されず環境上に存在しており、たんにスタックから外れているだけだと推測できます。
この状況ですと、今後の運用でNetwork.yamlテンプレート内でサブネットのID等を利用したい場合には値をべた書きすることになります。仮にこのテンプレートを1環境だけで利用する場合はこれでも構いませんが、複数環境で利用している場合には!Ref等で値を渡せないのはかなり厳しいです。
そのため、再度サブネットをスタック管理下に引き戻し、元の状態へ戻す必要がでてくるはずです。
本題:サブネットをスタック管理下に引き戻す
サブネットをスタック管理下に引き戻すためには、既存スタック(sample-network-stack)に対象のサブネットをインポートします。
既存スタックへのインポート機能の詳細については、以下の公式ドキュメントを参照ください。
スタックへの既存リソースのインポート – AWS CloudFormation (amazon.com)
では、順を追ってインポートを実行していきましょう。
1. インポート時の設定ファイルを作成する
インポートコマンド実行時に必要になる設定ファイルを組み立てていきます。
[
{
"ResourceType": "AWS::EC2::Subnet",
"LogicalResourceId": "PrivateSubnet1A",
"ResourceIdentifier": {
"SubnetId":"subnet-0dxxxxxxxxxxxxx25"
}
}
]
各種キーについて簡単に説明します。
- ResourceType
- インポート対象のリソースタイプを指定する
- LogicalResourceId
- インポート時に読み込ませるCloudFormationテンプレート上で、インポート対象のリソースに割り当てる論理IDを指定する
- この記事では最終的に元のNetwork.yamlで上書きするため、そこで使用していた論理IDを使っています
- ResourceIdentifier
- インポート対象リソース識別子を指定する
また、それぞれのキーに何を指定すればよいか(ここではSubnetId)は、get-tempate-summeryコマンドで大まかに調査可能です。
aws cloudformation get-template-summary \
--template-body file://./Network.yaml \
--query 'ResourceIdentifierSummaries'
2. インポート用のCloudFormationテンプレートを作成する
ここでは、現在デプロイされているCloudFormationテンプレートにインポート対象とするサブネットを追記したものを作成します。
次の2点に注意してください。
- 既存リソースの設定値は変更しない
- インポート対象のリソースには、DeletionPolicyとUpdateReplacePolicyを追記する
※その他の削除済みリソースは後ほど再作成します。
AWSTemplateFormatVersion: "2010-09-09"
Description: Network
Parameters:
Prefix:
Description: Enter Prefix.
Type: String
Default: sample
Resources:
#---------------------------------------------------#
# VPC
#---------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub ${Prefix}-vpc
#---------------------------------------------------#
# RouteTable
#---------------------------------------------------#
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-rt
#---------------------------------------------------#
# Subnet1A
#---------------------------------------------------#
PrivateSubnet1A:
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.16.0/20
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-subnet-1a
3. インポート実行
インポートは次の3ステップで実行していきます。
- 変更セットの作成 (CLIコマンド:create-change-set)
- 変更セットの確認 (CLIコマンド:describe-change-set)
- 変更セットの実行 (CLIコマンド:execute-change-set)
Import.jsonとImport-Network.yamlをCloudShellにアップロードし、コマンドを実行します。
# 1. 変更セットの作成
aws cloudformation create-change-set \
--stack-name sample-network-stack --change-set-name sample-network-change-set \
--change-set-type IMPORT \
--resources-to-import file://Import.json \
--template-body file://Import-Network.yaml
# 2. 変更セットの確認
aws cloudformation describe-change-set \
--stack-name sample-network-stack \
--change-set-name sample-network-change-set
# 3. 変更セットの実行
aws cloudformation execute-change-set \
--stack-name sample-network-stack \
--change-set-name sample-network-change-set
サブネットが再びスタック管理下となったことを確認します。
# ネットワークスタックの管理下にあるリソースを確認
aws cloudformation describe-stack-resources \
--stack-name sample-network-stack \
--query "join(', ', StackResources[*].LogicalResourceId)" --output text \
--output text
無事に成功しました。
4. 削除してしまったその他のリソースを再作成する
ここまでくればあと一息です。
最初のスタックではVPCエンドポイントなども管理していましたが、Destructive-Network.yamlで上書きした際にこれらのリソースは削除されてしまいました。
ですので、最後の手順として元のNetwork.yamlでデプロイして元の状態に戻しましょう。
# 1. ネットワークスタックを作成
aws cloudformation deploy \
--template-file Network.yaml \
--stack-name sample-network-stack
# 2. ネットワークスタックの管理下にあるリソースを確認
aws cloudformation describe-stack-resources \
--stack-name sample-network-stack \
--query "join(', ', StackResources[*].LogicalResourceId)" --output text \
--output text
5. 復旧を確認
最後にLambda関数を実行して、ネットワークの疎通に問題がないことを確認して終わりとします。
# 1. Lambda関数を実行
aws lambda invoke \
--function-name sample-function \
outputfile.txt
# 2. S3バケット内のオブジェクト数を確認
# → 「Total Objects: 2」と表示される
aws s3 ls s3://sample-check-network-status-bucket\
--recursive \
--summarize
おまけ + 参考文献
おまけ
VPC Lambdaを含むスタックを削除しようとすると、ENIが引っかかって削除に時間がかかることがあります。(私の環境では25分ほどかかりました。)
そんなときのTipsとして、先にCLI操作によってLambdaの設定からVPC設定を外してしまいましょう。
# 1. S3バケットを空にする
aws s3 rm s3://sample-check-network-status-bucket --recursive
# 2. Lambda関数からVPC設定を外す
aws lambda update-function-configuration \
--function-name sample-function \
--vpc-config SubnetIds=[],SecurityGroupIds=[]
# 3. Lambdaスタックを削除
aws cloudformation delete-stack \
--stack-name sample-lambda-stack
# 4. ネットワークスタックを削除(※依存関係があるため、Lambdaスタックが削除されたことを確認してから実行)
aws cloudformation delete-stack \
--stack-name sample-network-stack
以上です。
参考文献
- スタックへの既存リソースのインポート – AWS CloudFormation (amazon.com)
- 公式ドキュメント
- CloudFormationのresource importを試してみた #AWS – Qiita
- スタックへのインポートについて紹介しているQiita記事
コメント