AWS CDK(Python)で特定のIPからのみアクセスできるS3バケットを作成してみた

2021.07.02

データアナリティクス事業本部の鈴木です。

AWS CDKで特定のIPからのみアクセスできるS3バケットを作成してみました。

やりたいこと

以前バケットポリシーを使って、特定の条件からのみアクセスできるS3バケットを作った際に、全てのアクセス拒否から除外する、ただしこれらの条件に該当する場合は除外という設定をしました。このように、除外条件が複数あるバケットポリシーを作る場合に、CDKではどのように記述したらいいんだろうと思い、試してみました。

ちなみに、CloudFormationテンプレートを直接作る方法は、こちらの記事で紹介しました(以降、CloudFormationはCFnと記載します)。

複数のアクセス拒否設定をしたS3バケットをCloudFormationで作ってみた | DevelopersIO

この設定では、DENYポリシーのConditionキーのバリューに複数の条件を指定することで、該当する複数条件からのアクセスを許可していました。これってCDKだとどうやって書くのだろう?ということに相当します。

検証の前提

作成するS3バケットの設定

以下の2つの条件のどちらかを満たすときにアクセスを許可するように設定しました。

  • 特定のIPアドレスからのアクセス
  • CFnからのアクセス(忘れるとCFnからバケットが削除できなくなるので注意!)

環境

  • Python 3.9.0
  • AWS CDK 1.109.0

やってみた

プロジェクトの作成・準備

プロジェクトを以下のドキュメントを参考に作成しました。

Working with the AWS CDK in Python - AWS Cloud Development Kit (CDK)

まず、ディレクトリと雛形を作成します。

$ mkdir s3_iprestricted
$ cd s3_iprestricted
$ cdk init app --language python

続いて、仮装環境を有効化して、必要なパッケージをインストールしました。

# 仮装環境を有効化
$ source .venv/bin/activate

# パッケージをインストール
$ python -m pip install aws-cdk.aws-s3 aws-cdk.aws-iam

app.pyの編集

app.pyを以下のように編集しました。

※アクセス制限するIPは適当なものに置き換えてあります。

app.py

#!/usr/bin/env python3
from aws_cdk import (
    core as cdk,
    aws_iam as iam,
    aws_s3 as s3,
)


class IPRestrictedS3Stack(cdk.Stack):

    def __init__(self, scope: cdk.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # Public access block
        public_access_blockc_config = s3.CfnBucket.PublicAccessBlockConfigurationProperty(
                                        block_public_acls=True,
                                        block_public_policy=True,
                                        ignore_public_acls=True,
                                        restrict_public_buckets=True
                                        )

        # S3 bucket
        s3_bucket = s3.CfnBucket(self, 
                              id = "CMNayutaSampleBucket",
                              bucket_name = "cm-nayuta-sample-bucket",
                              public_access_block_configuration = public_access_blockc_config
                              )

        # Policy document
        policy_statement = iam.PolicyStatement(effect=iam.Effect.DENY)
        policy_statement.add_any_principal()
        policy_statement.add_actions("s3:*")
        policy_statement.add_resources(f'arn:aws:s3:::{s3_bucket.bucket_name}')
        policy_statement.add_resources(f'arn:aws:s3:::{s3_bucket.bucket_name}/*')
        policy_statement.add_condition("NotIpAddress", {'aws:SourceIp':"111.222.333.444"})
        policy_statement.add_condition("StringNotEquals", {'aws:CalledVia':"cloudformation.amazonaws.com"})
        policy_document = iam.PolicyDocument(statements=[policy_statement])

        # Bucket policy
        s3.CfnBucketPolicy(
            self, "CMNayutaSampleBucketPolicy",
            bucket=s3_bucket.ref,
            policy_document=policy_document.to_json()
        )

app = cdk.App()
IPRestrictedS3Stack(app, "cm-nayuta-cdk-stack")
app.synth()

特にポイントはハイライトした29-37行目の、バケットにアタッチするポリシードキュメントを定義する箇所です。一つのDENYポリシーに複数の条件を指定するには、そのポリシーのインスタンスのadd_conditionを2回呼べば良いことが分かりました。

スタックを作成する

cdk lsでスタック一覧を確認します。

$ cdk ls
cm-nayuta-cdk-stack

エラーが出なければ、cdk deployでスタックを作成します。

$ cdk deploy

ターミナルでコマンドが正常に終了していれば、スタックが作成されているはずです。許可されたIPからバケットを見に行くと、無事作成されていることが確認できました。

アクセス許可されている場合

バケットポリシーは以下のようになっており、期待通りにできていました。

※アクセス制限するIPは適当なものに置き換えてあります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::cm-nayuta-sample-bucket",
                "arn:aws:s3:::cm-nayuta-sample-bucket/*"
            ],
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "111.222.333.444"
                },
                "StringNotEquals": {
                    "aws:CalledVia": "cloudformation.amazonaws.com"
                }
            }
        }
    ]
}

許可されていないIPからアクセスしてみると、アクセス制限されていることも確認できました。

アクセス拒否されている場合

後片付け

以下のコマンドでスタックを削除しておきます。

$ cdk destroy

感想

初めは少し難しい印象を持っていましたが、PythonのAPIリファレンス(参考に挙げたもの)を見つつ、ターミナルからコマンドでスタックを作ったり消したりして次第に理解することができました。

リファレンスは対応するサービスごとに整理されています。どのサービスにはどんなAPIがあって、CFnテンプレートとどういう対応関係なのかな〜と見ていくと、理解が深まりました。

また、スタック内でほかのリソースを参照するときに、refで簡単にできるのがとても使いやすいなと思いました。CFnテンプレートだと、Ref関数で参照しますが、CDKの方がより直感的に使えるなと感じました。

今回はバケットポリシーを付けたS3バケットを作成しましたが、ほかのリソースにも挑戦していきたいです。

参考