Pile CloudFormation CloudFront et S3 Bucket

AWS CDK et CloudFormation Stack pour déployer une distribution CloudFront et un Bucket S3

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

Il s'agit d'un cas d'utilisation courant si vous utilisez Wagtail, mais il y a aussi d'autres applications.

Il s'agit d'un cas d'utilisation courant si vous utilisez Wagtail. Il est recommandé de créer un "site web" S3 pour stocker toutes les images, ainsi qu'un utilisateur IAM (accès API uniquement) pour permettre la lecture et l'écriture de fichiers dans ce seau.

Faire cela manuellement est fastidieux et source d'erreurs, c'est pourquoi j'ai voulu automatiser ce processus. Le code utilisé dans cet article se trouve également dans ce dépôt GitHub.

Le CDK AWS est un outil formidable, il vous permet de créer de manière programmatique les ressources que vous souhaitez créer, dans un format beaucoup plus facile à comprendre par rapport au fait de travailler directement avec votre template CloudFormation. Même si le CDK est également capable de déployer ces ressources dans votre compte AWS, je l'utilise pour générer un modèle CloudFormation traditionnel, je pense que c'est un processus plus reproductible.

Paramètres d'entrée du CDK

Afin de rendre cette pile réutilisable, elle demandera deux paramètres, un nom de domaine DNS et une adresse IP.

  1. Le nom de domaine DNS est évidemment le nom du domaine pour lequel vous voulez que votre distribution CloudFront soit configurée. Cela suppose que vous avez le contrôle de ce nom DNS et qu'il n'est pas enregistré comme un domaine Route53, nous utiliserons donc la validation DNS pour créer le certificat SSL dans AWS Certificate Manager.

Tags.of est une construction CDK très intéressante, qui indique à CDK de créer des tags pour chaque ressource déployée avec le nom et les valeurs fournis.

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

Le seau S3

Nous allons ensuite créer le seau S3, configuré avec un chiffrement par défaut et bloquant l'accès public au web, en suivant les dernières bonnes pratiques d'AWS. Seule la distribution CloudFront sera en mesure de lire à partir de ce seau et de servir des fichiers via un site web public.

Nous allons définir la politique de suppression sur RETAIN par précaution, ce qui signifie que lorsque vous supprimez le déploiement CloudFormation, le S3 Bucket et les objets ne seront PAS supprimés et vous ne perdrez pas vos données. N'hésitez pas à changer la politique de suppression en DESTRUIRE si vous ne faites que tester et que vous souhaitez minimiser le nettoyage.

Nous allons également créer deux politiques de cycle de vie S3. Pour plus d'informations, consultez la rubrique " Cost-effective S3 Buckets".

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

Maintenant, la partie amusante, l'utilisateur IAM et les politiques d'accès.

Nous allons d'abord créer un utilisateur IAM sans accès à la console AWS et générer une clé d'accès secrète. Pour l'instant, nous allons stocker cette clé d'accès secrète dans AWS Secrets Manager,

Nous allons également accorder à cet utilisateur l'accès à la gestion de ses propres clés d'accès (nous y reviendrons plus tard), ainsi que l'accès à la gestion des fichiers dans le seau S3 que nous venons de créer.

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

Le certificat SSL

Nous allons maintenant utiliser le gestionnaire de certificats AWS pour créer un certificat SSL gratuit pour notre distribution CloudFront. Nous utiliserons la méthode de validation DNS, qui vous demandera de créer un nouvel enregistrement DNS CNAME dans le DNS de votre domaine pour prouver à AWS que vous possédez ce domaine. Si vous utilisez Route53 pour votre DNS, vous pouvez choisir une autre option.

Notez que le déploiement de CloudFormation ne sera PAS terminé tant que vous n'aurez pas effectué cette étape.

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

La distribution CloudFront

Maintenant, créons la distribution CloudFront. Notez que nous utiliserons les nouveaux paramètres de contrôle d'accès à l'origine, qui sont maintenant recommandés par AWS, au lieu de l'ancienne identité d'accès à l'origine de CloudFront (OAI) pour accéder au seau S3. Comme cela n'est pas encore totalement pris en charge par le CDK, nous devrons mettre en œuvre une solution de contournement pour l'instant.

Nous configurerons également quelques comportements pour la mise en cache des objets dans notre distribution, la compression, la réponse par défaut et la réponse d'erreur, 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}"
                }
            }
        ))

Sorties CloudFormation

Enfin, nous produirons quelques détails spécifiques sur les composants clés qui ont été déployés dans la sortie CloudFormation. Ces détails seront importants par la suite.

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

Déploiement de CloudFormation

Vous pouvez utiliser le CDK AWS pour déployer ces ressources dans votre compte, mais je préfère utiliser le CDK AWS pour générer le modèle CloudFormation.

Après avoir installé toutes les exigences Python, ainsi que la CLI AWS, vous pouvez synthétiser le modèle CloudFormation pour ce code CDK à l'aide des commandes ci-dessous.

Ensuite, vous pouvez utiliser le fichier CloudFormation.yaml pour déployer cette pile CloudFormation.

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

Étapes post-déploiement

Après un déploiement réussi de notre pile, nous aurons quelques actions manuelles à effectuer.

  • Le certificat SSL créé par AWS utilise la validation DNS pour vérifier la propriété du nom DNS. La pile ne terminera pas le déploiement tant que la validation DNS n'aura pas été effectuée.
  • Vous devez créer un nom DNS pour votre domaine en tant que CNAME du nom de domaine de la distribution CloudFront.
  • La clé d'accès aws_secret du compte utilisateur IAM créé est stockée dans AWS Secrets Manager. Récupérez la valeur et supprimez le secret si vous ne voulez pas continuer à être facturé.
  • Renseignez les valeurs correctes dans le fichier de variables d'environnement .env de votre environnement.
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>

Le modèle CloudFormation complet est disponible à l'adresse suivante :

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

Commentaires publiés : 0

Tagged with:
AWS Wagtail