GCSとGLBで静的サイトを簡単に構築！

# 概要
静的サイトをGCPで簡単に構築する方法を共有します！
今回はTerraformを使用し、下記のような構成でサイトを構築しようと思います。

* GCS
    * GLBからのリクエストを受付、HTML, CSS, JSを表示する
* GLB
    * http→httpsリダイレクトを行う
    * GCSにリクエストを転送
* 証明書
    * GCPのマネージド証明書を利用

![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176573/7c33edb2-4bff-7b2e-1d7d-7d458077866d.png)

GCSではhtaccessなどが動作しないので、GLBでhttpsリダイレクトの役割を持たせることにします。

リダイレクトの主な仕組みについては、[公式ドキュメント](https://cloud.google.com/load-balancing/docs/https/setting-up-http-https-redirect)がわかりやすく解説してくれてますので、興味があればご覧ください！
また、証明書の更新もGCPのマネージド証明書を利用すると自動でやってくれるので、マネージド証明書を利用します。

# Terraformによる構築
## 共通設定
provider.tf

```tf
provider "google" {
  credentials = file(var.credentials)
  project     = var.project
}

provider "google-beta" {
  credentials = file(var.credentials)
  project     = var.project
}
```

## GCSバケット
下記の内容でバケットを構築します。

* バージョニングを有効化
* 特殊ページを設定
    * index.html
    * 404.html
* アクセス権を均一化

main.tf

```tf
resource "google_storage_bucket" "bucket" {
  name          = var.advent_calendar_bucket.bucket_name
  location      = var.advent_calendar_bucket.region
  storage_class = var.advent_calendar_bucket.storage_class
  force_destroy = true
  labels = {
    project = "advent-calendar"
  }
  # バージョニングの有効化
  versioning {
    enabled = true
  }
  # 特殊ページの割り当て
  website {
    main_page_suffix = "index.html"
    not_found_page   = "404.html"
  }
  # アクセスレベルを均一化
  uniform_bucket_level_access = true
}

# 誰でも見れるように一般公開(allUsers)にする
resource "google_storage_bucket_iam_member" "advent_calendar_bucket_iam_member_object_viewer" {
  bucket = google_storage_bucket.bucket.name
  role   = "roles/storage.legacyObjectReader"
  member = "allUsers"
}
```

variables.tf

```tf
variable "advent_calendar_bucket" {
  default = {
    bucket_name   = "バケット名"
    region        = "asia"
    storage_class = "MULTI_REGIONAL"
  }
}
```

GCSのアクセス制御は[GCPの推奨アーキテクチャ](https://cloud.google.com/storage/docs/access-control.html?authuser=2&hl=ja)通り、アクセスレベルを均一にしています。公開するバケットにてオブジェクト単位でアクセス制御を行っていると、オブジェクトを新しく作成するたびにアクセス権を設定しないといけません。こういった作業は事故の元なので、アクセスレベルは均一にしています。

また、指定されたページがない時や`https://ドメイン名/`にアクセスされた時にデフォルトのページを表示したいので特殊ページを設定しています。

そのほかの特徴として、`google_storage_bucket_iam_member`を使用してバケットに紐づくIAMを設定しています。今回はバケットを公開するため、`allUsers`に`roles/storage.legacyObjectReader`を付与しています。
バケット公開をするにあたって`roles/storage.objectViewer`でも良いですが、こちらの権限はバケットのオブジェクト一覧を表示する権限があります。権限を最小化するため今回は`roles/storage.legacyObjectReader`を付与しています。
[参考公式リンク](https://cloud.google.com/storage/docs/hosting-static-website)

### GLB

main.tf（GCSのリソースと同じファイルに書く想定で書いてます。）

```tf
resource "google_compute_managed_ssl_certificate" "cert" {
  provider = google-beta
  name     = var.certificate_name

  managed {
    domains = [var.certificate_domain]
  }
}

resource "google_compute_global_address" "lb_ip_address" {
  name = var.global_address_name
}

resource "google_compute_backend_bucket" "lb_backend_bucket" {
  name        = var.backend_name
  bucket_name = google_storage_bucket.bucket.name
  enable_cdn  = var.enable_cdn
}

# http→httpsリダイレクトを行うためにLBを二つに分ける
resource "google_compute_url_map" "lb_http_url_map" {
  name = var.http_lb_name
  # default_url_redirectを指定する場合は、default_servicは指定してはいけない
  default_url_redirect {
    strip_query    = false
    https_redirect = true
  }
}

resource "google_compute_url_map" "lb_https_url_map" {
  name            = var.https_lb_name
  default_service = google_compute_backend_bucket.lb_backend_bucket.self_link
}

resource "google_compute_target_http_proxy" "lb_http_proxy" {
  name    = var.http_proxy_name
  url_map = google_compute_url_map.lb_http_url_map.self_link
}

resource "google_compute_target_https_proxy" "lb_https_proxy" {
  name             = var.https_proxy_name
  url_map          = google_compute_url_map.lb_https_url_map.self_link
  ssl_certificates = [google_compute_managed_ssl_certificate.cert.id]
}

resource "google_compute_global_forwarding_rule" "lb_http_forwarding_rule" {
  name       = var.http_forwarding_rule_name
  target     = google_compute_target_http_proxy.lb_http_proxy.self_link
  ip_address = google_compute_global_address.lb_ip_address.address
  port_range = var.http_forwarding_rule_port
}

resource "google_compute_global_forwarding_rule" "lb_https_forwarding_rule" {
  name       = var.https_forwarding_rule_name
  target     = google_compute_target_https_proxy.lb_https_proxy.self_link
  ip_address = google_compute_global_address.lb_ip_address.address
  port_range = var.https_forwarding_rule_port
}
```

variables.tf

```tf
variable "certificate_name" {
  default = "advent-calendar2020"
}
variable "certificate_domain" {
  default = "example.com"
}
variable "backend_name" {
  default = "advent-calendaer2020-backend"
}
variable "http_lb_name" {
  default = "advent-calendar2020-http-lb"
}
variable "https_lb_name" {
  default = "advent-calendar2020-https-lb"
}
variable "http_proxy_name" {
  default = "advent-calendar2020-http-proxy"
}
variable "https_proxy_name" {
  default = "advent-calendar2020-https-proxy"
}
variable "http_forwarding_rule_name" {
  default = "advent-calendar2020-http-forwarding-rule"
}
variable "https_forwarding_rule_name" {
  default = "advent-calendar2020-https-forwarding-rule"
}
variable "http_forwarding_rule_port" {
  default = "80"
}
variable "https_forwarding_rule_port" {
  default = "443"
}
variable "enable_cdn" {
  default = true
}
variable "global_address_name" {
  default = "advent-calendar2020-address"
}
```

Googleのロードバランサーは複数のリソースから成り立っています。
そのため、必要なリソースをterraformで定義しています。特徴としては、今回HTTPSリダイレクトを行っているため、 `default_url_redirect`を`url_map`で定義しています。
この時、クエリストリングを削ることもできるようですが、特に削る必要がないので削らないように設定をしています。


各リソースがどう関係しているかは[公式](https://cloud.google.com/load-balancing/docs/https/setting-up-http-https-redirect)の下記画像を見ていただくとわかりやすいと思います。構築したサイトと異なる点としては`Compute HTTPS load balancer`の`Backend Service`の部分が`Backend Bucket`になっているだけですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176573/315f576d-b37c-52cb-9b16-be779f2e299f.png)
https://cloud.google.com/load-balancing/docs/https/setting-up-http-https-redirect

### 特殊ページのアップロード

下記のような簡単なファイルをGCSバケットにアップロードします。

index.html

```html
<h1>Hello world.</h1>
```

404.html

```html
<h1>Not Found.</h1>
```



## 構築したページ
TOP

![スクリーンショット 2020-12-23 12.07.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176573/8c3c06ca-2677-76bd-643a-fa2f4e721517.png)


指定したパスがない時

![スクリーンショット 2020-12-23 12.06.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176573/e1a29a5f-b01d-c42f-f3a9-b0614e79fcdb.png)

# ちょっと困ったところ
## GCSを用いたGLBでは、IPアドレス制御などによるアクセス制限が行えない
一般的にGLBでアクセス制御を行うには下記の方法があります。

* [Cloud Armor](https://cloud.google.com/armor?hl=ja)
* [Identity-Aware Proxy](https://cloud.google.com/iap)

しかし、両方とも対応しているのはバックエンドサービスのみという事実。。。バックエンドバケットは対応していませんでした。。。

すでに機能追加リクエストがGCPにあがっているものの、まだ対応されていません。

* https://issuetracker.google.com/issues/114133245
* https://issuetracker.google.com/issues/78000408

こちら、ステージング環境を社内のみ・関連会社のみに共有したいという場合などにとても困ります。
また、Cloud Armorを設定したとしてもGCSのオブジェクト自体は公開されているので、公開した際に発行されるGCS専用URLがわかると誰でもアクセスできてしまいます。

### 対応方法
バケットの公開範囲を絞る方法として[VPC Service Control](https://cloud.google.com/vpc-service-controls)があります。

こちらを用いることで、BigQueryやGCSといった非VPCリソースに対してIP制限を行うことが可能だと聞いています。
ただ、こちらの設定はGCPの組織に紐づく設定のようで、十分に検討・検証を行わないと既存のトラフィックにも影響が出てしまう可能性があります。


# 良かったところ
* HTTPSリダイレクトをGLBに持たせることができる
* Cloud CDNで簡単にキャッシュできる
* マネージド証明書による証明書管理
    * コストなし・自動更新

証明書の更新作業やリダイレクト処理をGCPが賄ってくれるのが非常にありがたいと感じました！
また、terraformのコードさえ書いて仕舞えば、同じ構成のリソースを簡単に作れるので一時的なサイトなどを構築する際は非常に便利だと感じています。

# まとめ
GCS・GLBを利用することで簡単に静的サイトを構築することができました。
しかし、IPアドレスの制限などを行いたい場合はひと工夫ふた工夫必要になってきます🤥

皆さんにおすすめしたいこととしては、「バックエンドサービス(GCE)でできるのだから、バックエンドバケット(GCS)でもできるでしょ〜🤪」などといった思い込みはもたずに、案件が来たらちゃんと実現できるか公式ドキュメントを漁りましょう！（こんな思い込みをしてて痛い目にあいました。）

それでは、良いお年を！