はじめに
本記事ではAWS VPN Client のコスト削減策を紹介します。Amazon EventBridge Scheduler と AWS Step Functions を利用して、クライアントVPNエンドポイントのサブネットの紐づけ・解除を自動化し、利用していない時間のコストを削減します。
AWS Client VPNの料金
AWS VPN Clientの料金は、「AWS Client VPN エンドポイントアソシエーション」 「AWS Client VPN 接続」の2つが発生します。
「AWS Client VPN エンドポイントアソシエーション」 は、VPNエンドポイントにサブネットを紐づけている時間に対して発生する料金です。Client VPNを2つのサブネットに24時間紐づけている場合、2 × 24時間 × 0.15\$ = 7.2\$ の料金が発生します。月額では 216\$ 程度の料金が発生します。
「AWS Client VPN 接続」はクライアントからの接続に対して発生する料金です。10個のクライアントがそれぞれ8時間接続する場合、10 × 8時間 × 0.05\$ = 4\$ の料金が発生します。
今回紹介する方法は主に「AWS Client VPN エンドポイントアソシエーション」の料金を削減するものとなりますが、「AWS Client VPN 接続」の削減にも効果があります。VPNエンドポイントにサブネットとの紐づけがないときは、接続が強制的に切断されるため、クライアントの切り忘れなどによる無駄なコストを削減することができます。
最新の料金は公式のドキュメントをご確認ください。
1.サブネットとVPNエンドポイントの紐づけを解除する
この部分はLambdaで実装する方法もありますが、今回は Step Functions を採用しました。Step Functionsを使えばGUIで直感的に構築できるだけでなく、ランタイムなどを気にする必要がありません。
1-1. ステートマシーンの作成
作成するステートマシーンには以下の処理を実行させます
- ClientVPNの詳細からAssociationIdを取得する
- AssociationIdを指定して紐づけを解除する
ステートマシーンは以下のようになります。
引数にはそれぞれ以下を設定します。
# DescribeClientVpnTargetNetworks
{
"ClientVpnEndpointId": "<クライアントVPNエンドポイントのID>"
}
# DisassociateClientVpnTargetNetwork
{
"AssociationId.$": "$.ClientVpnTargetNetworks[0].AssociationId",
"ClientVpnEndpointId": "<クライアントVPNエンドポイントのID>"
}
今回は単一のサブネットで運用する場合の実装となりますが、複数のサブネットを紐づけている場合は、"disassociateClientVpnTargetNetwork"の呼び出しをParallelで分岐させるような工夫が必要になります。
ステートマシーン作成時にロールが自動作成されますが、現在時点で DescribeClientVpnTargetNetworks
と DisassociateClientVpnTargetNetwork
のアクションが許可されません。ポリシーに手動で追加する必要があります。
ポリシーの参考
{
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"ec2:describeClientVpnTargetNetworks",
"ec2:disassociateClientVpnTargetNetwork"
],
"Resource" : "<クライアントVPNエンドポイントのARN>"
}
]
}
1-2. スケジューラの設定
EventBridge Scheduler を使います。ターゲットの選択で実行するステートマシーンを選択します。
起動時間は任意に設定してください。
2.サブネットとVPNエンドポイントを紐づける
2-1. スケジューラの設定
こちらはEventBridge Schedulerの設定のみです。ターゲットの選択で associateClientVpnTargetNetwork
のAPIの引数に、「VPNエンドポイント」と「紐づけるサブネット」を設定します。
起動時間は任意に設定してください。
紐づけには時間がかかるので、バッチの起動時間には余裕をもたせましょう。(目安15分)
まとめ
今回はAWS VPN Clientのコスト削減策を紹介しました。EventBridge Scheduler と Step Functions のみで簡単に設定できるので、ぜひ試してみてください。
Terraformのコードを記載しているので参考までにご活用ください。
1.サブネットとVPNエンドポイントの紐づけを解除する
# ステートマシーン
resource "aws_sfn_state_machine" "state_machine" {
name = "disassociate-state-machine"
role_arn = aws_iam_role.state_machine_role.arn
definition = <<EOF
{
"Comment": "For disassociate client vpn",
"StartAt": "DescribeClientVpnTargetNetworks",
"States": {
"DescribeClientVpnTargetNetworks": {
"Type": "Task",
"Parameters": {
"ClientVpnEndpointId": "<クライアントVPNエンドポイントのID>"
},
"Resource": "arn:aws:states:::aws-sdk:ec2:describeClientVpnTargetNetworks",
"Next": "DisassociateClientVpnTargetNetwork"
},
"DisassociateClientVpnTargetNetwork": {
"Type": "Task",
"Parameters": {
"AssociationId.$": "$.ClientVpnTargetNetworks[0].AssociationId",
"ClientVpnEndpointId": "<クライアントVPNエンドポイントのID>"
},
"Resource": "arn:aws:states:::aws-sdk:ec2:disassociateClientVpnTargetNetwork",
"End": true
}
}
}
EOF
}
# ステートマシーンに付与するロール
resource "aws_iam_role" "state_machine_role" {
name = "role-for-state-machine"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "states.amazonaws.com",
},
},
],
})
}
resource "aws_iam_role_policy_attachment" "state_machine_role" {
role = aws_iam_role.state_machine_role.name
policy_arn = aws_iam_policy.state_machine_role.arn
}
# ステートマシーンに付与するロールのポリシー
resource "aws_iam_policy" "state_machine_role" {
name = "role-for-state-machine"
policy = jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"ec2:describeClientVpnTargetNetworks",
"ec2:disassociateClientVpnTargetNetwork"
],
"Resource" : "<クライアントVPNエンドポイントのARN>"
}
]
}
)
}
# ステートマシーンを呼び出す Scheduler
resource "aws_scheduler_schedule" "disassociate_client_vpn" {
name = "scheduler-disassociate-client-vpn"
group_name = "default"
flexible_time_window {
mode = "OFF"
}
# 平日の19時に起動
schedule_expression = "cron(00 19 ? * MON-FRI *)"
schedule_expression_timezone = "Asia/Tokyo"
target {
arn = "arn:aws:scheduler:::aws-sdk:sfn:startExecution"
role_arn = aws_iam_role.scheduler.arn
input = jsonencode({
StateMachineArn = "<ステートマシーンのARN>"
})
retry_policy {
maximum_event_age_in_seconds = 600
maximum_retry_attempts = 5
}
}
}
# Schedulerに付与するロール
resource "aws_iam_role" "disassociate_scheduler_role" {
name = "role-for-disassociate-scheduler"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
"Service" : "scheduler.amazonaws.com"
},
},
],
})
}
resource "aws_iam_role_policy_attachment" "disassociate_scheduler_role" {
role = aws_iam_role.disassociate_scheduler_role.name
policy_arn = aws_iam_policy.disassociate_scheduler_role.arn
}
# Schedulerに付与するロールのポリシー
resource "aws_iam_policy" "disassociate_scheduler_role" {
name = "policy-for-disassociate-scheduler"
policy = jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"states:StartExecution"
],
"Resource" : "<ステートマシーンのARN>"
}
]
}
)
}
2.サブネットとVPNエンドポイントを紐づける
# 紐づけする Scheduler
resource "aws_scheduler_schedule" "associate_client_vpn" {
name = "scheduler-associate-client-vpn"
group_name = "default"
flexible_time_window {
mode = "OFF"
}
# 平日の9時に起動
# 紐づけに時間がかかるため、余裕を持った時間で設定することを推奨
schedule_expression = "cron(00 9 ? * MON-FRI *)"
schedule_expression_timezone = "Asia/Tokyo"
target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:associateClientVpnTargetNetwork"
role_arn = aws_iam_role.associate_scheduler_role.arn
input = jsonencode({
ClientVpnEndpointId = "<クライアントVPNエンドポイントのID>"
SubnetId = "<サブネットID>"
})
retry_policy {
maximum_event_age_in_seconds = 600
maximum_retry_attempts = 5
}
}
}
# Schedulerに付与するロール
resource "aws_iam_role" "associate_scheduler_role" {
name = "role-for-associate-scheduler"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
"Service" : "scheduler.amazonaws.com"
},
},
],
})
}
resource "aws_iam_role_policy_attachment" "associate_scheduler_role" {
role = aws_iam_role.associate_scheduler_role.name
policy_arn = aws_iam_policy.associate_scheduler_role.arn
}
# Schedulerに付与するロールのポリシー
resource "aws_iam_policy" "associate_scheduler_role" {
name = "policy-for-associate-scheduler"
policy = jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"ec2:AssociateClientVpnTargetNetwork"
],
"Resource" : [
"<クライアントVPNエンドポイントのARN>",
"<紐づけるサブネットのARN>"
]
}
]
}
)
}