CloudFrontとS3バケットのCloudFormationスタック

AWS CDKとCloudFormation StackによるCloudFrontディストリビューションとS3バケットのデプロイ

C05348A3-9AB8-42C9-A6E0-81DB3AC59FEB
           

これはWagtailを使っている場合によくある使用例だが、他にも応用が利く。

Wagtailを使用する場合、これは一般的なユースケースです。推奨は、すべての画像を保存するS3の "ウェブサイト "と、そのバケットへのファイルの読み書きを許可するIAMユーザー(APIアクセスのみ)を作成することです。

手作業でこれを行うのは面倒だし、多少ミスが起こりやすいので、このプロセスを自動化したかったのだ。この記事で使用したコードは、このGitHubリポジトリにもある。

AWS CDKは素晴らしいツールで、CloudFormationテンプレートで直接作業するのに比べてはるかに理解しやすいフォーマットで、作成したいリソースをプログラムで作成することができます。CDKはAWSアカウントにこれらのリソースをデプロイすることもできるが、私は従来のCloudFormationテンプレートを生成するためにCDKを使っている。

CDK入力パラメーター

このスタックを再利用可能にするために、DNSドメイン名とIPアドレスの2つのパラメーターを要求する。

  1. DNS Domain Nameは、明らかにCloudFront Distributionを構成するDomainの名前です。そのDNS名を管理し、Route53 Domainとして登録されていないことを前提としているので、AWS Certificate ManagerでSSL証明書を作成するためにDNS検証を使用します。

Tags.ofは、CDKの実にすてきな構成要素で、指定された名前と値でデプロイされたすべてのリソースにタグを作成するようCDKに指示します。

site_domain_name = CfnParameter(self,
            id='domain_name',
            description="Domain Name",
            default="static.example.com",
            type="String",
                    ).value_as_string
 
        stack_name = self.stack_name
        Tags.of(self).add('Project', "Wagtail Images");
        Tags.of(self).add('stackName', stack_name);
        Tags.of(self).add('Domain', site_domain_name);

S3バケット

そして、AWSの最新のベストプラクティスに従って、デフォルトの暗号化で構成されたS3バケットを作成し、公開ウェブアクセスをブロックします。CloudFrontディストリビューションだけがそのバケットから読み取ることができ、公開ウェブサイト経由でファイルを提供することができる。

つまり、CloudFormationデプロイメントを削除しても、S3バケットとオブジェクトは削除されず、データを失うことはありません。テスト中で、クリーンアップを最小限にしたい場合は、Removal PolicyをDESTROYに変更してください。

また、2つのS3ライフサイクルポリシーを作成する。詳細については、費用対効果の高いS3バケットを参照。

myBucket = aws_s3.Bucket(
            self,
            's3_bucket',
            bucket_name = site_domain_name,
            encryption=aws_s3.BucketEncryption.S3_MANAGED,
            access_control=aws_s3.BucketAccessControl.PRIVATE,
            public_read_access=False,
            block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL,
            # removal_policy=RemovalPolicy.DESTROY,
            removal_policy=RemovalPolicy.RETAIN,
            auto_delete_objects=False,
            lifecycle_rules = [
                aws_s3.LifecycleRule(
                    id="multipart",
                    enabled=True,
                    abort_incomplete_multipart_upload_after=Duration.days(1),
                    expired_object_delete_marker=True,
                ),
                aws_s3.LifecycleRule(
                    id="IA",
                    enabled=True,
                    transitions=[{
                        "storageClass": aws_s3.StorageClass.INTELLIGENT_TIERING,
                        "transitionAfter": Duration.days(0)
                    }]
                )
             ]
        )

AWS IAM

さて、お楽しみのIAMユーザーとアクセス・ポリシーだ。

まずはAWS ConsoleにアクセスできないIAMユーザーを作成し、シークレットアクセスキーを生成します。このシークレットアクセスキーはとりあえずAWS Secrets Managerに保存しておきます、

