Go,Nuxt,MySQLの環境をGCPにデプロイしてみた
開発環境から本番環境まで構築し、Google Cloud Platform(GCP)にデプロイしてみました。
環境
- フロントエンド:Nuxt.js (Firebase Hosting)
- バックエンド:Go (Cloud Run)
- DB:MySQL (Cloud SQL)
()内は使用するGCPのサービスです。
ブラウザからユーザーの氏名を送信するとDBに登録され、画面に全ユーザーの氏名を表示するだけの単純なアプリケーションを作成し、GCPにデプロイしました。最終的なソースは以下です。
ディレクトリ構成はこんな感じです。
.
├── client/
│ └── var/
│ └── www/
│ └── project/ <- Nuxtのプロジェクト
├── go/
│ ├── .env
│ ├── Dockerfile
│ └── src/
│ └── app/ <- Goのサーバーのコード
├── mysql/
│ ├── data/
│ ├── init/
│ └── my.cnf
└── docker-compose.ymldocker-compose.ymlを書きます。
| version: '3.7' | |
| services: | |
| go: | |
| build: | |
| context: ./go/ | |
| target: dev | |
| volumes: | |
| - ./go/src:/go/src:cached | |
| working_dir: /go/src/app | |
| ports: | |
| - 8080:8080 | |
| tty: true | |
| env_file: | |
| - ./go/.env | |
| mysql: | |
| image: mysql:latest | |
| volumes: | |
| - ./mysql/data:/var/lib/mysql | |
| - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf | |
| - ./mysql/init:/docker-entrypoint-initdb.d | |
| environment: | |
| MYSQL_ROOT_PASSWORD: password | |
| MYSQL_DATABASE: database | |
| MYSQL_USER: user | |
| MYSQL_PASSWORD: password | |
| TZ: 'Asia/Tokyo' | |
| node: | |
| image: node:latest | |
| ports: | |
| - 3000:3000 | |
| volumes: | |
| - ./client/var/www:/var/www:cached | |
| working_dir: /var/www/project | |
| tty: true | |
| depends_on: | |
| - go | |
| - mysql |
GoのDockerfileです。
| FROM golang:latest as dev | |
| RUN apt-get update \ | |
| && apt-get install -y vim \ | |
| && curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh |
DB
以下の記事を参考に設定ファイルを作成しました。

| [mysqld] | |
| character-set-server=utf8mb4 | |
| collation-server=utf8mb4_unicode_ci | |
| [client] | |
| default-character-set=utf8mb4 |
以下のファイルを作成し、マウントしておくと、DBの初回起動時にusersテーブルが自動で作成されます。
| create table IF not exists `users` | |
| ( | |
| `id` INT(20) AUTO_INCREMENT, | |
| `first_name` VARCHAR(20) NOT NULL, | |
| `last_name` VARCHAR(20) NOT NULL, | |
| PRIMARY KEY (`id`) | |
| ) DEFAULT CHARSET=utf8 COLLATE=utf8_bin; |
バックエンド
以下の記事を参考に、GoでAPIサーバーを実装しました。

