GKEを使う - LoadBalancerを使わずに外部にサービスを公開する(1)

勉強用としてはLoadBalancerの費用は高いため、LoadBalancerを使わずに外部にサービスを公開することを考えます。

一般的な構成

  • GKEのノードがPublicIPを持たない限定公開クラスタ、全てプリエンプティブルインスタンス
  • k8sのtype=LoadBalancerのService
  • GCPのCloud Load Balancing

外部からのアクセスはLoadBalancer->Serviceとなります。

案A

  • GKEのノードがPublicIPを持つクラスタ、1台だけ通常のインスタンスで残りはプリエンプティブルインスタンス
  • k8sのtype=NodePortのService
  • 通常インスタンスにセットアップしたプロキシ

外部からのアクセスをk8s内の通常のインスタンスで実行しているプロキシで受け付けます。
プロキシからプリエンプティブルで実行しているNodePortのサービスにリクエストを流します。
SSL終端はプロキシで処理します。
Clouad NATを利用しません。
全てのノードがPublicIPを持つためその分の費用がかかります。
以前はこの方法で構築しました。

案B

  • GKEのノードがPublicIPを持たない限定公開クラスタ、全てプリエンプティブルインスタンス
  • k8sのtype=NodePortのService
  • GCPのVMインスタンスにセットアップしたプロキシ

外部からのアクセスをVMインスタンスにセットアップしたプロキシで受け付けます。
プロキシからプリエンプティブルで実行しているNodePortのサービスにリクエストを流します。
SSL終端はプロキシで処理します。
外部へのアクセスはCloud NATを利用します。
今回はこの方法でやります。

ポイントは、プロキシ->Serviceへの通信です。
プロキシとワーカーノードを同じネットワーク内に配置しても、ワーカーノードのIPアドレスがわからないためリクエストを渡すことができません。
ワーカーノードのIPアドレスを特定できないのであれば、プロキシから自身を除いたネットワーク内の全てのIPアドレスを対象としてしまいます。
当然、サーバーが存在しないためアクセスできないIPアドレスもあります。これに関してはヘルスチェックを実施することで生存しているIPアドレスだけを対象とします。
ワーカーノードは今のところ数台を想定しているため、割り当てるIPアドレスの範囲も狭めます。
プロキシにはtraefikを利用します。

Google Domainsでドメインを取得

ドメインを取得します。安いもので年額1,400円です。
今回はoct-26-1985.comを取得しました。

IPアドレスを取得

1
2
gcloud compute addresses create traefik \
--region asia-northeast1
1
2
3
4
$ gcloud compute addresses list
NAME ADDRESS/RANGE TYPE PURPOSE NETWORK REGION SUBNET STATUS
traefik 34.146.91.23 EXTERNAL asia-northeast1 RESERVED

1
gcloud services enable dns.googleapis.com
1
EXTERNAL_IP=34.146.91.23

VPCネットワークの作成

1
2
gcloud compute networks create my-net \
--subnet-mode custom

サブネットの作成

1
2
3
4
5
6
gcloud compute networks subnets create my-subnet \
--network my-net \
--region asia-northeast1 \
--range 10.146.0.0/29 \
--secondary-range my-pods=10.4.0.0/14,my-services=10.0.32.0/20 \
--enable-private-ip-google-access

作成したネットワークは、ネットワーキング -> VPC ネットワーク -> VPC ネットワーク で確認できます。

DNSレコードの作成

ゾーンを作成

https://cloud.google.com/dns/docs/zones

1
2
3
4
gcloud dns managed-zones create oct-26-1985 \
--description=oct-26-1985.com \
--dns-name=oct-26-1985.com \
--visibility=public
1
gcloud dns managed-zones list

traefik用のVMインスタンスの作成

traefik用のインスタンスを作成します。IPアドレスには10.146.0.2を指定します。

ドキュメント:https://cloud.google.com/compute/docs/instances/create-start-instance?hl=ja#gcloud

