🐁

【GCP】AppEngineにGolangアプリをデプロイしちゃうぞ【DockerとCloudSQLもあるよ】

2022/12/15に公開

本記事は「つながる勉強会 Advent Calendar 2022」の 15 日目の記事です。
他の記事も要チェックやで!!!

はじめに

Go で簡単なアプリを作って GoogleAppEngine(以下「GAE」)のフレキシブル環境にデプロイしてみたので、ハンズオン風自分用のメモとして残します。
GAE はスタンダード環境だと超簡単にデプロイできますが、ランタイムのバージョンが限られているところがネックです。
例えば Go は執筆時点では1.19が最新バージョンですが、1.16までとまあまあ古いランタイムしか使えません。
ちなみにフレキシブル環境は何故か1.15までしか使えませんが、自分で Dockerfile 作ればそれをランタイムとして使えます。

https://twitter.com/golang/status/1600868557390479360?s=20&t=v2_vsziqj49vmfTSlzg7KQ

リポジトリ

参考までに今回作業していたリポジトリは以下です。

https://github.com/chillout2san/chi_authentication

$ git reset --hard 3726314

前準備

  • Google アカウントの作成は省略します。

  • 以下ドキュメントを参考に GCP にログインしましょう。執筆時点では有効期限 90 日で 300 ドル分クレジットがもらえます。

https://cloud.google.com/docs/get-started?hl=ja

  • コンソールに移動したら、My First Projectというサンプルのプロジェクトにいるはずです。そのままでも良いですし、新しくプロジェクト作成してもらっても OK です。今回はLearnGCPという名前にしました。

GCP の sdk をインストールする

  • 以下ドキュメントを参考に sdk をインストールします。gcloudコマンド等の GCP のコマンドを打てるようになります。
  • 各 OS ごとに用意されているフォルダをダウンロードして、解凍したものを配置したディレクトリでインストールのシェルスクリプトを叩くと、配置したディレクトリでgcloudコマンドのパスが通ります。ダウンロードフォルダとかでパス通るとめんどくさいので、ドキュメントにも記載がありますがホームディレクトリにファイルを解凍してからスクリプト叩くと良いでしょう。

https://cloud.google.com/sdk/docs/install?hl=ja

  • インストール終わったら、以下コマンドを叩きましょう。バージョン情報が出てきたら OK です。出ない人はターミナルを再起動してみてください。
$ gcloud version

image.png

CloudSQL の準備をする

  • データベース使わない人はスキップ可です。

  • メニューから CloudSQL を選択。

image.png

  • インスタンスを生成を押下。

image.png

  • 今回は MySQL を選択しました。

image.png

  • インスタンス ID とパスワードを設定します。インスタンス ID は分かりやすいものであれば何でも OK です。パスワードはこのデータベースに root ユーザーでアクセスするものになるので控えておきましょう。右にある生成を押すと安全なパスワードを生成できます。今回 MySQL のバージョンは8.0を選択しました。

image.png

  • 構成の選択を行います。本番環境用のマッチョな構成か、開発環境用のコスパ高い構成かを選べます。今回は開発用なのでdevelopmentを選択しました。画面にも説明がありますが、構成の選択と言ってもオススメの設定をとりあえず GCP が設定してくれるだけで、後ほど各項目をカスタマイズできます。なのであんまり深く考えるところではないですね。

image.png

  • リージョンとゾーンを選択します。今回は東京にしました。開発用なので高可用性にするかどうかはノーで大丈夫です。詳しく知りたい方は以下ドキュメントを参考にしてください。

image.png

https://cloud.google.com/solutions/cloud-sql-mysql-disaster-recovery-complete-failover-fallback?hl=ja

  • マシンタイプを選択します。共有コアが一番安くなりますが、何となく軽量を選択しました。

image.png

  • ストレージの設定を行います。SSD10GB、写真はオンになっちゃってますが自動増量はオフにしました。趣味レベルでちょろっと使うだけなら、HDDでも良いかも知れません。暗号化のところはあんまり分かってません・・・。

image.png

  • 接続の設定を行います。同じ GCP のサービスである GAE からは特段の設定なしにアクセスできました。おそらくデフォルトで認可済みネットワークに入っているっぽいですね。

