Skip to content

Instantly share code, notes, and snippets.

@swport
Last active March 14, 2025 17:27
Show Gist options
  • Save swport/5d457926f20ac5026a6c786fb9213df4 to your computer and use it in GitHub Desktop.
Save swport/5d457926f20ac5026a6c786fb9213df4 to your computer and use it in GitHub Desktop.
file
const { S3Client, CreateBucketCommand, ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand, PutBucketPolicyCommand } = require('@aws-sdk/client-s3');
const { CloudFrontClient, CreateDistributionCommand, GetDistributionCommand, UpdateDistributionCommand, CreateCloudFrontOriginAccessIdentityCommand } = require('@aws-sdk/client-cloudfront');
const { Upload } = require('@aws-sdk/lib-storage');
const fs = require('fs');
const path = require('path');

// Load environment variables
const bucketName = process.env.S3_BUCKET_NAME;
const region = process.env.AWS_REGION;

if (!bucketName || !region) {
  console.error('Error: S3_BUCKET_NAME and AWS_REGION environment variables are required.');
  process.exit(1);
}

// Configure AWS SDK v3 clients
const s3Client = new S3Client({ region });
const cloudFrontClient = new CloudFrontClient({ region });

// Function to create S3 bucket (if it doesn't exist)
const createBucket = async () => {
  try {
    await s3Client.send(new CreateBucketCommand({ Bucket: bucketName }));
    console.log(`Bucket created: ${bucketName}`);
  } catch (err) {
    if (err.name !== 'BucketAlreadyOwnedByYou') {
      console.error('Error creating bucket:', err);
      throw err;
    }
  }
};

// Function to list all objects in the bucket (excluding folders/directories)
const listObjects = async () => {
  try {
    const data = await s3Client.send(new ListObjectsV2Command({ Bucket: bucketName }));
    // Filter out objects that end with '/' (folders/directories)
    return data.Contents ? data.Contents.filter((object) => !object.Key.endsWith('/')) : [];
  } catch (err) {
    console.error('Error listing objects:', err);
    throw err;
  }
};

// Function to move objects to a backup directory
const moveObjectsToBackup = async (objects, backupDir) => {
  for (const object of objects) {
    const copyParams = {
      Bucket: bucketName,
      CopySource: `${bucketName}/${object.Key}`,
      Key: `${backupDir}/${object.Key}`,
    };

    const deleteParams = {
      Bucket: bucketName,
      Key: object.Key,
    };

    try {
      // Copy object to backup directory
      await s3Client.send(new CopyObjectCommand(copyParams));
      console.log(`Copied to backup: ${object.Key}`);

      // Delete original object
      await s3Client.send(new DeleteObjectCommand(deleteParams));
      console.log(`Deleted original: ${object.Key}`);
    } catch (err) {
      console.error(`Error moving ${object.Key}:`, err);
      throw err;
    }
  }
};

// Function to upload new files to S3
const uploadFiles = async () => {
  const distFolder = path.join(__dirname, 'dist');
  const files = fs.readdirSync(distFolder);

  for (const file of files) {
    const filePath = path.join(distFolder, file);
    const fileContent = fs.readFileSync(filePath);

    const params = {
      Bucket: bucketName,
      Key: file,
      Body: fileContent,
      ContentType: file.endsWith('.html') ? 'text/html' : file.endsWith('.css') ? 'text/css' : 'application/javascript',
    };

    try {
      // Use the Upload class for multipart uploads (better for larger files)
      const upload = new Upload({
        client: s3Client,
        params,
      });

      await upload.done();
      console.log(`Uploaded: ${file}`);
    } catch (err) {
      console.error(`Error uploading ${file}:`, err);
      throw err;
    }
  }
};

// Function to create an Origin Access Identity (OAI)
const createOriginAccessIdentity = async () => {
  try {
    const response = await cloudFrontClient.send(new CreateCloudFrontOriginAccessIdentityCommand({
      CloudFrontOriginAccessIdentityConfig: {
        CallerReference: `${Date.now()}`,
        Comment: 'OAI for S3 bucket',
      },
    }));
    return response.CloudFrontOriginAccessIdentity.Id;
  } catch (err) {
    console.error('Error creating Origin Access Identity:', err);
    throw err;
  }
};