また、そのユーザーには、自身のアクセスキーを管理するアクセス権(詳細は後述)と、先ほど作成したS3バケット内のファイルを管理するアクセス権を付与する。

user = iam.User(self, "User")
        access_key = iam.AccessKey(self, "AccessKey", user=user)
        secret = secretsmanager.Secret(self, "Secret",
            secret_string_value=access_key.secret_access_key
        )
 
        policy = iam.Policy(self, 'myPolicy', statements=[iam.PolicyStatement(
            resources=[user.user_arn],
            actions=["iam:ListAccessKeys",
                "iam:CreateAccessKey",
                "iam:DeleteAccessKey"
            ]
          )]
        )
        policy.attach_to_user(user)
 
        myBucket.add_to_resource_policy(
            iam.PolicyStatement(
                sid="AllowUserManageBucket",
                effect=iam.Effect.ALLOW,
                actions=["s3:ListBucket",
                        "s3:GetBucketLocation",
                        "s3:ListBucketMultipartUploads",
                        "s3:ListBucketVersions"],
                resources=[myBucket.bucket_arn],
                principals=[iam.ArnPrincipal(user.user_arn)],
            )
        )
 
        myBucket.add_to_resource_policy(
            iam.PolicyStatement(
                sid="AllowUserManageBucketObjects",
                effect=iam.Effect.ALLOW,
                actions=["s3:*Object"],
                resources=[myBucket.arn_for_objects("*")],
                principals=[iam.ArnPrincipal(user.user_arn)],
            )
        )

SSL証明書

では、AWS Certificate Managerを使用して、CloudFrontディストリビューション用の無料のSSL証明書を作成します。ドメインDNSに新しいCNAME DNSレコードを作成し、このドメインを所有していることをAWSに証明する必要があります。DNSにRoute53を使用している場合は、別のオプションを選択することをお勧めします。

このステップを実行するまでCloudFormationのデプロイは完了しないことに注意してください。

certificate = cm.Certificate(self, "MyCertificate",
          domain_name=site_domain_name,
          certificate_name=site_domain_name,
          validation=cm.CertificateValidation.from_dns()
        )

CloudFrontディストリビューション

では、CloudFront Distributionを作成しましょう。S3バケットへのアクセスには、従来のCloudFrontオリジンアクセスID(OAI)ではなく、AWSが推奨する新しいOriginアクセスコントロール設定を使うことに注意してください。これはまだCDKで完全にサポートされていないので、今のところ回避策を実装する必要があります。

また、ディストリビューション内のオブジェクトのキャッシュ、圧縮、デフォルトとエラーのレスポンスなど、いくつかのビヘイビアを設定します。

cfn_origin_access_control = cloudfront.CfnOriginAccessControl(self, "MyCfnOriginAccessControl",
            origin_access_control_config=cloudfront.CfnOriginAccessControl.OriginAccessControlConfigProperty(
                name=site_domain_name,
                origin_access_control_origin_type="s3",
                signing_behavior="always",
                signing_protocol="sigv4",
                description="CF S3 Origin Access Control"
            )
        )
 
        distribution = cloudfront.Distribution(
            self, "CloudFrontDistribution",
            default_behavior=cloudfront.BehaviorOptions(
                compress=True,
                allowed_methods=cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
                cached_methods=cloudfront.CachedMethods.CACHE_GET_HEAD,
                cache_policy=cloudfront.CachePolicy.CACHING_OPTIMIZED,
                origin_request_policy=cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
                viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                origin=origins.S3Origin(
                    myBucket, origin_id='s3-static-frontend',
                )
            ),
            domain_names=[site_domain_name],
            certificate=certificate,
            http_version=cloudfront.HttpVersion.HTTP2,
            enabled=True,
            default_root_object="index.html",
            price_class=cloudfront.PriceClass.PRICE_CLASS_ALL,
            error_responses=[
                cloudfront.ErrorResponse(
                    http_status=404, response_page_path="/index.html", response_http_status=200,
                    ttl=Duration.seconds(60)),
            ]
        )
 
        # Override the default behavior to use OAC and not OAI
        # Because CDK does not have full support for this feature yet
        # Based on: https://github.com/aws/aws-cdk/issues/21771#issuecomment-1282780627
        cf_cfn_distrib = t.cast(
            cloudfront.CfnDistribution,
            distribution.node.default_child,
        )
        cf_cfn_distrib.add_property_override(
            'DistributionConfig.Origins.0.OriginAccessControlId',
            cfn_origin_access_control.get_att('Id'),
        )
        cf_cfn_distrib.add_property_override(
            'DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity',
            '',
        )
 
        myBucket.add_to_resource_policy(iam.PolicyStatement(
            effect=iam.Effect.ALLOW,
            actions=['s3:GetObject', 's3:ListBucket'],
            resources=[f'{myBucket.bucket_arn}/*', myBucket.bucket_arn],
            principals=[iam.ServicePrincipal("cloudfront.amazonaws.com")],
            conditions={
                "StringEquals": {
                    "aws:SourceArn": f"arn:aws:cloudfront::{Aws.ACCOUNT_ID}:distribution/{distribution.distribution_id}"
                }
            }
        ))

