RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

Client VPNで分析環境を用意してみた

こんにちは。@hayata-yamamotoです。

普段は、EdTech Labという研究開発のチームで機械学習とデータ分析をしています。 AllenNLPmypyが好きです。

さて今回は、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

どのようなものか

少し触れてしまいましたが、このサービスは以下のような仕組みで動いています。

f:id:hayata-yamamoto:20190727133707p:plain
Client VPN 仕組み(AWSより)

この通り設定すると、環境が用意できるようになります。

  1. Client VPN Endpointを設定します
  2. VPNをサブネットに関連づけます
  3. ユーザーがリソースにアクセスできるように認証ルールを追加します
  4. 他のネットワークにアクセスできるようにルールやルートを追加します(インターネットとか)

設定方法

クライアント認証用の証明書を発行する

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つが終わっています。あとは、認証とルートテーブルを付け加えて、完成です。

認証は、以下から

f:id:hayata-yamamoto:20191004000754p:plain

このように、サブネットのCIDR範囲を設定しておきます。

f:id:hayata-yamamoto:20191004000820p:plain ルートテーブルも先ほどと同様の画面から

f:id:hayata-yamamoto:20191004000854p:plain

インターネットに出れるようにルートを設定しておきましょう

f:id:hayata-yamamoto:20191004000916p:plain

接続する

tunnelblickをインストールしておいてください。

tunnelblick.net

OpenVPNやTunnelblickで使う設定ファイルをクライアントエンドポイントのページからダウンロードしてください。 適当にファイル名は変えて大丈夫です。

f:id:hayata-yamamoto:20191004001056p:plain

最初に作った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を設定したりしてセキュリティをいい感じに高めながら安全に、かつ快適に開発を進めていきたいものです。