IP 制限付きで AWS EC2 から BigQuery にアクセスしてみた

2019.12.18

こんにちは、みかみです。

GCP & BigQuery 勉強中です。

実際に業務で使用するとなると、セキュリティの考慮は必須です。

普段業務で使っている AWS 環境では、セキュリティグループで IP 制限をかけ、許可されていない IP からのアクセスはできないようにしています。

GCP でも IP 制限かけられるの? どうやって? どの範囲で?

ということで。

やりたいこと

  • GCP ではどうやって IP 制限するのか学びたい
  • EC2 から BigQuery に IP 制限付きでアクセスしたい

GCP の IP 制限

AWS 同様 GCP でも VPC(VPC Service Controls)があり、Access Context Manager でアクセスレベルを設定できるそうです。

Access Context Manager では、IP アドレスやリージョン、デバイス単位でアクセスを制限できるとのこと。

こちらの記事が、非常に分かりやすかったです。

やってみた

GCP アカウントの権限設定

操作するアカウントに、以下2つの権限を付与してもらいました。

  • Access Context Manager 管理者(roles/accesscontextmanager.policyAdmin
  • Resource Manager 組織閲覧者(roles/resourcemanager.organizationViewer

VPC Service Controls も Access Context Manager も、プロジェクトより上位の「組織」に紐づくため、この2つの権限がないと、プロジェクトオーナーであってもアクセス制御を設定できません。

サービス境界の作成

ナビゲーションメニューの「セキュリティ」から「VPC Service Controls」をクリックし、サービス境界を作成します。

「新しい境界」リンクをクリックして、サービス境界を作成します。

「境界名」に任意の名前を入力し、「保護するプロジェクト」に自分のプロジェクトを追加。「保護するサービス」に「BigQuery API」と「Google Cloud Storage API」を追加して、「保存」をクリック。

無事サービス境界が作成できました。

ちなみに、Resource Manager 組織閲覧者(resourcemanager.organizationViewer) 権限がないと、「保存」ボタンクリックで下記エラーが表示されます。。(初め何のエラーか分かりませんでした。。。

サービス境界で保護設定した自分のプロジェクトを選択して、コンソールから BigQuery と GCS を参照しようとすると・・・

コンソールからのアクセスは、結局のところサービス境界外の local PC からのアクセスになるので、設定通り、参照できなくなりました。

同じプロジェクト内にあらかじめ作成しておいた GCE から BigQuery にアクセスしてみると・・・

mikami.yuki@test-cm-da-mikami:~$ bq ls
  datasetId
 -----------
  test_s3
mikami.yuki@test-cm-da-mikami:~$ bq query --use_legacy_sql=false \
> 'SELECT
>    name
>  FROM
>    `cm-da-mikami-yuki-258308`.test_s3.pref
>  WHERE
>    code = 1'
Waiting on bqjob_r5f3f0be91ace4063_0000016f12dca461_1 ... (0s) Current status: DONE
+------+
| name |
+------+
| 北海道  |
+------+

この GCE は サービス境界内のため、ちゃんとデータが参照できました。「保護するサービス」に GCE を設定しなくても、同じプロジェクト内ならサービス境界内に入るようです。

さらに、サービス境界外の EC2 からアクセスしようとすると・・・

[ec2-user@ip-172-31-31-170 ~]$ bq ls
BigQuery error in ls operation: VPC Service Controls: Request is prohibited by
organization's policy. vpcServiceControlsUniqueIdentifier: b9f2dd46d888e3ef.

ちゃんとアクセス拒否されています。

アクセスレベルを作成してサービス境界にアタッチ

続いてサービス境界に IP 制限を付与し、先ほどアクセスできなかった EC2 からのアクセスを許可してみます。

組織を選択した状態でナビゲーションメニュー「セキュリティ」の「Access Context Manager」画面で「新規」リンクをクリック。

「アクセスレベルのタイトル」に任意の名前を入力し、「条件」欄「属性を追加」から「IP サブネットワーク」を選択。許可する EC2 インスタンスのパブリック IP を入力したら「保存」をクリック。

アクセスレベルが作成できました。

作成したアクセスレベルを、サービス境界にアタッチします。

「VPC Service Controls」コンソールで初めに作成したサービス境界を選択し、編集画面でアクセスレベルを追加して「保存」。

先ほどアクセスエラーになった EC2 から、再度アクセスしてみます。

[ec2-user@ip-172-31-31-170 ~]$ bq ls
  datasetId
 -----------
  test_s3
[ec2-user@ip-172-31-31-170 ~]$ bq ls test_s3
  tableId   Type    Labels   Time Partitioning   Clustered Fields
 --------- ------- -------- ------------------- ------------------
  pref      TABLE
[ec2-user@ip-172-31-31-170 ~]$ bq query --use_legacy_sql=false \
> 'SELECT
>    name
>  FROM
>    `cm-da-mikami-yuki-258308`.test_s3.pref
>  WHERE
>    code = 1'
Waiting on bqjob_r4385587201489758_0000016f1336af95_1 ... (0s) Current status: DONE
+------+
| name |
+------+
| 北海道  |
+------+

無事、BigQueryが参照できるようになりました!

なお、初めは一部 bq コマンドでアクセスエラーが出たのですが、しばらく経ってからもう一度実行したら、無事データ参照できました。(設定後、少し待つ必要があるのかな?

アクセス許可していない IPからのアクセス(ローカルPCからコンソールでアクセス)は、拒否されたままです。

続いて EC2 で Python を実行して、GCS に格納済みのデータをロードしてみます。

ロードするデータはこちらからいただいてきた、アメリカの赤ちゃんの名前ファイルを.csv に変更したものです。

test_load.py

from google.cloud import bigquery

# データセット作成
client = bigquery.Client()
dataset_id = "{}.test_dataset".format(client.project)
dataset = bigquery.Dataset(dataset_id)
dataset.location = "asia-northeast1"

dataset = client.create_dataset(dataset)
print("Created dataset {}.{}".format(client.project, dataset.dataset_id))

# テーブル作成
table_id = "{}.{}.names_1880".format(client.project, dataset.dataset_id)

schema = [
    bigquery.SchemaField("name", "STRING", mode="REQUIRED"),
    bigquery.SchemaField("gender", "STRING", mode="REQUIRED"),
    bigquery.SchemaField("count", "INTEGER", mode="REQUIRED"),
]

table = bigquery.Table(table_id, schema=schema)
table = client.create_table(table)
print(
    "Created table {}.{}.{}".format(table.project, table.dataset_id, table.table_id)
)

# データロード
dataset_id = dataset_id.split(".")[-1]
table_id = table_id.split(".")[-1]

dataset_ref = client.dataset(dataset_id)
job_config = bigquery.LoadJobConfig()
job_config.schema = [
    bigquery.SchemaField("name", "STRING", mode="REQUIRED"),
    bigquery.SchemaField("gender", "STRING", mode="REQUIRED"),
    bigquery.SchemaField("count", "INTEGER", mode="REQUIRED"),
]
# The source format defaults to CSV, so the line below is optional.
job_config.source_format = bigquery.SourceFormat.CSV
uri = "gs://test-cm-mikami/yob1880.csv"

load_job = client.load_table_from_uri(
    uri, dataset_ref.table(table_id), job_config=job_config
)  # API request
print("Starting job {}".format(load_job.job_id))

load_job.result()  # Waits for table load to complete.
print("Job finished.")

destination_table = client.get_table(dataset_ref.table(table_id))
print("Loaded {} rows.".format(destination_table.num_rows))

実行してみると・・・

(test_bq) [ec2-user@ip-172-31-31-170 ~]$ python test_load.py
Created dataset cm-da-mikami-yuki-258308.test_dataset
Created table cm-da-mikami-yuki-258308.test_dataset.names_1880
Starting job d9d8fd70-e0ff-4fbd-a1fd-0534dc45950d
Job finished.
Loaded 2000 rows.

ロード完了。

確認してみます。

[ec2-user@ip-172-31-31-170 ~]$ bq ls
   datasetId
 --------------
  test_dataset
  test_s3
[ec2-user@ip-172-31-31-170 ~]$ bq ls test_dataset
   tableId     Type    Labels   Time Partitioning   Clustered Fields
 ------------ ------- -------- ------------------- ------------------
  names_1880   TABLE
[ec2-user@ip-172-31-31-170 ~]$ bq query --use_legacy_sql=false \
> 'SELECT
>    name,
>    gender
>  FROM
>    `cm-da-mikami-yuki-258308`.test_dataset.names_1880
>  WHERE
>    count = (SELECT max(count) FROM `cm-da-mikami-yuki-258308`.test_dataset.names_1880)'
Waiting on bqjob_r6573da5ad95dd040_0000016f15098a30_1 ... (0s) Current status: DONE
+------+--------+
| name | gender |
+------+--------+
| John | M      |
+------+--------+

無事データがロードできています。

BigQuery へのアクセスに IP 制限をつけることができました。

まとめ(所感)

GCP では、以下の 2step で IP 制限が設定できます。

※組織やプロジェクト、制限するサービスは構築済みで、アカウントへの権限付与は実施済みの前提です。

  • アクセスレベルの作成(Access Context Manager)
  • サービス境界の作成(VPC Service Controls)

コンソールからプルダウン選択などで操作していくだけで完了なので、思っていたより簡単に設定できました。

また、サービス境界を設定すれば、外からのアクセスだけではなく、サービス境界外のリソースへのデータ出力もできなくなります。

特に個人情報など大事なデータを格納しているリソースには、サービス境界を設定しておけば、コンソールからのアクセスもできなくなるため、流出のリスクがかなり軽減されますね。

参考