【AWS Client VPN】コスト削減!一定時間で強制切断させてみた

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

こんにちは、ディベロップメントサービス1課の山本です。
最近ハマっている飲み物はセブンイレブンさんから出てる『濃いライチサイダー』です。
ソルティライチに炭酸を付与したような飲み心地でとても美味しいです。

今回はAWS Client VPN のコスト削減方法についてご紹介します。
よく使われますが地味にコストのかかるサービスですので、料金を少しでも安くしたい方必見です。

この記事の対象者は?

  • AWS Client VPN を利用されている方
  • よく VPN を繋いだまま放置している方

AWS Client VPN の料金

東京リージョンでの料金は以下の通りです。

No. 項目 料金
AWS Client VPN エンドポイントアソシエーション USD 0.15/時間
AWS Client VPN 接続 USD 0.05/時間

1の料金は AWS Client VPN に関連づけられているサブネットの数に対して、
2の料金は アクティブなユーザーの数だけそれぞれ発生します。

ここでクイズです。
以下の場合、月額何円になるでしょうか

  • 前提条件(1USD = 160円)
    • 割り当てられているサブネット数:2
    • 1日でサブネットに割り当てられている時間:24時間 (常時)
    • アクティブなユーザー数:10
    • 1日でアクティブな時間:12時間

10人での利用なので、いっても1万円程度ですかね?

答えです。

  • AWS Client VPN エンドポイントアソシエーション
    • 0.15[USD/時間] × 24[時間] × 2[サブネット数] × 30[日] × 160[円/USD] = 34,560[円]
  • AWS Client VPN 接続
    • 0.05[USD/時間] × 12[時間] × 10[ユーザー数] × 30[日] × 160[円/USD] = 28,800[円]
  • 合計
    • 34,560[円] + 28,800[円] = 63,360[円]

月額63,360円は意外に高い。
128 MBの AWS Lambda でしたら、起動時間 1 秒で 月に2億回起動できます。

※ 上記の内容は、2024年6月27日時点のため正式料金は以下の公式ページを参考ください。 aws.amazon.com

進め方

今回はユーザーがアクティブな時間を減らす方法で、コスト削減を実現します。
必要があって繋いだけれども、そのまま放置している人たちを強制遮断してみます。

サブネットが割り当てられている時間を減らす方法は、弊社過去ブログにて紹介しておりますのでそちらをご参考ください。

blog.serverworks.co.jp

コンソールから強制切断してみた

クライアント VPN エンドポイント > 接続タブ からアクティブなユーザーを強制切断することが可能です。
まずは、長時間接続しているユーザーを手動で強制切断してみます。

AWS Client VPN 接続タブ

接続を終了

これで無事にVPNから切断できました。
と思ったら?!

VPN接続状態

AWS VPN Client 側が一時的な通信不良と思ったのか、すぐに再接続されました。
これでは意味がないですね。

対策

強制切断から一定期間(1分程度)の接続禁止時間を設けることで対策してみます。

以下のように定期監視VPN接続時に特定の処理をさせて実現します。

  • 定期監視
    • 1時間に1回、長時間接続ユーザーの有無を監視
    • 長時間接続ユーザーをブラックリストに登録
    • 長時間接続ユーザーを強制切断
  • VPN接続時
    • ユーザーがブラックリストに登録されている場合、接続を拒否する

構成図

AWS Lambda と DynamoDB を組み合わせて、実現します。

構成図

DynamoDB(ユーザー接続管理)

ユーザーはデバイスを一意に識別する common_name を利用します。
禁止期間が終わったらデータを削除したいので、禁止解除までの時間 で TTL を設定します。

キー名 説明 備考
common_name AWS Client VPN での接続名 パーティションキー に割り当て
expired_at 禁止解除までの時間 TTL に設定

AWS Lambda (接続時間の監視)

以下のコードで作成します。
イベント元を Event Bridge に設定して、1時間に1回起動させます。

import json
import boto3
import datetime
import os
  
