Go と Vue.js なアプリを Heroku で動かす

サーバサイドを Go で WebAPI として独立して作り、フロントは Vue.js で SPA チックに作り、Static な成果物（HTML/CSS/JS）を Go バイナリに内包して Heroku で動かすメモです。

サンプルのソースコードはこちらです。

- https://github.com/zaru/go-vuejs-heroku

## 前提条件

- Go 1.9+
  - パッケージ管理: dep
  - DBマイグレーション: pressly/goose
- Vue.js 2.5+
  - vue-cli
  - ビルドツール: webpack
- Heroku
  - PostgreSQL
  - Redis

### デプロイの動作フロー

- git push heroku master で Heroku にデプロイ
- デプロイ
  - dep で必要パッケージをインストール
  - npm で必要パッケージをインストール
  - Vue.js を webpack でビルド
  - Go アプリをビルド
  - マイグレーションコマンドをビルド
  - マイグレーションコマンドを実行
- デプロイ完了

## Go

### 初期設定

```
cd $GOPATH/your-name
mkdir go-vuejs-heroku
cd go-vuejs-heroku
```

### dep パッケージ管理

```
dep init
dep ensure
```

#### Heroku でコマンドビルド

`pressly/goose` のマイグレーションコマンドをデプロイ時に実行できるように、アプリのバイナリとは別にコマンドをビルドできる Heroku 用の設定を `Gopkg.toml` に追記します。

```toml:Gopkg.toml
[metadata.heroku]
  root-package = "github.com/zaru/go-vuejs-heroku"
  go-version = "go1.9.1"
  install = [ ".", "./cmd/..." ]
  ensure = "true"
```

ポイントは `install = [ ".", "./cmd/..." ]` です。これは Heroku の環境変数である `GO_INSTALL_PACKAGE_SPEC` と同じ役割になります。 dep 以外の glide などのパッケージ管理の場合はこちらの環境変数を修正します。

```
heroku config:set GO_INSTALL_PACKAGE_SPEC=". ./cmd/... "
```

#### goose コマンドスクリプトを配置