// Function to update the S3 bucket policy to allow access only via OAI
const updateBucketPolicyForOAI = async (oaiId) => {
  const bucketPolicy = {
    Version: '2012-10-17',
    Statement: [
      {
        Sid: 'AllowCloudFrontAccess',
        Effect: 'Allow',
        Principal: {
          CanonicalUser: oaiId,
        },
        Action: 's3:GetObject',
        Resource: `arn:aws:s3:::${bucketName}/*`,
      },
    ],
  };

  const params = {
    Bucket: bucketName,
    Policy: JSON.stringify(bucketPolicy),
  };

  try {
    await s3Client.send(new PutBucketPolicyCommand(params));
    console.log('Bucket policy updated to allow access only via OAI.');
  } catch (err) {
    console.error('Error updating bucket policy:', err);
    throw err;
  }
};

// Function to create or update a CloudFront distribution and return the domain name
const configureCloudFront = async (oaiId) => {
  const distributionConfig = {
    CallerReference: `${Date.now()}`, // Unique identifier for the distribution
    Comment: 'CloudFront distribution for React app',
    Enabled: true,
    Origins: {
      Quantity: 1,
      Items: [
        {
          Id: 'S3-origin',
          DomainName: `${bucketName}.s3.amazonaws.com`, // S3 bucket domain
          S3OriginConfig: {
            OriginAccessIdentity: `origin-access-identity/cloudfront/${oaiId}`, // Use OAI
          },
        },
      ],
    },
    DefaultCacheBehavior: {
      TargetOriginId: 'S3-origin',
      ViewerProtocolPolicy: 'redirect-to-https', // Redirect HTTP to HTTPS
      AllowedMethods: {
        Quantity: 2,
        Items: ['GET', 'HEAD'], // Only allow GET and HEAD requests
      },
      CachedMethods: {
        Quantity: 2,
        Items: ['GET', 'HEAD'],
      },
      ForwardedValues: {
        QueryString: false, // Do not forward query strings
        Cookies: {
          Forward: 'none', // Do not forward cookies
        },
      },
      MinTTL: 0, // Minimum TTL for caching
    },
    ViewerCertificate: {
      CloudFrontDefaultCertificate: true, // Use the default CloudFront certificate
    },
    DefaultRootObject: 'index.html', // Default file to serve
  };

  try {
    // Check if a distribution already exists
    const listDistributions = await cloudFrontClient.send(new ListDistributionsCommand({}));
    const existingDistribution = listDistributions.DistributionList.Items.find(
      (dist) => dist.Origins.Items[0].DomainName === `${bucketName}.s3.amazonaws.com`
    );

    if (existingDistribution) {
      // Update the existing distribution
      const updateParams = {
        Id: existingDistribution.Id,
        IfMatch: existingDistribution.ETag,
        DistributionConfig: distributionConfig,
      };
      const updateResponse = await cloudFrontClient.send(new UpdateDistributionCommand(updateParams));
      console.log('CloudFront distribution updated.');
      return updateResponse.Distribution.DomainName;
    } else {
      // Create a new distribution
      const createResponse = await cloudFrontClient.send(new CreateDistributionCommand({ DistributionConfig: distributionConfig }));
      console.log('CloudFront distribution created.');
      return createResponse.Distribution.DomainName;
    }
  } catch (err) {
    console.error('Error configuring CloudFront:', err);
    throw err;
  }
};

// Run the deployment
(async () => {
  try {
    // Create bucket if it doesn't exist
    await createBucket();

    // List existing objects in the bucket (excluding folders/directories)
    const objects = await listObjects();

    if (objects.length > 0) {
      // Create a timestamped backup directory
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
      const backupDir = `backup/${timestamp}`;

      // Move existing objects to the backup directory
      await moveObjectsToBackup(objects, backupDir);
    }

    // Upload new files
    await uploadFiles();

    // Create an Origin Access Identity (OAI)
    const oaiId = await createOriginAccessIdentity();

    // Update the S3 bucket policy to allow access only via OAI
    await updateBucketPolicyForOAI(oaiId);

    // Configure CloudFront distribution and get the domain name
    const cloudFrontDomain = await configureCloudFront(oaiId);

    // Log the CloudFront URL
    console.log(`CloudFront URL: https://${cloudFrontDomain}`);

    console.log('Deployment completed successfully!');
  } catch (err) {
    console.error('Deployment failed:', err);
    process.exit(1); // Exit with a non-zero code to indicate failure
  }
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment