CloudFront 및 S3 버킷 클라우드포메이션 스택

CloudFront 배포 및 S3 버킷 배포를 위한 AWS CDK 및 CloudFormation Stack

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

와그테일을 사용하는 경우 일반적인 사용 사례이지만 다른 용도로도 사용할 수 있습니다.

Wagtail을 사용하는 경우 일반적인 사용 사례로, 모든 이미지를 저장할 S3 "웹사이트"를 만들고 해당 버킷에 파일을 읽고 쓸 수 있는 IAM 사용자(API 액세스 전용)를 생성하는 것이 좋습니다.

이 작업을 수동으로 하는 것은 지루하고 오류가 발생하기 쉽기 때문에 이 프로세스를 자동화하고 싶었습니다. 이 글에 사용된 코드는 이 GitHub 리포지토리에서도 찾을 수 있습니다.

AWS CDK는 훌륭한 도구로, CloudFormation 템플릿으로 직접 작업하는 것보다 훨씬 이해하기 쉬운 형식으로 원하는 리소스를 프로그래밍 방식으로 생성할 수 있습니다. CDK를 사용하여 AWS 계정에 이러한 리소스를 배포할 수도 있지만, 저는 기존의 CloudFormation 템플릿을 생성하는 데 사용하지만 더 반복 가능한 프로세스라고 생각합니다.

CDK 입력 매개변수

이 스택을 재사용할 수 있도록 하기 위해 DNS 도메인 이름과 IP 주소라는 두 가지 파라미터를 요청합니다.

  1. DNS 도메인 이름은 CloudFront 배포를 구성하려는 도메인의 이름입니다. 해당 DNS 이름을 제어할 수 있고 해당 도메인이 Route53 도메인으로 등록되어 있지 않다고 가정하므로 DNS 유효성 검사를 사용하여 AWS 인증서 관리자에서 SSL 인증서를 만들 것입니다.

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 배포만 해당 버킷에서 읽고 공개 웹사이트를 통해 파일을 제공할 수 있습니다.

예방 조치로 제거 정책을 RETAIN으로 설정하여 CloudFormation 배포를 삭제해도 S3 버킷과 개체는 삭제되지 않으며 데이터가 손실되지 않도록 합니다. 테스트 중이며 정리를 최소화하려는 경우 제거 정책을 DESTROY로 변경해도 됩니다.

또한 두 가지 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 콘솔 액세스 권한이 없는 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 인증서 관리자를 사용하여 CloudFront 배포를 위한 무료 SSL 인증서를 만들겠습니다. 이 도메인을 소유하고 있음을 AWS에 증명하기 위해 도메인 DNS에 새 CNAME DNS 레코드를 생성해야 하는 DNS 유효성 검사 방법을 사용할 것입니다. 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 배포를 생성해 보겠습니다. S3 버킷에 액세스하기 위해 레거시 CloudFront OAI(오리진 액세스 ID) 대신 현재 AWS에서 권장하는 새로운 오리진 액세스 제어 설정을 사용하겠습니다. 이 기능은 아직 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 출력에 배포된 주요 구성 요소에 대한 몇 가지 구체적인 세부 정보를 출력합니다. 이러한 세부 사항은 나중에 중요하게 다룰 것입니다.

# 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

배포 후 단계

스택을 성공적으로 배포한 후에는 몇 가지 수동 작업을 수행해야 합니다.

  • AWS에서 생성한 SSL 인증서는 DNS 유효성 검사를 사용하여 DNS 이름의 소유권을 확인합니다. 스택은 DNS 유효성 검사가 완료될 때까지 배포를 완료하지 않습니다.
  • 도메인의 DNS 이름을 CloudFront 배포 도메인 이름의 CNAME으로 만들어야 합니다.
  • 생성된 IAM 사용자 계정의 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