😽

SRE ディビジョンの新卒向け研修に取り組んだ話

2023/11/28に公開

はじめに

こんにちは。クラウドエース SRE ディビジョンの工藤です。

SRE とは Site Reliability Engineering (サイト信頼性エンジニアリング)の略でクラウドエースの SRE ディビジョンは Google Cloud を利用したインフラ構築などを担当しています。

SRE ディビジョンでは新卒向け研修として、Google Cloud のサービスに触れながらアプリケーションを構築する課題が用意されています。
今回はその課題の内容を新卒で文系出身の僕が紹介します。
僕は入社時点では開発経験などがほとんどない状態で、本課題もかなり苦慮しながら取り組んでいました。
そんな僕が課題をどのように行ったのか、どのような技術を学んだのかなどを具体的な手順や実際に作成したものとともにお伝えできればと思います。

なお、本記事では詳細なコードや Google Cloud 環境の作成方法に関しては深く言及していません。参考にしたドキュメントなどは掲載しておりますのでそちらをご参照いただければと存じます。予めご了承ください。

目標成果物

本課題はクラウド ネイティブ サービスを利用したクラウド ネイティブ アーキテクチャの実装が目標です。
クラウド ネイティブとはクラウドを利用することを前提に、クラウドを徹底的に活用したシステムを設計することです。
詳細は以下のドキュメントや記事をご参照ください。
https://cloud.google.com/learn/what-is-cloud-native?hl=ja#section-1
https://cloud-ace.jp/column/detail239/

クラウド ネイティブ アーキテクチャ実装の成果物として、Web の操作画面とデータベースで構成される、一般的な Web アプリケーションを想定したシステムを作成します。
具体的には以下のプロダクトを用いるシステムを構築しました。

  • Cloud Spanner (以下、Spanner) のデータベースに対して Create/Read/Update/Delete(以下、CRUD)を実行するアプリケーションを作成
  • 管理者用に Cloud Spanner のデータベースの内容を表示するアプリケーションを作成
  • それぞれのアプリケーションをコンテナ化し、Cloud Run にデプロイ
  • 負荷分散やセキュリティのために Cloud Load Balancing を立て、Cloud Armor や Identity-Aware Proxy を設定

構成図

上記の内容を構成図にすると以下のようになります。

課題の概要

本課題は以下の5つのステップから構成されています。

  • α. Cloud Run の基本
  • β. Spanner の基本
  • γ. Cloud Run+Spanner で CRUD 作成
  • δ. 管理画面の作成
  • Example. γ と δ を実案件で使える構成に変更

各ステップごとにさらに細かい要件が設定されており、その要件にしたがって進めていきました。
前半の α と β のステップは準備段階のような位置付けで、γ 以降のステップから実際にこの課題のゴールである成果物を作成するという流れで行いました。
ここからは各ステップごとの内容とそれらをどのように実装していったか、順に説明していきます。

α. Cloud Run の基本

最初のステップである α は Cloud Run の概要と基本的な使い方を理解するという内容でした。
ここで学んだ Cloud Run の概要を簡単にまとめてみると、次の点などが挙げられます。

  • フルマネージドでサーバレスなコンピューティング サービス
    • 自分が作ったコンテナを Google が管理するサーバで動かせます。
      インフラを管理することなく、アプリケーション開発に専念できます。
  • コンテナ イメージを簡単にデプロイ
    • ローカルで開発したアプリケーションをコマンド一つでデプロイできます。
  • 自動スケーリング
    • リクエスト数に応じて自動でコンテナの起動/停止をしてくれます。
      アクセスがない場合はインスタンスはゼロになり、リソースを使いません。
  • 従量課金
    • 料金が発生するのはリクエストを処理する間だけです。
      トラフィックがなければ料金がかかりません。

他にも多くの機能がありますが、詳細については以下のドキュメントを参照ください。
https://cloud.google.com/run/docs/overview/what-is-cloud-run?hl=ja

Cloud Run の概要を学んだところでここからは実際に Cloud Run を動かしていきます。
α のステップで満たすべき要件を順に説明していきます。

1. Golang の net/http パッケージを使用して”Hello World”を表示する Web サーバを作成

net/http パッケージ

Golang では net/http パッケージを使用することで簡単に Web サーバを立ち上げることができます。
net/http パッケージの仕様は理解した上で進めると良いと思います。
僕は裏側でどう動いているのか理解するためにかなり時間を費やしました。
詳細は以下の公式ドキュメントをご参照ください。
https://pkg.go.dev/net/http

Golang 開発環境の準備

ローカル環境でアプリケーションを作成していきます。
ローカルの開発環境構築につきましては、以下の公式ドキュメントをご参照ください。
https://go.dev/doc/install

ソースコードの作成

