AWS

初期構築の時だけ使用するNATゲートウェイをCFNカスタムリソースで削除する

初期構築の時だけ使用するリソース(NATゲートウェイ)をCFNカスタムリソースで削除する

例えばECSを作成する際に、Dockerhubからイメージをプルする場合、インターネットにアクセスできる必要があります。
ECSクラスターをプライベートサブネットに設置する場合、パブリックサブネットにNATゲートウェイを配置し、インターネットへのルートを用意します。
しかし例えば検証環境ですと、一度イメージのプルが完了すると、NATゲートウェイを使用する用途がなくなり、不要なリソースが動作し続けることになる場合もあります。

そこで本ページでは、CloudFormationカスタムリソースを使用することによって、使用済みのリソース(NATゲートウェイ等)を削除することを目指します。

構築する環境

Diagram of delete NAT Gateay used only during initial build with CFN Custom Resource.

FargateタイプのECSを作成し、プライベートサブネットに配置します。
Dockerhubから取得した最新のNginxイメージからコンテナを作成します。

DockerHubからNginxの公式イメージを取得するために、パブリックサブネットにNATゲートウェイを配置します。

ECSの構築が完了後は、NATゲートウェイやインターネットゲートウェイは不要となります。
これらを削除するLambda関数を作成し、これをCloudFormationカスタムリソースに関連づけます。
CloudFormationスタック作成時に、この関数を実行するように設定します。

EC2インスタンスを作成します。
コンテナにアクセスするためのクライアントして使用します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

awstut-fa/071 at main · awstut-an-r/awstut-fa
Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

テンプレートファイルのポイント解説

今回の目的はCloudFormationカスタムリソースを使って、不要なリソースを削除することです。
具体的には以下の4つのリソースを削除します。

  • NATゲートウェイ
  • インターネットゲートウェイ
  • ECS用サブネット(プライベートサブネット)に関連づけたルートテーブル
  • NATゲートウェイ用サブネット(パブリックサブネット)に関連づけたルートテーブル

カスタムリソース

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CustomResourceLambdaFunction.Arn
Code language: YAML (yaml)

特別な設定は不要です。
ServiceTokenプロパティに後述のLambda関数のARNを設定し、関数をカスタムリソースに関連づけます。

CloudFormationカスタムリソースの基本的な事項は、以下のページをご確認ください。

Lambda関数

Resources:
  CustomResourceLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          CONTAINER_ROUTE_TABLE: !Ref ContainerRouteTable
          EIP_ALLOCATION_ID: !Ref EIPAllocationId
          INTERNET_GATEWAY: !Ref IGW
          NAT_GATEWAY: !Ref NATGateway
          PUBLIC_ROUTE_TABLE: !Ref PublicRouteTable
          REGION: !Ref AWS::Region
          VPC: !Ref VPC
      Code:
        ZipFile: |
          ...
      FunctionName: !Sub "${Prefix}-custom-resource-lambda-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt CustomResourceLambdaFunctionRole.Arn
      Timeout: !Ref Timeout
Code language: YAML (yaml)

Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

Environmentプロパティで関数に渡すことができる環境変数を定義できます。
具体的には、削除対象のNATゲートウェイやインターネットゲートウェイ、2つのルートテーブルのIDです

以下が実行するコードです。

import boto3
import cfnresponse
import os
import time

container_route_table = os.environ['CONTAINER_ROUTE_TABLE']
eip_allocation_id = os.environ['EIP_ALLOCATION_ID']
internet_gateway = os.environ['INTERNET_GATEWAY']
nat_gateway = os.environ['NAT_GATEWAY']
public_route_table = os.environ['PUBLIC_ROUTE_TABLE']
region = os.environ['REGION']
vpc = os.environ['VPC']

client = boto3.client('ec2', region_name=region)

CREATE = 'Create'
response_data = {}

def lambda_handler(event, context):
  try:
    if event['RequestType'] == CREATE:
      response1 = client.delete_route(
        DestinationCidrBlock='0.0.0.0/0',
        RouteTableId=container_route_table)
      print(response1)
      
      response2 = client.delete_route(
        DestinationCidrBlock='0.0.0.0/0',
        RouteTableId=public_route_table)
      print(response2)
      
      response3 = client.delete_nat_gateway(
        NatGatewayId=nat_gateway)
      print(response3)
      
      time.sleep(60)
      
      response4 = client.release_address(
        AllocationId=eip_allocation_id)
      print(response4)
      
      response5 = client.detach_internet_gateway(
        InternetGatewayId=internet_gateway,
        VpcId=vpc)
      print(response5)
      
      response6 = client.delete_internet_gateway(
        InternetGatewayId=internet_gateway)
      print(response6)
      
    cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      
  except Exception as e:
    print(e)
    cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
Code language: YAML (yaml)

cfnresponseモジュールを使用して、関数をLambda-backedカスタムリソースとして実装します。

コードの内容をまとめますと、以下の通りです。

  1. CloudFormationテンプレートで定義した環境変数を、os.environにアクセスして取得する。
  2. Boto3でEC2用クライアントオブジェクトを作成する。
  3. delete_routeメソッドで、プライベートサブネット用ルートテーブルから、NATゲートウェイ向けのデフォルトルートを削除する。
  4. delete_routeメソッドで、パブリックサブネット用ルートテーブルから、インターネットゲートウェイ向けのデフォルトルートを削除する。
  5. delete_nat_gatewayメソッドで、NATゲートウェイを削除する。
  6. NATゲートウェイを削除してから60秒待機した後で、release_addressメソッドで、NATゲートウェイにアタッチしていたElastic IPアドレスをリリースする。
  7. detach_internet_gatewayメソッドで、インターネットゲートウェイをVPCからデタッチする。
  8. delete_internet_gatewayメソッドで、インターネットゲートウェイを削除する。

