Google Cloud Storage(GCS)にIP制限をかける with Terraform

# はじめに
本記事は[QualiArts Advent Calendar 2020](https://qiita.com/advent-calendar/2020/qualiarts) 14日目の記事です。

今回は、[Google Cloud Platform (GCP)](https://cloud.google.com/docs)のオブジェクトストレージである[Google Cloud Storage (GCS)](https://cloud.google.com/storage/docs/introduction)に対して、同じくGCPのサービスである[VPC Servive Controls (VPC-SC)](https://cloud.google.com/vpc-service-controls/docs/overview)と[Access Context Manager (ACM)](https://cloud.google.com/access-context-manager/docs/overview)でIP制限をかける構成の紹介と、その構成を[Terraform](https://www.terraform.io/docs/index.html)で管理する方法についてまとめます。

なお、**Access Context Manager**の一般的に広まっている略語を見つけられなかったため、本記事では勝手に**ACM**としています。(AWS Certificate Managerと被ってしまいますがご了承ください。)

# 前提知識

**VPC Servive Controls**
https://cloud.google.com/vpc-service-controls/docs/overview

**Access Context Manager**
https://cloud.google.com/access-context-manager/docs/overview

**組織リソース**
https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#organizations

いずれも公式ドキュメントに詳しくまとめられています。
また、本記事で出てくるVPC-SC、ACM関連の用語は下記に簡単にまとめられているので軽く目を通しておくと良いかもしれません。
https://cloud.google.com/vpc-service-controls/docs/overview#terminology


# 構成
VPC-SCでプロジェクトのGCSを含むサービス境界を作成します。

また、そのサービス境界に特定のIPアドレスのみを許可するアクセスレベル（ACMで作成）を設定し、1部のクライアントはアクセスできる状態にします。

VPC-SCの制約でバケット毎に制限をかけるかけることはできないので、プロジェクトの全てのバケットがサービス境界に含まれ、アクセスが制限されます。

構成図は以下の通りです。
![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878075/a8c90d3b-1d9a-7d0e-e058-707f7beb5fec.png)


# 実践
### GCPプロジェクトの準備
VPC-SCとACMを使用するためにはプロジェクトとは別に、そのプロジェクトが所属する[組織リソース](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#organizations)が必要となります。組織リソースの作成方法は本記事では解説しないので公式ドキュメントなどを参考にお願いします。

https://cloud.google.com/resource-manager/docs/creating-managing-organization

### Terraform用のサービスアカウントの準備
自分のローカル環境からterraformコマンドを実行する場合、通常であれば`gcloud auth application-default login`などで生成したエンドユーザーのcredentialファイルを認証に使用するかと思います。しかし、ACMのAPIではエンドユーザーのcredentialによる認証がサポートされていないため、ACM関連のresourceを使おうとすると下記のようなエラーが発生します。

```shell
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

google_access_context_manager_access_policy.access-policy: Refreshing state... [id=${your AccessPolicy id}]

Error: Error when reading or editing AccessContextManagerAccessPolicy "${your AccessPolicy id}": googleapi: Error 403: Your application has authenticated using end user credentials from the Google Cloud SDK or Google Cloud Shell which are not supported by the accesscontextmanager.googleapis.com. We recommend configuring the billing/quota_project setting in gcloud or using a service account through the auth/impersonate_service_account setting. For more information about service accounts and how to use them in your application, see https://cloud.google.com/docs/authentication/.
Details:
[
  {
    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
    "domain": "googleapis.com",
    "metadata": {
      "consumer": "projects/${your project number}",
      "service": "accesscontextmanager.googleapis.com"
    },
    "reason": "SERVICE_DISABLED"
  }
]
```

そこで、必要な権限を持ったサービスアカウントを作成し、そのサービスアカウントのcredentialファイルで認証するようにします。

必要な権限は以下の1つです。

* **Access Context Manager 管理者 (組織レベル)**

ここで注意しなければならないのは、**Access Context Manager 管理者**の権限を**組織リソースのIAM**で割り当てる必要があることです。また、組織リソースではサービスアカウントの管理ができないため、いずれかのプロジェクトで作成したサービスアカウントを組織リソースのIAMに追加して権限を割り当てることになります。特に理由がなければ制限をかける予定のプロジェクトでサービスアカウントを管理するのが丸いかと思います。

サービスアカウントを作成して権限を付与したら、キーをjson形式でDLしてterraformで使用するように環境変数を設定します。

```shell
$ export GOOGLE_CLOUD_KEYFILE_JSON=${your_credential_file_path}
```
<br>

ちなみに、GCP Consoleで操作する場合は加えて下記の権限を自分のIAMに付与しておく必要があります。

* Access Context Manager 管理者 (組織レベル)
* **Resource Manager 組織閲覧者**

https://cloud.google.com/access-context-manager/docs/access-control


### アクセスレベルの作成
まずはACMのアクセスレベルを作成します。
アクセスレベルにはアクセスを許可する条件を設定します。
ソースコードは下記の通りです。

```tf:main.tf
provider "google" {
  project = "${your project id}"
  region  = "${your project default region}"
  version = "~> 3.43"
}

resource "google_access_context_manager_access_policy" "access_policy" {
  parent = "organizations/${your organization id}"
  title  = "default policy"
}

resource "google_access_context_manager_access_level" "access_level" {
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/accessLevels/${your access level name}"
  title  = "${your title}"
  basic {
    conditions {
      ip_subnetworks = [
        "${your ip range}"
      ]
    }
  }
}
```

google_access_context_manager_access_levelがアクセスレベルを管理するresourceです。
アクセスレベルのnameには他のアクセスレベルのnameと重複しないような文字列を設定する必要があります。
また、特殊文字( _ や - )も使用できないので注意が必要です。

google_access_context_manager_access_levelのbasicブロックにアクセスを許可する条件を記述します。
今回は特定のIPアドレスからのアクセスを許可したいのでip_subnetworksを条件として使います。
${your ip range}の箇所に許可したいIPアドレス範囲を指定します。

`terraform apply`を実行すると[console上](https://console.cloud.google.com/security/access-level)でアクセスレベルが作成されているのを確認できると思います。
![accesslevel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878075/2028e688-4065-c0e1-3f1f-f75533b2d312.png)

### サービス境界の作成
次にVPC-SCのサービス境界を作成してGCSにアクセス制限をかけます。
なお、GCSにアクセス制限をかける関係上、tfstateの保管先に同じプロジェクトのGCSバケットを指定してしまうと**terraformコマンド実行時にアクセス制限に引っかかる場合がある**ので注意が必要です。

```tf:main.tf
resource "google_access_context_manager_service_perimeter" "service_perimeter" {
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/servicePerimeters/${your service perimeter name}"
  title  = "${your title}"

  status {
    resources = [
      "projects/${your project number}"
    ]

    restricted_services = [
      "storage.googleapis.com"
    ]

    access_levels = [
      google_access_context_manager_access_level.access_level.name,
    ]
  }
}
```

statusブロックのaccess_levelsで先ほど許可するIP範囲を設定したアクセスレベルを指定します。
複数のアクセスレベルを指定した場合は、いずれかのアクセスレベルを満たしていればアクセスを許可されます。

また、restricted_servicesにも複数のサービスを指定することが可能です。
GCS以外にも様々なGCPサービスに対してアクセス制限をかけることができます。

`terraform apply`を実行すると[console上](https://console.cloud.google.com/security/service-perimeter)でサービス境界が作成されているのを確認できると思います。
![vpc-sc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878075/0cfd5d93-df90-1258-dc1c-9f3f4ef1ef88.png)

### アクセス確認

では、実際にアクセスを制限できているか確認してみましょう。

GCSにバケットを作成し、適当なファイルを一般公開状態で配置してそのファイルを閲覧できるか試してみます。
バケットの作成、ファイルの配置は**許可したIPアドレスで行うように注意**してください。
許可したIPアドレス以外ではconsole上でもアクセス拒否されてしまいます。

許可したIPアドレスでアクセスすると以下の通りです。
![non-restricted.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878075/81e651c6-9f8f-5425-468e-ddcaf1174ee0.png)
次に、IPアドレスを許可されてないものに切り替えアクセスします。
![restricted.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878075/2a0467e8-ee47-08cc-be9c-4ee93c76a454.png)
拒否されていることが確認できました。

なお、ドキュメントには記載がありませんが、VPC-SCの設定が反映されるまでに**最大15分の遅延が発生する場合がある**[^1]そうなので、applyしたのにアクセスが制限されてない方は少し待ってみると良いと思います。

# おわりに
VPC-SCとACMを駆使することでGCSにIP制限をかけることができました。
サービスアカウントの権限周りなど、いくつかハマりポイントはあるものの、シンプルな設定で気軽にアクセス制限をかけられるサービスだと思います。
GCSバケット毎に境界を設定できるともっと使い勝手がよくなるのですが...

明日は[tmana](https://qiita.com/tmana)さんの記事です！

[^1]: GCPのサポートに問合せを行いました。