image.png

  • データの保護の箇所です。実験用なので全てオフにします。

image.png

  • メンテナンスの設定です。例えば営業ツールのような日中によく使われるシステムだからメンテナンスは夜に実行されるように、みたいな調整ができます。今回はこだわりがないのでおまかせにしてます。

image.png

  • フラグとラベルは設定不要です。インスタンスを生成をクリックしてください。5 分程度時間かかりますが、インスタンス生成完了まで待ちましょう。完了したらインスタンス名をクリックします。

image.png

  • 接続名も後ほど使いますので控えておいてください。ここでデータベースの作成をしたいので、CLOUD SHELLで開くを押下してインスタンスの中に入ります。

image.png

  • コマンドが入力された状態で立ち上がるのでエンターで実行します。

image.png

  • 先ほど控えておいたパスワードを入力します。写真省略しますが、SQL が打てるようになるので、データベース作成するなり、テーブル作成するなりやってください。今回はテーブル作成するスクリプトをサボって書いていないので SQL 直叩きしました。

image.png

CREATE DATABASE chi_authentication;
migration/schema/create_table.sql
CREATE TABLE users (
    id varchar(26) NOT NULL PRIMARY KEY,
    name varchar(255) NOT NULL,
    mail varchar(255) NOT NULL,
    imagePath varchar(255) NOT NULL,
    pass varchar(255) NOT NULL
);

CloudSQL と Go アプリケーションを接続する

  • 以下記事を参考に、CloudSQL と Go のアプリケーションを接続します。TCP ソケット経由、Unix ソケット経由、CloudSQL コネクタの 3 パターン方法があり、TCP ソケット経由が見慣れた形式で分かりやすいですが、今回は CloudSQL コネクターで接続してみました。

https://cloud.google.com/sql/docs/mysql/connect-app-engine-flexible?hl=ja#connect_to

infrastructure/database.go
var Db *sql.DB

func init() {
    // 今更タイポ見つけたけど、見なかったことにする。
    e := config.Enviroment

    d, err := cloudsqlconn.NewDialer(context.Background())
    if err != nil {
        log.Println("CloudSQLConn NewDaialer failed:", err)
    }
    mysql.RegisterDialContext("cloudsqlconn",
        func(ctx context.Context, addr string) (net.Conn, error) {
            // DbHostはCloudSQL作成時の「接続名」のところ。
            // {project名}:{リージョン}:{インスタンスのid}の形になっているはず。
            return d.Dial(ctx, e.DbHost)
        })

    dsn := fmt.Sprintf("%s:%s@cloudsqlconn(localhost:3306)/%s?parseTime=true",
        // DbUserはroot、DbPassは設定したパスワード。
        // DbNameはCloudShellでMySQLインスタンスの中に入ってデータベースを作成する。
        e.DbUser, e.DbPass, e.DbName)

    db, err := sql.Open("mysql", dsn)

    if err != nil {
        log.Println("Database connection failed:", err)
        return
    }

    Db = db
    log.Println("Database connection success")
}

ローカルで CloudSQL への接続のテストをする

  • ここまで準備出来たら一度ローカルで実行してみましょう。前述のリポジトリで作業いただいている方は以下コマンド実行してください。
$ DB_USER=root \
DB_PASS={控えたパスワード} \
DB_NAME={ご自身で入力したもの} \
INSTANCE_CONNECTION_NAME={控えた接続名} \
JWT_SECRET_KEY=secret \
go run cmd/main.go
  • すると以下のようなメッセージが出て怒られます。ローカルから CloudSQL に接続するための認証情報を加える必要があることが分かります。
2022/12/10 23:47:10 CloudSQLConn NewDaialer failed:
failed to create token source: google:
could not find default credentials.
See https://developers.google.com/accounts/docs/application-default-credentials
for more information.

https://developers.google.com/accounts/docs/application-default-credentials

  • ベストプラクティスではないようですが、簡単に認証情報の json をダウンロードして環境変数に渡すやり方を取ります。IAMと管理からサービスアカウントを作成を押下します。

image.png

  • サービスアカウントの ID を入力します。適当に入力します。

