【Client VPN】Endpoint の Subnet 関連付けを自動で設定/解除してみよう【コスト削減ワザ】

記事タイトルとURLをコピーする

こんにちは、去年買ったアイスがまだ冷凍庫に残っています。駒井です

いつ食べれば良いのでしょうか

今回は Client VPN のコスト削減方法についてご紹介したいと思います

はじめに

Client VPN って保持しているだけで案外コストがかかって困ることありませんでしょうか?

特に複数の Endpoint を持っている場合は、維持コストだけで結構な額になっている方もいらっしゃるかもしれません

Client VPN の料金は

AWS Client VPN では、アクティブなクライアント接続の数に対して、および Client VPN に関連付けられているサブネットの数に対してそれぞれ 1 時間単位の料金が発生します。

とある通り、関連付けられている Subnet の数に対しての課金と、接続数及び時間の大きく2つの料金がかかります

使わない時間が決まっている場合は、この Subnet の関連付けを解除することでコストを抑えることができるというわけですね

今回はすでに Client VPN の設定が終わっている前提のお話となりますので、参考にされる際は実際にご利用されている環境に合わせて読み替えていただければと思います

実現方法

では、実際に 『Client VPN を使わない時間帯で Subnet の関連付けを解除』→『使う時間の前に Subnet の関連付けを実施』という流れで自動化を行っていきます

いくつか手段はあると思うのですが、今回は AWS Lambda を利用して自動化することとします

具体的には特定の時間にトリガし、かつ複数の Client VPN Endpoint に対して実行ができるように汎用的な形としたいと思います

構成

  • EventBridge にて cron による自動実行時間を設定し、これを Lambda トリガとします
    • Client VPN を利用する時間を Serverworks の営業時間の平日 10:00 ~ 19:00 と想定します
    • 実際に関連付けが完了するまでには数分間の pending 期間を要するため、10~15分程度前に設けます
  • EventBridge では、関連付けを行う Endpoint ID や、ルートテーブル(Client VPN 設定上のルートテーブル)の情報を EventBridge 側から Lambda 関数へ引き渡すことで、複数の Endpoint への命令実行を 1つの Lambda 関数から行えるように設計しています
    • 関連付け解除の Lambda 関数についても同様ですが、こちらはルートテーブル情報が関連付け解除と同時に削除されるため、Endpoint ID のみを引き渡します

Lambda の実装

先の構成の通り、Lambda は今回2つ作成します

  1. Client VPN に Subnet を関連付ける関数…①
  2. Client VPN から Subnet の関連付けを解除する関数…②

紹介するサンプルコードは執筆時点での最新のランタイム python 3.9 で動作します

必要な IAM 権限について

今回作成する 2つの Lambda からは Client VPN の Subnet の関連付け設定を変更するための権限が必要となります

また、関連付けの際にはルートテーブル情報を同時に設定できるような形とするため、ルーティング設定変更が可能な権限も付与します

よって、以下の権限が必要となります

  • ① に必要な権限
    • AssociateClientVpnTargetNetwork
      • 関連付けの実行に必要な権限
    • CreateClientVpnRoute
      • ルートテーブル設定も仕込む場合に必要な権限
  • ② に必要な権限
    • DisassociateClientVpnTargetNetwork
      • 関連付け解除の実行に必要な権限
    • DescribeClientVpnTargetNetworks
      • 関連付け ID を取得するのに必要な権限

設定方法については割愛します

事前に IAM Role を作成する場合はログ出力の必要に応じて CloudWatchLogs の権限をアタッチするのをお忘れなく

① Client VPN に Subnet を関連付ける関数

以下サンプルコードです

import boto3
import time
​
def lambda_handler(event, context):
    #Client VPN Endpoint 情報
    client_vpn_id = event['client_vpn_id']
    
    #関連付けをする Subnet 情報
    subnet_list = event['subnet_list']
    
    #ルートテーブルに登録する送信先 CIDR 情報
    destination_cidr_block_list = event['destination_cidr_block_list']
    
    client = boto3.client('ec2')
    
    for i in range(len(subnet_list)):
        #Client VPN に Subnet をアタッチ
        client.associate_client_vpn_target_network(
            ClientVpnEndpointId = client_vpn_id,
            SubnetId = subnet_list[i]
            )
        
        for j in range(len(destination_cidr_block_list)):
            #設定が入りきらない場合があるので、ウェイトをかける
            time.sleep(3)
            
            #Client VPN の Route Table 設定
            client.create_client_vpn_route(
                ClientVpnEndpointId = client_vpn_id,
                DestinationCidrBlock = destination_cidr_block_list[j],
                TargetVpcSubnetId = subnet_list[i]
                )