1
2
3
4
gcloud compute instances create traefik \
--machine-type=e2-micro \
--network-interface=network-tier=PREMIUM,subnet=my-subnet,private-network-ip=10.146.0.2,address=$EXTERNAL_IP \
--tags=http-server,https-server

確認します。

1
2
3
$ gcloud compute instances list
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
traefik asia-northeast1-c e2-micro 10.146.0.2 34.146.91.23 RUNNING

SSH接続できるようにファイアウォールを設定します。ついでに80と443ポートも開けておきます。

1
2
3
$ gcloud compute firewall-rules create allow-ssh --network my-net --allow tcp:22
$ gcloud compute firewall-rules create http-server --network my-net --allow tcp:80
$ gcloud compute firewall-rules create https-server --network my-net --allow tcp:443

DNSレコードの作成

作成したVMインスタンスのPublic IPとDNSを関連付けます。

https://cloud.google.com/dns/docs/records#gcloud

1
gcloud dns record-sets transaction start --zone="oct-26-1985"
1
2
3
4
gcloud dns record-sets transaction add $EXTERNAL_IP --name="foo.oct-26-1985.com" \
--ttl="300" \
--type="A" \
--zone="oct-26-1985"
1
gcloud dns record-sets transaction execute -z="oct-26-1985"

NameServerを確認します。

1
gcloud dns record-sets list -z="oct-26-1985"

ここで、NameServerをGoogleDomainsのCustom name serversに設定します。

traefik用のサービスアカウントを作成

LetsEncryptの証明書発行に必要です。

1
2
3
4
5
6
7
PROJECT_ID=$(gcloud config get-value project)
PROJECT_NUMBER="$(gcloud projects describe ${PROJECT_ID} \
--format='get(projectNumber)')"
gcloud iam service-accounts create traefik-dns
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member serviceAccount:traefik-dns@$PROJECT_ID.iam.gserviceaccount.com \
--role roles/dns.admin

キーもファイルに出力します。

1
2
gcloud iam service-accounts keys create traefik-dns.json \
--iam-account traefik-dns@$PROJECT_NUMBER.iam.gserviceaccount.com

traefikをインストール

まず、作成したVMにGCPのコンソールからログインします。
続いてtraefikをインストールします。

1
2
3
4
5
6
7
sudo apt install -y wget
wget https://github.com/traefik/traefik/releases/download/v2.5.4/traefik_v2.5.4_linux_amd64.tar.gz
tar xvzf traefik_v2.5.4_linux_amd64.tar.gz
sudo mv ./traefik /usr/local/bin
sudo chown root:root /usr/local/bin/traefik
sudo chmod 755 /usr/local/bin/traefik
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/traefik

実行用のユーザーとグループを作成します。

1
2
3
4
5
6
sudo groupadd -g 500 traefik
sudo useradd \
-g traefik --no-user-group \
--home-dir /var/www --no-create-home \
--shell /usr/sbin/nologin \
--system --uid 500 traefik

ディレクトリを作成します。

1
2
3
4
5
6
7
8
sudo mkdir /etc/traefik
sudo mkdir /etc/traefik/acme
sudo chown -R root:root /etc/traefik
sudo chown -R traefik:traefik /etc/traefik/acme
sudo mkdir /var/log/traefik
sudo chown -R traefik:traefik /var/log/traefik
sudo mkdir -p /usr/local/var/traefik
sudo chown -R traefik:traefik /usr/local/var/traefik

設定ファイルを配置します。ファイルの内容は後述します。

1
2
sudo mv ./traefik.toml /etc/traefik/
sudo mv ./traefik.route.toml /etc/traefik/

OSのサービスとして登録します。ファイルの内容は後述します。

1
2
3
4
sudo mv ./traefik.service /etc/systemd/system/
sudo chown root:root /etc/systemd/system/traefik.service
sudo chmod 644 /etc/systemd/system/traefik.service
sudo systemctl daemon-reload

traefik-dns.json/etc/traefik/traefik-dns.jsonに配置します。

traefikを実行します。

1
sudo systemctl start traefik.service

ログファイルは/var/log/traefik/traefik.logです。エラーが出力されている確認します。

