AWSのPrivate LinkをつかってVPC間でバーチャルホストなWebAPIにアクセスする方法を考える


Private Linkは特定のサーバー・ポートのみにネットワークアドレスの衝突を気にせずに特定のVPCと接続できる非常に有用なサービスです。

VPC間通信を行うためのサービスとしてVPCピアリングというものもあります。こちらは簡単にVPC間を接続できますがネットワークそのものを繋げるため、セキュリティ面や、ネットワークアドレスの衝突など気にしなくてはならない点が非常に多くあります。

プロダクションではパブリックなネットワークから接続可能だが、開発用でプライベートネットワークからしかアクセスできないバーチャルホストで運用されているWebAPIがあると想定して、そのAPIにVPCピアリングを使わずに、Private Linkを利用して以下のような構成をしたい場合に、どのようにするのがいいのかを考えます。


# TL;DR

- Private LinkのおかげでVPC間通信がやりやすくなった
- リバースプロキシを使った方法が一番スマート
- できたらバーチャルホストでの運用はやめたい

# 前提条件

Private Linkについてある程度知っていること

開発用ということで大量のアクセスは想定しない
VPC Endpoint毎に専用のDNS名が割り振られるので、共通のドメインをDNSに登録することはできない
サービス間のネットワーク構成は以下
![privatelink1.png](https://qiita-image-store.s3.amazonaws.com/0/45645/a237320d-011b-4335-be18-f41b0c5ab317.png "privatelink1.png")


# 課題

Private Linkを利用する場合、サービス利用側からアクセスする先はVPC EndpointのDNS Namesで提供されたドメイン名にたいしてになる
APIサーバーがバーチャルホストで運用されているため、別のドメインではAPIサービスにアクセスできない
なんらかの方法を使ってホストヘッダにAPIサーバーのドメインを設定してVPC Endpointにアクセスできるようにする必要がある

# リバースプロキシを利用したアクセス

Webサーバーの設定でVPC Endpointへリバースプロキシするように設定する

## Webサーバーの/etc/hostsを書き換えて、APIサーバーへのアクセスをローカルホストに流す

```
127.0.0.1 api.example.com
```

## Webサーバーの設定を書き換えて、APIサーバーのバーチャルホストとしてリバースプロキシを設定する

以下のような設定を追加して、WebサーバーにAPIサーバーのホスト名でアクセスした時にリバースプロキシとして動くように設定する


```nginx.conf
upstream api {
	server vpce-xxxxxxxx1.vpce-svc-yyyyyyyyy.ap-northeast-1.vpce.amazonaws.com:80;
}

server {
        listen 127.0.0.1:80;
        server_name  api.example.com;

        location / {
        	proxy_set_header Host $host;
        	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        	proxy_set_header X-Forwarded-Host $host;
        	proxy_set_header X-Forwarded-Server $host;
        	proxy_set_header X-Real-IP $remote_addr;
                proxy_pass http://api;
        }
}
```

## 問題点

インフラ側の設定がプロダクション環境と変わってしまう
設定項目が増えるためインフラの構築が複雑になってしまう

# アプリケーションの改修(ホストヘッダ付与)

Webサーバー側のアプリケーションを改修して、ホストヘッダとアクセス先ホストを別に設定できるようにする

## 改修前

DNS名が解決できる環境であれば、以下のようなコードになる

```nodejs
"use strict";

const url = require("url")
    , http = require("http")
    ;

const endpoint = "http://api.example.com/path/to/resource";

const parsed = url.parse(endpoint);
const options = {
    host: parsed.hostname,
    port: parsed.port || 80,
    path: parsed.path,
    method: "GET"
};

const request = http.request(options, (response) => {
    let data = "";
    response.on("data", (d) => {
        data += d.toString();
    });
    response.on("end", () => {
        if (response.statusCode === 200) {
            console.log(`Response Succeeded: ${data}`);
        } else {
            console.log(`Response Failed [${response.statusCode}]: ${data}`);
        }
    });
});
request.on("error", (error) => {
    console.log(`Request Failed: ${error}`);
});
request.end();
```

## 改修後

接続先ホスト名を指定できるように書き換えることで、リクエストにHostヘッダを付与しバーチャルホストにアクセスできるようする

```nodejs
"use strict";

const url = require("url")
    , http = require("http")
    ;

// 実際はAZ毎にDNS名が作成されるので注意
const hostname = "vpce-xxxxxxxx.vpce-svc-yyyyyyyyy.ap-northeast-1.vpce.amazonaws.com";
const endpoint = "http://api.example.com/path/to/resource";

const parsed = url.parse(endpoint);
const options = {
    host: hostname,
    port: parsed.port || 80,
    path: parsed.path,
    method: "GET",
    headers: {
        "Host": parsed.hostname
    }
};

const request = http.request(options, (response) => {
// 以下略
```

## 問題点

プロダクション環境では接続先ホスト名とURLは一致しているため、プロダクションとテストの違いを吸収するための分岐が増える
設定項目が増えるためアプリケーションの管理が複雑になる

# 専用ドメインによるアクセス(サービス利用者毎のDNS登録)

サービス利用者それぞれに専用にドメイン名を用意してVPC EndpointのDNS NamesをCNAMEとして登録してアクセスする

以下の2つの設定を行うことで、それぞれのサービスからは専用のドメインにてアクセスすることが可能になる

## APIサーバーのバーチャルホストの別名として各サービス用にドメインを登録する

nginxであれば以下のように書き換える

```nginx.conf
server_name api.example.com;
```

↓

```nginx.conf
server_name api.example.com api-for-service-a.example.com api-for-service-b.example.com;
```

## DNSに追加したドメインと各サービスの接続先ホスト名を紐づける

```
$ dig api-for-service-a.example.com
;; ANSWER SECTION:
api-for-service-a.example.com. 60 IN CNAME	vpce-xxxxxxxx1.vpce-svc-yyyyyyyyy.ap-northeast-1.vpce.amazonaws.com.
vpce-xxxxxxxx1.vpce-svc-yyyyyyyyy.ap-northeast-1.vpce.amazonaws.com. 60 IN A 172.31.0.156
```

```
$ dig api-for-service-b.example.com
;; ANSWER SECTION:
api-for-service-b.example.com. 60 IN CNAME	vpce-xxxxxxxx2.vpce-svc-yyyyyyyyy.ap-northeast-1.vpce.amazonaws.com.
vpce-xxxxxxxx2.vpce-svc-yyyyyyyyy.ap-northeast-1.vpce.amazonaws.com. 60 IN A 172.31.0.24
```

### 問題点

サービス毎にバーチャルホスト設定、ドメイン設定が必要なためサービス提供側の設定が面倒
DNSに登録するため、外部からIPアドレスを引けてしまう

# あとがき

どの方法も一長一短だが、個人的には3つ目のリバースプロキシ利用パターンが一番楽かなという印象

アプリケーションコードと違い、インフラ側で変更を行った方が疎結合になり、リバースプロキシを分離するなどの変更が容易というのが一番の理由


# 参考

- [AWS PrivateLinkのアップデート – お客様のアプリケーション&サービス向けのVPCエンドポイント
](https://aws.amazon.com/jp/blogs/news/aws-privatelink-update-vpc-endpoints-for-your-own-applications-services/)