上記で設定したコマンドのスクリプト `cmd/goose/main.go` に下記コードを配置します。これは[公式で用意されているサンプルコード](https://github.com/pressly/goose/blob/master/cmd/goose/main.go)です。

```go:cmd/goose/main.go
package main

import (
	"database/sql"
	"flag"
	"log"
	"os"

	_ "github.com/lib/pq"
	"github.com/pressly/goose"
)

var (
	flags = flag.NewFlagSet("goose", flag.ExitOnError)
	dir   = flags.String("dir", ".", "directory with migration files")
)

func main() {
	flags.Usage = usage
	flags.Parse(os.Args[1:])

	args := flags.Args()

	if len(args) > 1 && args[0] == "create" {
		if err := goose.Run("create", nil, *dir, args[1:]...); err != nil {
			log.Fatalf("goose run: %v", err)
		}
		return
	}

	if len(args) < 3 {
		flags.Usage()
		return
	}

	if args[0] == "-h" || args[0] == "--help" {
		flags.Usage()
		return
	}

	driver, dbstring, command := args[0], args[1], args[2]

	switch driver {
	case "postgres", "mysql", "sqlite3", "redshift":
		if err := goose.SetDialect(driver); err != nil {
			log.Fatal(err)
		}
	default:
		log.Fatalf("%q driver not supported\n", driver)
	}

	switch dbstring {
	case "":
		log.Fatalf("-dbstring=%q not supported\n", dbstring)
	default:
	}

	if driver == "redshift" {
		driver = "postgres"
	}

	db, err := sql.Open(driver, dbstring)
	if err != nil {
		log.Fatalf("-dbstring=%q: %v\n", dbstring, err)
	}

	arguments := []string{}
	if len(args) > 3 {
		arguments = append(arguments, args[3:]...)
	}

	if err := goose.Run(command, db, *dir, arguments...); err != nil {
		log.Fatalf("goose run: %v", err)
	}
}

func usage() {
	log.Print(usagePrefix)
	flags.PrintDefaults()
	log.Print(usageCommands)
}

var (
	usagePrefix = `Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
Drivers:
    postgres
    mysql
    sqlite3
    redshift
Examples:
    goose sqlite3 ./foo.db status
    goose sqlite3 ./foo.db create init sql
    goose sqlite3 ./foo.db create add_some_column sql
    goose sqlite3 ./foo.db create fetch_user_data go
    goose sqlite3 ./foo.db up
    goose postgres "user=postgres dbname=postgres sslmode=disable" status
    goose mysql "user:password@/dbname?parseTime=true" status
    goose redshift "postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" status
Options:
`

	usageCommands = `
Commands:
    up                   Migrate the DB to the most recent version available
    up-to VERSION        Migrate the DB to a specific VERSION
    down                 Roll back the version by 1
    down-to VERSION      Roll back to a specific VERSION
    redo                 Re-run the latest migration
    status               Dump the migration status for the current DB
    version              Print the current version of the database
    create NAME [sql|go] Creates new migration file with next version
`
)
```

これによって dep の `Gopkg.lock` に `pressly/goose` が追記されるようになっていると思います。より明示的に指定したい場合は `Gopkg.toml` で管理しても良いかもしれません。

```toml:Gopkg.toml
[[constraint]]
  name = "github.com/pressly/goose"
  version = "2.1.0"
```

## Vue.js

### 初期設定

```
cd $GOPATH/your-name/go-vuejs-heroku
vue init webpack vue-app
cd vue-app
npm install
```

Vue.js は特別なことはないです。 vue-cli を使って webpack なアプリをテンプレを作っています。

## Heroku

Heroku にアプリを作成します。そして Go と Node.JS のビルドパックを追加します。

```
heroku create go-vuejs-heroku
heroku buildpacks:add heroku/go --app go-vuejs-heroku
heroku buildpacks:add heroku/nodejs --app go-vuejs-heroku
```

PostgreSQL と Redis のアドオンを追加します。

```
heroku addons:create heroku-postgresql:hobby-dev --app go-vuejs-heroku
heroku addons:create heroku-redis:hobby-dev --app go-vuejs-heroku
```

PostgreSQL と Redis の接続情報は環境変数に入っているので、アプリ側が参照するように修正しておきます。

```
heroku config
```

デプロイ時に webpack でビルドできるように `NPM_CONFIG_PRODUCTION` を false にしておきます。そうしないと `devDependencies` のパッケージがインストールされません。ついでに Go のバージョン指定もしておきます。

```
heroku config:set NPM_CONFIG_PRODUCTION=false
heroku config:set GOVERSION=go1.9
```

### 別ディレクトリの npm を動かす

今回のサンプルアプリでは Go アプリの中に Vue.js のアプリを内包するようにしているので、 `package.json` はサブディレクトリにあります。そうすると Heroku の Node.JS ビルドパックではビルドしてくれません。

```
.
├── Procfile
├── server.go
└── vue-app
    ├── package.json
```

そこで、ルートディレクトリにも `package.json` を配置してビルドコマンドを記述します。

```
touch package.json
```

```
.
├── Procfile
├── server.go
├── package.json
└── vue-app
    ├── package.json
```

`postinstall` で vue-app ディレクトリの中で npm install と npm run build でビルドを実行しています。

```package.json
{
  "name": "go-vuejs-heroku",
  "version": "0.0.1",
  "engines": {
    "node": "7.10.0",
    "npm": "4.2.0"
  },
  "scripts": {
    "postinstall": "npm --prefix ./vue-app install ./vue-app && cd ./vue-app && npm run build"
  }
}
```

### マイグレーションコマンドを実行

`Procfile` にリリース時に実行するコマンドを指定できます。そこでマイグレーションコマンドを実行指定します。

```Procfile
release: goose postgres $DATABASE_URL up
web: go-vuejs-heroku
```

以上で、Heroku で Go と Vue.js を使った Web アプリケーションを動かすことができるようになりました。