CloudFront y S3 Bucket CloudFormation Stack

AWS CDK y CloudFormation Stack para implementar una distribución de CloudFront y un S3 Bucket

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

Este es un caso de uso común si se utiliza Wagtail, pero también tiene otras aplicaciones.

Este es un caso de uso común si se utiliza Wagtail, la recomendación es crear un "sitio web" S3 para almacenar todas las imágenes, junto con un usuario IAM (sólo acceso API) para permitir la lectura y escritura de archivos en ese cubo.

Hacer esto manualmente es tedioso y algo propenso a errores, así que quise automatizar este proceso. El código utilizado en este artículo también se puede encontrar en este repositorio de GitHub.

El CDK de AWS es una gran herramienta, te permite crear programáticamente los recursos que quieras crear, en un formato mucho más fácil de entender comparado con trabajar directamente con tu plantilla de CloudFormation. Aunque el CDK también es capaz de desplegar estos recursos en tu cuenta de AWS, yo lo uso para generar una plantilla tradicional de CloudFormation, creo que es un proceso más repetible.

Parámetros de entrada CDK

Para que esta pila sea reutilizable, pedirá dos parámetros, un nombre de dominio DNS y una dirección IP.

  1. El nombre de dominio DNS es, obviamente, el nombre del dominio para el que desea configurar la distribución de CloudFront. Asume que tiene el control de ese nombre DNS y que no está registrado como un dominio Route53, por lo que utilizaremos la validación DNS para crear el certificado SSL en AWS Certificate Manager.

Tags.of es un constructor CDK realmente bueno, le dice a CDK que cree etiquetas para cada recurso desplegado con el nombre y los valores proporcionados.

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);

El cubo S3

A continuación, crearemos el bucket de S3, configurado con cifrado predeterminado y bloqueando el acceso web público, siguiendo las prácticas recomendadas más recientes de AWS. Solo la distribución de CloudFront podrá leer de ese bucket y servir archivos a través de un sitio web público.

Estableceremos la Política de Eliminación a RETAIN como precaución, lo que significa que cuando elimines el despliegue de CloudFormation, el Bucket de S3 y los objetos NO se eliminarán y no perderás tus datos. Siéntete libre de cambiar la Política de Eliminación a DESTRUIR si sólo estás probando y quieres minimizar la limpieza.

También crearemos dos políticas de ciclo de vida de S3, consulte Cubos de S3 rentables para obtener más detalles.

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

Ahora, la parte divertida, el usuario IAM y las Políticas de acceso.

Primero crearemos un usuario IAM sin acceso a la consola de AWS y generaremos una clave de acceso secreta. Por ahora almacenaremos esa clave de acceso secreta en AWS Secrets Manager,

También concederemos a ese usuario acceso para gestionar sus propias claves de acceso (más sobre esto más adelante), y también acceso para gestionar archivos en el bucket de S3 que acabamos de crear.

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)],
            )
        )

El certificado SSL

Ahora, utilizaremos el Administrador de certificados de AWS para crear un certificado SSL gratuito para nuestra distribución de CloudFront. Utilizaremos el método de validación de DNS, que requerirá que cree un nuevo registro DNS CNAME en su DNS de dominio para demostrar a AWS que es el propietario de este dominio. Si utiliza Route53 para su DNS, es posible que desee elegir una opción diferente.

Tenga en cuenta que el despliegue de CloudFormation NO estará completo hasta que realice este paso.

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

La distribución de CloudFront

Ahora, vamos a crear la distribución de CloudFront. Tenga en cuenta que utilizaremos la nueva configuración de control de acceso de origen, que ahora recomienda AWS, en lugar de la identidad de acceso de origen de CloudFront heredada (OAI) para acceder al bucket de S3. Dado que esto aún no es totalmente compatible con el CDK, tendremos que implementar una solución provisional por ahora.

También configuraremos algunos comportamientos para el almacenamiento en caché de objetos en nuestra distribución, compresión, respuesta por defecto y por error, etc.

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}"
                }
            }
        ))

Salidas de CloudFormation

Por último, publicaremos algunos detalles específicos de los componentes clave que se desplegaron en la salida de CloudFormation. Estos detalles serán importantes más adelante.

# 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,
        )

Despliegue de CloudFormation

Puedes usar el CDK de AWS para desplegar estos recursos en tu cuenta, pero yo prefiero usar el CDK de AWS para generar la plantilla de CloudFormation.

Después de instalar todos los requisitos de Python, así como la CLI de AWS, puede sintetizar la plantilla de CloudFormation para este código CDK utilizando los comandos siguientes.

A continuación, puede utilizar el archivo CloudFormation.yaml para desplegar esta pila de CloudFormation.

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

Pasos posteriores a la implantación

Tras un despliegue satisfactorio de nuestra pila, tendremos que realizar algunas acciones manuales.

  • El Certificado SSL que fue creado por AWS utiliza la Validación DNS para verificar la propiedad del nombre DNS. La pila no completará el despliegue hasta que se haya completado la validación DNS.
  • Debe crear un nombre DNS para su dominio como CNAME del nombre de dominio de distribución de CloudFront.
  • La aws_secret_access_key de la cuenta de usuario IAM que se crea se almacena en AWS Secrets Manager. Recupere el valor y elimine el secreto si no desea que se le siga cobrando por él.
  • Introduzca los valores correctos en el archivo de variables de entorno .env de su entorno.
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>

La plantilla completa de CloudFormation está disponible en:

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

Comentarios publicados: 0

Tagged with:
AWS Wagtail