こんにちは。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]
システム概要
図のように、クライアント側は、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に設定している。
- 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を用いました。
# vue --version
3.10.0
基本的には、以下の私の過去記事を拡張した実装にしている。(Git: https://github.com/k-washi/example-vue-cli.git )
Qiita: Vue.js (Vuex, vue-router, vuetify) とFirebaseで始めるユーザー管理
Golang
Pythonを使い続けていたのですが、静的型付け言語も使用してみたかったので最近流行っているGolangをバックエンドの言語として選択。
並列処理が書きやすく、標準パッケージのサポートが強力。
本システムでは、各サービスをコンテナ化しており、コンテナを軽量化できるという利点がある。
例えば、今回Golangで作成したサービスは、以下のように20MB程度でDockerコンテナ化できる。
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コンテナ化までと同様の構成で実装している。
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開発ハンズオン!を参考にした。
JWT
以下のフォーマットに従って構成された文字列である。
{base64エンコードしたhead1er}.{base64エンコードしたclaims}.{署名}
発行者(今回は、Firebase)が鍵を使用して、JSONに署名することでトークンとして扱うことができる。また、鍵を使用して検証することで改善を検知できる。
claims部には、ユーザー名などの任意の情報を含めることができる。
JWTを分解してclaims部分を抽出、そして、Firebaseにおける情報をデコードするライブラリを、k-washi/jwt-decodeに作成した。
Qiita: Golang によるfirebase AuthenticationにおけるJWT解析
nginx
OSとアプリケーションソフトウェアとアプリケーションとの仲立ちをするミドルウェアの一つで、HTTPリクエストなどを送ったときに、レスポンスを返すWebサーバーソフトウェア。リバースプロキシやロードバランサ機能があり、Apacheと比較して、早くて高付加に強い。
SPAを配布するためのWebサーバーとして用いた。
Qiita: Vue.jsプロジェクトにおけるnginxの設定とDockerによるコンテナ化の例
gRPC
gRPCは、RPC(Remoto Procedure Call)を実現するためにGoogleが開発したプロトコルの一つです。Protocol Buffersを使ってデータをシリアライズし、高速な通信を実現できる点が特徴です。gRPCに関しては、gRPCって何?が参考になりました。
本システムでは、バックエンドにおいてサービス間の通信に使用しました。
Qiita: Golangで始めるgRPC
Docker
ホストマシンのカーネルを利用しプロセスやユーザなどを隔離することで、あたかも別のマシンが動いているかのように動かします。そのため、軽量で高速に仮想環境を起動、停止などが可能です。
私が以前書いたはじめてのGolang Webアプリケーション ~ テスト, Dockerコンテナ化までという記事にGolangのDockerコンテナ化の方法を載せています。
今回、システムに用いたDockerImageは以下の通りです。
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
公式より
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
# 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が設定されている。
一方で、
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の設定ファイルなど漏らしたくない秘密情報を設定する。
google_app_creds=/tmp/ex-firebase-auth-firebase-adminsdk-xxxxxxx.json
のファイルを作成し、
kubectl create secret generic --save-config firebase-secret --from-env-file ./env/env-secret.txt
のコマンドを用いて以下の、ファイルの設定を読み込む。
確認は、
kubectl get secret
#NAME TYPE DATA AGE
#firebase-secret Opaque 1 5d21h
ここで設定したファイルのパスへ、DockerImageをデプロイするときに、Firebase SDKの設定ファイルをコピーする。
API Gateway Ambassadorの設定
k8sはRBACという、各種リソースへのアクセス権限を管理する仕組みです。それが有効であるとして、公式で提供されている、manifestをインストールします。
kubectl apply -f https://getambassador.io/yaml/ambassador/ambassador-rbac.yaml
一応Git:example-k8s-ambassador/ambassador/ambassador-rbac.yamlに、実際に使用したプログラムをおいています。
基本的には、
- ambassadorというサービスアカウントとサービス等に権限を与えるClusterRoleをClusterRoleBindingで紐付け
- ambassadorコンテナを使用するport番号(http:80, admin:8877)で作成し、レプリカ数3でDeploymentを作成。
- NodoPortとして、port(admin:8877)を紐付けてサービスを作成
を行っている。
NodePortととしてServiceを作成したので、以下のようにLoodBalancerを設定している。
httpのportは30000に上書きし、コンテナで受け付けるポートを8080に設定している。
kubectl apply -f 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内で使用する変数を設定している。
このあと、各サービスをデプロイするので、その際の変数として使用する。
kubectl apply -f 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に合わせている。
kubectl apply -f 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用に変換したファイルに記載されている。
kubectl apply -f example-golang-vue/example-jwt-server.yaml
kubectl apply -f 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
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の設定が気になる方は、記事の途中に参照した記事をぜひ見てみてください。