Skip to content

Commit

Permalink
Merge pull request #28 from BjornMelin/dev
Browse files Browse the repository at this point in the history
release(0.1.1): Release v0.1.1
  • Loading branch information
BjornMelin authored Jan 19, 2025
2 parents 2393006 + 3f23f28 commit 8411c7c
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 68 deletions.
68 changes: 35 additions & 33 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ on:
branches:
- main
paths-ignore:
- "infrastructure/**" # Don't trigger on CDK changes
- "infrastructure/**"
- "**.md"
workflow_dispatch:

# Allow only one concurrent deployment
concurrency:
group: "deploy"
cancel-in-progress: true

env:
AWS_REGION: us-east-1
NEXT_PUBLIC_BASE_URL: https://bjornmelin.io
NEXT_PUBLIC_API_URL: https://api.bjornmelin.io

jobs:
deploy:
Expand All @@ -34,6 +34,7 @@ jobs:
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: "**/yarn.lock"

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
Expand All @@ -42,64 +43,65 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

# Install and build frontend
- name: Install Dependencies
# Install root dependencies (i.e., for Next.js)
- name: Install Root Dependencies
run: yarn install --frozen-lockfile

- name: Build Next.js App
# Install dependencies in the infrastructure directory (if needed)
- name: Install Infrastructure Dependencies
working-directory: ./infrastructure
run: yarn install --frozen-lockfile

# Build the Next.js app & export static files
- name: Build Next.js App (Static Export)
env:
NEXT_PUBLIC_BASE_URL: ${{ env.NEXT_PUBLIC_BASE_URL }}
NEXT_PUBLIC_API_URL: ${{ env.NEXT_PUBLIC_API_URL }}
run: yarn build