項番6で60秒待機する理由ですが、NATゲートウェイの削除が完了を待つための処理です。
削除に十分な時間待機しない場合、以下のエラーが発生しますのでご注意ください。

An error occurred (AuthFailure) when calling the ReleaseAddress operation: You do not have permission to access the specified resource.

Elastic IPアドレスをリリースする理由ですが、VPCとの関連付けを完全に解消するためです。

VPC に関連付けられたパブリック IP アドレスまたは Elastic IP アドレスを持つリソースがある場合、インターネットゲートウェイをデタッチすることはできません。

インターネットゲートウェイを使用してインターネットに接続する

Lambda関数用IAMロール

Resources:
  CustomResourceLambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: DeleteTemporaryResourcePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DeleteInternetGateway
                  - ec2:DetachInternetGateway
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:internet-gateway/${IGW}"
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:vpc/${VPC}"
              - Effect: Allow
                Action:
                  - ec2:DeleteRoute
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:route-table/${PublicRouteTable}"
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:route-table/${ContainerRouteTable}"
              - Effect: Allow
                Action:
                  - ec2:DeleteNatGateway
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:natgateway/${NATGateway}"
              - Effect: Allow
                Action:
                  - ec2:ReleaseAddress
                Resource: "*"
                #Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:eip/${EIPAllocationId}"
Code language: YAML (yaml)

AWS管理ポリシーであるAWSLambdaVPCAccessExecutionRoleに加えて、4つのリソースを削除・デタッチするための権限を付与します。

Elastic IPアドレスをリリースための権限設定にご注意ください。
Resourceプロパティに指定する値は「*」です。
仮にこのプロパティにElastic IPアドレスのARNを指定したとしても、権限エラーが発生しますのでご注意ください。

(参考)タスク定義

Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: !Sub "${Prefix}-container"
          Image: nginx:latest
      Cpu: !Ref TaskCpu
      ExecutionRoleArn: !Ref FargateTaskExecutionRole
      Memory: !Ref TaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      TaskRoleArn: !Ref TaskRole
Code language: YAML (yaml)

Imageプロパティに「nginx:latest」を指定して、Dockerhubから最新版のNginxのイメージからコンテナを作成するように設定します。
これでNATゲートウェイ経由でインターネットにアクセスし、Dockerhubからイメージをプルする動作を発生させます。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • ECSクラスター:fa-071-cluster
  • ECSサービス:fa-071-service
  • EC2インスタンス:i-090a6eb7047448e04
  • NATゲートウェイ:nat-0cbbcb99b52367f68
  • インターネットゲートウェイ:igw-079babd830354ac3a
  • Elastic IPアドレス:54.248.104.49
  • プライベートサブネットに関連づけたルートテーブル:rtb-06e6f97bd2f575051
  • パブリックサブネットに関連づけたルートテーブル:rtb-039ff9202847ec4a5

作成されたリソースをAWS Management Consoleから確認します。
まずCloudFormationスタックを確認します。

Detail of CloudFormation Stacks 1.

削除対象のリソースが確かに作成されていることがわかります。

次にCloudFormationカスタムリソースの作成状況を確認します。

Detail of CloudFormation Stacks 2.

こちらも確かに作成されていることがわかります。
つまり一度作成されたNATゲートウェイ等が、このカスタムリソースによって削除されたということになります。

ECSタスクを確認します。

Detail of ECS 1.

作成されたタスクに付与されたプライベートアドレスが「10.0.3.171」ということがわかります。

動作確認

準備が整いましたので、状況を確認します。

リソースの状況確認

まずCloudFormationカスタムリソースに関連づけられたLambda関数の実行ログを確認します。

Detail of CloudFormation Stacks 3.

確かに各種リソース削除が正常に実行されたことがわかります。

続いて2つのルートテーブルのルーティング情報を確認します。

Result of CloudFormation Custom Resource 1.
Result of CloudFormation Custom Resource 2.

確かに2つのルートテーブルからデフォルトルートが削除されています。

次にNATゲートウェイの状況を確認します。

Result of CloudFormation Custom Resource 3.

Statusが「Deleted」となっています。
確かにNATゲートウェイが削除されていることがわかります。

NATゲートウェイにアタッチしていたElastic IPアドレスを確認します。

Result of CloudFormation Custom Resource 4.

こちらもリリース済みであることが確認できます。

最後にインターネットゲートウェイの状況を確認します。

Result of CloudFormation Custom Resource 5.

既に削除済みであることがわかります。

コンテナにアクセス

EC2インスタンスにアクセスし、コンテナの動作を確認します。

EC2インスタンスへのアクセスはSSM Session Managerを使用します。

% aws ssm start-session --target i-090a6eb7047448e04

Starting session with SessionId: root-03345556116251505
sh-4.2$
Code language: Bash (bash)

SSM Session Managerの詳細につきましては、以下のページをご確認ください。

curlコマンドを使って、タスク内のコンテナにアクセスします。

sh-4.2$ curl http://10.0.3.171
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
Code language: Bash (bash)

正常にアクセスすることができました。
確かにNginxコンテナが動作しています。

つまりECSの初期構築時は、NATゲートウェイを通じてインターネットにアクセスして、DockerhubからNginxイメージを取得したということです。
そして構築が完了後にCloudFormationカスタムリソースによって、NATゲートウェイ等が削除されたということです。

まとめ

CloudFormationカスタムリソースを使用することによって、使用済みのリソース(NATゲートウェイ等)を削除する方法を確認しました。

タイトルとURLをコピーしました