image.png

  • ロールを編集者にします。もっと細かく設定が出来ますが、今回は実験用なのでざっくり行きます。

image.png

  • こちらはスルーで OK です。サービスアカウントのアクセスをユーザーに渡すかどうかです。実験用なので使いません。今回は GCP にログインしている Google アカウントを使ってしまいます。

image.png

  • サービスアカウントが作成できたら、鍵を追加を押下してキータイプは json で秘密鍵を生成します。ダウンロードされるので、/home/sshとかに保存してください。
  • 先ほど実行したコマンドにGOOGLE_APPLICATION_CREDENTIALSという環境変数を一つ足します。値は秘密鍵のパスです。これで実行すればエラーが出てこないはずです。
$ GOOGLE_APPLICATION_CREDENTIALS={秘密鍵のパス} \
DB_USER=root \
DB_PASS={控えたパスワード} \
DB_NAME={ご自身で入力したもの} \
INSTANCE_CONNECTION_NAME={控えた接続名} \
JWT_SECRET_KEY=secret \
go run cmd/main.go
  • 動作確認として以下のようにリクエスト投げてみてください。今回は Postman を使ってリクエスト投げました。

image.png

  • データベースにもレコード作成されてますね。

image.png

Dockerfile を準備する

  • デプロイする用の超シンプルな Dockerfile を用意します。コンテナへの接続うんぬんは GAE がやってくれます。GCE でやろうとすると、Apache なり Nginx なりと格闘する必要があります。
FROM golang:1.19-alpine

WORKDIR /app

COPY . .

RUN go mod tidy

RUN go build ./cmd/main.go

CMD ["./main"]

app.yaml を準備する

  • さてここからはいよいよ GAE へのデプロイの準備です。ドキュメントをガン見しながら進めていきます。GCP はドキュメントが本当に見やすくて助かります。

https://cloud.google.com/appengine/docs?hl=ja

  • app.yaml は GAE のデプロイ用の設定ファイルのようなものです。ちなみに必ず app.yaml という名前にする必要はないので、production.yaml とか development.yaml みたいな名前もアリです。まずは必要最低限の構成を考えてみます。
app.yaml
# 自前のDockerfileを使用する
runtime: custom
# フレキシブル環境を使う
env: flex
# GAEにデプロイした後のサービス名。何でも良いです。今回はリポジトリの名前そのまんまです。
# 指定しないとdefaultという名前そのまんまのデフォルトのサービスにデプロイされます。
# サービスとは簡単にいうと、AppEngineにデプロイされたアプリのことです。
service: chi-authentication

# 環境変数を使う場合はここに書く。
env_variables:
  ALLOW_ORIGIN:
  DB_USER:
  DB_PASS:
  DB_NAME:
  INSTANCE_CONNECTION_NAME:
  JWT_SECRET_KEY:
  • app.yaml でスケールの設定を細かく決めることが出来ます。詳細はリファレンスは公式を参照してください。重要そうな項目を抜粋してみます。

https://cloud.google.com/appengine/docs/flexible/reference/app-yaml?hl=ja&tab=go

  • GAE のフレキシブル環境はデフォルトでは自動スケーリング、つまり勝手にスケールしていきます。その設定を詳しくできます。
app.yaml
automatic_scaling:
  # 最小のインスタンス数。最小は1。デフォルトは2。
  # 趣味の開発用であれば1にしてコストカットできそう。
  min_num_instances: 1
  # 最大のインスタンス数。デフォルトは20。
  # 趣味レベルならどうでも良さそうだけど、怖いので数字は絞っておきたい。
  max_num_instances: 15
  # 新しいインスタンスが立ち上がってから利用されるまでの時間っぽい。
  # 起動中のインスタンスにリクエストが行って変な待ち時間を作らないように、ということかも。
  # Javaとか起動に時間がかかるやつの場合は長めに取ると良いのかも。
  # 60秒以上にする必要があり、デフォルトは120秒。あんまり課金には関係なさそう。
  cool_down_period_sec: 180
  cpu_utilization:
    # これもよく分からなかった。
    # 多分インスタンス増減を決めるCPU使用率の閾値っぽい。
    target_utilization: 0.6
  # まだベータ版だったので一旦無視。
  target_concurrent_requests: 100
  • 趣味開発レベルで課金が怖い場合は手動スケーリングにしてインスタンス数を決め打ちしておくという手もあります。