EventBridge から渡される event 情報に以下を含めます

  • Client VPN Endpoint ID
  • 関連付ける Subnet 情報
    • 基本的に Multi-AZ 対応を行うものと思いますが、1つでも問題はないです
  • ルートテーブルに設定する送信先 CIDR 情報
    • 関連付ける Subnet それぞれに等しく送信先 CIDR の Route を設定するサンプルとなっています
{
  "client_vpn_id": "cvpn-endpoint-xxxxxxxxxxxxxx",
  "subnet_list": [
    "subnet-xxxxxxxxxxxxxxxxxx",
    "subnet-yyyyyyyyyyyyyyyyy"
  ],
  "destination_cidr_block_list": [
    "192.168.xx.00/24",
    "192.168.yy.0/24",
    "192.168.zz.0/24"
  ]
}

② Client VPN から Subnet の関連付けを解除する関数

以下サンプルコードです

import boto3

def lambda_handler(event,context):
    #Client VPN Endpoint 情報
    client_vpn_id = event['client_vpn_id']

    client = boto3.client('ec2')
    
    #Client VPN の Subnet Association ID の取得
    client_vpn_data = client.describe_client_vpn_target_networks(
        ClientVpnEndpointId = client_vpn_id
        )
        
    for i in range(len(client_vpn_data['ClientVpnTargetNetworks'])):
        association_id = client_vpn_data['ClientVpnTargetNetworks'][i]['AssociationId']
    
        #Client VPN の Subnet のデタッチ
        client.disassociate_client_vpn_target_network(
            ClientVpnEndpointId = client_vpn_id,
            AssociationId = association_id
            )

Client VPN に関連付け(association)される度に設定値 AssociationId が変化するので、都度 Describe で取得する必要がある点に注意です

EventBridge から渡される event 情報に以下を含めます

  • Client VPN Endpoint ID
{
  "client_vpn_id": "cvpn-endpoint-xxxxxxxxxxxx"
}

テスト実行

実際の挙動について確認してみましょう

① Lambda の テストイベント の項目から、JSON 定数を保存して実行してみます(先程のサンプル JSON)

Lambda の実行が完了したら、実際に Client VPN Endpoint のコンソールにて情報を確認してみましょう

Associating として関連付けが進んでいるのが、確認できるかと思います

ルートテーブルにも Creating として設定が書き込まれている状態であることがわかります

5分ほど経つとそれぞれ、Available / Active といったステータスに変わりました 問題なく設定が入ったようです

それでは、② Lambda 側(Subnet の関連付け解除)も確認します

テストイベント の項目から、JSON 定数を保存して実行します

Lambda の実行が完了したら、こちらも再度ステータスを確認します

関連付け側は Disassociationg というステータスになっていますね

ルートテーブル側は Deleting となっています

関連付けを解除することで、自動でルートテーブルが消える挙動となっていることがわかりますね

関連付けの解除には 8分ほどかかりましたが、それぞれまっさらな状態に戻りました

EventBridge の設定

① Client VPN に Subnet を関連付ける関数へのトリガ(EventBridge Rule)

テスト実行にも問題がないことを確認したので、定期実行のための EventBridge のルール作成を行います

今回は平日(月〜金)の 09:50 に起動する cron 式としています

最後に起動する Lambda 関数の設定と、引き渡す JSON を定数として入力します

※ Lambda 関数に ClientVPN_auto_attach とありますが、作成した① Lambda 関数名に適宜置き換えてください

起動したい(Subnet の関連付けを行いたい) Client VPN Endpoint ごとに EventBridge ルールを同様に作成します

② Client VPN から Subnet の関連付けを解除する関数へのトリガ(EventBridge Rule)

こちらも停止したい時間を cron 式で設定し、トリガする Lambda 関数、引き渡す JSON 定数の設定を Endpoint ごとに行います

Subnet の関連付け解除は平日(月〜金)の 19:00 に設定します

※ Lambda 関数に ClientVPN_auto_dettach とありますが、作成した② Lambda 関数名に適宜置き換えてください

関連付けの解除には Client VPN ID 以外の情報は不要としているので、 JSON 定数も簡素です

定期実行を見届けよう

以上で、不必要な時間に Subnet の関連付けを外しておく設定ができました

よくある設定だと思うので今回は割愛しますが、私は CloudWatch Alarm にてエラー時に Chatbot 経由で Slack 通知する設定も行っておきました

みなさんも定期実行が問題なく行われているかは、なにかしらウォッチできる手段を設けておきましょう

アラーム設定記事については以下を参照ください

blog.serverworks.co.jp

みなさんの参考となりましたら幸いです

駒井 基 (記事一覧)

SRE 2課所属

ヴァンダル派です