基本的に上の記事と同じものを書きましたが、APIのパスを少し変えています。
api := router.Group("/api")
{
api.POST("/users", ...)
api.GET("/users/:id", ...)
api.GET("/users", ...)
}
Router = routerDBへの接続部分は、以下のように環境変数でセットするようにしました。
user := os.Getenv("DB_USER")
pass := os.Getenv("DB_PASSWORD")
host := os.Getenv("DB_HOST")
name := os.Getenv("DB_DATABASE")
sock := os.Getenv("DB_SOCKET")
dns := fmt.Sprintf("%s:%s@%s(%s)/%s", user, pass, sock, host, name)
conn, err := sql.Open("mysql", dns)DBの設定に合わせて .envを作成します。
| DB_USER=user | |
| DB_PASSWORD=password | |
| DB_HOST=mysql:3306 | |
| DB_DATABASE=database | |
| DB_SOCKET=tcp |
Goのコンテナに入り、go run server.goでサーバー立ち上げると、以下のエンドポイントができます。
POST /api/users
GET /api/users/:id
GET /api/usersフロントエンド
Nuxtのプロジェクトを作成し、氏名の入力フォームとユーザー情報を表示するページを作成します。
| <template> | |
| <div> | |
| <input v-model="lastName" placeholder="姓"> | |
| <input v-model="firstName" placeholder="名"> | |
| <button @click="addUserFromForm" class="button">登録</button> | |
| <div class="table"> | |
| <div class="table-row"> | |
| <div class="table-column table-column-1">ID</div><div class="table-column table-column-2">名前</div> | |
| </div> | |
| <template v-for="user in users"> | |
| <div :key="user.ID" class="table-row"> | |
| <div class="table-column table-column-1">{{user.ID}}</div><div class="table-column table-column-2">{{user.LastName}} {{user.FirstName}}</div> | |
| </div> | |
| </template> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import axios from 'axios'; | |
| import { mapState, mapActions } from 'vuex'; | |
| export default { | |
| created() { | |
| this.getAllUsers() | |
| }, | |
| data() { | |
| return { | |
| lastName: "", | |
| firstName: "", | |
| } | |
| }, | |
| computed: mapState({ | |
| users: 'users' | |
| }), | |
| methods: { | |
| ...mapActions([ | |
| 'getAllUsers', | |
| 'addUser', | |
| ]), | |
| async addUserFromForm() { | |
| await this.addUser({ | |
| firstName: this.firstName, | |
| lastName: this.lastName, | |
| }) | |
| this.firstName = ""; | |
| this.lastName = ""; | |
| this.getAllUsers() | |
| }, | |
| } | |
| } | |
| </script> | |
| <style lang="scss"> | |
| .table { | |
| margin: 10px; | |
| .table-row { | |
| display: flex; | |
| align-items: center; | |
| height: 30px; | |
| } | |
| .table-column { | |
| text-align: center; | |
| &.table-column-1 { | |
| width: 50px; | |
| } | |
| &.table-column-2 { | |
| width: 150px; | |
| } | |
| } | |
| } | |
| .button { | |
| margin: 10px; | |
| } | |
| </style> |
| import axios from 'axios'; | |
| export const state = () => ({ | |
| users: [] | |
| }) | |
| export const mutations = { | |
| setUsers(state, users) { | |
| state.users = users; | |
| } | |
| } | |
| export const actions = { | |
| async getAllUsers(context) { | |
| try { | |
| const { data } = await axios.get('/api/users') | |
| context.commit('setUsers', data) | |
| } catch (error) { | |
| console.log(error) | |
| } | |
| }, | |
| async addUser(context, params) { | |
| try { | |
| await axios.post('/api/users', params) | |
| } catch (error) { | |
| console.log(error) | |
| } | |
| } | |
| } |
package.jsonです。 docker内で起動するので、scripts.devを HOST=0.0.0.0 PORT=3000 nuxt としています。
Firebaseにデプロイするので、そのためのツールも追加しています。
| { | |
| "name": "project", | |
| "version": "1.0.0", | |
| "description": "My kickass Nuxt.js project", | |
| "author": "", | |
| "private": true, | |
| "scripts": { | |
| "dev": "HOST=0.0.0.0 PORT=3000 nuxt", | |
| "build": "nuxt build", | |
| "start": "nuxt start", | |
| "generate": "nuxt generate" | |
| }, | |
| "dependencies": { | |
| "@nuxtjs/axios": "^5.3.6", | |
| "firebase": "^6.6.1", | |
| "firebase-tools": "^7.3.2", | |
| "nuxt": "^2.0.0", | |
| "ress": "^1.2.2" | |
| }, | |
| "devDependencies": { | |
| "node-sass": "^4.12.0", | |
| "sass-loader": "^8.0.0" | |
| } | |
| } |
nuxt.config.js のproxy部分の設定を以下のようにします。
axios: {
proxy: true
},
proxy: {
'/api': 'http://go:8080'
},Nodeコンテナに入り、yarn run devを実行すると、 localhost:3000にアクセスできます。
ここまで終わったらdocker-composeを少し修正しました。
| version: '3.7' | |
| services: | |
| go: | |
| build: | |
| context: ./go/ | |
| target: dev | |
| volumes: | |
| - ./go/src:/go/src:cached | |
| working_dir: /go/src/app | |
| tty: true | |
| env_file: | |
| - ./go/.env | |
| mysql: | |
| image: mysql:latest | |
| volumes: | |
| - ./mysql/data:/var/lib/mysql | |
| - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf | |
| - ./mysql/init:/docker-entrypoint-initdb.d | |
| environment: | |
| MYSQL_ROOT_PASSWORD: password | |
| MYSQL_DATABASE: database | |
| MYSQL_USER: user | |
| MYSQL_PASSWORD: password | |
| TZ: 'Asia/Tokyo' | |
| node: | |
| image: node:latest | |
| ports: | |
| - 3000:3000 | |
| volumes: | |
| - ./client/var/www:/var/www:cached | |
| working_dir: /var/www/project | |
| command: ["yarn", "run", "dev"] | |
| depends_on: | |
| - go | |
| - mysql |
goのportsとnodeのttyを削除し、nodeのcommandを追加しました。
Cloud Run
デプロイ用のDockerfileを作成します。開発用のDockerfileに追記しましたが、分けて書いても良いと思います。
| # 開発 | |
| FROM golang:latest as dev | |
| RUN apt-get update \ | |
| && apt-get install -y vim \ | |
| && curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh | |
| # 本番ビルド用 | |
| FROM golang:latest as builder | |
| WORKDIR /go/src/app/ | |
| COPY ./src/app . | |
| RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build server.go | |
| # 本番 | |
| FROM alpine:latest as prod | |
| EXPOSE 8080 | |
| WORKDIR /api | |
| COPY --from=builder /go/src/app/server . | |
| CMD ["./server"] |
buildし、Container Registryにpushします。
docker build -t us.gcr.io/go-nuxt-mysql-b35d9/go-nuxt-mysql --target prod ./go/
docker push us.gcr.io/go-nuxt-mysql-b35d9/go-nuxt-mysqlGCPのコンソールでCloud Runのサービスを新規作成します。
DB_HOSTは /cloudsql/<project-id>:<region>:<instance-id> です。/cloudsql/以下は「Cloud SQL 接続」で表示されている文字列をそのまま設定すればOKです。詳しくは公式ドキュメントを参照下さい。