今回はこちらのサイトを参考にし、”Hello World”を表示する Web サーバを作成します。
作成したコードを実行し、ブラウザで localhost:9090 にアクセスします。
以下画像のように”Hello World”の表示を確認できれば成功です。


これで Golang を使い、”Hello World”を表示するアプリケーションを作成することができました。

2. Buildpacks を使ってコンテナ イメージを Artifact Registry に push

ローカル環境で動くコードが完成したので、Cloud Run で動作させるための設定を実装します。

Artifact Registry

Artifact Registry はパッケージやコンテナ イメージを管理し、保管するサービスです。
リポジトリを作成し、コンテナ イメージを push することで Artifact Registory に保存し管理することができます。

https://cloud.google.com/artifact-registry?hl=ja

Buildpacks

Buildpacks はソースコードからどの言語で書かれているか検出し、イメージを作成してくれるツールです。そのため、本来はコンテナ イメージを作成するために必要となる Dockerfile を書かずにコンテナ イメージを生成することができます。

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

https://buildpacks.io/
配属前の研修では Dockerfile を書いて、コンテナ イメージを作成するというのを行っていたので、Buildpacks が便利すぎて感動しました。

コンテナ イメージの保存

では、Buildpacks を使用して Hello World のソースコードから作成したコンテナイ メージを Artifact Registry に Push していきます。

事前に Artifact Registry のリポジトリを用意します。
リポジトリの作成方法は以下の公式ドキュメントをご参照ください。

https://cloud.google.com/artifact-registry/docs/docker/store-docker-container-images?hl=ja#create

そして、ローカル環境のソースコードがあるディレクトリに移動し、以下のコマンドを実行していきます。

$PROJECT_IDには使用している Google Cloud プロジェクトのプロジェクト ID を入力し、$AR_REPO_NAMEには先ほど作成した Artifact Registry リポジトリの名前を入力します。

# ビルダーバージョン
$BUILDER=gcr.io/buildpacks/builder:v1

# ArtifactRepository パス/イメージ名:タグ(※ ArtifactRepository リポジトリは事前に作成しておく)
$IMAGE=asia-northeast1-docker.pkg.dev/$PROJECT_ID/$AR_REPO_NAME/sample:v1

# Cloud Build でビルドして Artifact Repository にプッシュ
$ gcloud builds submit --pack image=$IMAGE

このコマンドを実行すると Cloud Build がソースコードを build し、Artifact Registry にイメージが Push されます。
成功すると Artifact Registry リポジトリ内にイメージの一覧が表示されます。

3.作成したコンテナ イメージを Cloud Run にデプロイ

Google Cloud Console(以下、Console)で Cloud Run サービスを作成していきます。
以下の公式ドキュメントの手順に従って行います。
コンテナ イメージの URL を選択する際に、先ほど Artifact Registry に push されたコンテナ イメージを選択してください。
その他の設定はデフォルトのままで大丈夫です。

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

作成が完了すると、このようなサービスの詳細が表示されます。


生成された URL にアクセスし、Hello World と表示されたら OK です。


これで α のステップで必要な要件は全て完了です。

β. Spanner の基本

二つ目のステップである β は Spanner の概要を理解します。基本的な使い方と次のステップに向けて、Golang で Spanner との接続、Read、Write を行うという内容です。

学んだ Spanner の概要を以下にまとめます。

  • フルマネージドなリレーショナル データベース
    • Gmail など Google が内部で使用しているシステムをベースにしたサービスです。
  • 水平方向の柔軟なスケーリング
    • アクセス数に応じたリソースの最適化などが可能です。
  • リージョナル99.99% / マルチリージョン99.999%の可用性を提供
    • マルチリージョン構成であれば年間で5分間しか停止しない計算となります。

大きな特徴としてはこのような点が挙げられると思います。 もちろん、これ以外にもたくさんの機能を有しているので詳細について知りたい方は公式ドキュメントをご参照ください。

https://cloud.google.com/spanner?hl=ja

Cloud Spanner の概要に触れたところでここからは β のステップで満たすべき要件を順に説明していきます。

1. Console でクイック スタート

まずは、Console を使用して Spanner の基本操作を覚えていきます。
公式ドキュメントのクイック スタートを参照しながら、手順に沿って Spanner を操作していきます。

https://cloud.google.com/spanner/docs/quickstart-console?hl=ja

Spanner はデータベースとしては比較的高額であり、東京リージョンにコンピューティング容量を既定値で作成すると約$1/hourの料金が発生するので注意が必要です。
インスタンス作成時にコンピューティング容量を processing units(以下、PU)、もしくは nodes で指定します。
1 node は 1000 PU であり、100 PU 単位で設定することができます。
デフォルトでは 1000 PU ですが、今回はコスト面を考慮し 100 PU にして作成します。