app.yaml
manual_scaling:
  # ドキュメントには書いてなかったけど、1以上であれば設定できそう。
  instances: 1

https://cloud.google.com/appengine/docs/the-appengine-environments?hl=ja

デプロイする

  • まず以下コマンドでログインしましょう。ブラウザが立ち上がるので、GCP にログインしている Google アカウントで認証をします。CLI 上から GCP に接続するための認証ですね。先ほどはローカル環境から CloudSQL に接続するためにサービスアカウントの json を使って認証しましたが、それと似た話です。
$ gcloud auth login
  • ログインが出来たら以下コマンドで認証状態を確認してみましょう。
$ gcloud auth list

image.png

  • ちなみにユーザーアカウントで認証するのかサービスアカウントで認証するのか選ぶことが出来ます。そもそもユーザーアカウントとサービスアカウントの違いもごっちゃになりやすいので、一読すると GCP の認証についてスッキリ整理できると思います。

https://cloud.google.com/sdk/docs/authorizing?hl=ja

  • 以下コマンドを叩いてデプロイを実行します。5〜10 分くらいかかるので気長に待ちます。
$ gcloud app deploy {yamlファイルの名前} --project={プロジェクトの名前}
  • 前述のリポジトリで作業されている方は、deploy 用のシェルスクリプトを用意しているので、それも参考にしてみてください。app.yaml はリポジトリに共有されること前提のサンプルファイルで、実際のデプロイ用のファイルは deploy.yaml を使用する形にしてみました。
deploy.sh
#!/bin/bash

# 定数を置く
DEPLOY_FILE="deploy.yaml"
PROJECT="learngcp-361805"

# mainブランチかどうか確認する
if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then
    echo "デプロイできるのはmainブランチのみです。"
    exit 1
fi

# deploy.yamlが準備されているか確認する。
if [ ! -e $DEPLOY_FILE ]; then
    cp app.yaml deploy.yaml
    echo "deploy.yamlを生成しました。"
    echo "環境変数を記載した後、再度実行してください。"
    exit 1
fi

# deploy.yamlに環境変数を記載しているか確認する。
echo "deploy.yamlに環境変数を記載しているか確認してください。"
read -p "Y/n> " answer1
if [[ "${answer1}" == "n" ]]; then
    echo "環境変数を記載した後、再度実行してください。"
    exit 1
fi

# GCPの認証情報を確認する。
echo "GCPの認証情報を表示します。"
echo "******************************"
gcloud auth list
echo "******************************"

echo "デプロイしたいGCPプロジェクトに所属するアカウントでしたか?"
read -p "Y/n> " answer2
if [[ "${answer2}" == "n" ]]; then
    echo "gcloud auth loginでアカウントを切り替えてください。"
    echo "デプロイを中断します。"
    exit 1
fi

# GCPへデプロイする。
echo "デプロイを開始します。"
gcloud app deploy deploy.yaml --project="${PROJECT}"
echo "デプロイを完了しました。"
  • デプロイ完了したら、URL を控えておきます。

image.png

動作確認

  • 動作確認しましょう。バッチリですね。

image.png

image.png

おわりに

  • 普段業務ではインフラ周りを触らないので楽しかったです。今更って感じですがボタンぽちぽちでデータベースサーバ建てられるクラウドやべーなと思いました。

  • 趣味レベルで AWS も触ったことありますが、GCP の方がかなりドキュメントが読みやすかったです。

  • GAE は環境変数を app.yaml 内に持たせるので、ローカルでアプリケーション実行する際には別途 env ファイルを用意しないと行けなさそう?それか app.yaml には持たせずに env ファイルに全部書いちゃって、Dockerfile でその env ファイル読み込めば行けるのかしら?

  • 本記事執筆前に GAE フレキシブルかつ Docker 使った記事を探したが案外なかった。何でだろうと考えてたけど、それなら CloudRun 使うわっていう話なのかも?次は CloudRun 試してみたい。それ終わったら Terraform で IaC やってみたい。

GitHubで編集を提案

Discussion