Saving technique! ? I tried starting and stopping AWS Client VPN with Lambda

**This article is an automatic translation of the article[a07ff10533cfe55b7936] below.
https://qiita.com/speaktech/items/a07ff10533cfe55b7936**

# What I did
Made AWS Client VPN start and stop on Lambda at a fixed time.

AWS Client VPN charges are incurred by associating a subnet with a Client VPN (end point), as quoted below.
If association is left, charges will be generated even during non-use hours such as night time, so subnet association is performed only during use time.

> In the AWS Client VPN, for the number of active client connections,
There is an hourly charge for each of the> and the number of subnets associated with the Client VPN.
>
> Quoted source: [AWS Client VPN Fee] (https://aws.amazon.com/jp/vpn/pricing/#AWS%20Client%20VPN%20%E%3%81%E%E%6%96%99%E9 % 87% 91)

# Assumption
-Since subnet connection does not result in VPN connection being broken and there is no charge, we define"Subnet association = Start AWS Client VPN"and"Subnet association = Stop AWS Client VPN".

-The procedure for setting up AWS Client VPN is omitted because it is based on [this article (AWS Client VPN configuration memo)] (https://qiita.com/hmatsu47/items/ce0cc8b7f5747dbb3190).
(The configuration procedure by the AWS CLI is summarized. Thank you very much for your help.)

# Environment
-** Client PC **

 -Windows 10 version 1803 (OS build 17134.829)
 -PowerShell version 5.1.17134.765
 -AWS CLI version 1.16.148
 -Python version 2.7.15
 -pip version 9.0.1
 -OpenVPN version 2.4.7

-** AWS Service **

 -AWS Client VPN
 -AWS IAM
 -AWS Lambda
 -AWS CloudWatch Events
 -AWS CloudWatch Logs

# Setup steps

** Please set values ​​for variables appearing in the procedure beforehand, as shown in the example below.
Please note that variables are used for AWS Client VPN start/stop. **

```PowerShell:設定例
# 1. IAM の Lambda 実行ロールを作成する
$Region="ap-northeast-1"
$RoleName="client_vpn_admin_role"

# 2. Lambda 関数の作成
# 起動用 Lambda 関数作成時に設定
$FunctionName="start_client_vpn"
$handlerName="start_client_vpn_function.lambda_handler"

# 停止用 Lambda 関数作成時に設定
$FunctionName="stop_client_vpn"
$handlerName="stop_client_vpn_function.lambda_handler"

# 3. CloudWatch Events の作成
# 起動用 CloudWatch Events 作成時に設定
$RuleName="start_client_vpn"
$Schedule="cron(0 23 * * ? *)" # UTC 時間で設定

# 停止用 CloudWatch Eventss 作成時に設定
$Schedule="cron(0 15 * * ? *)"
$RuleName="stop_client_vpn" # UTC 時間で設定
```

## 1. Create a Lambda Execution Role for IAM
The Lambda function requires a role created in IAM, and the role provides the access necessary to execute the function.
Here, the access privileges required to create a role and execute the Lambda function"** Log output to Cloudwatch Logs **"and"** Subnet association/Disassociation ** for Client VPN endpoint **"Attach the policy) to the role.

(Please create role-policy.json described below to the current directory to run AWS CLI beforehand.)

```bash
# ロールの作成
aws iam create-role --role-name $RoleName --assume-role-policy-document file://role-policy.json

# ロールの確認
aws iam get-role --role-name $RoleName

# ポリシーのアタッチ
aws iam attach-role-policy --role-name $RoleName --policy-arn "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"

aws iam attach-role-policy --role-name $RoleName --policy-arn "arn:aws:iam::aws:policy/AmazonEC2FullAccess"

# アタッチしたポリシーの確認
aws iam list-attached-role-policies --role-name $RoleName
```



```json:role-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
       {
         "Action": "sts:AssumeRole",
         "Principal": {
           "Service": "lambda.amazonaws.com"
          },
          "Effect": "Allow",
          "Sid": ""
       }
    ]
}
```

## 2. Create Lambda Function
### 2.1 Create a Deployment Package
Create a package to deploy to Lamda in any directory.
(Reference: [Move AWS CLI on Lambda to S3 Sync] (http://blog.serverworks.co.jp/tech/2019/04/11/lambda-aws-cli/), Lambda with [aws-cli Create a Scheduled Event] (https://qiita.com/chroju/items/aa65fddadbc18a2e717a))

```bash
mkdir lambda-cli

cd lambda-cli

pip install awscli -t .
```

Create the following Python script (aws, start_client_vpn_function.py, stop_client_vpn_function.py) in the created lambda-cli directory.

```python:aws
import sys
import awscli.clidriver
 
 
def main():
    return awscli.clidriver.main()
 
 
if __name__ == '__main__':
    result = main()
    sys.exit(result)
```



```python:start_client_vpn_function.py
import subprocess


def lambda_handler(event, context):
    client_vpn_endpoint_id = "[あなたの Client VPN エンドポイント ID]"
    subnet_id_list = ["あなたの 関連付けるサブネット ID(カンマ区切りで複数記述可)"]
    
    for subnet_id in subnet_id_list :
        cmd = "python aws ec2 associate-client-vpn-target-network --client-vpn-endpoint-id " + client_vpn_endpoint_id + " --subnet-id " + subnet_id
        result = subprocess.run(
            cmd.split(" "),
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT
            )
        print(result.stdout.decode())
```



```python:stop_client_vpn_function.py
import subprocess


def lambda_handler(event, context):
    client_vpn_endpoint_id = "[あなたの Client VPN エンドポイント ID]"
    cmd = "python aws ec2 describe-client-vpn-target-networks --client-vpn-endpoint-id " + client_vpn_endpoint_id + " --query ClientVpnTargetNetworks[].AssociationId --output text"
    
    result = subprocess.run(
        cmd.split(" "),
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT
    )
    association_id_list = result.stdout.decode().strip().split()
    print("[DEBUG] association_id_list: %s" %  association_id_list)

    for association_id in association_id_list:
        cmd = "python aws ec2 disassociate-client-vpn-target-network --client-vpn-endpoint-id " + client_vpn_endpoint_id + " --association-id " + association_id

        result = subprocess.run(
        cmd.split(" "),
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT
        )

        print(result.stdout.decode())
```

After creating the Python script, zip the lambda-cli directory.

### 2.2 Deploy on Lambda

Create a Lambda function, specifying a ZIP file. (If you are using bash, replace the newline character with"`"→"\")

```bash
# ロール ARN の取得
$RoleArn=$(aws iam get-role --role-name $RoleName --query 'Role.Arn' --output text)

# Lambda 関数の作成
aws lambda create-function `
    --function-name $FunctionName `
    --runtime python3.7 `
    --role $RoleArn `
    --handler $handlerName `
    --timeout 120 `
    --zip-file fileb://lambda-cli.zip `
    --region $Region

# Lambda 関数の確認
aws lambda get-function --function-name $FunctionName

# 補足：Lambda 関数を更新
# update lambda function
aws lambda update-function-code `
    --function-name $FunctionName `
    --zip-file fileb://lambda-cli.zip `
    --publish
```

### 2.3 Test execution of Lambda function
You can do a test run with the GUI, but here we run it with the AWS CLI.

```bash
# Lambda 関数のテスト実行
aws lambda invoke --invocation-type Event `
    --function-name $FunctionName `
    --region $Region `
    --log-type Tail outfile.txt
```

It is OK if the following log is output to CloudWatch Logs.

```json:起動
{
"AssociationId": "cvpn-assoc-XXXXXXXXXXXXXXXX",
"Status": {
"Code": "associating"
}
}
```



```json:停止
{
"AssociationId": "cvpn-assoc-011618912b11f5840",
"Status": {
"Code": "disassociating"
}
}
```

# 3. Create CloudWatch Event
Create a triggering CloudWatch Event rule to schedule the Lambda function.

```bash
# CloudWatch Event のルールの作成
aws events put-rule --name $RuleName --schedule-expression "$Schedule" --state ENABLED

# CloudWatch Event のルールの確認
aws events describe-rule --name $RuleName

# CloudWatch Event の ARN の取得
$EventArn=$(aws events describe-rule --name $RuleName --query Arn --output text)

# Lambda 関数へのトリガー追加
aws lambda add-permission `
--function-name $FunctionName `
--statement-id $FunctionName `
--action 'lambda:InvokeFunction' `
--principal events.amazonaws.com `
--source-arn $EventArn

# Lambda 関数のARNの取得
$FunctionArn=$(aws lambda get-function --function-name $FunctionName --query Configuration.FunctionArn --output text)

# CloudWatch Event へのターゲット追加
aws events put-targets --rule $RuleName --targets "Id=$FunctionName,Arn=$FunctionArn"

# CloudWatch Event のターゲットの確認
aws events list-targets-by-rule --rule $RuleName
```

The settings are complete. Thank you for your hard work.

# Finally

Using Client VPN is very easy to operate because you can directly access the resources in the VPC without going through a springboard server etc.

The cost comparison with the springboard server + EIP/NAT Gateway configuration is certainly high, but I think that unused time zone can reduce the large cost by the operation of disassociating the subnet.

Thank you for reading to the end.