import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; export interface TrypostMediaStackProps extends cdk.StackProps { /** * Name of the S3 bucket. * @default 'trypostmedia' */ readonly bucketName?: string; /** * Removal policy for S3 bucket when stack is deleted. * @default cdk.RemovalPolicy.RETAIN */ readonly removalPolicy?: cdk.RemovalPolicy; /** * Whether to auto delete objects when the bucket is removed. * Only applicable if removalPolicy is DESTROY. * @default false */ readonly autoDeleteObjects?: boolean; } export class TrypostMediaStack extends cdk.Stack { public readonly mediaBucket: s3.Bucket; public readonly distribution: cloudfront.Distribution; constructor(scope: Construct, id: string, props?: TrypostMediaStackProps) { super(scope, id, props); const bucketName = props?.bucketName ?? 'trypostmedia'; const removalPolicy = props?.removalPolicy ?? cdk.RemovalPolicy.RETAIN; const autoDeleteObjects = props?.autoDeleteObjects ?? false; // 1. S3 Bucket for Media Storage this.mediaBucket = new s3.Bucket(this, 'TrypostMediaBucket', { bucketName, removalPolicy, autoDeleteObjects, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // Direct public access to S3 is blocked encryption: s3.BucketEncryption.S3_MANAGED, enforceSSL: true, cors: [ { allowedMethods: [ s3.HttpMethods.GET, s3.HttpMethods.HEAD, s3.HttpMethods.PUT, s3.HttpMethods.POST, s3.HttpMethods.DELETE, ], allowedOrigins: ['*'], allowedHeaders: ['*'], exposedHeaders: ['ETag'], maxAge: 3000, }, ], }); // 2. CloudFront Distribution with Origin Access Control (OAC) // S3BucketOrigin.withOriginAccessControl automatically configures OAC // and attaches the required IAM policy to the S3 bucket allowing CloudFront read access. this.distribution = new cloudfront.Distribution(this, 'TrypostMediaDistribution', { comment: 'CloudFront distribution for trypostmedia S3 bucket', defaultBehavior: { origin: origins.S3BucketOrigin.withOriginAccessControl(this.mediaBucket), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS, cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_AND_SECURITY_HEADERS, compress: true, }, priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL, }); // 3. CloudFormation Outputs new cdk.CfnOutput(this, 'BucketName', { value: this.mediaBucket.bucketName, description: 'The name of the media S3 bucket', exportName: `${this.stackName}-BucketName`, }); new cdk.CfnOutput(this, 'BucketArn', { value: this.mediaBucket.bucketArn, description: 'The ARN of the media S3 bucket', exportName: `${this.stackName}-BucketArn`, }); new cdk.CfnOutput(this, 'DistributionId', { value: this.distribution.distributionId, description: 'The ID of the CloudFront distribution', exportName: `${this.stackName}-DistributionId`, }); new cdk.CfnOutput(this, 'DistributionDomainName', { value: this.distribution.distributionDomainName, description: 'The domain name of the CloudFront distribution', exportName: `${this.stackName}-DistributionDomainName`, }); new cdk.CfnOutput(this, 'DistributionUrl', { value: `https://${this.distribution.distributionDomainName}`, description: 'The URL of the CloudFront distribution (use for AWS_URL in Laravel .env)', exportName: `${this.stackName}-DistributionUrl`, }); } }