NLBでオンプレ・AWSの様々なターゲットIPと重複するポートへの通信を振り分ける

NLBを使えば、リスナーとターゲットグループのポート番号をうまく活用してトラフィックを操作できることを学びました
2021.03.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ちゃだいん(@chazuke4649)です。

先日 Network Load Balancer (以下NLB)の特性を活かしたネットワーク構成を組む機会があったので紹介します。

概要

構成図

背景や要件

  • 2つのVPCピアリングでつなげる
  • VPC-1のクライアントサーバからVPC-2のターゲットサーバへアクセスしたい
  • ターゲット側には複数台存在し、複数同じ受けポートでリッスンしている
  • これをクライアント側でポート番号を指定することによって意図したターゲットにアクセスできるように振り分けたい
  • 待ち受けポートはhttp/httpsに限らず、SSHなど様々なポートへ通信させる必要がある
  • ターゲットによってはVPC-2とサイト間VPN接続でつながっているオンプレ環境とも通信させる必要がある

対応方法

  • NLBに中継させる
  • NLB側でリスナー側のポートとターゲットグループ側のポートを必要な通信の数だけ用意する
  • 一部ターゲットグループの宛先としてオンプレのターゲットサーバのIPを指定する

リスナー・ターゲットグループ組み合わせ一覧

NLBでは以下のようにリスナーとターゲットグループを組み合わせることができます。

No. リスナー側ポート番号 アクション ターゲットグループ名 ターゲットグループ側ポート番号 ターゲットIP
1 TCP:10022 転送 tg-10022 TCP:22 TargetServer1
2 TCP:10080 転送 tg-10080 TCP:80 TargetServer1
3 TCP:20022 転送 tg-20022 TCP:22 TargetServer2
4 TCP:20080 転送 tg-20080 TCP:80 TargetServer2
5 TCP:30080 転送 tg-30080 TCP:80 TargetServer3

クライアント側で宛先側のポート指定が可能であれば、それぞれ一意のポート番号単位で、ターゲット側のポート番号とIPの組み合わせを変えるといった方法です。

これを行うと、クライアントとNLB間はリスナー側ポート番号で通信し、NLBとターゲット間はターゲットグループ側ポート番号で通信することが可能です。

やってみた

NLBがメインなので以下リソースの構築は割愛します。

  • VPC関連(サブネット、ルートテーブルなど)
  • VPCピアリング
  • クライアント・ターゲットサーバのEC2インスタンス
  • セキュリティグループ

NLBを作成する

今回はTerraformで構築しています。 以下がサンプルのTerraformのtfファイルとなります。

nlb.tf

#########################################################
# NLB: example-nlb
#########################################################

resource aws_lb example-nlb {
  name                             = "example-nlb"
  load_balancer_type               = "network"
  internal                         = true
  enable_deletion_protection       = true
  enable_cross_zone_load_balancing = true
  subnet_mapping {
    subnet_id            = aws_subnet.private-1a-1.id
    private_ipv4_address = "172.31.128.10"
  }

  subnet_mapping {
    subnet_id            = aws_subnet.private-1c-1.id
    private_ipv4_address = "172.31.136.10"
  }
}

#########################################################
# NLB Listener & Target group 10022
#########################################################

resource aws_lb_listener listner-10022 {
  load_balancer_arn = aws_lb.example-nlb.arn
  port              = "10022"
  protocol          = "TCP"

  default_action {
    target_group_arn = aws_lb_target_group.tg-10022.arn
    type             = "forward"
  }
}

resource aws_lb_target_group tg-10022 {
  name                 = "example-tg-10022"
  port                 = "22"
  protocol             = "TCP"
  target_type          = "ip"
  vpc_id               = aws_vpc.example.id
  deregistration_delay = "300"
  depends_on           = [aws_lb.example-nlb]

  health_check {
    port                = "traffic-port"
    protocol            = "TCP"
    healthy_threshold   = "3"
    unhealthy_threshold = "3"
    interval            = "30"
  }
}

resource aws_lb_target_group_attachment tg-10022 {
  target_group_arn = aws_lb_target_group.tg-10022.arn
  target_id        = "172.31.129.10"
  port             = 22
}

#########################################################
# NLB Listener & Target group 30080
#########################################################

resource aws_lb_listener listner-30080 {
  load_balancer_arn = aws_lb.example-nlb.arn
  port              = "30080"
  protocol          = "TCP"

  default_action {
    target_group_arn = aws_lb_target_group.tg-30080.arn
    type             = "forward"
  }
}