CloudFormationの出力

最後に、CloudFormation Outputでデプロイされた主要コンポーネントの具体的な詳細を出力する。これらの詳細は後で重要になる。

# Add stack outputs
        CfnOutput(
            self,
            "userNAME",
            value=user.user_name,
        )
        CfnOutput(
            self,
            "accessKeyId",
            value=access_key.to_string(),
        )
        CfnOutput(
            self,
            "SiteBucketName",
            value=myBucket.bucket_name,
        )
        CfnOutput(
            self,
            "DistributionId",
            value=distribution.distribution_id,
        )
        CfnOutput(
            self,
            "distributionDomainName",
            value=distribution.distribution_domain_name,
        )
        CfnOutput(
            self,
            "CertificateArn",
            value=certificate.certificate_arn,
        )

CloudFormationのデプロイメント

これらのリソースをアカウントにデプロイするためにAWS CDKを使用することもできるが、私はCloudFormationテンプレートを生成するためにAWS CDKを使用することを好む。

全てのPython要件とAWS CLIをインストールした後、以下のコマンドを使用してこのCDKコード用のCloudFormationテンプレートを合成することができます。

その後、CloudFormation.yamlファイルを使ってこのCloudFormationスタックをデプロイできる。

python3 -m venv .env
source .env/bin/activate
pip install -r requirements.txt
cdk synth --path-metadata false --version-reporting false > CloudFormation.yaml

配備後のステップ

Stackのデプロイに成功したら、いくつかの手動アクションを実行しよう。

  • AWSによって作成されたSSL証明書は、DNS検証を使用してDNS名の所有権を確認します。DNSバリデーションが完了するまで、Stackはデプロイを完了しません
  • CloudFrontのDistributionDomainNameのCNAMEとしてDomainのDNS名を作成する必要があります。
  • 作成されたIAM Userアカウントのaws_secret_access_keyはAWS Secrets Managerに保存されています。課金され続けたくなければ値を取得し、シークレットを削除してください。
  • あなたの環境の.env環境変数ファイルに正しい値を入力してください。
DJANGO_SETTINGS_MODULE=myapp.settings.production
AWS_ACCESS_KEY_ID=<removed>
AWS_SECRET_ACCESS_KEY=<removed>
AWS_REGION=us-east-1
AWS_STORAGE_BUCKET_NAME=static.example.com
AWS_S3_CUSTOM_DOMAIN=static.example.com
DJANGO_SECRET_KEY=<removed>

完全なCloudFormationテンプレートは以下で入手できる:

https://github.com/Christophe-Gauge/cdk-s3-cloudfront/blob/main/CloudFormation.yaml

投稿コメント 0

Tagged with:
AWS Wagtail