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
}
})();
Last active
March 14, 2025 17:27
-
-
Save swport/5d457926f20ac5026a6c786fb9213df4 to your computer and use it in GitHub Desktop.
file
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment