はじめに
皆さんKubernetesを使っていますか?
宣言的にコンテナベースのインフラを構成できるKubernetesは便利ですが、Internetへ何らかのサービスを公開する場合には必須となる「公開サービスの経路暗号化」や「公開サービスの認証認可」は、残念ながらKubernetes自体には備わっていません(Kubernetes自体には認証認可機構が有りますし、Kubernetes上に起動されるサービスのアレコレはKubernetesの範疇外なので、当たり前といえば当たり前ですが)。
Kubernetesのパッケージマネージャ的な位置づけであるHelmには、nginxでL7ロードバランサーを構成するnginx-ingressが公開されています。このnginx-ingressは非常に高機能で、パスベースのルーティングもできますしTLSの終端やBASIC認証等をさせることもできます。上手くハマるならば、nginx-ingressを使うのが良い手でしょう。
しかしより細やかなルールでREST APIのToken認証認可を行わせたい場合など、nginx-ingressではなくAmbassadorというKubernetes上で動作するAPI Gatewayを用いるほうが、イイカンジにサービスを構成できる場合もありそうです。
今回はこのAmbassadorを、Azure AKS上で動作させてみようと思います。
Ambassadorとは
OPEN SOURCE, KUBERNETES-NATIVE API GATEWAY FOR MICROSERVICES BUILT ON ENVOY
https://www.getambassador.io/
Ambassadorは、Envoyを用いて構成されたKubernetes上で動作するAPI Gatewayです。
そもそもEnvoyとは、マイクロサービスを構成する際に横串で必要となる機能(Service discoveryやHealth Check、Circuit Breaker等)を提供するC++で書かれたプロキシソフトウェアです(Sidecarパターンと呼ばれるものですね)。元々はLyftのインフラを構成するコンポーネントとして開発され、Cloud Native Computing FoundationにApache License 2.0のOSSとして寄贈されました。
Ambassadorは、Kubernetesのsecretやannotation経由で与えた設定を用いて、そのEnvoyをイイカンジに構成してくれるOSSです。自分のServiceのコードを書き換えること無く、またEnvoyそのものを意識すること無く、自分のServiceに経路の暗号化や認証認可を付け加えることができます。
(位置づけとしてはIstioとよく似ていますが、もう少し小規模な感じですね。)
Ambassadorを使ってみる
ということで、Ambassadorを使ってAPI Gatewayを構成してみます。今回はAzureを使いますが、原理上他のクラウドでも動作すると思います。
検証した環境
Kubernetesクラスタ | バージョン |
---|---|
AKS | Microsoft AKS 米国中部 |
Kubernetes | 1.8.11 |
検証用端末 | バージョン |
---|---|
azure cli | 2.0.31 |
kubectl | 1.10.1 |
OS | macOS Sierra 10.12.6 |
Azure DNSへDNSゾーンの作成
まずは事前に準備した nmatsui.work
というドメインをAzure DNSゾーンとして定義し、ドメインレジストラにこのDNSゾーンのnameserverを設定します。
注意)ドメインレジストラによっては、nameserverの反映に時間がかかる場合があります。
$ az network dns zone create --resource-group nmatsui_dns --name nmatsui.work
サーバ証明書の準備
次にLet's EncryptからDNS-01 challengeを用いて、 api.nmatsui.work
というFQDNのサーバ証明書を取得します(Verisign等から発行されたサーバ証明書があるならば、それで良いですが)。
Dockerコンテナ経由で certobot
コマンドを起動
certbotコマンドをローカルにインストールするのは面倒なので、dockerコンテナ経由で使います。
-
証明書とログを保存するディレクトリを先に作っておきます。
create_directories$ mkdir secrets $ mkdir certbot-logs
-
LetsEncrypt公式のDockerイメージを用い、DNS-01 challengeで
certbot
コマンドを実行します。- 現時点ではグローバルIPを持ったインスタンスはどこにもないので、よく解説されているHTTP-01 challengeではなくDNS-01 Challengeでサーバ証明書を取得します。
start_certbot$ docker run -it -v $(PWD)/secrets:/etc/letsencrypt -v $(PWD)/certbot-logs:/var/log/letsencrypt certbot/certbot certonly --manual --domain api.nmatsui.work --email nobuyuki.matsui@gmail.com --agree-tos --manual-public-ip-logging-ok --preferred-challenges dns
- 上記のコマンドを実行すると、指定されたvalueを持つ指定されたnameのTXT recordをDNSへ登録するように促されます。
txt_record_msg------------------------------------------------------------------------------- Please deploy a DNS TXT record under the name _acme-challenge.api.nmatsui.work with the following value: XXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Before continuing, verify the record is deployed. ------------------------------------------------------------------------------- Press Enter to Continue
- このDockerコンテナのターミナルはそのままにしておき、別のターミナルを開きます。
Azure DNSへTXT Recordを追加
別のターミナルを開き、指定されたTXT recordをAzure DNSへ追加します。
-
Azure DNSへ指定されたTXT recordを追加します。
create_text_record$ az network dns record-set txt add-record --resource-group nmatsui_dns --zone-name nmatsui.work --record-set-name "_acme-challenge.api" --value "XXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
-
TXT recordが追加されていることを確認します。
list_record$ az network dns record-set txt show --resource-group nmatsui_dns --zone-name nmatsui.work --name "_acme-challenge.api"
証明書を取得
TXT recordが追加できたら、最初のcertbotコマンドを実行しているターミナルでEnterキーを押し、certbotに処理を継続させます。指定されたTXT recordが正しく登録されていれば、 ./secrets
以下に api.nmatsui.work
に対するサーバ証明書が取得されます。
-------------------------------------------------------------------------------
Press Enter to Continue
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/api.nmatsui.work/fullchain.pem
...
Azure DNSからTXT recordを削除
サーバ証明書が取得できたので、登録したTXT recordは削除しておきます。
$ az network dns record-set txt remove-record --resource-group nmatsui_dns --zone-name nmatsui.work --record-set-name "_acme-challenge.api" --value "XXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Kubernetes上へAmbassadorを起動
サーバ証明書が準備できたので、Kubernetes上へAmbassadorを起動します。今回使ったyaml定義ファイルはgithub( https://github.com/nmatsui/kubernetes-ambassador )上にありますので、参考にしてください。
サーバ証明書をKubernetes secretに登録
取得した証明書をKubernetesのsecretへ、 ambassador-certs
として登録します。
$ kubectl create secret tls ambassador-certs --cert=$(PWD)/secrets/live/api.nmatsui.work/fullchain.pem --key=$(PWD)/secrets/live/api.nmatsui.work/privkey.pem
AmbassadorのServiceとPodを起動
LoadBalancer Serviceとして、Ambassadorを起動します(執筆時点での最新版は 0.31
でした)。とりあえずPODは3つ起動させていますが、KubernetesのNode数や負荷量で調整してください。
$ kubectl apply -f ambassador/ambassador.yaml
apiVersion: v1
kind: Service
metadata:
name: ambassador
creationTimestamp: null
labels:
service: ambassador
spec:
type: LoadBalancer
ports:
- name: ambassador
port: 443
targetPort: 443
selector:
service: ambassador
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ambassador
spec:
replicas: 3
template:
metadata:
labels:
service: ambassador
spec:
containers:
- name: ambassador
image: quay.io/datawire/ambassador:0.31.0
resources:
limits:
cpu: 1
memory: 400Mi
requests:
cpu: 200m
memory: 100Mi
env:
- name: AMBASSADOR_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
livenessProbe:
httpGet:
path: /ambassador/v0/check_alive
port: 8877
initialDelaySeconds: 30
periodSeconds: 3
readinessProbe:
httpGet:
path: /ambassador/v0/check_ready
port: 8877
initialDelaySeconds: 30
periodSeconds: 3
- name: statsd
image: quay.io/datawire/statsd:0.31.0
restartPolicy: Always
- ポート8877にアクセスするとAmbassadorの管理コンソールが開けますが、Internetに公開する意味はないのでこのポートはServiceに登録しません。
LoadBalancerのExternal IPをDNSに登録
起動したAmbassador ServiceのExternal IPをapi.nmatsui.work
で名前解決できるように、Azure DNSにA recordを登録します。
-
Ambassador ServiceのExternal IPを確認します。
check_externalip$ kubectl get services -l service=ambassador NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ambassador LoadBalancer 10.0.149.207 XX.YY.ZZ.WWW 443:32704/TCP 6m
-
このExternal IPに対して、
api.nmatsui.work
をA recordとして登録します。create_a_record$ az network dns record-set a add-record --resource-group nmatsui_dns --zone-name nmatsui.work --record-set-name "api" --ipv4-address "XX.YY.ZZ.WWW"
AmbassadorのTLS終端を確認
この段階ですでに、AmbassadorがTLSを終端してくれています。 api.nmatsui.work
にhttpsでアクセスすると、TLSで接続されていることが確認できます。
$ curl -v https://api.nmatsui.work
* Rebuilt URL to: https://api.nmatsui.work/
* Trying XX.YY.ZZ.WWW...
* TCP_NODELAY set
* Connected to api.nmatsui.work (XX.YY.ZZ.WWW) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: api.nmatsui.work
* Server certificate: Let's Encrypt Authority X3
* Server certificate: DST Root CA X3
> GET / HTTP/1.1
> Host: api.nmatsui.work
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< date: Mon, 23 Apr 2018 04:24:49 GMT
< server: envoy
< content-length: 0
<
* Connection #0 to host api.nmatsui.work left intact
httpをhttpsにリダイレクト
Ambassador Serviceの annotations
に tls
Moduleを設定すると、クライアント認証の設定やALPNプロトコルの設定など、AmbassadorのTLS設定をカスタマイズすることができます。たとえば次のように redirect_cleartext_from
を設定すれば、http(80)アクセスをhttps(443)にリダイレクトするようになります。詳細は公式ドキュメントを参照してください。
apiVersion: v1
kind: Service
metadata:
name: ambassador
creationTimestamp: null
labels:
service: ambassador
annotations:
getambassador.io/config: |
---
apiVersion: ambassador/v0
kind: Module
name: tls
config:
server:
enabled: True
redirect_cleartext_from: 80
spec:
type: LoadBalancer
ports:
- name: ambassador-tls
port: 443
targetPort: 443
- name: ambassador
port: 80
targetPort: 80
selector:
service: ambassador
---
apiVersion: extensions/v1beta1
kind: Deployment
...
$ curl -v http://api.nmatsui.work
* Rebuilt URL to: http://api.nmatsui.work/
* Trying XX.YY.ZZ.WWW...
* TCP_NODELAY set
* Connected to api.nmatsui.work (XX.YY.ZZ.WWW) port 80 (#0)
> GET / HTTP/1.1
> Host: api.nmatsui.work
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< location: https://api.nmatsui.work/
< date: Mon, 23 Apr 2018 04:49:40 GMT
< server: envoy
< content-length: 0
<
AmbassadorによるAPI Gateway
では実際に、Kubernetes上のServiceに対してAmbassadorを適用し、API Gatewayとして動作させてみましょう。
パスベースのルーティング
検証用にダミーのREST APIサービスを二つ起動し、パスベースのルーティングを検証します。
このダミーREST APIサービスは、どのようなパスに対しても環境変数として与えられたメッセージを返すシンプルなサービスです。詳細は、DockerHubのnmatsui/hello-world-apiを参照してください。
パスベースルーティングのポイントは、以下二つです。
- 各serviceの
annotations
にMapping
定義を追加することで、Ambassadorにパスベースのルーティングを指示することができます(今回の例ですと、/api1/
に到達したリクエストはhttp://dummy-restapi-1:3000
へ、/api2/
に到達したリクエストはhttp://dummy-restapi-2:8888
にルーティングされます)。 - Internetとの接続はAmbassadorが全て中継しますので、ダミーREST APIサービスは
ClusterIP
Serviceにし、外部からの直接アクセスを禁止します。
-
ルーティング定義を設定し、ダミーREST APIサービスを起動する。
start_dummyapi$ kubectl apply -f dummy-restapi/dummy-restapi.yaml
dummy-restapi.yamlapiVersion: v1 kind: Service metadata: name: dummy-restapi-1 labels: service: dummy-restapi-1 annotations: getambassador.io/config: | --- apiVersion: ambassador/v0 kind: Mapping name: dummy-restapi-1 prefix: /api1/ service: http://dummy-restapi-1:3000 spec: type: ClusterIP selector: pod: dummy-restapi-1 ports: - name: dummy-restapi-1 port: 3000 targetPort: 3000 --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: dummy-restapi-1 spec: replicas: 3 template: metadata: labels: pod: dummy-restapi-1 spec: containers: - name: hello-world-api image: nmatsui/hello-world-api:latest env: - name: PORT value: "3000" - name: MESSAGE value: "dummy restapi 1" ports: - name: dummy-restapi-1 containerPort: 3000 # dummy-restapi-2も同様
-
パスベースのルーティングを確認する。
dummy-restapi-1$ curl -i https://api.nmatsui.work/api1/ HTTP/1.1 200 OK content-type: application/json date: Mon, 23 Apr 2018 05:38:43 GMT x-envoy-upstream-service-time: 2 server: envoy transfer-encoding: chunked {"message":"dummy restapi 1"}
dummy-restapi-2$ curl -i https://api.nmatsui.work/api2/foo/ HTTP/1.1 200 OK content-type: application/json date: Mon, 23 Apr 2018 05:38:59 GMT x-envoy-upstream-service-time: 1 server: envoy transfer-encoding: chunked {"message":"dummy restapi 2"}
- Serviceのannotationで指示した設定はリアルタイムにAmbassadorに反映されますので、Ambassadorサービスを再起動することなく、リクエストがダミーREST APIサービスへルーティングされることが確認できます。
認証認可
次に、Ambasssadorを用いた認証認可について検証します。Ambassadorは、ターゲットとなるサービスにリクエストをルーティングする前に、認証認可用のサービスを割り込ませ、リクエストをフィルタリングすることができます。
この認証認可用のサービスは、以下の要求を満たすように自分で作る必要があります。DB等を使って自力で認証認可を行っても良いですし、外部のIdPを使って認証認可を行っても良いでしょう。
- Ambassadorは、クライアントからリクエストされたRequest Headerとパスを用いて、認証認可用サービスにPOSTでリクエストを行います。
- 認証認可用サービスは、そのリクエストを受け付けるならば
200 OK
を返します。そうすると、Ambassadorが実際のサービスにリクエストをルーティングします。 - 認証認可用サービスが
200 OK
以外を返すと、実際のサービスへのルーティングは行われません。クライアントへは、認証認可サービスがAmbassadorへ返したレスポンスが(Ambassadorを経由して)返されます。 - 必要であれば、実際のサービスが返すResponse Headerへ、認証認可サービスが追加のHeaderを付与することもできます。
今回は、検証用に作成したnmatsui/bearer-auth-apiというコンテナで認証認可サービスを立ち上げます。
この認証認可サービスは、環境変数
AUTH_TOKEN
経由で与えられるTOKEN定義を元にして、Bearer認証とパスベースの認可を行うものです。詳細はnmatsui/bearer-auth-apiを参照してください。
-
TOKEN定義(
secrets/auth-tokens.json
)を作成する。- 二つのTOKENを設定し、最初のTOKENは
/api1/*
も/api2/*
も使え、二つ目のTOKENは/api1/*
しか使えないというTOKEN定義です。
secrets/auth-token.json{ "Znda7iglaqdoltsp7kDl60TvkkszcEGU": ["^/api1/.*$", "^/api2/.*$"], "fANtLRTszYAayjtmLFllSHBrt2zRyoqV": ["^/api1/.*$"] }
- 二つのTOKENを設定し、最初のTOKENは
-
KubernetesのSecretにTOKENを定義を登録する。
store_tokens$ kubectl create secret generic auth-tokens --from-file=./secrets/auth-tokens.json
-
認証認可サービスを起動し、Ambassadorへ
authentication
Moduleとして登録する。start_auth_service$ kubectl apply -f bearer-auth/bearer-auth.yaml
bearer-auth.yamlapiVersion: v1 kind: Service metadata: name: bearer-auth labels: service: bearer-auth annotations: getambassador.io/config: | --- apiVersion: ambassador/v0 kind: Module name: authentication config: auth_service: "bearer-auth:8080" spec: type: ClusterIP selector: pod: bearer-auth ports: - name: bearer-auth port: 8080 targetPort: 8080 --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: bearer-auth spec: replicas: 3 template: metadata: labels: pod: bearer-auth spec: containers: - name: bearer-auth-api image: nmatsui/bearer-auth-api:latest env: - name: LISTEN_PORT value: "8080" - name: AUTH_TOKENS valueFrom: secretKeyRef: name: "auth-tokens" key: "auth-tokens.json" ports: - name: bearer-auth containerPort: 8080
-
認証認可を確認する。
no_auth$ curl -i https://api.nmatsui.work/api1/ HTTP/1.1 401 Unauthorized content-type: application/json; charset=utf-8 www-authenticate: Bearer realm="token_required" date: Mon, 23 Apr 2018 07:03:16 GMT content-length: 60 x-envoy-upstream-service-time: 2 server: envoy {"authorized":false,"error":"missing Header: authorization"}
valid_auth$ curl -i -H "Authorization: bearer fANtLRTszYAayjtmLFllSHBrt2zRyoqV" https://api.nmatsui.work/api1/ HTTP/1.1 200 OK content-type: application/json date: Mon, 23 Apr 2018 07:04:37 GMT x-envoy-upstream-service-time: 1 server: envoy transfer-encoding: chunked {"message":"dummy restapi 1"}
not_allowd$ curl -i -H "Authorization: bearer fANtLRTszYAayjtmLFllSHBrt2zRyoqV" https://api.nmatsui.work/api2/ HTTP/1.1 403 Forbidden content-type: application/json; charset=utf-8 www-authenticate: Bearer realm="token_required" error="not_allowed" date: Mon, 23 Apr 2018 07:06:09 GMT content-length: 41 x-envoy-upstream-service-time: 1 server: envoy {"authorized":false,"error":"not allowd"}
- ダミーREST APIサービスには何も手を加えずとも、TOKEN定義に設定されている認証認可情報に従ってリクエストのフィルタリングが行われていることが確認できます。
さいごに
ということで、Ambassadorを用いることで、複数のサービスに対して横串でTLS終端や認証認可機能を追加できました。Ambassadorには他にも、Rate LimitingやgRPCのサポートなどが含まれています。上手く活用して、幸せなKubernetesライフを過ごしましょう!