Firebase Hosting
ローカルでNodeのコンテナに入り、yarn firebase login --no-localhost
でFirebaseにログインします。Firebaseのコマンドにyarnを毎回つけるのが地味に面倒なので、Dockerfileを書いてグローバルに追加する等した方が良いかもしれません。
yarn firebase init
でプロジェクトを作成し、適当に設定します。デプロイするディレクトリは dist としておきます。
自動で作成されたfirebase.jsonを、以下のように修正します。
| { | |
| "hosting": { | |
| "public": "dist", | |
| "ignore": [ | |
| "firebase.json", | |
| "**/.*", | |
| "**/node_modules/**" | |
| ], | |
| "headers": [ | |
| { | |
| "source": "/api/**", | |
| "headers": [ | |
| { | |
| "key": "Cache-Control", | |
| "value": "no-cache, no-store, must-revalidate" | |
| } | |
| ] | |
| } | |
| ], | |
| "rewrites": [ | |
| { | |
| "source": "/api/**", | |
| "run": { | |
| "serviceId": "go-nuxt-mysql", | |
| "region": "us-central1" | |
| } | |
| }, | |
| { | |
| "source": "**", | |
| "destination": "/index.html" | |
| } | |
| ] | |
| } | |
| } |
これで /api/...へのアクセスが全てCloud Runにリダイレクトされます。詳しくは公式ドキュメントを参照下さい。

