AWS VPNエンドポイントの関連付けを作成・解除するPythonスクリプト

AWSのVPNエンドポイントって、知らない間にお金かかりますね。知らずに放っておいたら、高額の請求が来てギャフンとなりました。何か対策を打たなくてはなりません。使わない時は、VPNエンドポイントとサブネットとの関連付けを解除、使う時だけ作成すればよいのですが、それをAWSのコンソールにいちいち入って、作成・解除するのは面倒です。そこで、以下の記事を参考に、
[AWS Client VPN節約術！～AWS Lambdaで関連付け自動化～](https://cantabile.alhinc.jp/technology/4824/)
そして、Anthlopic Clude3 Sonnetの力も借りながらPythonスクリプトを作ってみました。なお使う前に
```
pip install boto3
```
をお忘れなく。

::: note info
> AWS Client VPN では、アクティブなクライアント接続の数に対して、および Client VPN に関連付けられているサブネットの数に対してそれぞれ 1 時間単位の料金が発生します。
> [料金 - AWS VPN | AWS](https://aws.amazon.com/jp/vpn/pricing/)
とあるように、関連付けられているサブネットの数に対して時間単位で課金され、OpenVPN等で接続している間も課金されるわけです。使っていない間は、VPNエンドポイントとサブネットの関連付けを解除することで料金を節約できます。
:::

# VPNエンドポイント関連付け作成
こんな感じのスクリプトが出来上がりました。スクリプト内に、クライエントVPNエンドポイントIDとサブネットIDを記述する部分があります。AWSコンソールでIDを調べておきましょう。なお、AWSをターミナルから制御する際に権限が必要になりますので、それを設定する必要があります(後述)。
```Python:VPN_Start.py
#
#　VPNエンドポイントとサブネット関連付けを設定する
#

import boto3
import time

client = boto3.client(
    'ec2',
    region_name='ap-northeast-3',
    #本当は直接アクセスキーを記述しない方が良いのですが
    aws_access_key_id='xxxxxxxxxxxxxxxxx', #AWSアクセスキーを記述
    aws_secret_access_key='YYYYYYYYYYYYYYYYY' #AWSシークレットアクセスキーを記述
)

def wait_for_association(vpn_endpoint_id, association_id):
    while True:
        response = client.describe_client_vpn_target_networks(
            ClientVpnEndpointId=vpn_endpoint_id,
            AssociationIds=[association_id]
        )
        state = response['ClientVpnTargetNetworks'][0]['Status']['Code']
        if (state == 'available') or (state == 'associated') :
            print("Association is now available")
            break
        elif state == 'pending-associate':
            print("Association is still pending, waiting...")
            time.sleep(30)  # wait for 30 seconds before checking again
        elif state == 'associating':
            print("Associating now, waiting...")
            time.sleep(30)  # wait for 30 seconds before checking again
        else:
            raise Exception(f"Unexpected state: {state}")

clientVpnEndpointId = 'cvpn-xxxxxxxxxxxxxxxx' #クライアントVPNエンドポイントのIDを記述
subnetId = 'subnet-xxxxxxxxxxxxxxx' #関連付けしたいサブネットIDを記述

response = client.associate_client_vpn_target_network(
    ClientVpnEndpointId = clientVpnEndpointId, 
    SubnetId = subnetId
)
association_id = response['AssociationId']
wait_for_association(clientVpnEndpointId,association_id)
client.close()
```
# VPNエンドポイント関連付け解除
同じく、今度は解除する時には知らせるスクリプトです。スクリプト内に、クライエントVPNエンドポイントIDとサブネットIDを記述する部分があります。また例によって、AWSをターミナルから制御する際に権限が必要になりますので、それを設定する必要があります。
```python:VPN_End.py
#
#　VPNエンドポイントとサブネットの関連付けを解除する
#

import boto3

client = boto3.client(
    'ec2',
    region_name='ap-northeast-3',
    #本当は直接アクセスキーを記述しない方がいいのですが
    aws_access_key_id='xxxxxxxxxxxxxxxxx',#AWSアクセスキーを記述
    aws_secret_access_key='YYYYYYYYYYYYYYYYYYYY'#AWSシークレットアクセスキーを記述
)

clientVPNEndpointID = 'cvpn-endpoint-xxxxxxxxxxxx' #ここにエンドポイントIDを記述

ClientVpnData = client.describe_client_vpn_target_networks(
    ClientVpnEndpointId = clientVPNEndpointID
)

AssociationId = ClientVpnData['ClientVpnTargetNetworks'][0]['AssociationId']

client.disassociate_client_vpn_target_network(
    ClientVpnEndpointId = clientVPNEndpointID,
    AssociationId = AssociationId
)
```
# IAMで必要な権限を設定
(1) まず、IAM(Identity and Access Management)の`IAM>ユーザー`にてVPNのエンドポイントを操作する専用のアカウントを作っておきます。今回は`VPN_Controller`という名前にしておきました。
(2) 次に、アクセスキーを作成し、アクセスキーとシークレットアクセスキーを記録しておきます。
(3) `VPN_Controller`にアタッチする専用のポリシーを作ります。今回は`VPN_Control`という名前にしました。以下のJSONを丸々貼り付ける様にすれば大丈夫なはずです。
```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:AssociateClientVpnTargetNetwork",
                "ec2:DisassociateClientVpnTargetNetwork",
                "ec2:DescribeClientVpnTargetNetworks",
                "ec2:DescribeSubnets",
                "ec2:DescribeVpcs"
            ],
            "Resource": "*"
        }
    ]
}
```
::: note info
権限については以下の通りです。（参照[「Amazon のアクション、リソース、および条件キー EC2」](https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonec2.html)）
AssociateClientVpnTargetNetwork:ターゲットネットワークをクライアントVPNエンドポイントに関連付けるアクセス許可を付与します。
DisassociateClientVpnTargetNetwork:クライアントVPNエンドポイントからターゲットネットワークの関連付けを解除する許可を付与します。
DescribeClientVpnTargetNetworks:クライアントVPNエンドポイントに関連付けられているターゲットネットワークを記述するアクセス許可を付与します。
DescribeSubnets:1 つ以上のサブネットを記述する許可を付与します。
DescribeVpcs:1 つ以上の VPC を記述するアクセス許可を付与します。
:::
(4) ユーザー`VPN_Controller`に許可ポリシー`VPN_Control`をアタッチします。
(5) アクセスキー、シークレットアクセスキーを設定します。

# 使ってみる
`VPN_Start.py`は実際にVPNエンドポイントが使えるようになるまで、`Associating now, waiting...`と出力しながら待ってくれます。
```bash
$ python VPN_Start.py
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Associating now, waiting...
Association is now available
```
`VPN_End.py`の方は、実行したらすぐに終了するスクリプトです。

(2024/9/3追記)それにしても、VPNエンドポイントとサブネットとの関連付けの作成、解除は結構完了するのに時間がかかりますね。どうしてでしょう？