ENDPOINT_ID = os.environ['ENDPOINT_ID']  # Client VPN エンドポイント ID
TERMINATE_TIME = int(os.environ['TERMINATE_TIME'])  # 接続を切断するまでの時間(時間)
TABLE_NAME = os.environ['TABLE_NAME']  # DynamoDB テーブル名
TTL = int(os.environ['TTL'])  # 禁止期間(秒)1分程度で設定
  
  
def lambda_handler(event, context):
    client = boto3.client('ec2')
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(TABLE_NAME)
  
    # クライアント VPN の接続情報を取得
    response = client.describe_client_vpn_connections(
        ClientVpnEndpointId=ENDPOINT_ID
    )
  
    for connection in response['Connections']:
        # 接続がアクティブかどうかを判定
        if connection["Status"]["Code"] == "active":
            connect_time = datetime.datetime.strptime(connection['ConnectionEstablishedTime'], "%Y-%m-%d %H:%M:%S")
            diff_time = datetime.datetime.now() - connect_time
            # 接続が一定時間経過している場合は接続を切断
            if diff_time > datetime.timedelta(hours=TERMINATE_TIME):
                common_name = connection["CommonName"]
  
                # DynamoDBに接続情報を登録
                table.put_item(
                    Item={
                        'common_name': common_name,
                        'expired_at': int(datetime.datetime.now().timestamp()) + TTL
                    }
                )
  
                # 接続を切断
                client.terminate_client_vpn_connections(
                    ClientVpnEndpointId=ENDPOINT_ID,
                    ConnectionId=connection['ConnectionId']
                )
  
    return {
        'statusCode': 200,
        'body': json.dumps('Success')
    }

AWS Lambda (クライアント接続ハンドラー)

AWS Client VPN には接続承認を管理する AWS Lambda ハンドラーを設定できます。
接続承認 - AWS クライアント VPN

入力(event)

{
    "connection-id": <connection ID>,
    "endpoint-id": <client VPN endpoint ID>,
    "common-name": <cert-common-name>,
    "username": <user identifier>,
    "platform": <OS platform>,
    "platform-version": <OS version>,
    "public-ip": <public IP address>,
    "client-openvpn-version": <client OpenVPN version>,
    "aws-client-version": <AWS client version>,
    "groups": <group identifier>,
    "schema-version": "v3"
}

出力(response)

{
    "allow": boolean,
    "error-msg-on-denied-connection": "",
    "posture-compliance-statuses": [],
    "schema-version": "v3"
}

入力データからcommon-name を取得し、ブラックリストへの登録有無を確認します。
ブラックリスト登録時は出力データのalllow を Falseに設定。
ブラックリスト未登録時は出力データのalllow を Trueに設定して返却します。

以下のコードで作成します。

import boto3
import datetime
import os
  
ENDPOINT_ID = os.environ['ENDPOINT_ID']
TABLE_NAME = os.environ['TABLE_NAME']
  
  
# Lambda VPN クライアントハンドラー用の関数
def lambda_handler(event, context):
    # リクエストデータから接続情報を取得
    common_name = event["common-name"]
  
    # DynamoDB へ接続
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(TABLE_NAME)
  
    try:
        response = table.get_item(
            Key={
                'common_name': common_name
            }
        )
        expired_at = response['Item']["expired_at"]
        # 禁止期間が切れているかどうかを判定
        if int(datetime.datetime.now().timestamp()) < expired_at:
            is_allowed = False
        else:
            is_allowed = True
  
    except Exception:
        # DynamoDBに登録されていない場合は接続を許可
        is_allowed = True
  
    # レスポンスデータを作成
    response_data = {
        "allow": is_allowed,
        "error-msg-on-denied-connection": "",
        "posture-compliance-statuses": [],
        "schema-version": "v3"
    }
  
    # レスポンスデータを返却
    return response_data

注意点

Lambda 関数の名前は、 AWSClientVPN- プレフィックスで始まる必要があります。

要件と考慮事項
クライアント接続ハンドラーの要件と考慮事項を次に示します。
Lambda 関数の名前は、 AWSClientVPN- プレフィックスで始まる必要があります。

確認

無事、接続エラーとなり強制切断することに成功しました。

強制切断_成功

まとめ

  • コンソール上からAWS Client VPN接続の強制切断は可能だが、クライアント側ですぐ再接続される
  • 接続承認を管理する AWS Lambda ハンドラー を設定することで、ユーザーの接続可否は管理可能

さいごに

賢いクライアントソフトに対抗するため、頑張りました。
本ブログがどなかたのお役に立てれば幸いです。

山本 真大(執筆記事の一覧)

アプリケーションサービス部 ディベロップメントサービス1課

2023年8月入社。カピバラさんが好き。