resource aws_lb_target_group tg-30080 {
  name                 = "example-tg-30080"
  port                 = "22"
  protocol             = "TCP"
  target_type          = "ip"
  vpc_id               = aws_vpc.example.id
  deregistration_delay = "300"
  depends_on           = [aws_lb.example-nlb]

  health_check {
    port                = "traffic-port"
    protocol            = "TCP"
    healthy_threshold   = "3"
    unhealthy_threshold = "3"
    interval            = "30"
  }
}

resource aws_lb_target_group_attachment tg-30080 {
  target_group_arn  = aws_lb_target_group.tg-30080.arn
  target_id         = "10.0.0.10"
  port              = 80
  # オンプレなどVPC外のIPを指定する場合にallに指定する
  availability_zone = "all"
}
  • 上記では長くなりすぎるのでリスナー・ターゲットグループ一覧の No.1No.5 のみ記述しています。(実際はmoduleを使って共通化すべきでしょう)
  • 11~18行目では、NLBの特徴である静的IPの指定を行っています。今回は2つのサブネットにまたがる形で両サブネットにそれぞれプライベートIPを入力しています
  • 99行目では、ターゲットIPがオンプレ側(VPC外)の場合のみ、availability_zone = "all"の指定が必要です

疎通確認(HTTP)

まず、クライアントサーバにSSHで入ります。

実際に通信ができるかどうか確認していきます。以下ブログで紹介されているようなコマンドを使用します。

【初心者向け】各OSのTCP通信チェックコマンド入門(2018年 更新版) | DevelopersIO

以下コマンドを実行します。

[ec2-user@ip-192-168-1-214 ~]$ curl -v telnet://172.31.128.10:10080
* Rebuilt URL to: telnet://172.31.128.10:10080/
*   Trying 172.31.128.10...
* TCP_NODELAY set
* Connected to 172.31.128.10 (172.31.128.10) port 10080 (#0)

TCPコネクションが貼られていることが確認できました。

ただし、本当にHTTPを返してくれるかわかりません。次は、 curl -I でHTTPレスポンスヘッダを取得してみます。

[ec2-user@ip-192-168-1-214 ~]$ curl -I 172.31.128.10:10080
HTTP/1.1 200 OK
Date: Fri, 12 Mar 2021 08:59:48 GMT
Server: Apache/2.4.46 ()
Upgrade: h2,h2c
Connection: Upgrade
Last-Modified: Fri, 12 Mar 2021 08:59:11 GMT
ETag: "12-5bd531d172fe5"
Accept-Ranges: bytes
Content-Length: 18
Content-Type: text/html; charset=UTF-8

ターゲットサーバ側で起動しているApahcheのテストページのレスポンスヘッダを取得することができました。HTTPの疎通確認は以上です。

疎通確認(SSH)

次はSSHです。こちらは同じくNLBの1a側のAZの静的プライベートIP宛にポート番号を指定して、SSH接続を試みます。

[ec2-user@ip-192-168-1-214 ~]$ssh -p 10022 -i test.key ec2-user@172.31.128.10

The authenticity of host '54.168.234.185 (54.168.234.185)' can't be established.
ECDSA key fingerprint is SHA256:NfbQno8PS3VjO8BlXdJzIhs7fvfgl4QwFxtQsJHqg6y.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '54.168.234.185' (ECDSA) to the list of known hosts.


       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/

上記の通り成功しました。

成功ついでに、netstatコマンドで 本当に22番ポートが使用されているか確認してみます。

[ec2-user@ip-172-31-129-10 ~]$ netstat -n |grep 22
tcp        0      0 172.31.129.10:22        172.31.128.10:44387     SYN_RECV
tcp        0      0 172.31.129.10:22        xxx.xx.xx.xxx:22814     ESTABLISHED
...

上記の通り、2つのうちどちらも22番ポートで受け付けており、NLB側のIPとはTCPのある通信状態(SYN_RECV)を示しています。また、xxx.xx.xx.xxx(ローカルマシンのグローバルIP)とESTABLISHED(TCPコネクションが確立できている状態)であることが確認できました。

トラブルシュート

最初、疎通確認がうまくいかないことがありましたが、以下ポイントを確認すると概ね解決しました。

  • ルートテーブルの設定に不備がないか
  • ターゲットグループのヘルスチェックがHealthyかどうか
  • ターゲットサーバのセキュリティグループでNLBからのIPとポートを許可しているか など

終わりに

NLBを触るとネットワークにおけるトランスポート層の理解が深まることがわかりました。

それではこの辺で。ちゃだいん(@chazuke4649)でした。

参考情報

Network Load Balancer とは - Elastic Load Balancing

TCP/IP通信の状態を調べる「netstat」コマンドを使いこなす:Tech TIPS(1/2 ページ) - @IT