動作確認

この状態でhttp://foo.oct-26-1985.comにアクセスするとhttps://foo.oct-26-1985.comにリダイレクトします。
ただし、Proxyの先にサービスは存在しないのでGateway TimeoutService Unavailableと表示されます。

今回はここまでです。
VMインスタンスとIPアドレスはお金かかるので気になるようなら一旦削除します。

traefik.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# this file based on https://github.com/containous/traefik/blob/master/traefik.sample.toml
################################################################
# Global configuration
################################################################
[global]
checkNewVersion = false
sendAnonymousUsage = false
################################################################
# Entrypoints configuration
################################################################
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.websecure]
address = ":443"
################################################################
# Traefik logs configuration
################################################################
[log]
level = "ERROR"
filePath = "/var/log/traefik/traefik.log"
################################################################
# Access logs configuration
################################################################
[accessLog]
filePath = "/var/log/traefik/access.log"
################################################################
# API and dashboard configuration
################################################################
[api]
# insecure = true
dashboard = false
################################################################
# Ping configuration
################################################################
[ping]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
# entryPoint = "traefik"
################################################################
# backend configuration
################################################################
[providers]
[providers.file]
watch = true
filename = "/etc/traefik/traefik.route.toml"
debugLogGeneratedTemplate = true
################################################################
# Let's encript configuration
################################################################
[certificatesResolvers.le.acme]
email = "***@gmail.com"
storage = "/etc/traefik/acme/acme.json"
[certificatesResolvers.le.acme.dnsChallenge]
provider = "gcloud"

traefik.route.toml

https://doc.traefik.io/traefik/v2.4/middlewares/redirectscheme/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Dynamic Configuration
[http.routers]
[http.routers.my-api]
rule = "Host(`foo.oct-26-1985.com`)"
entryPoints = ["web"]
service = "my-service"
middlewares = ["https-redirect"]
[http.routers.my-api-ssl]
rule = "Host(`foo.oct-26-1985.com`)"
entryPoints = ["websecure"]
service = "my-service"
middlewares = []
[http.routers.my-api-ssl.tls]
certResolver = "le"
[[http.routers.my-api-ssl.tls.domains]]
main = "foo.oct-26-1985.com"
# sans = ["*.oct-26-1985.com"]
[http.services]
[http.services.my-service.loadBalancer]
[[http.services.my-service.loadBalancer.servers]]
url = "http://10.146.0.3/"
[[http.services.my-service.loadBalancer.servers]]
url = "http://10.146.0.4/"
[[http.services.my-service.loadBalancer.servers]]
url = "http://10.146.0.5/"
[[http.services.my-service.loadBalancer.servers]]
url = "http://10.146.0.6/"
[[http.services.my-service.loadBalancer.servers]]
url = "http://10.146.0.7/"
[http.services.my-service.loadBalancer.healthCheck]
path = "/healthz"
[http.middlewares]
[http.middlewares.https-redirect.redirectScheme]
scheme = "https"
permanent = true

traefik.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[Unit]
Description=traefik proxy
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service

[Service]
Restart=on-abnormal

; User and group the process will run as.
User=traefik
Group=traefik

Environment=GOOGLE_APPLICATION_CREDENTIALS=/etc/traefik/traefik-dns.json

; WorkingDirectory=/usr/local/var/traefik

; Always set "-root" to something safe in case it gets forgotten in the traefikfile.
ExecStart=/usr/local/bin/traefik --configfile=/etc/traefik/traefik.toml

; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
LimitNOFILE=1048576

; Use private /tmp and /var/tmp, which are discarded after traefik stops.
PrivateTmp=true
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
PrivateDevices=false
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
; Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full
; … except /etc/ssl/traefik, because we want Letsencrypt-certificates there.
; This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
ReadWriteDirectories=/etc/traefik/acme

; The following additional security directives only work with systemd v229 or later.
; They further restrict privileges that can be gained by traefik. Uncomment if you like.
; Note that you may have to add capabilities required by any plugins in use.
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target