GKEハンズオンをつくってみた -Vue.js + Spring Boot アプリケーション-
Kubernetesについて学んだことのアウトプットとして自分で簡単なMVCアプリ(Vue.js + Spring Boot + MySQL)をGKEにデプロイしてみました。以下にソースコードアップしてます。(期待通りの動きしなかったらGithub上で教えてください)
ソースコードを見れば動きは大抵分かりますが、作成した中での設計ポイントについて以下解説していきます。シンプルなMVCアーキテクチャですが、ドキュメントには設計方針的な部分がまとめて書かれていないので、自分も迷った部分などをシェアハピできれば嬉しいです。
GitHub - yellow-high5/easy-mvc-gke: Easy MVC Application for Google Kubernetes Engine
設計した流れ
「開発環境ではDocker環境上で実行できる(docker-compose up
一発で立ち上げ)。本番環境ではシェルスクリプト一発でGKE上にアプリケーションをデプロイして実行できる」という理想を念頭に置いて設計しました。
ローカル上ではDBコンテナを簡単に立ててOKですが、GKE上では永続化するストレージはきちんとKubernetesクラスターの外部に持っておくことが一般的のようです。詳しくはコンテナ運用のおすすめの方法にも記載されていますが、コンテナ内部をステートレスに保つことが重要になってきます。また、永続ストレージにアクセスするためのユーザー名やパスワードはシークレットリソースで定義しておきます。
配置構成については、Redisなどのキャッシュ用途が強く、コンテナと結合度が高いコンポーネントがあったりすると配置が変わってくるかもしれません...。
ローカル環境
ローカル環境での構築は、ごく普通に各アプリのフォルダごとにDockerfileをビルドして実行するdocker-compose.ymlを作成していくだけです。
Dockerfileの記述
それぞれVue.jsのコンテナ化とSpringBootのコンテナ化についてのドキュメントが非常に分かりやすいです。npmはマルチステージビルドを利用しており、SpringBootは事前にシェル上でビルドしてからコンテナ化しているケースが紹介されています。
Vue.jsはビルドして静的コンテンツとしてNGINXにデプロイしてしまうと、APIへのURLなどは動的に変更できなくなるのでビルド段階で環境変数を渡しておくようにします。
# ビルド環境
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG API_PATH
ENV VUE_APP_API_ORIGIN=${API_PATH}
RUN npm run build
# 本番環境(NGINX)
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Cloud Shellは起動時にすでにmvnコマンドまたはgradleコマンドがあるので、本番実行用のシェルスクリプトにmvn, gradleが使えちゃいます。自分はこれを知らず、「Cloud Shellにmvnをインストールするのはダルい」と勘違いしていて、SpringBootのDockerfileでマルチステージビルドを利用しています。Javaはnpmと違ってビルド時間が長いので、もしかしたらシェル上でビルドした方が良かったのかもしれません。どちらにせよ、ビルドをコンテナでするか、シェル上でするかの違いになります。
# ビルド環境
FROM maven:3.6-jdk-8 as build-stage
WORKDIR /app
COPY src /app/src
COPY pom.xml /app
RUN mvn install
# 本番環境
FROM openjdk:8-jdk-alpine
COPY --from=build-stage /app/target/*.jar app.jar
COPY wait-for ./
RUN chmod 700 ./wait-for
# ローカル環境では、docker-composeのentrypointに上書きされる
ENTRYPOINT ["java","-Dspring.profiles.active=gke","-jar","app.jar"]
# mvnがインストールされているならmvn install実行後に以下でDockerビルドした方が早い
# FROM openjdk:8-jdk-alpine
# VOLUME /tmp
# ARG JAR_FILE=target/*.jar
# COPY ${JAR_FILE} app.jar
# ENTRYPOINT ["java","-Dspring.profiles.active=gke","-jar","app.jar"]
DB接続情報については、Spring Bootの定義ファイルのresourcesフォルダ配下のapplication.yml(application.properties)に記載します。dockerで構築する場合とgkeで構築する場合で接続情報を分けておき、-Dspring.profiles.active
プロパティで起動環境を指定する形にしています。
コンテナ間の接続
あとはコンテナ構築で厄介になるのが①Client-API間、②API-DB間のコネクションです。
①については、ソースコードを見てもらうとわかると思いますが、環境変数で定義して繋いでいます。
client:
build:
context: ./vuejs-app-gke/.
args:
- API_PATH=http://localhost:8080
container_name: vuejs
environment:
NODE_ENV: development
ports:
- "80:80"
networks:
- frontend
②については、DBが起動してからでないとアプリ側のコネクションエラーが発生してしまうので、DBの準備完了まで待つようにwait-for-itやdockerizeのようなラッパー用のスクリプトを利用するべきらしく、自分はこのスクリプトを使ってます。depends_on
などのdocker-compose上で依存関係を指定していても、DB起動までは待ってくれてもDB準備完了までは待ってくれないようです。
api:
build: ./springboot-app-gke/.
container_name: springboot
depends_on:
- "database"
# DockerfileにあるENTRYPOINTを上書きしている
entrypoint: sh -c "./wait-for database:3306 -t 60 -- java -Dspring.profiles.active=docker -jar app.jar"
ports:
- "8080:8080"
networks:
- frontend
- backend
GKE
Kubernetesクラスターの作成からクラスターへのアプリデプロイまでの大まかな流れは以下の感じでした。
Kubernets Engine APIとService Networking APIをEnabledにする必要があります。
1. VPCネットワーク作成
2. サブネット作成
3. ファイアウォール設定
4. Kubernetesクラスターを作成
5. VPCネットワーク内でIPアドレス範囲を割り当てる
6. プライベート接続の作成
7. CloudSQL MySQLインスタンス(プライベートIPアドレス割り当て)の作成
8. YAMLファイル書き換え
9. ConfigMapを作成
10. API(SpringBoot)イメージをビルド&プッシュ
11. kubectlでバックエンド用のリソースを適用
12. フロントエンド(Vue.js)イメージをビルド&プッシュ
13. kubectlでフロントエンド用のリソースを適用
1-7については、GCPの一般的なドキュメントを読めば書いてあることを実行していくだけなので詳細は省略します。
ポイントとしてはCloudSQL用のプライベートIPを取得するところだと思います。ローカルではコンテナをホストに公開して開発を行った方が良いですが、本番のGCP内部ではDBへのアクセスはプライベートIPを使って接続するのが一般的です。
また、今回GKE上に作ったAPIは外部公開されてしまう仕様となっていますが、内部APIとして公開するにはCloud IAP for GKEの有効化が必要になるようです。
8-13については、Kuberntes上でアプリ構築とそれに対するリソースを定義を行っています。プロジェクト構成はmanifestsフォルダに各リソースのYAMLファイルを作り、サービス単位(FrontendとBackend)でそれらをまとめたものをall-in-oneフォルダに格納しています。
バックエンドサービスが先に立ち上がってIPを確立してくれないと、フロントエンドのアクセスする対象のIPが見つからなくなるので、シェル内での実行順序は試行錯誤しました。
イメージビルドとGCRヘプッシュ
Dockerfileをビルドしてプッシュするには、GKEのチュートリアルだとgcloud builds submit
コマンドを使って構築しているケースがよく見られましたが、今回はdockerコマンドを使ってます。理由はフロントエンドビルド時にAPIのURLを--build-arg
で渡したかったからです。gcloud builds submit
でもDockerfileに変数を渡せるのであれば、ぜひ知りたいです...。
# echo "=> STEP10: Build and Push Container Image -Backend-"
docker build --tag gcr.io/${PROJECT_ID}/springboot-app-gke springboot-app-gke
docker push gcr.io/${PROJECT_ID}/springboot-app-gke
# echo "=> STEP12: Build and Push Container Image -Frontend-"
docker build --build-arg API_PATH=http://${API_SERVICE_IP}:${API_SERVICE_PORT} --tag gcr.io/${PROJECT_ID}/vuejs-app-gke vuejs-app-gke
docker push gcr.io/${PROJECT_ID}/vuejs-app-gke
ワークロードとサービスの定義
Podをデプロイするリソースをワークロードと呼んでいます。ReplicationControllerを使って、フロントエンドもバックエンドもそれぞれ3つのPodを立てるようにしています。
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
spec:
replicas: 3
template:
metadata:
labels:
app: easy-mvc
tier: frontend
spec:
containers:
- name: vue
image: gcr.io/YOUR_PROJECT_ID/vuejs-app-gke
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 80
ワークロードの作成が終わったら、外部からの接続ができるようにサービスを定義します。spec.selector.app
で先ほど建設したワークロードを指定してあげれば、あっという間に外部IPを取得してくれます。
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
app: easy-mvc
tier: frontend
spec:
type: LoadBalancer
ports:
- port: 80
selector:
app: easy-mvc
tier: frontend
あとはkubectl apply -f
を使えば、リソースが作成されます。GUIコンソール上でも作成できますが、やはり作成にはyamlファイルを自分で用意してコマンドを叩いた方が楽だと思います。GUIコンソールは、作成したリソースの確認に使うのが良いでしょう。
SecretとConfigMap
SecretオブジェクトとConfigMapオブジェクトについては、ドキュメントでも説明されていますが、シンプルに以下のような棲み分けで考えました。
Secret | ConfigMap |
---|---|
機密度が高いDB認証やSSL認証 | Pod実行に必要な引数や変数 |
Secretリソースの定義には、パスワードをbase64でエンコードしてKey-Value形式で保存します。
apiVersion: v1
kind: Secret
metadata:
name: easy-mvc-secret
type: Opaque
data:
# base64で暗号化している
mysql_username: c3ByaW5nYm9vdA== # => springboot
mysql_password: cEBzc3cwcmQ= # => p@ssw0rd
SecretやConfigMapに保存した情報をワークロード定義ファイルから参照する場合は、secretKeyRefやconfigMapKeyRefなどのプロパティを指定してあげます。
apiVersion: v1
kind: ReplicationController
metadata:
name: backend
spec:
replicas: 3
template:
metadata:
labels:
app: easy-mvc
tier: backend
spec:
containers:
- name: springboot
image: gcr.io/YOUR_PROJECT_ID/springboot-app-gke
env:
- name: CLOUDSQL_MYSQL_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db.host
- name: CLOUDSQL_MYSQL_USER
valueFrom:
secretKeyRef:
name: easy-mvc-secret
key: mysql_username
...
[省略]
感想
GKEは開発するのにとにかく使いやすいです。Kubernetesの学習コストを高く感じる人は、GKE上で手を動かして理解していくことをオススメします。
個人的に作っていて悩まさせられたのは、環境変数の受け渡しと、ローカル/本番共通して使えるDockerfileの定義です。
環境変数の受け渡しの部分をシェルスクリプトやdocker-composeでの上書きや、ConfigMap/Secretリソースの作成で対応せねばならないのがなかなかに考えさせられます。ここら辺のコンテナ間接続(あるいはコンテナ-インスタンス間接続)がシンプルにできると嬉しいのになと思うことはあります。環境変数については、DIコンテナのようにシングルトンな設計が実現できてくれれば嬉しいのですが、自分が知らないだけでベストプラクティスがあるのでしょうか...?
ちなみに今回構築したPodがどんな感じで配置されていたのか絵にしてみたら、こんな感じでした。
アプリをデプロイするだけだと、GKEの圧倒的な抽象力に怠けてKubnernetesのオーケストレーション機能をあまり深く見れませんでした。アクセス負荷などの実験ができれば、Kubernetesが真価を発揮しそうなのでまた記事書けるように勉強します。
Author And Source
この問題について(GKEハンズオンをつくってみた -Vue.js + Spring Boot アプリケーション-), 我々は、より多くの情報をここで見つけました https://qiita.com/mush/items/fa15bea51becaed0b69f著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .