Oasys Nodeを本番環境に立てるとき、SGとNLBで悩んだ話
Epic Quest CTOの森本です。
Oasysのノードを構築する機会があり、セキュリティグループ(SG)の設定でけっこう悩んだので共有します。結論から言うと、NLB(Network Load Balancer)を立てることで解決しました。
Oasysとは
軽く説明すると、Oasysはゲームに特化したブロックチェーン。2層構造になっていて:
- Hub層:メインのチェーン。バリデータが動いてる
- Verse層:ゲームごとのL2チェーン。Homeverseとか
今回構築したのはVerse層のレプリカノード。チェーンのデータを同期して、APIリクエストに応答できるようにするやつ。
基本構成
EC2 + Docker Composeでノードを動かす。構成はシンプル。
EC2 (Ubuntu)
├── geth (実行クライアント)
└── (必要に応じて) Blockscout
Terraformで構築を自動化してる。インスタンスタイプはm5.xlargeくらいあれば十分。ディスクは同期するチェーンの長さによるけど、500GB〜1TBあると安心。
P2P通信に必要なポート
ブロックチェーンノードはP2Pで他のノードと通信する。gethの場合、以下のポートを使う:
| ポート | プロトコル | 用途 |
|---|---|---|
| 30303 | TCP/UDP | P2P通信(ノード間同期) |
| 8545 | TCP | JSON-RPC(HTTP) |
| 8546 | TCP | JSON-RPC(WebSocket) |
30303は他ノードとブロックデータをやり取りするのに必須。8545/8546はAPIエンドポイント。
SGの罠:全開放したくなる誘惑
開発環境でサクッと動かすなら、SGはこうなる:
# 開発環境:雑に全開放
ingress {
from_port = 30303
to_port = 30303
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 30303
to_port = 30303
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
これで動く。他ノードからの接続も来るし、同期も始まる。
問題は、本番環境でこれが許されないこと。
セキュリティ要件的に「0.0.0.0/0でインバウンド全開放」は突っ込まれる。当然といえば当然。
でもP2P通信の相手は世界中のノード。IPアドレスを事前に特定して許可リストに入れるのは現実的じゃない。
解決策:NLBを前段に立てる
NLB(Network Load Balancer)を前段に置くことで解決した。
インターネット
↓
NLB (固定IP)
↓
EC2 (プライベートサブネット)
NLBを使う理由
- 固定IPが使える:Elastic IPをNLBに紐づけられる
- L4ロードバランサー:TCP/UDPをそのまま通せる(ALBはHTTP/HTTPSのみ)
- セキュリティグループの分離:EC2のSGはNLBからのみ許可すればいい
構成のポイント
# NLB
resource "aws_lb" "node" {
name = "oasys-node-nlb"
internal = false
load_balancer_type = "network"
subnets = var.public_subnet_ids
enable_cross_zone_load_balancing = true
}
# ターゲットグループ(P2P用)
resource "aws_lb_target_group" "p2p_tcp" {
name = "oasys-p2p-tcp"
port = 30303
protocol = "TCP"
vpc_id = var.vpc_id
health_check {
protocol = "TCP"
port = 30303
}
}
resource "aws_lb_target_group" "p2p_udp" {
name = "oasys-p2p-udp"
port = 30303
protocol = "UDP"
vpc_id = var.vpc_id
health_check {
protocol = "TCP"
port = 30303
}
}
# リスナー
resource "aws_lb_listener" "p2p_tcp" {
load_balancer_arn = aws_lb.node.arn
port = 30303
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.p2p_tcp.arn
}
}
resource "aws_lb_listener" "p2p_udp" {
load_balancer_arn = aws_lb.node.arn
port = 30303
protocol = "UDP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.p2p_udp.arn
}
}
EC2側のSG
EC2はプライベートサブネットに置いて、SGはNLBからのトラフィックのみ許可。
# EC2のSG:NLBからのみ許可
ingress {
from_port = 30303
to_port = 30303
protocol = "tcp"
cidr_blocks = var.nlb_subnet_cidrs # NLBがいるサブネットのCIDR
}
ingress {
from_port = 30303
to_port = 30303
protocol = "udp"
cidr_blocks = var.nlb_subnet_cidrs
}
これで「EC2に直接0.0.0.0/0を開けてない」状態になる。セキュリティ監査的にも説明しやすい。
gethの設定
NLBを使う場合、gethに外部IPを教えてあげる必要がある。
# docker-compose.yml
services:
geth:
image: ghcr.io/oasysgames/oasys-validator:v1.x.x
command:
- --nat=extip:${NLB_PUBLIC_IP}
- --port=30303
- --http
- --http.addr=0.0.0.0
- --http.port=8545
- --http.api=eth,net,web3
- --ws
- --ws.addr=0.0.0.0
- --ws.port=8546
--nat=extip:${NLB_PUBLIC_IP} がポイント。これを指定しないと、他ノードに自分のプライベートIPを広告してしまい、接続が確立できない。
Blockscoutも立てた
ノードだけだとAPIは使えるけど、トランザクションを人間が見るのは辛い。Blockscout(ブロックエクスプローラー)も一緒に立てた。
# docker-compose.yml に追加
services:
blockscout:
image: blockscout/blockscout:latest
environment:
- ETHEREUM_JSONRPC_HTTP_URL=http://geth:8545
- DATABASE_URL=postgresql://...
- NETWORK=Homeverse
ports:
- "4000:4000"
これでトランザクションやアドレスの残高をブラウザから確認できるようになる。開発時のデバッグにも便利。
ハマったポイント
1. UDPリスナーのヘルスチェック
NLBのUDPリスナーはヘルスチェックにUDPを使えない。TCPでやる必要がある。同じポートでTCP/UDP両方リッスンしてるgethだから問題なかったけど、UDP専用のサービスだと工夫が必要。
2. 同期に時間がかかる
チェーンの長さによるけど、フル同期には数時間〜数日かかる。最初は「動いてないのでは?」と不安になるけど、ログを見て着実にブロックが増えてれば大丈夫。
# 同期状況の確認
docker compose logs -f geth | grep "Imported new"
3. ディスクI/O
同期中はディスクI/Oがボトルネックになりやすい。gp3でIOPS上げるか、io1/io2を検討。
まとめ
Oasys Nodeを本番環境に立てるとき、SGの全開放は避けたい。NLBを前段に立てることで:
- EC2のSGは限定的な許可で済む
- 固定IPが使える
- セキュリティ監査で説明しやすい
P2P通信を扱うサービス全般に使えるパターンなので、参考になれば。
質問や感想があれば、お問い合わせからどうぞ。