yarn build でデプロイ用のファイルを作成します。
最後にyarn firebase deployでデプロイします。
発行されたURLにアクセスすると、ページが表示されます。
以上で全ての設定が完了しました。
セキュリティ関連を何も気にせず作成したので、その辺はもう少し勉強しようと思います。
ハマったところ
備忘録も兼ねて、主に本番環境にデプロイする時にハマった箇所についていくつか挙げておきます。
Cloud SQL への接続情報
・tcpでなくunix
・ポート番号はセットしない
最初、ローカルと同じノリで接続情報を以下のように設定していました。
user:password@tcp(/cloudsql/go-nuxt-mysql-b35d9:us-central1:go-nuxt-mysql:3306)/databaseが、何度試しても接続できず。
正しくは以下になります。
user:password@unix(/cloudsql/go-nuxt-mysql-b35d9:us-central1:go-nuxt-mysql)/database公式ドキュメントはきちんと読みましょう!
Goのクロスコンパイル
・alpine用のビルドにはGOOS=linux GOARCH=amd64 CGO_ENABLED=0
今回、軽量なイメージを作るためにmulti stage buildを使用し、デプロイ用のbaseイメージにはalpineを使用しました。最初はGoのビルドに以下のコマンドを実行していました。
GOOS=linux GOARCH=amd64 go build server.goが、これでCloud Runにデプロイしても動きませんでした。alpineイメージで動かすためには設定が足りなかったようです。

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build server.goこれで動くようになりました。
Firebaseのリダイレクト設定
・上から順に判定
初めはfirebase.jsonに以下のようなリダイレクトの設定を書いていました。
"rewrites": [
{
"source": "**",
"destination": "/index.html"
},
{
"source": "/api/**",
"run": {
"serviceId": "go-nuxt-mysql",
"region": "us-central1"
}
}
]今思うと当然ですが、これだと全てのリクエストが"**"に引っかかり、Cloud Runにリダイレクトされません。ステータスは200で返ってくるのに正常に取得できず、地味に混乱しました。
以下のように逆に書きます。
"rewrites": [
{
"source": "/api/**",
"run": {
"serviceId": "go-nuxt-mysql",
"region": "us-central1"
}
},
{
"source": "**",
"destination": "/index.html"
}
]Firebaseのキャッシュ
・リクエストヘッダに Cache-Controlを追加する
DBのデータは更新しているのに、画面に表示する情報が全く変わりませんでした。Cloud Runへ直接アクセスすると、正常に取得できていました。
どうやらCloud Runへのリクエスト結果がFirebase側でキャッシュされていたようです。
以下のページを参考に、firebase.json でヘッダ情報を追加すると回避できました。

"headers": [
{
"source": "/api/**",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
}
]
}
],Firebaseのキャッシュ?
Firebaseへのデプロイ直後、ブラウザからアクセスすると、APIのレスポンスが404になることが何度かありました。ブラウザのキャッシュを消してもダメでしたが、Cloud Runへ直接アクセスしたり、FirebaseのURL(…firebaseapp.com/api/users 等)に対してローカルからcurlを叩いたりすると正常に取得できました。
しばらく放置した後にリロードすると、正常に取得できるようになっていました。おそらくこれもキャッシュが原因ぽいです。何度か同じ状況を再現できた時に試しましたが、デプロイ後30分くらい待つと正常に取れるようになりました。
回避方法があれば教えていただきたいです。
参考文献
https://qiita.com/hirotakan/items/698c1f5773a3cca6193e
https://qiita.com/Azizim_A/items/66564b5dc7597717932b
https://infraya.work/posts/alpine_with_go-singlebinary/
https://ja.nuxtjs.org/guide/installation
https://qiita.com/reflet/items/e7c33f84ab43ab237ee4
https://qiita.com/at-946/items/08de3c9d7611f62b1894
https://qiita.com/Statham/items/046649bff81c00ba275e
https://qiita.com/A-Kira/items/f401aea261693c395966
https://qiita.com/moaikids/items/f7c0db2c98425094ef10
https://firebase.google.com/docs/hosting/full-config#rewrites
https://stackoverflow.com/questions/48589821/firebase-hosting-how-to-prevent-caching-for-the-index-html-of-an-spa
https://firebase.google.com/docs/hosting/cloud-run?hl=ja
https://cloud.google.com/functions/docs/sql?hl=ja#functions_sql_mysql-go