PU と nodes についてさらに詳しく知りたい方は、以下の公式ドキュメントをご参照ください。

https://cloud.google.com/spanner/docs/compute-capacity?hl=ja

それ以外はクイック スタートの通りに行います。

2. Hello World のコードを改修して Spanner と接続するコードを作成

クライアント ライブラリ

Spanner の操作をある程度理解したところで、先ほど作成したインスタンスとデータベースに対して Golang を使って接続します。

プログラムを通じて、Google Cloud を扱う場合はアプリケーション・プログラミング・インタフェース(API)として Google Cloud APIs が用意されています。

Google Cloud APIs へのアクセスはクライアント ライブラリを使用することで可能になります。
Google は多種多様なクライアントライブラリを提供していますが、使用している言語がサポートされているなら Cloud クライアント ライブラリの使用を推奨しています。
Golang はサポートされている言語の一つですので Cloud クライアント ライブラリを使用します。

今回は Cloud Spanner API にアクセスします。

ディレクトリ構造

α で作成した main.go のファイルに追加していくことも可能ですが、main パッケージはプログラムの起点となるため、main パッケージから他のパッケージをインポートして動かす方がわかりやすいと思います。

そのため、新しいディレクトリを作成し、そこにファイルを追加していきます。
僕は spanner ディレクトリを作成し、そこに connection.go ファイルを追加しました。ディレクトリ名とファイル名はなんでも大丈夫です。

.
├── README.md
├── go.mod
├── go.sum
├── main.go
└── spanner
 └── connection.go //今回追加

コードの作成

今回、Spanner と接続するコードは以下のものを参照し、connection.go に Connect 関数を作成していきます。

https://cloud.google.com/spanner/docs/reference/libraries#client-libraries-usage-go

Connect 関数を作成し終えたら、main.go で呼び出しましょう。
Golang では関数が外部から呼び出される場合、その関数名の先頭を大文字にする必要があるようなので注意してください。
僕はそれを知らずに関数が使えないことでかなりの時間格闘してしまいました。

main.go
// func connect()だと外部から呼び出せない
func Connect(w http.ResponseWriter, r *http.Request){
}

呼び出し方ですが、今回の要件は Hello World のコードを改修することが含まれているので、Hello World のコードとなんらか応用する形にしたかったのでHandleFunc()の引数として呼び出すことにしました。

