k8sとAPI Gateway(Ambassador)を用いたコンテナ管理 



こんにちは。kwashiです。SPAによるフロントエンドと、APIサーバの組み合わせで構築したシステムを実装したので紹介します。前職で、モノリシックなシステムに、大規模な機能を追加して苦労したため機能や構成の分離を意識して実装しました。もし、Ambassadorやk8sのマニフェストを参考にしたい場合は、記事の後半に書いています。

※ 実装したシステムは、何らかのサービスを提供する仕様ではなく、技術的な検証・ポートフォリオを目的として作成しました。

SPAは、サーバーサイド側でのHTMLレンダリングを行わず、サーバーサイド側の役割をAPIサーバーに徹底させるために用いています。各APIはサービスごとにコンテナ化することで分離し、コンテナ管理ツールで各サービスを管理することで、スケーリングやデプロイが容易にできるようにしています。また、各APIへのトラフィックをルーティングするためAPI Gatewayを使用しています。

上記のような構成にした場合、認証機能をAPIサーバーとフロントエンド両方でセキュアに実装する必要があり面倒なので、Firebase Authenticationという認証基盤を用いて認証させます。

使用技術の実装に関しては、他の記事との重複も多いため、参考も含め別のQiita記事を紹介することとし、本記事では、システムの概要、k8sでの管理やAPI Gateway Ambassadorの設定ファイルに関して、記述していきます。

※ 今回の実装では、データベースにアクセスする部分を隔離して実装し、DBに関する実装は省いています。