# Get stack outputs
# Retrieve the S3 bucket name and CloudFront distribution from your CF stack
- name: Get Stack Outputs
id: stack-outputs
continue-on-error: true
run: |
BUCKET_NAME=$(aws cloudformation describe-stacks \
--stack-name prod-portfolio-storage \
--query 'Stacks[0].Outputs[?OutputKey==`WebsiteBucketName`].OutputValue' \
--output text)
--output text --region ${{ env.AWS_REGION }})
DIST_ID=$(aws cloudformation describe-stacks \
--stack-name prod-portfolio-storage \
--query 'Stacks[0].Outputs[?OutputKey==`DistributionId`].OutputValue' \
--output text)
echo "::set-output name=bucket::$BUCKET_NAME"
echo "::set-output name=distribution::$DIST_ID"
--output text --region ${{ env.AWS_REGION }})
# Upload static assets
- name: Upload Static Assets
echo "bucket=$BUCKET_NAME" >> $GITHUB_OUTPUT
echo "distribution=$DIST_ID" >> $GITHUB_OUTPUT
- name: Check if Stack Outputs were retrieved
if: steps.stack-outputs.outcome != 'success'
run: |
aws s3 sync ./out s3://${{ steps.stack-outputs.outputs.bucket }} \
--delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "index.html" \
--exclude "*.json"
# Upload HTML and JSON with no-cache
- name: Upload Dynamic Files
echo "Failed to retrieve stack outputs. Check your user permissions and make sure 'prod-portfolio-storage' stack exists."
exit 1
# Upload the exported static files to your S3 bucket
- name: Upload to S3
run: |
aws s3 sync ./out s3://${{ steps.stack-outputs.outputs.bucket }} \
--delete \
--cache-control "public, max-age=0, must-revalidate" \
--include "index.html" \
--include "*.json"
echo "Uploading static files to s3://${{ steps.stack-outputs.outputs.bucket }}"
aws s3 sync out/ s3://${{ steps.stack-outputs.outputs.bucket }} --delete
# Invalidate CloudFront cache
# Invalidate CloudFront cache so your changes go live
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ steps.stack-outputs.outputs.distribution }} \
--paths "/*"
# Verify deployment
# Verify the site is live at your real domain
- name: Verify Deployment
run: |
# Wait for CloudFront invalidation
sleep 30
# Check if website is accessible
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" ${{ env.NEXT_PUBLIC_BASE_URL }})
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" https://bjornmelin.io)
if [ "$HTTP_CODE" != "200" ]; then
echo "Website verification failed with HTTP code: $HTTP_CODE"
echo "Website verification failed at https://bjornmelin.io with HTTP code: $HTTP_CODE"
exit 1
fi
echo "Deployment verified. Received HTTP 200 from https://bjornmelin.io"
1 change: 1 addition & 0 deletions infrastructure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ yarn deploy:dns
yarn deploy:storage
yarn deploy:deployment
yarn deploy:monitoring
yarn deploy:email
```

### Troubleshooting Steps
Expand Down
3 changes: 3 additions & 0 deletions infrastructure/lib/stacks/deployment-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export class DeploymentStack extends cdk.Stack {
actions: ["cloudformation:DescribeStacks"],
resources: [
`arn:aws:cloudformation:${this.region}:${this.account}:stack/${this.stackName}/*`,
`arn:aws:cloudformation:${this.region}:${this.account}:stack/prod-portfolio-storage/*`,
`arn:aws:cloudformation:${this.region}:${this.account}:stack/prod-portfolio-dns/*`,
`arn:aws:cloudformation:${this.region}:${this.account}:stack/prod-portfolio-monitoring/*`,
],
});

Expand Down
42 changes: 40 additions & 2 deletions infrastructure/lib/stacks/email-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as targets from "aws-cdk-lib/aws-route53-targets"; // Correct import fo
import { Construct } from "constructs";
import { EmailStackProps } from "../types/stack-props";
import * as path from "path";
import * as logs from "aws-cdk-lib/aws-logs";

export class EmailStack extends cdk.Stack {
public readonly emailFunction: lambda.NodejsFunction;
Expand Down Expand Up @@ -95,6 +96,24 @@ export class EmailStack extends cdk.Stack {
}
);

// Create API Gateway Logging Role
const apiGatewayLoggingRole = new iam.Role(this, "ApiGatewayLoggingRole", {
assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonAPIGatewayPushToCloudWatchLogs"
),
],
});

// Create CloudWatch Log Group for API Gateway
const apiLogGroup = new logs.LogGroup(this, "ApiGatewayLogs");

// Set up API Gateway Account settings
new apigateway.CfnAccount(this, "ApiGatewayAccount", {
cloudWatchRoleArn: apiGatewayLoggingRole.roleArn
});

// Create API Gateway
this.api = new apigateway.RestApi(this, "ContactApi", {
restApiName: "Contact Form API",
Expand All @@ -103,11 +122,12 @@ export class EmailStack extends cdk.Stack {
types: [apigateway.EndpointType.REGIONAL],
},
deployOptions: {
stageName: "prod", // Consider using a stage variable
stageName: "prod",
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: true,
tracingEnabled: true,
metricsEnabled: true,
accessLogDestination: new apigateway.LogGroupLogDestination(apiLogGroup),
},
});

Expand Down Expand Up @@ -145,9 +165,27 @@ export class EmailStack extends cdk.Stack {
const contact = this.api.root.addResource("contact");
contact.addMethod(
"POST",
new apigateway.LambdaIntegration(this.emailFunction)
new apigateway.LambdaIntegration(this.emailFunction),
{
authorizationType: apigateway.AuthorizationType.NONE, // Allow unauthenticated access
apiKeyRequired: false,
}
);

// Add CORS options to the API
const corsOptions: apigateway.CorsOptions = {
allowOrigins: [
`https://${props.domainName}`,
`https://www.${props.domainName}`,
process.env.ALLOWED_ORIGIN!,
],
allowMethods: ['POST', 'OPTIONS'],
allowHeaders: ['Content-Type'],
};

// Apply CORS to the contact resource
contact.addCorsPreflight(corsOptions);

// Create custom policy for SES permissions
const sesPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
Expand Down
13 changes: 5 additions & 8 deletions infrastructure/lib/stacks/storage-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { StorageStackProps } from "../types/stack-props";
export class StorageStack extends cdk.Stack {
public readonly bucket: s3.IBucket;
public readonly distribution: cloudfront.IDistribution;
private readonly props: StorageStackProps;

constructor(scope: Construct, id: string, props: StorageStackProps) {
super(scope, id, props);
this.props = props;

// Website bucket
this.bucket = new s3.Bucket(this, "WebsiteBucket", {
Expand Down Expand Up @@ -82,7 +84,7 @@ export class StorageStack extends cdk.Stack {
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: this.createCachePolicy(),
responseHeadersPolicy: this.createSecurityHeadersPolicy(props.domainName),
responseHeadersPolicy: this.createSecurityHeadersPolicy(),
},
domainNames: [props.domainName, `www.${props.domainName}`],
certificate: props.certificate,
Expand Down Expand Up @@ -120,11 +122,6 @@ export class StorageStack extends cdk.Stack {
oac.getAtt("Id")
);

// Grant CloudFront access to the bucket via OAC
this.bucket.grantRead(
new cloudfront.OriginAccessIdentity(this, "WebsiteOAI")
);

// DNS records
new route53.ARecord(this, "AliasRecord", {
recordName: props.domainName,
Expand Down Expand Up @@ -199,7 +196,7 @@ export class StorageStack extends cdk.Stack {
});
}

private createSecurityHeadersPolicy(domainName: string): cloudfront.ResponseHeadersPolicy {
private createSecurityHeadersPolicy(): cloudfront.ResponseHeadersPolicy {
return new cloudfront.ResponseHeadersPolicy(this, "SecurityHeaders", {
responseHeadersPolicyName: `${this.stackName}-security-headers`,
securityHeadersBehavior: {
Expand All @@ -211,7 +208,7 @@ export class StorageStack extends cdk.Stack {
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"font-src 'self' data:",
`connect-src 'self' https://api.${domainName}`,
`connect-src 'self' https://api.${this.props.domainName}`,
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
Expand Down
18 changes: 9 additions & 9 deletions infrastructure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@
},
"devDependencies": {
"@types/aws-lambda": "^8.10.0",
"@types/jest": "^29.5.5",
"@types/node": "20.7.1",
"aws-cdk": "^2.173.4",
"@types/jest": "^29.5.14",
"@types/node": "^20.7.1",
"aws-cdk": "^2.173.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "~5.2.2"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.0.0",
"aws-cdk-lib": "^2.173.4",
"constructs": "^10.0.0",
"@aws-sdk/client-ses": "^3.721.0",
"aws-cdk-lib": "^2.173.0",
"constructs": "^10.4.0",
"source-map-support": "^0.5.21"
}
}
}
2 changes: 1 addition & 1 deletion infrastructure/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"typeRoots": ["./node_modules/@types"],
"outDir": "dist"
},
"exclude": ["node_modules", "cdk.out"]
"exclude": ["node_modules", "cdk.out", "dist"]
}
29 changes: 18 additions & 11 deletions infrastructure/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"@smithy/util-utf8" "^2.0.0"
tslib "^2.6.2"

"@aws-sdk/client-ses@^3.0.0":
"@aws-sdk/client-ses@^3.721.0":
version "3.721.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.721.0.tgz#1f6f8e24e31e912c4e76691ad16e79a6b3cfd6c5"
integrity sha512-JWHVQm0JH1fsyXxHH+oIXgMacnJ25NrQY3zfBDikBSW/sM1xdgctBjc1o+tLyHIKe+iS3cIAkWwSohZG0dPEdw==
Expand Down Expand Up @@ -1510,7 +1510,7 @@
dependencies:
"@types/istanbul-lib-report" "*"

"@types/jest@^29.5.5":
"@types/jest@^29.5.14":
version "29.5.14"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5"
integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==
Expand All @@ -1525,10 +1525,12 @@
dependencies:
undici-types "~6.20.0"

"@types/node@20.7.1":
version "20.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.1.tgz#06d732ead0bd5ad978ef0ea9cbdeb24dc8717514"
integrity sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==
"@types/node@^20.7.1":
version "20.17.11"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.11.tgz#2c05215fc37316b1596df7fbdba52151eaf83c50"
integrity sha512-Ept5glCK35R8yeyIeYlRIZtX6SLRyqMhOFTgj5SOkMpLTdw3SEHI9fHx60xaUZ+V1aJxQJODE+7/j5ocZydYTg==
dependencies:
undici-types "~6.19.2"

"@types/stack-utils@^2.0.0":
version "2.0.3"
Expand Down Expand Up @@ -1623,7 +1625,7 @@ async@^3.2.3:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==

aws-cdk-lib@^2.173.4:
aws-cdk-lib@^2.173.0:
version "2.173.4"
resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.173.4.tgz#a690ea79e976e86c761e118fd055aa24a51b7852"
integrity sha512-0reN94TzkWmyVZDDBlYB4qzJUig8wTHEe82YLHlWRUhrU78fT+drVGUr+lYZwwETaZ+8fLdCOl9ULvFNq7iczQ==
Expand All @@ -1644,7 +1646,7 @@ aws-cdk-lib@^2.173.4:
table "^6.8.2"
yaml "1.10.2"

aws-cdk@^2.173.4:
aws-cdk@^2.173.0:
version "2.173.4"
resolved "https://registry.yarnpkg.com/aws-cdk/-/aws-cdk-2.173.4.tgz#a4805cfdf2e9b50297e18271bf506e40a0cda706"
integrity sha512-zgs3xU28VEKIwHwJHu0ZHeoEmwLGnHS2jPCMc2MsIMZu+a7CKyE77Tw6LwJkuuB96BQyqr6xJB3SbeWjXmcFhQ==
Expand Down Expand Up @@ -1859,7 +1861,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==

constructs@^10.0.0:
constructs@^10.4.0:
version "10.4.2"
resolved "https://registry.yarnpkg.com/constructs/-/constructs-10.4.2.tgz#e875a78bef932cca12ea63965969873a25c1c132"
integrity sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==
Expand Down Expand Up @@ -3151,7 +3153,7 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"

ts-jest@^29.1.1:
ts-jest@^29.2.5:
version "29.2.5"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63"
integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==
Expand All @@ -3166,7 +3168,7 @@ ts-jest@^29.1.1:
semver "^7.6.3"
yargs-parser "^21.1.1"

ts-node@^10.9.1:
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
Expand Down Expand Up @@ -3205,6 +3207,11 @@ typescript@~5.2.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==

undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==

undici-types@~6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
Expand Down
Loading

0 comments on commit 8411c7c

Please sign in to comment.