main.go
func main() {
    
http.HandleFunc("/connect",spanner.Connect)
    err := http.ListenAndServe(":9090", nil) //監視するポートを設定します。
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

このようにすることで、HTTP レスポンスとして接続を確認することができます。

HandleFunc()の引数で指定したパスへアクセスし、ブラウザで "Got Value 1" と "Done" という文字列が出力できていれば完了です。

3. Spanner データベースのテーブルからデータを読み取るコードを作成

ディレクトリ構造

今回は Read 関数を作成するので spanner ディレクトリに read.go ファイルを追加しました。

.
├── README.md
├── go.mod
├── go.sum
├── main.go
└── spanner
    ├── connection.go
    └── read.go //今回追加

コードの作成

ここでは以下の公式ドキュメントを参考にし、read.go ファイルに Read 関数を作成していきます。

https://cloud.google.com/spanner/docs/getting-started/go#using_the_client_library_for

あとは Connect 関数と同様に main.go で呼び出しましょう。
実行し、以下のように作成した Spanner データベースのテーブルの内容が表示されれば完了です。

4. Spanner データベースのテーブルにデータを挿入するコードを作成

DML と Mutation

書き込みをする場合 DML と Mutation という二つの種類があり、どちらかを選択する必要があります。
違いを簡単に説明すると、DML では SQL のクエリを使ったデータの挿入が可能で、Mutation ではMutation.Insert()などのメソッドを使ってデータを挿入します。
両者の比較は以下の公式ドキュメントでご参照ください。

https://cloud.google.com/spanner/docs/dml-versus-mutations?hl=ja#feature_comparison_between_dml_and_mutations

今回は、これまでの工程で SQL のクエリを使っていたので DML を使ってコードを作成しました。

ディレクトリ構造

Write 関数を作成していきます。spanner ディレクトリに write.go ファイルを追加しました。

.
├── README.md
├── go.mod
├── go.sum
├── main.go
└── spanner
    ├── connection.go
    ├── read.go
    └── write.go //今回追加

コードの作成

ここでは、以下のドキュメントを参考にし、write.go ファイルに Write 関数を作成していきます。

https://cloud.google.com/spanner/docs/getting-started/go#write-data

main.go で呼び出す処理を追加した後、実行し、以下のような表示が出るのを確認します。


出力を確認したら、Console で Spanner データベースにデータが追加されているか確認しましょう。
追加されていれば OK です。

これで β のステップで必要な要件は全て完了です。

γ. Cloud Run + Spannerで CRUD 作成

三つ目のステップである γ ではいよいよ本課題のゴールであるクラウド ネイティブ アーキテクチャ実装の第一段階として CRUD アプリケーションの作成をしていきます。

CRUD

CRUD とはデータベースを操作する際の基本的な機能である Create/Read/Update/Delete の頭文字を取ったものです。今回は Spanner データベースに対して CRUD 機能を実行する Web アプリケーションを作成します。

では、γ のステップで満たすべき要件を順に説明していきます。

1.Spanner データベースの設定

スキーマ

データベースの構造であるスキーマについて確認しましょう。
これまではクイック スタートで作成したスキーマを使用していましたが、ここからはスキーマの内容が指定されています。

以下はスキーマの内容です。
RPG ゲームのようなものを想定した内容になっています。プレイヤーの ID にユーザー名や、やくそうなどのアイテムとその数、さらにデータが作成された時間が紐づきます。

インターリーブ

インタリーブとは Spanner においてテーブルの親子関係を定義する方法の一つです。
今回においては Users テーブルを親テーブルとし、そこに子テーブルとして UserItems テーブルを関連付けています。
詳細は以下の公式ドキュメントをご参照ください。

https://cloud.google.com/spanner/docs/schema-and-data-model?hl=ja

データベースとスキーマの作成

実際に指定された内容に従って、データベース及びスキーマの作成をしていきます。
最初はクイック スタートで行ったように Console で、インスタンスとデータベースを作成し、スキーマの内容に沿ったステートメントを記述する手順を取りました。
しかし、本課題においてはコスト面を考慮し、Spanner を適宜削除していたので、削除するたびに毎回この方法を行う必要がありました。

それは面倒なので、僕はインスタンスの作成をコマンドラインツールである Google Cloud CLI で行い、データベースの作成は Golang を使って行うようにしていました。
こうすることで、Spanner を削除してもコマンドラインとコードを実行するだけでインスタンスとデータベース、スキーマの再作成が完了し、GUIでの操作が不要になります。
詳細な方法は以下の公式ドキュメントをご参照ください。
公式ドキュメントでは 1 node(1000 PU)を指定しており、本課題は最小の 100 PU であることに注意して下さい。

https://cloud.google.com/spanner/docs/getting-started/go?hl=ja#create_an_instance

https://cloud.google.com/spanner/docs/getting-started/go?hl=ja#create_a_database

2.テーブルにデータを挿入する CREATE の作成

データベースができたので、CRUD を一つずつ作成していきます。
CREATE を実装していきましょう。

ディレクトリ構造

まずは、CREATE の機能を持つ Create 関数を実装していきます。
spanner ディレクトリに create.go を追加していきます。

Create 関数の動作は前回のステップで実装した Write 関数と基本は一緒なので write.go のファイル名を変更しました。(connection.go は今回のステップでは必要ないのでここで削除しても問題ありません)

db.go ファイルというのが追加されていますが、これは上記で説明したデータベースを作成する関数を記述したファイルです。

.
├── README.md
├── go.mod
├── go.sum
├── main.go
└── spanner
  ├── connection.go //削除して良い
  ├── create.go //write.goの名称を変更
 ├── db.go
 └── read.go

機能の詳細

CREATE の機能がどのように動作すれば良いか、さらに詳細な要件が用意されています。
以下の項目が CREATE を実装する上で満たす必要のある要件になります。

a.パス「/register」

「/register」に対してリクエストがあった際に、CREATE が実行されるように実装します。

b.JSON で名前を受け取り、User と UserItem テーブルに Insert する

ユーザー側が {"Name":"aaa"} のような JSON 形式で名前のデータを送信し、アプリケーション側はそれを受け取って、Spanner にデータを挿入するように実装します。

c.User を Insert する際に ItemID:1のやくそうx3を UserItem に Insert するロジックを含める

ユーザー側から名前を受け取り、User テーブルにデータを挿入すると同時に ItemID と数量を UserItem に挿入するように実装します。

d.レスポンスは {“message”:”OK”} とレスポンスする

ユーザからのリクエストが成功した場合 {“message”:”OK”} とレスポンスを返すように実装します。

e.ユーザが見つからない場合は {“message”:”Not Found”} とレスポンスする

ユーザからのリクエストが失敗した場合 {“message”:”Not Found”} とレスポンスを返すように実装します。

コードの作成

上記の a~e を順に作成していきましょう。

β のステップで実装した Write 関数を参考にコードの作成を行いましょう。

ここから JSON 形式での受け取りや返すといった動作の実装を加える必要があります。
僕はそこの動作についての理解や実装方法がわからず苦労しました。
Golang では encoding/json パッケージを使用して、それらの動作を実装することができます。
詳細は以下の公式ドキュメントをご参照ください。

https://pkg.go.dev/encoding/json

コードの作成を終えたら実行して「/register」に対して JSON 形式のリクエストを送ってみましょう。
以下のように Curl でリクエストを送信します。

curl -X POST -H "Content-Type: application/json" -d '{"Name":"Kurt"}' localhost:9090/register
{"message":"OK"}

{“message”:”OK”} とレスポンスが返ってきたことを確認し、Console の画面で以下のようにデータが挿入されていれば完了です。

3.テーブルのデータを読み取る READ の作成

ディレクトリ構造

READ 機能をもつ、Read 関数を実装していきます。
今回は read.go の中身を修正して、コードを作成していきましょう。
ディレクトリの構造には変更はないです。

READ の詳細

READ の動作として満たす必要のある要件は以下のようになっています。

a. パス「/」

「/」へのリクエストがあった際に READ が実行されるように実装します。

b. User と UserItem テーブルをレスポンスして表示(JSON,HTML どちらでもよい)

「/」にアクセスすると User と UserItem テーブルの内容が JSON 形式もしくは HTML 形式で表示されるように実装する

コードの作成

上記の a~b を順に作成していきましょう。
Read 関数は β のステップですでに実装したものをベースにコードを作成します。

レスポンスを JSON 形式で返すか、HTML で返すかは自由ですが今回は JSON 形式で返すよう実装していきます。

コードの作成を終えたら実行して localhost:9090 にアクセスしてみましょう。
JSON 形式だとこのように表示されるかと思います。


どちらの形式でも localhost:9090 へのアクセスで Spanner の内容を見ることができれば OK です。

4.テーブルのデータを更新する UPDATE の作成

ディレクトリ構造

Update 関数を実装していきます。
spanner ディレクトリに update.go ファイルを追加します。

.
├── README.md
├── go.mod
├── go.sum
├── main.go
└── spanner
  ├── create.go 
  ├── db.go
  ├── read.go
  └── update.go //今回追加

UPDATE の詳細

UPDATE の動作として満たす必要のある要件は以下のようになっています。

a.パス「/update」

「/update」に対してリクエストがあった際に、UPDATE が実行されるように実装します。

b. JSON で UserID と ItemID と Qty を受け取り、数量を更新

ユーザ側が {"UseID":"A","ItemID":"1:やくそう","Qty":5} のような JSON 形式で UserID、ItemID、Qty のデータを送信し、
アプリケーション側は受け取った UserID と ItemID に紐づく Qty の値を更新するように実装します。

c.レスポンスは {“message”:”OK”} とレスポンスする

ユーザからのリクエストが成功した場合 {“message”:”OK”} とレスポンスを返すように実装します。

d.ユーザが見つからない場合は {“message”:”Not Found”} とレスポンスする

ユーザからのリクエストが失敗した場合 {“message”:”Not Found”} とレスポンスを返すように実装します。

コードの作成

では、上記の a~d を順に作成していきましょう。

以下の公式ドキュメントを参考にしながらデータベースの値を更新するコードを作成していきます。

https://cloud.google.com/spanner/docs/samples/spanner-dml-standard-update?hl=ja#spanner_dml_standard_update-go

それ以外の要件に関しては CREATE のコードで作成したものを参照すれば問題ありません。

コードの作成を終えたら、「/update」に対して JSON 形式で数量を変更するようにリクエストを送りましょう。
以下のように Curl でリクエストを送信します

curl -X POST -H "Content-Type: application/json" -d '{"UserID":"2fa56411-9228-4ee4-bc5f-96f5493a4d99","ItemID":"1:やくそう","Qty":5}' localhost:9090/update
{"message":"OK"}

{“message”:”OK”}とレスポンスが返ってきたことを確認し、Console 画面で以下のように数量が変わっていれば OK です。

5.テーブルのデータを削除する DELETE の作成

ディレクトリ構造
Delete 関数を実装していきます。
spanner ディレクトリに delete.go ファイルを追加しましょう。

.
├── README.md
├── go.mod
├── go.sum
├── main.go
└── spanner
  ├── connection.go
  ├── create.go 
  ├── db.go
 ├── delete.go //今回追加
  ├── read.go
  └── update.go

DELETE の詳細

DELETE の動作として満たす必要のある要件は以下のようになっています。

a.パス「/delete」

「/delete」に対してリクエストがあった際に、DELETE が実行されるように実装します。

b. JSON で UserID を受け取り、ユーザとそれに紐ついたアイテムを削除する

ユーザー側は {"UserID":"A"} のような JSON 形式で UserID を送信し、
アプリケーション側は受け取った UserID に紐づくデータを全て削除します。

c.レスポンスは {“message”:”OK”} とレスポンスする

ユーザからのリクエストが成功した場合 {“message”:”OK”} とレスポンスを返すように実装します。

d.ユーザが見つからない場合は {“message”:”Not Found”} とレスポンスする

ユーザからのリクエストが失敗した場合 {“message”:”Not Found”} とレスポンスを返すように実装します。

コードの作成

上記の a~d を順に作成していきましょう。

以下の公式ドキュメントを参考にしながら、データベースの値を削除するコードを作成します。

https://cloud.google.com/spanner/docs/samples/spanner-dml-standard-delete?hl=ja#spanner_dml_standard_delete-go

UPDATE と同様に、それ以外の要件に関してはこれまで作成したものを参照します。
コードの作成を終えたら、「/delete」に対して JSON 形式でデータを削除をするように curl でリクエストを送りましょう。

curl -X POST -H "Content-Type: application/json" -d '{"UserID":"6f662c40-fc68-4eb5-b8af-0a86c5730ca3"}' localhost:9090/delete
{"message":"OK"}

{"message":"OK"}とレスポンスが返ってきたので Conslole 画面でデータが削除されているはずです。

削除した UserID を指定して、もう一度リクエストを送ってみましょう。
データが削除されていれば、ユーザーは見つからないので
以下のように{“message”:”Not Found”}とレスポンスが返ってきます。

curl -X POST -H "Content-Type: application/json" -d '{"UserID":"6f662c40-fc68-4eb5-b8af-0a86c5730ca3"}' localhost:9090/delete
{"message":"Not Found"}

これで CRUD の全ての機能が完成しました。

6.CRUD 用の Cloud Run サービスをデプロイ

ソースコードが完成したので、CRUD の Cloud Run サービスを作成していきます。
α のステップで行ったことと同様に、Buildpacks を使って CRUD のソースコードからコンテナ イメージを Artifact Registry にPushします。
リポジトリはすでに作成しているものを使用して大丈夫です。

Cloud Run 上で先ほど保存された新たなコンテナ イメージを選択し、サービスを作成します。

生成された URL にアクセスすると、Spanner のデータが表示されるはずです。
これで READ が正常に動作していることを確認できます。

CREATE が動作するか確認します。
以下のように生成された URL に対して、curl でリクエストを送りましょう。

curl -X POST -H "Content-Type: application/json" -d '{"Name":"Jimi"}' https://native-service-d3xjmesdsa-an.a.run.app/register
{"message":"OK"}

Console 画面でデータが追加されていればデータの挿入もできています。
同様に他の機能も動作するか確認してみましょう。
全て正常に動作すれば、γ のステップは完了です。

δ.管理画面の作成

四つ目のステップである δ はクラウド ネイティブ アーキテクチャ実装の第二段階として、Spanner のデータを表示することを想定した管理画面の作成を行います。

δ のステップで満たすべき要件を順に説明していきます。

1.CRUD と同一リポジトリに追加する

Web サーバ用と管理画面用でコンテナ イメージが異なるのでリポジトリを分けた方が管理しやすいです。しかし、本課題においては研修ということもあり同一リポジトリに管理画面のソースコードを追加していきます。

ディレクトリ構造

CRUD のソースコードと管理画面のソースコードが混同してわかりにくくなることを防ぐためにディレクトリを整理します。
今回、以下のように app ディレクトリと manage ディレクトリを作成し、app ディレクトリの方に CRUD のファイルを移動しました。

.
├── README.md
├── app // 今回追加し、app 以下にファイルを移動
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── spanner
│      ├── create.go
│      ├── db.go
│      ├── delete.go
│      ├── read.go
│      └── update.go
│  
│       
├── go.work
├── go.work.sum
└── manage  // 今回追加
    ├── go.mod
    ├── go.sum
    └── main.go

2.「/」にアクセスで、テーブル全件をレスポンスする

γのステップで実装した READ と同様に、「/」にアクセスすることで Spanner のデータが表示されるようにします。
READ のソースコードを元に作成していきましょう。

3.表示形式は HTML かつ表にする

html/templateパッケージ

CRUD のアプリケーションでは、READ のレスポンスを JSON 形式で返すように実装しましたが、今回の管理画面では HTML 形式でレスポンスを返すように実装します。

今回のようにデータベースのデータを HTML に渡すなど動的な HTML を作成したい場合、テンプレート エンジンという仕組みを使う必要があります。

Golang ではそのようなテンプレート エンジンとして html/template パッケージが用意されており、HTML に対して値を渡すことができます。

https://pkg.go.dev/html/template

datatables

さらに、Spanner のデータを HTML で表にして閲覧できるようにします。

今回は datatables を使って表を作成します。
datatables とは Javascript のライブラリである、jquery のプラグインツールです。
HTML で表を作る<table>タグに対してソートやページング、検索などの機能を追加できます。

https://datatables.net/

ディレクトリ構造

read ディレクトリを作成し、新しい read.go ファイルと template ディレクトリに manage.html ファイルを追加します。

.
├── README.md
├── app 
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── spanner
│        ├── create.go
│        ├── db.go
│        ├── delete.go
│        ├── read.go
│        └── update.go
│  
│       
├── go.work
├── go.work.sum
└── manage  
  |    ├── go.mod
  |    ├── go.sum
  |    └── main.go
  ├── read
  │    └── read.go //今回追加
  └── template
    └── manage.html //今回追加

コード作成

read.go ファイルに READ のコードを作成していきます。
基本的にはこれまでのステップで作成した、Read 関数のソースコードを参照しましょう。
そこに html/template パッケージを追加し、html ファイルにデータを渡すようにコードを少し改修していきます。
以下の公式ドキュメントをご参照ください。

https://pkg.go.dev/html/template#ParseFiles

manage.html の作成に移ります。
基本的には<table>タグを用いた表を作成する方法をベースに HTML を記述していきましょう。

datatables を使用するには datatables を読み込む必要があります。
詳細は以下の公式ドキュメントをご参照ください。

https://www.datatables.net/download/index

html/template パッケージで受け取ったデータを表示させるように HTML を記述していきます。
以下の golang 公式ドキュメントの Example などを参考にしながらコードを作成します。

https://pkg.go.dev/html/template#pkg-examples

作成を終えたら、実行し localhost:9090 にアクセスしてみましょう。
以下のような画面が表示されれば OK です。

4.管理画面用の Cloud Run サービスを別途デプロイする

最後に、管理画面用の新しい Cloud Run サービスを作成します。
手順はこれまでと同様に Buildpacks を使用して行います。
今回は新しいサービスを作成するので、以下のように CRUD 用と管理画面用の二つのサービスが存在する形になります。


管理画面用のサービスで生成された URL にアクセスし、作成した管理画面と同じものが表示されれば δ のステップは完了です。

Example. γ と δ を実案件で使える構成に変更

五つ目のステップである Example では、クラウドネイティブ アーキテクチャの構築を行います。
γ と δ のステップで作成した Web アプリケーションに対してインフラ構成を適用していきます。

Example のステップで満たすべき要件を順に説明していきます。

1.構成図

本記事の冒頭で紹介した、構成図通りにアーキテクチャを構築していきます。
改めて構成図の記載をします。
今回は以下の赤枠で囲った部分の構築を行います。

2.CRUD アプリケーションのレイテンシーを高める

マルチリージョン構成

Cloud Run では複数リージョンにサービスをデプロイし、ユーザーから最も近いリージョンにルーティングさせることで迅速にレスポンスを返すことができます。

詳細は以下のドキュメントをご参照ください。

https://cloud.google.com/run/docs/multiple-regions?hl=ja#deploy-service

Cloud Load Balancing

ユーザーのルーティングを設定するためには Cloud Load Balancing の外部アプリケーションロードバランサを構成する必要があります。

Cloud Load Balancing はユーザーからのリクエストを最初に受け取り、背後にある様々な Google Cloud サービスにルーティングさせ、トラフィックを分散させます。
詳細は以下の公式ドキュメントをご参照ください。

https://cloud.google.com/load-balancing/docs/load-balancing-overview?hl=ja

構成を変更

まずは、CRUD 用 Cloud Run サービスをマルチリージョン構成にしていきます。
今回は東京リージョンと大阪リージョンの二つにデプロイします。
以下の公式ドキュメントを参照しながら行いましょう。

https://cloud.google.com/run/docs/multiple-regions?hl=ja#deploy-service

以下のように同じサービスが複数リージョンでデプロイされている形になります。


では、ロードバランサの作成をしていきましょう。
今回は背後にあるバックエンド サービスを Cloud Run にし、グローバル外部アプリケーション ロードバランサを設定します。
以下の公式ドキュメントを参照し、設定を行います。

https://cloud.google.com/load-balancing/docs/https/setup-global-ext-https-serverless?hl=ja

作成を終えたら、ロードバランサ経由でアクセスしてみましょう。CRUD のアプリケーションがこれまでと同じように動作すれば OK です。

3.CRUD アプリケーションのセキュリティを高める

これまでで Cloud Run の前段にロードバランサを立たせる構成ができました。
現状の構成だと、誰でもアクセスできるようになっています。
そのため、外部や内部を問わず悪意のあるユーザからの攻撃を防ぐことができません。
攻撃からシステムを守るためにもセキュリティについて考える必要があります。

Cloud Armor

Cloud Armor は分散型サービス拒否(DDoS)攻撃や SQL インジェクションといったアプリケーション攻撃などの様々な攻撃からアプリケーションを保護することができます。
ロードバランサのバックエンド サービスとして実行されているアプリケーションを攻撃から保護するためにはセキュリティ ポリシーを構成する必要があります。
詳細は以下の公式ドキュメントをご参照ください。

https://cloud.google.com/armor/docs/cloud-armor-overview?hl=ja

構成を変更

今回は日本リージョン以外からのアクセスを拒否する地理制限と自宅 IP からのアクセスのみを許可する IP 制限をもつセキュリティ ポリシーを構成していきます。

まずは、地理制限を作成します。
以下の公式ドキュメントを参照しながら、セキュリティ ポリシー ルールを作成していきます。

https://cloud.google.com/armor/docs/configure-security-policies?hl=ja#create-example-policies

セキュリティ ポリシー ルールは一致条件を構成することでアクセス可否の定義ができます。
地理制限は詳細モードで、式を記述するためのカスタムルール言語を使用し、一致条件を構成する必要があります。
下記の公式ドキュメントも併せて参照し、作成しましょう。

https://cloud.google.com/armor/docs/rules-language-reference?hl=ja#expression-examples

作成が完了したら、正しく設定できているか確認していきましょう。
今回は、海外リージョンでGoogle Compute Engine インスタンスを作成し、SSH で接続したのちに Curl コマンドでリクエストを送信します。
Google Compute Engine については以下の公式ドキュメントをご参照ください。

https://cloud.google.com/compute/docs/create-linux-vm-instance?hl=ja

Curl コマンドでリクエストを送信し、以下のようにアクセスが拒否されれば地理制限は完了です。


次に IP 制限をしていきましょう。
事前に「グローバル IP アドレス 確認」といった検索ワードを用いて自宅の IP アドレスを調べておいてください。
地理制限と同様に以下の公式ドキュメントを参照しながら、自宅 IP のみを許可するセキュリティ ポリシー ルールを作成します。
IP 制限は基本モードで作成することができるので以下の公式ドキュメントを参照するだけで十分かと思います。

https://cloud.google.com/armor/docs/configure-security-policies?hl=ja#create-example-policies

セキュリティ ポリシーの作成が完了したら、スマホを使うなどして、自宅 IP 以外からアクセスしてみましょう。
正しく設定できていれば、以下のようになります。
これで IP 制限も完了です。

4.アクセスするユーザを制御する

CRUD 用 Cloud Run サービスに対するインフラ構成の作成が完了したので管理画面用 Cloud Run サービスのインフラ構成を作成していきましょう。

Identity-Aware Proxy

管理画面には管理者のみが閲覧できるように設定していきます。

Identity-Aware Proxy(以下、IAP) はアプリケーションやリソースに対してアクセス制御を適用することができます。

IAP によって保護されたアプリケーションやリソースには適切な権限を持つユーザがプロキシ経由でのみアクセス可能になり、アクセスしようとすると IAP は認証と認可のチェックを行います。

詳細は以下の公式ドキュメントをご参照ください。

https://cloud.google.com/iap?hl=ja

構成を変更

Cloud Run サービスを IAP で保護するためにはロードバランサを設定する必要があります。
先ほどと同様に Cloud Load Balancing のグローバル外部ロードバランサを設定していきましょう。

ロードバランサが作成されたら IAP を有効化します。
以下の公式ドキュメントを参考に行いましょう。

https://cloud.google.com/iap/docs/enabling-cloud-run?hl=ja

IAP を有効にした後、ロードバランサ経由でアクセスしてみると以下のようにアカウントの選択画面が表示されるはずです。


アカウントを選択し、そのアカウントに適切な権限を与えていれば管理画面が表示されます。
権限を持っていないアカウントでログインした場合、以下のような画面が表示されます。


IAP によるアクセス制御ができていることを確認できました。
これで構成図通りに全て構築したので Example は完了です。

おわりに

本記事では、SRE ディビジョンの研修課題の内容や手順、課題を行う中で学んだことを紹介しました。
SRE ディビジョンの新卒向け研修では何をしているのかその雰囲気がお伝えできていれば幸いです。

冒頭でもお話ししましたが、僕は入社時点で開発経験がほとんどない状態で、一人でプログラムを書いて、アプリケーションを作成するのはこの新卒向け研修が初めてでした。
そのよう中でも同時に課題に取り組んでいた同期や先輩の助けを得ながらではありますが、本記事で紹介した内容を約1ヶ月で全てクリアし、少しは成長できたかなと課題を終えた今は感じています。

本課題を通じてアプリケーションやインフラ構成の基礎から応用まで様々なことを学ぶことが出来ました。
同時に、自分の足りない部分もたくさん認識することが出来たので、今後も Google Cloud のサービスに触れ、学習を続けていきたい所存です。
その際はまた、学んだことのアウトプットとして記事にしていければと思うのでよろしくお願いいたします。
最後まで読んでいただきありがとうございました。

Discussion