こんにちは。@hayata-yamamotoです。
普段は、EdTech Labという研究開発のチームで機械学習とデータ分析をしています。 AllenNLPとmypyが好きです。
さて今回は、AWSのClientVPNを用いて手軽にVPN環境を用意し、Jupyterを動かしてみたいと思います。 ClientVPNの使い方はAWSの公式にありますが、Terraformと組み合わせたものは見当たらなかったので誰かの参考になれば嬉しいです。
今回出てくる技術
なお今回の内容はGitHubにも置いてあります。
Client VPN
AWSが用意するVPNサービスの1つです。2018年12月にリリースされました。東京リージョンは、2019年5月から利用可能になっています。
そもそもVPNは、あるネットワークやデバイスからある別のネットワークへの安全でプライベートに接続するために設定します。 公式によるとClient VPNは、ユーザー需要に合わせて利用可能な Client VPN 接続の数を自動的にスケールアップまたはスケールダウンする、完全マネージド型で伸縮自在な VPN サービスです。使ってみた感じ、クラウド側でそれぞれのクライアントのIPをClient VPNに割り振っているCIDR範囲と紐づけてくれています。それゆえ、クライアント側はVPN用のソフトウェアをダウンロードするだけで済みます。
詳細な説明やユースケースについては、AWSの公式も合わせて確認されると良いと思います。
どのようなものか
少し触れてしまいましたが、このサービスは以下のような仕組みで動いています。
この通り設定すると、環境が用意できるようになります。
- Client VPN Endpointを設定します
- VPNをサブネットに関連づけます
- ユーザーがリソースにアクセスできるように認証ルールを追加します
- 他のネットワークにアクセスできるようにルールやルートを追加します(インターネットとか)
設定方法
クライアント認証用の証明書を発行する
acmに登録するサーバーとクライアントの証明書を発行します。
$ git clone https://github.com/OpenVPN/easy-rsa.git $ cd easy-rsa/easyrsa3
新しいPKI, CA環境を作って、サーバーとクライアントの証明書を発行します。
$ ./easyrsa init-pki $ ./easyrsa build-ca nopass $ ./easyrsa build-server-full server nopass $ ./easyrsa build-client-full client1.domain.tld nopass
適当な場所にファイルを移動して、awsコマンドでACMにインポートします。
$ cp pki/ca.crt /custom_folder/ $ cp pki/issued/server.crt /custom_folder/ $ cp pki/private/server.key /custom_folder/ $ cp pki/issued/client1.domain.tld.crt /custom_folder $ cp pki/private/client1.domain.tld.key /custom_folder/ $ cd /custom_folder/ # サーバー証明書のインポート $ aws acm import-certificate --certificate file://server.crt --private-key file://server.key --certificate-chain file://ca.crt --region region # クライアント証明書のインポート $ aws acm import-certificate --certificate file://client1.domain.tld.crt --private-key file://client1.domain.tld.key --certificate-chain file://ca.crt --region region
環境を用意
今回は、Terraformを使って設定します。ただ、Terraformだと執筆時点でAPIへのサポートが微妙に足りませんので、そこだけはコンソールから操作をします。具体的には、aws_client_vpn_route_table, aws_client_vpn_authorization_ruleに相当するものがありません。
# variables.tf variable "region" { default = "ap-northeast-1" } variable "availability_zone" { default = "ap-northeast-1a" } variable "instance_info" { type = "map" default = { ami = "ami-0c6dcc2b75586bc5d" instance_type = "t2.small" } } variable "cidr_blocks" { type = "map" default = { vpc = "10.0.0.0/16" subnet = "10.0.0.0/24" global = "0.0.0.0/0" client_vpn = "100.0.0.0/16" } } variable "my_cidr_block" {} variable "ssh_keyname" {}
# data.tf data "aws_acm_certificate" "server_certificate" { domain = "server" } data "aws_acm_certificate" "client_certificate" { domain = "client1.domain.tld" }
# resources.tf provider "aws" { region = var.region } resource "aws_vpc" "vpc" { cidr_block = var.cidr_blocks.vpc enable_dns_hostnames = true enable_dns_support = true tags = { Name = "vpc" } } resource "aws_subnet" "subnet" { vpc_id = aws_vpc.vpc.id cidr_block = var.cidr_blocks.subnet availability_zone = var.availability_zone map_public_ip_on_launch = true tags = { Name = "subnet" } } resource "aws_route_table" "rt" { vpc_id = aws_vpc.vpc.id route { cidr_block = var.cidr_blocks.global gateway_id = aws_internet_gateway.igw.id } tags = { Name = "rt" } } resource "aws_route_table_association" "a" { route_table_id = aws_route_table.rt.id subnet_id = aws_subnet.subnet.id } resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.vpc.id tags = { Name = "igw" } } resource "aws_security_group" "sg" { name = "sg" vpc_id = aws_vpc.vpc.id ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = [var.my_cidr_block] } ingress { from_port = 22 to_port = 22 protocol = "tcp" security_groups = aws_ec2_client_vpn_network_association.vpn.security_groups } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = [var.cidr_blocks.global] } tags = { Name = "sg" } } resource "aws_instance" "myinstance" { ami = var.instance_info.ami instance_type = var.instance_info.instance_type vpc_security_group_ids = [aws_security_group.sg.id] subnet_id = aws_subnet.subnet.id key_name = var.ssh_keyname associate_public_ip_address = true tags = { Name = "myinstance" } } resource "aws_ec2_client_vpn_endpoint" "vpn" { description = "client vpn endpoint test" server_certificate_arn = data.aws_acm_certificate.server_certificate.arn client_cidr_block = var.cidr_blocks.client_vpn authentication_options { type = "certificate-authentication" root_certificate_chain_arn = data.aws_acm_certificate.client_certificate.arn } connection_log_options { enabled = false } tags = { Name = "vpn" } } resource "aws_ec2_client_vpn_network_association" "vpn" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id subnet_id = aws_subnet.subnet.id }
# outputs.tf output "instance_public_ip" { value = aws_instance.myinstance.public_ip } output "instance_private_ip" { value = aws_instance.myinstance.private_ip }
適当な場所に上記の.tfを作ってもらって、
$ terraform init $ terraform plan $ terraform apply
とし、自分のCIDR範囲を入力してください。(123.1.1.1/32みたいなやつです) 実行が終わると、outputでインスタンスのpublic_ipが吐き出されるので、その情報を使ってsshでインスタンスに接続してください。
ssh -i keyname ubuntu@instance-public-ip
認証とルートテーブルを追加する
実は先ほどの動作で、上記の4つの手順のうち2つが終わっています。あとは、認証とルートテーブルを付け加えて、完成です。
認証は、以下から
このように、サブネットのCIDR範囲を設定しておきます。
ルートテーブルも先ほどと同様の画面から
インターネットに出れるようにルートを設定しておきましょう
接続する
tunnelblickをインストールしておいてください。
OpenVPNやTunnelblickで使う設定ファイルをクライアントエンドポイントのページからダウンロードしてください。 適当にファイル名は変えて大丈夫です。
最初に作ったclient用の証明書と鍵と同じ場所に設定ファイルを配置し、
# hoge.vpn ... cert /path/to/client1.domain.tld.crt key /path/to/client1.domain.tld.key
と追記してこの.opvnファイルを開いてください。設定ファイルを読み込んだらTunnelblickを起動し、接続できるか確認しましょう。
Jupyterを立てる
さて、最終ゴールはJupyterの環境にアクセスすることでした。 ひとまず設定したVPNの環境を切断して、sshからEC2のインスタンスにアクセスしてください。
$ ssh -i keyname ubuntu@instance-global-ip
ちなみに、このインスタンスはDeep Learning AMIを使っています。なので最初からJupyterNotebookは使えます。sshでトンネリングしてjupyterにアクセスすればOKです。サーバー側で
$ jupyter notebook --no-browser --NotebookApp.token=""
を実行し、別のタブで以下を実行してトンネリングを行います。
$ ssh -i keyname ubuntu@instance-private-ip -L 8888:localhost:8888 -N
ブラウザでlocalhost:8888/treeにアクセスして、jupyterの画面が表示されれば設定は完了です。
まとめ
手軽にできるVPNの設定を書いてみました。利便性が高く、クライアント側の設定がほとんどいらないのがメリットです。分析環境などを用意する際は、データをバケットやRDSに用意することが多いと思いますが、作業するEC2だけにARNを設定したりしてセキュリティをいい感じに高めながら安全に、かつ快適に開発を進めていきたいものです。