【ハンズオン】AWS IoT1-Click で AWS Client VPN を起動・停止してみた

# やったこと
AWS Client VPN を AWS IoT1-Click のクリック・ダブルクリックで起動・停止(＝エンドポイントのサブネットへの関連付け・関連付け解除)するようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/4ddcea86-5314-34a1-76cb-4413f6f7ec39.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/603f4231-9deb-fbae-96fb-d893e094d475.png)


AWS Client VPN の料金は、下の引用の通り、Client VPN（エンドポイント）にサブネットを関連付けることで発生します。
関連付けを行ったままだと、夜間など利用しない時間帯にも料金が発生してしまうため、利用する時だけサブネットの関連付けを行うようにしました。

> AWS Client VPN では、アクティブなクライアント接続の数に対して、
> および Client VPN に関連付けられているサブネットの数に対してそれぞれ 1 時間単位の料金が発生します。
>
> 引用元：[AWS Client VPN の料金](https://aws.amazon.com/jp/vpn/pricing/#AWS%20Client%20VPN%20%E3%81%AE%E6%96%99%E9%87%91)

# 前提
- IoTデバイスとして、[AWS IoT エンタープライズボタン](https://www.amazon.co.jp/AWS-IoT-%E3%82%A8%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%97%E3%83%A9%E3%82%A4%E3%82%BA%E3%83%9C%E3%82%BF%E3%83%B3-%E2%80%93-%E3%82%92%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AB%E3%80%82/dp/B075FPHHGG/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&keywords=IoT+AWS&qid=1572172251&s=amazon-devices&sr=8-1)が必要です。今回はLTE-MのeSIM内蔵の [SORACOM LTE-M Button powered by AWS](https://www.amazon.co.jp/SORACOM-LTE-M-Button-powered-AWS/dp/B07L59ZFVF)を使用しました。

- サブネットの関連付けを解除すると VPN 接続ができなくなり、料金も発生しないため、「サブネットの関連付け ＝ AWS Client VPN の起動」、「サブネットの関連付け解除 ＝ AWS Client VPN の停止」と定義しています。

- AWS Client VPN の設定手順は、[こちらの記事(AWS Client VPN 設定メモ)](https://qiita.com/hmatsu47/items/ce0cc8b7f5747dbb3190)を参考にして行ったため割愛します。
（AWS CLI による設定手順がまとめられています。非常に参考になりました、ありがとうございます）

# 環境
- **IoT デバイス**

 - SORACOM LTE-M Button powered by AWS
 
- **クライアントPC**

 - Windows 10 バージョン 1803 (OSビルド 17134.829)
 - PowerShell バージョン 5.1.17134.765
 - AWS CLI バージョン 1.16.148
 - Python バージョン 2.7.15
 - pip バージョン 9.0.1
 - OpenVPN バージョン 2.4.7

- **AWS サービス**

 - AWS Client VPN
 - AWS IAM
 - AWS Lambda
 - AWS CloudWatch Logs
 - AWS IoT1-Click

# 設定手順

**手順に出てくる変数には、以下の例のように、予め値を設定してください。**

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

# 2. Client VPN を操作する Lambda 関数作成用
$FunctionName="manage_client_vpn"
$handlerName="manage_client_vpn_function.lambda_handler"
```

## 1. IAM の Lambda 実行ロールを作成する
Lambda 関数は、IAM で作成されたロールを必要とし、ロールは関数を実行するのに必要なアクセス権限を提供します。
ここでは、ロールを作成し、Lambda 関数が「**Cloudwatch Logsへのログ出力**」と「**Client VPN エンドポイントに対するサブネット関連付け/関連付け解除**」を実行するのに必要なアクセス権限（ポリシー）をロールへアタッチします。

（予め、AWS CLI を実行するカレントディレクトリへ下記の role-policy.json を作成してください。）

```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. Lambda 関数の作成
### 2.1 デプロイパッケージの作成
任意のディレクトリで、Lamda へデプロイするパッケージを作成します。
（参考：[Lambda上でAWSCLIを動かしてS3 Syncする](http://blog.serverworks.co.jp/tech/2019/04/11/lambda-aws-cli/)、[aws-cliでLambdaのScheduled Eventを作成する](https://qiita.com/chroju/items/aa65fddadbc18a2e717a)）

```bash
mkdir lambda-cli

cd lambda-cli

pip install awscli -t .
```

作成した lambda-cli ディレクトリに以下の Python スクリプト（ aws、manage_client_vpn_function.py ）を配置します。

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

```python:manage_client_vpn_function.py
import subprocess


def lambda_handler(event, context):
    client_vpn_endpoint_id = "[あなたの Client VPN エンドポイント ID]"
    subnet_id_list = ["あなたの 関連付けるサブネット ID(カンマ区切りで複数記述可)"]
    click_type = event['deviceEvent']['buttonClicked']['clickType']
    if (click_type == "SINGLE"):
        message = "The button is clicked once."
        start_client_vpn(client_vpn_endpoint_id, subnet_id_list)
    elif (click_type == "DOUBLE"):
        message = "The button is double clicked."
        stop_client_vpn(client_vpn_endpoint_id)
    elif (click_type == "LONG"):
        message = "The button is long pressed."
    else:
        message = "click_type is unknown."
    print(message)


def start_client_vpn(client_vpn_endpoint_id, subnet_id_list):
    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())


def stop_client_vpn(client_vpn_endpoint_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())
```

Python スクリプトを作成したら、lambda-cli ディレクトリをZIP圧縮します。

### 2.2 Lambda へのデプロイ

ZIPファイルを指定して、Lambda 関数を作成します。（bash で実行する場合は、改行文字を「`」→「\」に置換してください）

```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 関数を更新
aws lambda update-function-code `
    --function-name $FunctionName `
    --zip-file fileb://lambda-cli.zip `
    --publish
```

### 2.3 Lambda 関数のテスト実行
GUI でテスト実行をしてもよいですが、ここでは AWS CLI で実行します。

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

CloudWatch Logs に次のようなログが出力されていれば、OK です。

```json:起動
{
"AssociationId": "cvpn-assoc-XXXXXXXXXXXXXXXX",
"Status": {
"Code": "associating"
}
}
The button is clicked once.
```

```json:停止
{
"AssociationId": "cvpn-assoc-XXXXXXXXXXXXXXXX",
"Status": {
"Code": "disassociating"
}
}
The button is double clicked.
```

## 3. AWS IoT 1-Click に登録する

こちらの記事[ステップ 1: AWS IoT 1-Clickに登録する](https://dev.soracom.io/jp/start/aws_button_slack/)を参考にしてください。


## 4. AWS IoT 1-Click のプロジェクトを作成する

AWS管理コンソールから AWS IoT 1-Click のコンソールを開き、以下の画面ショットの通り入力していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/b6f39d30-f7db-e407-4a4c-4aa332a20edc.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/db92caaf-65d9-3637-c274-3dfefdb1bc48.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/8097b88a-dfcd-82c7-1e49-caefbd7bb0dd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/bbb725e4-4852-2767-0ad9-1542357cb16f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/8501a009-7714-d360-e38f-07563a7f303b.png)


登録したデバイスを選択し、プレイスメントの作成を完了すれば、設定は全て完了です。
試しに、IoTデバイスのボタンをクリックして、AWS IoT 1-Click のコンソールのレポートから Lambda関数呼び出しに成功したことを確認できれば合格です。
お疲れ様でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218139/4919847b-e7dd-a154-b02c-a0e35418158b.png)

# 最後に

前の記事 [節約術！？ AWS Client VPN を Lambda で定時に起動・停止してみた](https://qiita.com/speaktech/items/a07ff10533cfe55b7936) で、定時に Client VPN エンドポイントの関連付け・関連付け解除をする方法を紹介しました。
今回は以前から気になっていたIoTボタンと連携して、関連付け・関連付け解除をできるようにしてみました。
定時実行よりも融通がききますし、ボタンをクリックしたら、よし！開発するぞっというやる気スイッチも入るのでおすすめです（笑）。

最後まで読んでいただきまして、ありがとうございました。