こんにちは。@hayata-yamamoto です。
普段は、EdTech Lab という研究開発のチームで機械学習 とデータ分析をしています。
AllenNLP とmypy が好きです。
さて今回は、AWS のClientVPNを用いて手軽にVPN 環境を用意し、Jupyterを動かしてみたいと思います。
ClientVPNの使い方はAWS の公式にありますが、Terraformと組み合わせたものは見当たらなかったので誰かの参考になれば嬉しいです。
今回出てくる技術
なお今回の内容はGitHub にも置いてあります。
github.com
Client VPN
AWS が用意するVPN サービスの1つです。2018年12月にリリースされました。東京リージョンは、2019年5月から利用可能になっています。
aws.amazon.com
そもそもVPN は、あるネットワークやデバイス からある別のネットワークへの安全でプライベートに接続するために設定します。
公式によるとClient VPN は、ユーザー需要に合わせて利用可能な Client VPN 接続の数を自動的にスケールアップまたはスケールダウンする、完全マネージド型で伸縮自在な VPN サービスです。使ってみた感じ、クラウド 側でそれぞれのクライアントのIPをClient VPN に割り振っているCIDR範囲と紐づけてくれています。それゆえ、クライアント側はVPN 用のソフトウェアをダウンロードするだけで済みます。
詳細な説明やユースケース については、AWS の公式も合わせて確認されると良いと思います。
aws.amazon.com
どのようなものか
少し触れてしまいましたが、このサービスは以下のような仕組みで動いています。
Client 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に相当するものがありません。
github.com
# 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をインストールしておいてください。
tunnelblick.net
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を設定したりしてセキュリティをいい感じに高めながら安全に、かつ快適に開発を進めていきたいものです。