git: (k-washi/example-k8s-ambassador)[https://github.com/k-washi/example-k8s-ambassador]

# システム概要
<div align="center">
<img width="600" alt="overview2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481395/1fc37cc7-501a-2d87-91ca-e026b8cb0bf4.png">
</div>

図のように、クライアント側は、Vueを用いており、Nginxから発行されます。APIサーバーは、k8sで管理しており、認証が必要ないREST-API1とJWTの検証が必要なREST-API2、そして、JWTの検証を行うJWT-AutoOのDockerImageを実行しています。
また、ルーティング制御などを行うAPI GatewayとしてAmbassadorを用いています。

クライアント側では、Firebaseと連携し、ログイン、ユーサー登録、ユーザ情報の取得、更新を可能としています。
また、REST-API1では、簡易なJSONによるGET, POSTの機能を提供しています。
REST-API2は、ユーザー情報に基づいたサービス提供の例として、JWT依存の認証、認可を伴うユーザー情報を提供しています。REST-API2の認証、認可は、gRPCを用いてJWTをJWT-AuthOに送信し、JWT-AuthOでFirebaseと連携してJWTの検証を行っています。
また、ユーザidに紐付けてJWTに証明部分を保存しておき、認証タイミングで新たに発行されたJWTであるかどうか確認も行っています。

※基本的にJWTを用いる場合は、重要なデータは、管理すべきではないという意見もあります。そこで、クライアント側がFirebaseと連携し認証することで取得した新たなJWTを添えて、サーバーにアクセスした場合に、JWT-AuthOでは認証の状態であると認識し、最新のJWTであるかどうかに関わらず、JWTが送られてきた場合は認可の状態であると認識するような仕組みにしています。
つまり、重要なデータにアクセスするときは、常にクライアント側がFirebaseと連携した認証を行うということになります。
（このJWT周りに関しては議論すべきであると思います、、、）

API Gatewayは以下のルーティングを制御しています。また、各APIに対するDocker ImageとGitも以下の通りです。
※API Gatewayのportは30000に設定している。

```bash
- path: "/:80"
 - image: kwashizaki/example-vue-cli
 - git: https://github.com/k-washi/example-vue-cli.git
 - msg:vueにより構築したフロントエンド
 
 
- paths: ["/api/ex-golang/rest-api/", "/api/ex-golang/health/"]
 - image: kwashizaki/example-golang-rest-api
 - git: https://github.com/k-washi/example-golang-rest-api.git
 - msg: REST-API1が提供するAPI(GET, POSTで文を提供、保存, & healthでstatus 200を返答)

- paths: ["/api/ex-jwt/jwt/ex-jwt-auth", "/api/ex-jwt/auth/ex-authentication"]
 - images: kwashizaki/example-golang-jwt-auth-client:v1.0.0
 - git: https://github.com/k-washi/example-golang-jwt-auth/tree/master/testApp
 - msg: REST-API2が提供するJWT-AuthOによるJWT検証を伴うユーザー情報の提供

- paths: ["/ex-jwt-sr/"] #gRPC
 - image: kwashizaki/example-golang-jwt-auth-server:v1.0.0
 - git: https://github.com/k-washi/example-golang-jwt-auth
 - msg: JWT-AuthOにおけるJWT検証( gRPCサーバー)
```

# 使用技術の概要

次に、使用した技術を紹介します。私が以前書いた記事にリンクを飛ばしているので、参考にしてみてください。
個人的には、golangの記事が気に入っているのでぜひ！！

## Vue.js

現在人気のJavaScriptフレームワークです。個人的に、Reactの非同期処理に使用するRedux-sagaの学習コストが高いと感じ、また、使用感もVue.jsの方が良かったのでVue.jsを選択しました。
フロントエンドにおいてSPAを作成するために使用。ルーティングにvue-router, ユーザー名など全体で使用する状態の管理にVuex, UIコンポーネントフレームワークとしてVuetifyを用いました。

```bash
# vue --version
3.10.0
```

基本的には、以下の私の過去記事を拡張した実装にしている。(Git: https://github.com/k-washi/example-vue-cli.git )
Qiita: [Vue.js (Vuex, vue-router, vuetify) とFirebaseで始めるユーザー管理](https://qiita.com/kwashi/items/0bc56d79f7d198396a3f)


## Golang

Pythonを使い続けていたのですが、静的型付け言語も使用してみたかったので最近流行っているGolangをバックエンドの言語として選択。
並列処理が書きやすく、標準パッケージのサポートが強力。
本システムでは、各サービスをコンテナ化しており、コンテナを軽量化できるという利点がある。

例えば、今回Golangで作成したサービスは、以下のように20MB程度でDockerコンテナ化できる。

```bash
REPOSITORY                                  TAG                 IMAGE ID            CREATED             SIZE
kwashizaki/example-golang-jwt-auth-server   v1.0.0              4cf9b4595b01        7 hours ago         23.9MB
kwashizaki/example-golang-jwt-auth-client   v1.0.0              baa4bce6c5fe        44 hours ago        24.5MB
kwashizaki/example-golang-rest-api          v1.0.0              8d92d819d8ad        8 days ago          22.6MB
```

また、Webフレームワークとして、軽量かつシンプルなginを使用した。

本システムのAPIは、基本的には、私が以前書いた[はじめてのGolang Webアプリケーション ~ テスト, Dockerコンテナ化まで](https://qiita.com/kwashi/items/b02894c407a3fefd6909)と同様の構成で実装している。



## Firebase

Firebaseは、Googleが提供している、MBaas(Mobile Backend as a service)の一つです。機能として、データーベースや認証があり、本システムでは、認証機能であるFirebase Authenticationを使用した。趣味程度の範囲では無料で使用できます。

Firebase Authenticationは、フロントエンドにおいて認証の結果、JWT((Json Web Token))を送り返す。
サーバー側は、フロントエンドから送られてきたJWTを用いてFirebaseに検証してもらい、有効なJWTかどうか判断することが可能である。
つまり、サーバー側で、ログイン済のユーザーかどうか、JWTを用いて検証できる。

※ サーバー側では、検証に必要なパラメータを定義したex-firebase-auth-firebase-adminsdk-xxxx.jsonファイルが必要である。
  firebase SDKにて提供されている。

また、Golangによる実装は、[「Vue.js + Go言語 + Firebase 」で始める! Frontend & Backend API 両方で認証するセキュアなSPA開発ハンズオン!](https://qiita.com/po3rin/items/d3e016d01162e9d9de80)を参考にした。


## JWT

以下のフォーマットに従って構成された文字列である。

```bash
{base64エンコードしたhead１er}.{base64エンコードしたclaims}.{署名}
```

発行者(今回は、Firebase)が鍵を使用して、JSONに署名することでトークンとして扱うことができる。また、鍵を使用して検証することで改善を検知できる。
claims部には、ユーザー名などの任意の情報を含めることができる。

JWTを分解してclaims部分を抽出、そして、Firebaseにおける情報をデコードするライブラリを、[k-washi/jwt-decode](https://github.com/k-washi/jwt-decode)に作成した。

Qiita: [Golang によるfirebase AuthenticationにおけるJWT解析](https://qiita.com/kwashi/items/ca83a23c54ce0c0636ee)


## nginx

OSとアプリケーションソフトウェアとアプリケーションとの仲立ちをするミドルウェアの一つで、HTTPリクエストなどを送ったときに、レスポンスを返すWebサーバーソフトウェア。リバースプロキシやロードバランサ機能があり、Apacheと比較して、早くて高付加に強い。

SPAを配布するためのWebサーバーとして用いた。

Qiita: [Vue.jsプロジェクトにおけるnginxの設定とDockerによるコンテナ化の例](https://qiita.com/kwashi/items/da5420dca2d1b3c53ee7)


## gRPC

gRPCは、RPC(Remoto Procedure Call)を実現するためにGoogleが開発したプロトコルの一つです。Protocol Buffersを使ってデータをシリアライズし、高速な通信を実現できる点が特徴です。gRPCに関しては、[gRPCって何?](https://qiita.com/oohira/items/63b5ccb2bf1a913659d6)が参考になりました。

本システムでは、バックエンドにおいてサービス間の通信に使用しました。

Qiita: [Golangで始めるgRPC](https://qiita.com/kwashi/items/533bbb7d09e723c8b56f)

## Docker

ホストマシンのカーネルを利用しプロセスやユーザなどを隔離することで、あたかも別のマシンが動いているかのように動かします。そのため、軽量で高速に仮想環境を起動、停止などが可能です。

私が以前書いた[はじめてのGolang Webアプリケーション ~ テスト, Dockerコンテナ化まで](https://qiita.com/kwashi/items/b02894c407a3fefd6909)という記事にGolangのDockerコンテナ化の方法を載せています。

今回、システムに用いたDockerImageは以下の通りです。

```bash
REPOSITORY                                  TAG                 IMAGE ID            CREATED             SIZE
kwashizaki/example-vue-cli                  v1.0.0              7d1f0394bec3        2 hours ago         25.3MB
kwashizaki/example-golang-jwt-auth-server   v1.0.0              4cf9b4595b01        7 hours ago         23.9MB
kwashizaki/example-golang-jwt-auth-client   v1.0.0              baa4bce6c5fe        44 hours ago        24.5MB
kwashizaki/example-golang-rest-api          v1.0.0              8d92d819d8ad        8 days ago          22.6MB
quay.io/datawire/ambassador                 0.83.0              d8caf63d933c        3 weeks ago         691MB
```



## Kubernetes(k8s)

自動デプロイ、スケーリング、アプリ・コンテナの運用自動化のために設計されたオープンソースのプラットフォームです。
本記事では、本システムのk8s設定方法に関して、説明していきます。



## Ambassador



[公式](https://www.getambassador.io/)より

Ambassadorはマイクロサービス用のAPIGatewayです。

### 機能


+ AWS APIGatewayのようなAPIGatewayのホスティング機能
+ Kongのような伝統的なAPIGateway機能
+ Nginxや, Envoy, k8sのIngressのようなProxy機能

### 具体的な機能

+ 細かなルーティング制御、正規表現ベースのルーティング、ホストルーティングなどが可能
+ 認証
+ gRPC, HTTP/2をサポート
+ カナリアリリース
+ シャドートラッキング機能
+ 特定サービスへのL7トラフィックの透過的な監視

### 運用者(Ops)の観点

+ ルーティングとスケーリングをEnvoyとKubernetesに依存しているので、展開と操作が簡単
+ TLSTerminationとリダイレクトを広範囲に運用可能
+ トラブルシューティング時に統合的に診断する事が可能
+ 複数の異なるバージョンのAmbassadorを運用出来て、簡単にテスト、更新する事が可能
+ Istioと連携してサービスメッシュ化が可能

### 内部実装

+ k8sを最大限に利用し、信頼性(reliability), 可用性(availability), スケーラビリティ(scalability)を担保
+ 状態管理の為に、データベースのようなストレージを必要とせず、Kubernetes内に全ての状態管理を維持
+ スケール時は、k8sのReplicasetでレプリカ数を変更するか、horizontal pod autoscalerを利用する事で簡単にスケール事が可能
+ EnvoyProxyを使用して、全てのトラフィックのルーティングとプロキシーを実行



# プログラム等のVersion

```bash
# go version
go version go1.12.7 darwin/amd64

# docker -v
Docker version 19.03.2, build 6a30dfc

# kubectl version
>lient Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.6", GitCommit:"96fac5cd13a5dc064f7d9f4f23030a6aeface6cc", GitTreeState:"clean", BuildDate:"2019-08-19T11:13:49Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.6", GitCommit:"96fac5cd13a5dc064f7d9f4f23030a6aeface6cc", GitTreeState:"clean", BuildDate:"2019-08-19T11:05:16Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}

# vue --version
3.10.0
```

# k8s マニフェスト

本章では、以下のようなpod, service, deploymentsを立ち上げる。
サービスを見て分かるように、localhost:30000としてExternal ipが設定されている。
一方で、

```bash
kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
ambassador-55d75bc95b-29ckv   1/1     Running   0          3m59s
ambassador-55d75bc95b-2lxzv   1/1     Running   0          3m27s
ambassador-55d75bc95b-9vjnz   1/1     Running   0          2m18s
ex-go-5c5747dbdb-ddkpx        1/1     Running   1          5d21h
ex-jwt-cl-576556588d-7sdgc    1/1     Running   1          5d21h
ex-jwt-sr-78bdf9f4d4-6dmft    1/1     Running   1          5d21h

kubectl get svc
NAME               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)           AGE
ambassador         LoadBalancer   10.97.238.197    localhost     30000:30685/TCP   5d21h
ambassador-admin   NodePort       10.103.162.214   <none>        8877:31915/TCP    5d21h
ex-go              ClusterIP      10.101.114.104   <none>        8080/TCP          5d21h
ex-jwt-cl          ClusterIP      10.106.203.56    <none>        8080/TCP          5d21h
ex-jwt-sr          ClusterIP      10.99.209.88     <none>        8080/TCP          5d21h
kubernetes         ClusterIP      10.96.0.1        <none>        443/TCP           92d

kubectl get deployment
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
ambassador   3/3     3            3           5d22h
ex-go        1/1     1            1           5d22h
ex-jwt-cl    1/1     1            1           5d22h
ex-jwt-sr    1/1     1            1           5d22h
```

## Secret機能

k8sのSecret機能を用いて、Firebaseの設定ファイルなど漏らしたくない秘密情報を設定する。

```text:env/env-secret.txt
google_app_creds=/tmp/ex-firebase-auth-firebase-adminsdk-xxxxxxx.json
```
のファイルを作成し、

```bash
kubectl create secret generic --save-config firebase-secret --from-env-file ./env/env-secret.txt


```

のコマンドを用いて以下の、ファイルの設定を読み込む。
確認は、

```bash
kubectl get secret
#NAME                     TYPE                                  DATA   AGE
#firebase-secret          Opaque                                1      5d21h
```

ここで設定したファイルのパスへ、DockerImageをデプロイするときに、Firebase SDKの設定ファイルをコピーする。

## API Gateway Ambassadorの設定

k8sはRBACという、各種リソースへのアクセス権限を管理する仕組みです。それが有効であるとして、公式で提供されている、manifestをインストールします。

```bash
kubectl apply -f https://getambassador.io/yaml/ambassador/ambassador-rbac.yaml
```
一応Git:[example-k8s-ambassador/ambassador/ambassador-rbac.yaml](https://github.com/k-washi/example-k8s-ambassador/blob/master/ambassador/ambassador-rbac.yaml)に、実際に使用したプログラムをおいています。

基本的には、
1. ambassadorというサービスアカウントとサービス等に権限を与えるClusterRoleをClusterRoleBindingで紐付け
2. ambassadorコンテナを使用するport番号(http:80, admin:8877)で作成し、レプリカ数3でDeploymentを作成。
3. NodoPortとして、port(admin:8877)を紐付けてサービスを作成
を行っている。


NodePortととしてServiceを作成したので、以下のようにLoodBalancerを設定している。
httpのportは30000に上書きし、コンテナで受け付けるポートを8080に設定している。

```bash
kubectl apply -f ambassador/ambassador-service.yaml 

```

```yaml:ambassador/ambassador-service.yaml 
---
apiVersion: v1
kind: Service
metadata:
  name: ambassador
spec:
  type: LoadBalancer
  #externalTrafficPolicy: Local
  ports:
   - name: http
     port: 30000
     targetPort: 8080
  selector:
    service: ambassador
```

## Config Map

k8s内で使用する変数を設定している。
このあと、各サービスをデプロイするので、その際の変数として使用する。

```bash
kubectl apply -f example-golang-vue/example-jwt-config.yaml
```

```yaml:example-golang-vue/example-jwt-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ex-jwt-map
data:
  origin.host: localhost
  #数字は, ""で囲む
  origin.port: "80"
  ambassador.host: ex-jwt-sr
  ambassador.port: "8080"
  jwtserver.host: localhost
  jwtserver.port: "50051"
```

## コンテナのデプロイ

getambassador.io/configにルーティングの設定を行っている。
そこでは、Ambassadorを経由し、http://ex-go:8080としてリクエストするように設定している。
このex-goは、Deploymentに合わせている。

```bash
kubectl apply -f example-golang-vue/example-golang.yaml 
```

```yaml:example-golang-vue/example-golang.yaml 
apiVersion: v1
kind: Service
metadata:
  name: ex-go
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v0
      kind:  Mapping
      name:  ex-health-map
      prefix: /api/ex-golang
      service: http://ex-go:8080
spec:
  type: ClusterIP
  ports:
  - name: cl-ip-ex-go
    port: 8080
    targetPort: 8080
  selector:
    app: ex-go

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ex-go
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ex-go
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ex-go
    spec:
      containers:
      - name: ex-go-ui
        image: kwashizaki/example-golang-rest-api:v1.0.0
        ports:
        - name: ex-go
          containerPort: 8080
        resources:
          limits:
            cpu: "0.1"
            memory: 100Mi
```

他のサービス、デプロイメントのマニフェストもデプロイする。他のサービスも、上のマニフェストと同様に設定している。
コンテナ内で使用する環境変数として、env配下に設定している。


また、Firebaseの設定に関してはデプロイ時に、Volume配下のパスのファイルをVolumeMountのパスへコピーしている。
これによって、DockerImage内に秘匿すべき設定ファイルを含む必要がなくなる。

gRPCに関しては、ルーティングの制御が特殊で、getambassador.io/confiのserviceを ex-jwt-srとしスキームを書く必要がない。
また、/jwtauth.JwtService/は、gRPCのプロトコルをgolang用に変換したファイルに記載されている。


```bash
kubectl apply -f example-golang-vue/example-jwt-server.yaml
kubectl apply -f example-golang-vue/example-jwt-client.yaml
```

```yaml:example-golang-vue/example-jwt-client.yaml
apiVersion: v1
kind: Service
metadata:
  name: ex-jwt-cl
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v0
      kind:  Mapping
      name:  ex-jwt-cl
      prefix: /api/ex-jwt
      service: http://ex-jwt-cl:8080
spec:
  type: ClusterIP
  ports:
  - name: cl-ip-ex-jwt
    port: 8080
    targetPort: 8080
  selector:
    app: ex-jwt-cl

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ex-jwt-cl
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ex-jwt-cl
    spec:
      containers:
      - name: ex-jwt-ui
        image: kwashizaki/example-golang-jwt-auth-client:v1.0.0
        ports:
        - name: ex-jwt-cl
          containerPort: 8080
        resources:
          limits:
            cpu: "0.1"
            memory: 100Mi
        env:
          - name: ORIGIN_HOST
            valueFrom:
              configMapKeyRef:
                name: ex-jwt-map
                key: origin.host
          - name: ORIGIN_PORT
            valueFrom:
              configMapKeyRef:
                name: ex-jwt-map
                key: origin.port
          - name: AMBASSADORHOST
            valueFrom:
              configMapKeyRef:
                name: ex-jwt-map
                key: ambassador.host
          - name: PORT
            valueFrom:
              configMapKeyRef:
                name: ex-jwt-map
                key: ambassador.port
```


```yaml:example-golang-vue/example-jwt-server.yaml
apiVersion: v1
kind: Service
metadata:
  name: ex-jwt-sr
  annotations:
    getambassador.io/config: |
      ---
      apiVersion: ambassador/v0
      kind:  Mapping
      name:  ex-jwt
      grpc: True
      prefix: /jwtauth.JwtService/
      rewrite: /jwtauth.JwtService/
      service: ex-jwt-sr
spec:
  type: ClusterIP
  ports:
  - name: cl-ip-ex-jwt-sr
    port: 8080
    targetPort: 50051
  selector:
    app: ex-jwt-sr

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ex-jwt-sr
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ex-jwt-sr
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ex-jwt-sr
    spec:
      containers:
      - name: ex-jwt-sr
        image: kwashizaki/example-golang-jwt-auth-server:v1.0.0
        ports:
        - name: ex-jwt-sr-api
          containerPort: 50051
        resources:
          limits:
            cpu: "0.1"
            memory: 100Mi
        env:
          - name: AMBASSADORHOST
            valueFrom:
              configMapKeyRef:
                name: ex-jwt-map
                key: jwtserver.host
          - name: PORT
            valueFrom:
              configMapKeyRef:
                name: ex-jwt-map
                key: jwtserver.port
          - name: GOOGLE_APPLICATION_CREDENTIALS
            valueFrom:
              secretKeyRef:
                name: firebase-secret
                key: google_app_creds
        volumeMounts:
          - name: firebase-creds
            mountPath: /tmp #firebase-auth-credファイルを置く場所
            readOnly: true
      volumes:
      - name: firebase-creds
        hostPath:
          path: /Users/washizakikai/DevLocal/git/kwashi/example-k8s-ambassador/env
            
                
```

# まとめ

ここでは、k8sによるコンテナ管理とAPI Gateway であるambassadorの設定をしめし、コンテナ間で連携したマイクロサービスの例を示した。
他のQiita記事と重複をさけ、特にk8sのマニフェストに関して説明している。

もし、VueやGolangの設定が気になる方は、記事の途中に参照した記事をぜひ見てみてください。