First try at pipeline publish
This commit is contained in:
13
aws/index.ts
Normal file
13
aws/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ForgePortfolioStack } from "./stack.ts";
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
new ForgePortfolioStack(app, 'JaredForgePortfolio', {
|
||||
env: {
|
||||
account: process.env.AWS_ACCOUNT,
|
||||
region: 'us-east-1',
|
||||
},
|
||||
domainName: 'portfolio.kling.dev',
|
||||
hostedZoneId: process.env.AWS_ROUTE53_ZONE
|
||||
});
|
||||
202
aws/stack.ts
Normal file
202
aws/stack.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
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';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import * as route53 from 'aws-cdk-lib/aws-route53';
|
||||
import * as targets from 'aws-cdk-lib/aws-route53-targets';
|
||||
import * as certificatemanager from 'aws-cdk-lib/aws-certificatemanager';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export interface ForgePortfolioStackProps extends cdk.StackProps {
|
||||
domainName?: string; // Optional custom domain
|
||||
hostedZoneId?: string; // Required if using custom domain
|
||||
}
|
||||
|
||||
export class ForgePortfolioStack extends cdk.Stack {
|
||||
public readonly bucket: s3.Bucket;
|
||||
public readonly distribution: cloudfront.Distribution;
|
||||
public readonly deploymentUser: iam.User;
|
||||
|
||||
constructor(scope: Construct, id: string, props: ForgePortfolioStackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// S3 Bucket for hosting static files
|
||||
this.bucket = new s3.Bucket(this, 'PortfolioBucket', {
|
||||
bucketName: `${id.toLowerCase()}-portfolio-${this.account}`,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY, // Be careful with this in production
|
||||
autoDeleteObjects: true,
|
||||
publicReadAccess: false, // CloudFront will handle access
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
versioned: false, // Keep costs low
|
||||
websiteIndexDocument: 'index.html',
|
||||
websiteErrorDocument: 'error.html',
|
||||
});
|
||||
|
||||
// Origin Access Control for CloudFront to access S3
|
||||
const originAccessControl = new cloudfront.S3OriginAccessControl(this, 'OAC', {
|
||||
description: 'OAC for portfolio website',
|
||||
});
|
||||
|
||||
// SSL Certificate (only if custom domain is provided)
|
||||
let certificate: certificatemanager.Certificate | undefined;
|
||||
if (props.domainName) {
|
||||
certificate = new certificatemanager.Certificate(this, 'Certificate', {
|
||||
domainName: props.domainName,
|
||||
validation: certificatemanager.CertificateValidation.fromDns(),
|
||||
});
|
||||
}
|
||||
|
||||
// CloudFront Distribution
|
||||
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
|
||||
defaultBehavior: {
|
||||
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket, {
|
||||
originAccessControl,
|
||||
}),
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
||||
compress: true,
|
||||
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
|
||||
},
|
||||
defaultRootObject: 'index.html',
|
||||
errorResponses: [
|
||||
{
|
||||
httpStatus: 404,
|
||||
responseHttpStatus: 200,
|
||||
responsePagePath: '/index.html', // For SPA routing
|
||||
ttl: cdk.Duration.minutes(5),
|
||||
},
|
||||
{
|
||||
httpStatus: 403,
|
||||
responseHttpStatus: 200,
|
||||
responsePagePath: '/index.html', // For SPA routing
|
||||
ttl: cdk.Duration.minutes(5),
|
||||
},
|
||||
],
|
||||
domainNames: props.domainName ? [props.domainName] : undefined,
|
||||
certificate: certificate,
|
||||
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
||||
priceClass: cloudfront.PriceClass.PRICE_CLASS_100, // Use only NA and Europe for cost optimization
|
||||
});
|
||||
|
||||
// Grant CloudFront access to S3 bucket
|
||||
this.bucket.addToResourcePolicy(
|
||||
new iam.PolicyStatement({
|
||||
actions: ['s3:GetObject'],
|
||||
resources: [this.bucket.arnForObjects('*')],
|
||||
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
|
||||
conditions: {
|
||||
StringEquals: {
|
||||
'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${this.distribution.distributionId}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Ensure bucket allows CloudFront to list objects (needed for error handling)
|
||||
this.bucket.addToResourcePolicy(
|
||||
new iam.PolicyStatement({
|
||||
actions: ['s3:ListBucket'],
|
||||
resources: [this.bucket.bucketArn],
|
||||
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
|
||||
conditions: {
|
||||
StringEquals: {
|
||||
'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${this.distribution.distributionId}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Custom domain DNS record (if provided)
|
||||
if (props.domainName && props.hostedZoneId) {
|
||||
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
|
||||
hostedZoneId: props.hostedZoneId,
|
||||
zoneName: props.domainName,
|
||||
});
|
||||
|
||||
new route53.ARecord(this, 'ARecord', {
|
||||
zone: hostedZone,
|
||||
target: route53.RecordTarget.fromAlias(
|
||||
new targets.CloudFrontTarget(this.distribution)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// IAM User for GitHub Actions deployment
|
||||
this.deploymentUser = new iam.User(this, 'DeploymentUser', {
|
||||
userName: `${id}-github-actions-user`,
|
||||
});
|
||||
|
||||
// Policy for deployment user
|
||||
const deploymentPolicy = new iam.Policy(this, 'DeploymentPolicy', {
|
||||
statements: [
|
||||
// S3 permissions for uploading files
|
||||
new iam.PolicyStatement({
|
||||
actions: [
|
||||
's3:PutObject',
|
||||
's3:PutObjectAcl',
|
||||
's3:GetObject',
|
||||
's3:DeleteObject',
|
||||
's3:ListBucket',
|
||||
],
|
||||
resources: [
|
||||
this.bucket.bucketArn,
|
||||
this.bucket.arnForObjects('*'),
|
||||
],
|
||||
}),
|
||||
// CloudFront invalidation permissions
|
||||
new iam.PolicyStatement({
|
||||
actions: ['cloudfront:CreateInvalidation'],
|
||||
resources: [
|
||||
`arn:aws:cloudfront::${this.account}:distribution/${this.distribution.distributionId}`,
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.deploymentUser.attachInlinePolicy(deploymentPolicy);
|
||||
|
||||
// Create Access Key for the deployment user
|
||||
const accessKey = new iam.AccessKey(this, 'DeploymentUserAccessKey', {
|
||||
user: this.deploymentUser,
|
||||
});
|
||||
|
||||
// Outputs for GitHub Actions
|
||||
new cdk.CfnOutput(this, 'BucketName', {
|
||||
value: this.bucket.bucketName,
|
||||
description: 'S3 Bucket name for deployment',
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'DistributionId', {
|
||||
value: this.distribution.distributionId,
|
||||
description: 'CloudFront Distribution ID for cache invalidation',
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'DistributionDomainName', {
|
||||
value: this.distribution.distributionDomainName,
|
||||
description: 'CloudFront Distribution domain name',
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'AccessKeyId', {
|
||||
value: accessKey.accessKeyId,
|
||||
description: 'Access Key ID for GitHub Actions (store as secret)',
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'SecretAccessKey', {
|
||||
value: accessKey.secretAccessKey.unsafeUnwrap(),
|
||||
description: 'Secret Access Key for GitHub Actions (store as secret)',
|
||||
});
|
||||
|
||||
if (props.domainName) {
|
||||
new cdk.CfnOutput(this, 'WebsiteURL', {
|
||||
value: `https://${props.domainName}`,
|
||||
description: 'Website URL',
|
||||
});
|
||||
} else {
|
||||
new cdk.CfnOutput(this, 'WebsiteURL', {
|
||||
value: `https://${this.distribution.distributionDomainName}`,
|
||||
description: 'Website URL',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user