🤖

Go + MySQL + Dockerの構成でfly.ioにデプロイしてみる

2024/03/10に公開

こんにちはKabosです。
今回は私のPortfolioサイトのバックエンド(Golang + MySQL + Docker)をFly.ioにデプロイしたときのハマったところなどを備忘録的な感じで書いていきたいと思います。
基本的にFly Docsに書いてある通りです。

Fly.ioに登録する

https://fly.io/app/sign-up

こちらからFly.ioにアカウント登録します。
カード情報の入力を求められると思いますが、Fly.ioは従量課金制のサービスですが月5ドル分までは無料枠として利用できます。
1つのAppを1か月(30日)毎日24時間起動し続けても大体2.5ドルに行くか行かないかぐらいだそうなので、安心していただいて大丈夫です。
ただし5ドル分の無料枠を超過するとそこからは従量課金となるので注意してください。

flyctlを導入する

fly.ioではターミナルからAppの作成やデプロイなどができる便利なCLIが用意されています。
ご自身のOSに合わせて以下のコマンドを実行してflyctlをインストールしましょう。

macOS

$ brew install flyctl

OR

$ curl -L https://fly.io/install.sh | sh

Linux

$ curl -L https://fly.io/install.sh | sh

Windows

$ pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"

動作確認

$ flyctl version

でバージョンが表示されればインストール完了です。

flyctlのコマンドが見つからない場合

パスが通っていない可能性があります。
./bash_profileなどにexport FLYCTL_INSTALL="/home/{ご自身のユーザ名}/.fly",
export PATH="$FLYCTL_INSTALL/bin:$PATH"を追記してください。

Appの作成(Go App)

flyctlをインストールできたところで、fly.io上にGoアプリケーションをデプロイするAppを作成します。

CLI上でログイン

$ flyctl auth login

このコマンドでfly.ioに登録したアカウントにログインします。
ログインを行わないとこの先の作業ができませんので必ず行ってください。

Appの作成

$ flyctl launch

このコマンドでAppの初期設定および作成を行います。

Scanning source code
Could not find a Dockerfile, nor detect a runtime or framework from source code. Continuing with a blank app.
Creating app in /home/user/yourapp
We're about to launch your app on Fly.io. Here's what you're getting:

Organization: user                  (fly launch defaults to the personal org)
Name:         your_app_name (derived from your directory name)
Region:       Tokyo, Japan           (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres:     <none>                 (not requested)
Redis:        <none>                 (not requested)

? Do you want to tweak these settings before proceeding? (y/N)

実行すると以上のように作成するAppの設定を行うか聞かれるのでCPUやPORT, メモリ, App名などを設定したい場合はyを入力して設定画面へ移動します。

こんな画面に移動するので各々の設定をしましょう。
設定が終わるとfly.tomlなどが作成されターミナル上でデプロイが始まります。
こちらが完了すればGo APIのデプロイは完了です。

Dockerfileの記述

FROM golang:1.20-alpine AS builder

WORKDIR /app
COPY . .
RUN go mod download

ARG MYSQL_ROOT_PASSWORD
ARG MYSQL_DATABASE
ARG MYSQL_USER
ARG MYSQL_PASSWORD
ARG MYSQL_HOST

ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD \
    MYSQL_DATABASE=$MYSQL_DATABASE \
    MYSQL_USER=$MYSQL_USER \
    MYSQL_PASSWORD=$MYSQL_PASSWORD \
    MYSQL_HOST=$MYSQL_HOST

RUN CGO_ENABLED=0 go build -o main cmd/server/main.go

FROM alpine AS prod

RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main /app/main

EXPOSE 8000
ENTRYPOINT [ "/app/main" ]

細かい説明は割愛しますが大雑把に説明すると、builderステージではgolang:1.20-alipineイメージをベースに環境変数の設定、buildファイルの生成を行っています。
またprodステージではca証明書を追加し、builderステージから生成したbuildファイルをコピーし、8000ポートをエクスポートして/app/mainをエントリーポイントとしています。

MySQL Appの作成

Appの作成

$ mkdir myapp-db
$ cd myapp-db
$ fly launch

まず適当なディレクトリを作成し、移動します。
移動先のディレクトリでGo Appと同様にfly launchでfly.io上にMySQLを動かすためのAppを作成します。
こちらも同様にfly.tomlなどが作成されます。

volumeの作成

$ fly volumes create mysqldata --size 10 # (GB)

上記のコマンドで先ほど作成したApp内にmysqldataというvolumeを作成します。
Dockerで作るvolumeと一緒でDB保存されたデータの永続化を行います。

fly.tomlの変更

fly.toml(MySQL)

# fly.toml app configuration file generated for myapp-db on 2024-02-11T15:54:03+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = '{YOUR_DB_APP_NAME}'
primary_region = 'nrt'
kill_signal = 'SIGINT'
kill_timeout = 5

[processes]
  app = "--datadir /data/mysql --default-authentication-plugin mysql_native_password --performance-schema=OFF --innodb-buffer-pool-size 64M"

[mounts]
  source="{YOUR_VOLUME_NAME}"
  destination="/data"
  
# MYSQL_DATABASE can name your database and will created aftering deploy
[env]
  MYSQL_DATABASE = "{YOUR_DB_NAME}" 
  MYSQL_USER = "{YOUR_DB_USERNAME}"

[build]
  image = "mysql:8.1"

[[vm]]
  cpu_kind = 'shared'
  cpus = 1
  memory_mb = 1024

MySQLのfly.tomlはこんな感じになります。

[processes]

app = "--datadir /data/mysql --default-authentication-plugin mysql_native_password --performance-schema=OFF --innodb-buffer-pool-size 64M"

ここではMySQLを起動する際の設定を記述しています。
各optionごとの説明は以下の通りです。

Option やってること
--datadir /data/mysql データを保存するディレクトリを指定
--default-authentication-plugin mysql_native_password MySQLのデフォルト認証方式をnative passwordに設定
--performance-schema=OFF ここではPerformance Schemaの使用をオフにしています
--innodb-buffer-pool-size 64M innoDBのバッファプールサイズを64MBに設定しています

[mounts]

[mounts]
  source="{YOUR_VOLUME_NAME}"
  destination="/data"

/dataディレクトリに先ほど作成したvolumeをマウントしています。
やっていることはDockerにおけるvolumeのマウントと同じです。

[env]

[env]
  MYSQL_DATABASE = "{YOUR_DB_NAME}" 
  MYSQL_USER = "{YOUR_DB_USERNAME}"

ここでは環境変数としてMYSQL_DATABASE, MYSQL_USERを設定しています。
MySQLに関する環境変数としてMYSQL_ROOT_PASSWORD, MYSQL_PASSWORDなどがありますが、こちらに関してはsecretとしてデプロイするようにしましょう。

以下のコマンドでsecretとして値をSetすることができます

$ fly secrets set MYSQL_PASSWORD="test-pass" 

[build]

[build]
  image = "mysql:8.1"

buildイメージを指定しています。
ご自身の環境に合わせて指定しましょう。

[vm]

[[vm]]
  cpu_kind = 'shared'
  cpus = 1
  memory_mb = 1024

VMに関する設定を記述しています。
ここで特筆すべき点はとくにはないです。

fly.toml (Go App)

# fly.toml app configuration file generated for kabos-dev-api on 2024-02-11T15:37:44+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = '{YOUR_APP_NAME}'
primary_region = 'nrt'

[build]
  build-target='prod'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  cpu_kind = 'shared'
  cpus = 1
  memory_mb = 1024

[env]
# MYSQL_USREなどの変数を定義

[experimental]
  auto_rollback = true

[[services]]
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"
  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80
  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

Goのfly.tomlはこんな感じになります。

[build]

[build]
  build-target='prod'

build時のターゲットを指定しています。
今回はDockerfileで定義したprodステージをターゲットとしています。

[http_service]

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

ここではhttpサービスに関する設定を行っています。

設定項目 何をしているか
internal_port Goアプリケーションがリクエストを受け付ける内部portの設定
force_https trueにすることでhttps接続を強制します
auto_stop_machines trueとするとアプリケーションの利用が一定時間ない場合に自動的にマシンを停止します
auto_start_machines trueとするとアプリケーションの利用があった場合に自動的にmachineの起動を行います
min_machines_running 常に実行される最小マシン数を設定します。今回は0としているため呼び出しがない場合は自動的にマシンはストップされます
processes アプリケーションのプロセスを指定しています

[[vm]]

MySQLと同じなので割愛

[experimental]

[experimental]
  auto_rollback = true

ここではauto_rollbackをtrueとしています。
Auto Rollbackとはfly.ioへのdeployがFailした際に自動的にアプリケーションのRollbackを行ってくれる機能です。
この機能のおかげでdeployがFailしたことによるサービスの停止を防ぐことができます。

[[services]]

[[services]]
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"
  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80
  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

ここではserviceに関する設定をしています。
services.concurrencyではhard_limitやsoft_limitなど接続に関する制限の設定を行っています。
services.portsは使用するポートの数だけ複数設定することができます。
ここでは80ポートでhttpリクエストを受け取り、自動的にhttpsにリダイレクトされ443ポートで処理を行っています。

Go AppからMySQL Appに接続する

GoアプリケーションからMySQLに接続する際は大体以下のようなコードになると思います。

package mysql

import (
	"database/sql"
	"fmt"
        "os"

	"github.com/go-sql-driver/mysql"
)

const (
	DRIVER = "mysql"
	NET    = "tcp"
)

func ConnectDB() (*sql.DB, error) {
	c := mysql.Config{
		User:                 os.Getenv("MYSQL_USER"),
		Passwd:               os.Getenv("MYSQL_PASSWORD"),
		Net:                  NET,
		Addr:                 fmt.Sprintf("%s:%s", os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_PORT")),
		DBName:               os.Getenv("MYSQL_DATABASE"),
		AllowNativePasswords: true,
		ParseTime:            true,
	}

	db, err := sql.Open(DRIVER, c.FormatDSN())
	if err != nil {
		return nil, err
	}
	return db, nil
}

基本的にはこの通りですが、DB_HOSTにセットする値は{MYSQL_APP_NAME}.internalとすることに注意してください。

改めてデプロイする

ここまで出来たらそれぞれのfly.tomlが存在するディレクトリ内でfly deployを実行し改めて各Appにデプロイしましょう。
insertやfetchする処理をGoで書いて正しく実行できればokです。

まとめ

今回はGo + Docker + MySQLでfly.ioにデプロイする手順をまとめてみました。
ただ正直なところfly.ioではpostgresがサポートされているのでそっち使った方がこんな手順踏まなくてももっと楽に構築できるので、よほどのことがない限りはそちらをお勧めします。
備忘録的な感じて書いたのでちょいちょいテキトーになってしまった部分もあるかもしれませんが少しでも参考になれば幸いです。

Discussion