Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-on-main-push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
aws-region: us-west-2

- name: Deploy
run: bun run deploy -v
run: bun run deploy -v --env=production

permissions:
id-token: write
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/deploy-to-staging.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
on:
push:
branches:
- staging

jobs:
deploy:
name: Deploy to Staging
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 'latest'

- name: Install Dependencies
run: bun install

- name: Build Project
run: bun run build -v

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::253016134262:role/TallyUpBackendStackStagin-tallyupGithubActionsRole1-H5x6v0wDEjb7
aws-region: us-west-2

- name: Deploy
run: DB_URL_SECRET_ARN='arn:aws:secretsmanager:us-west-2:253016134262:secret:dbConnectionSecretB11A2D98-a53954JuxmdC-RFmGcz' bun run deploy -v --env=staging

permissions:
id-token: write
contents: read
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ dist
.pnp.*

.aws-sam/
openapi.yaml

# Temporarily ignore the following files until they are properly handled
openapi.json
*.zip
src/gen/
cdk.out
819 changes: 759 additions & 60 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"acknowledged-issue-numbers": [
34892
]
}
13 changes: 13 additions & 0 deletions deployment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { App } from 'aws-cdk-lib';
import { TallyUpBackendStack } from './tally-up-backend-stack';

const stackName = process.env['STACK_NAME']!;
const customDomainName = process.env['CUSTOM_DOMAIN_NAME']!;
const customDomainCertificateArn = process.env['CUSTOM_DOMAIN_CERTIFICATE_ARN']!;

const app = new App();
new TallyUpBackendStack(app, stackName, {
branch: process.env['BRANCH'] as 'main' | 'staging',
customDomainName,
customDomainCertificateArn,
});
84 changes: 84 additions & 0 deletions deployment/lib/http-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Duration, RemovalPolicy, type Stack } from 'aws-cdk-lib';
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import { getDbConnectionSecret, getJwtSecret } from './secrets';
import assert from 'assert';

type FunctionParams = {
codePath: string;
layers?: lambda.FunctionOptions['layers'];
managedPolicyNames?: string[];
name: string;
route: {
api: apigatewayv2.HttpApi;
method: NonNullable<apigatewayv2.AddRoutesOptions['methods']>[number];
path: string;
stage: apigatewayv2.IHttpStage | undefined;
timeoutInMillis?: number;
};
stack: Stack;
};

const buildGetLambdaLogs = () => {
let lambdaLogs: logs.LogGroup | undefined;
return (stack: Stack) => {
if (lambdaLogs) return lambdaLogs;

lambdaLogs = new logs.LogGroup(stack, 'lambdaLogs', {
logGroupName: `/aws/lambda/${stack.stackName}`,
retention: logs.RetentionDays.ONE_DAY,
removalPolicy: RemovalPolicy.DESTROY, // TODO: Change to RETAIN for production
});
return lambdaLogs;
};
};

const getLambdaLogs = buildGetLambdaLogs();

export const instantiateHttpFunction = (params: FunctionParams): lambda.Function => {
const { codePath, layers, managedPolicyNames, name, stack } = params;
const { api, method, path, stage, timeoutInMillis } = params.route;
assert(stage, 'Stage must be defined for HTTP function instantiation');

const fn = new lambda.Function(stack, name, {
// code: lambda.Code.fromAsset(`${tallyupConfig.outDir.functions}/login-function`),
code: lambda.Code.fromAsset(codePath),
handler: 'index.handler',
memorySize: 128,
role: new iam.Role(stack, `${name}Role`, {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: managedPolicyNames?.map((policyName) => {
return iam.ManagedPolicy.fromAwsManagedPolicyName(policyName);
}),
}),
runtime: lambda.Runtime.NODEJS_22_X,
timeout: Duration.seconds(100),
environment: {
DB_URL_SECRET_ARN: getDbConnectionSecret(stack).secretArn,
JWT_SECRET_ARN: getJwtSecret(stack).secretArn,
},
layers,
architecture: lambda.Architecture.X86_64,
logGroup: getLambdaLogs(stack),
});

api.addRoutes({
path,
methods: [method],
integration: new integrations.HttpLambdaIntegration(`${name}Integration`, fn, {
payloadFormatVersion: apigatewayv2.PayloadFormatVersion.VERSION_2_0,
timeout: Duration.millis(timeoutInMillis ?? 5000),
}),
});

fn.addPermission(`${name}ApiInvokePermission`, {
action: 'lambda:InvokeFunction',
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: api.arnForExecuteApi(method, path, stage.stageName),
});

return fn;
};
29 changes: 29 additions & 0 deletions deployment/lib/secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as cdk from 'aws-cdk-lib';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';

const buildGetDbConnectionSecret = () => {
let dbConnectionSecret: secretsmanager.Secret | undefined;
return (stack: cdk.Stack) => {
if (dbConnectionSecret) return dbConnectionSecret;
dbConnectionSecret = new secretsmanager.Secret(stack, 'dbConnectionSecret');
return dbConnectionSecret;
};
};

export const getDbConnectionSecret = buildGetDbConnectionSecret();

const buildGetJwtSecret = () => {
let jwtSecret: secretsmanager.Secret | undefined;
return (stack: cdk.Stack) => {
if (jwtSecret) return jwtSecret;
jwtSecret = new secretsmanager.Secret(stack, 'jwtSecret', {
generateSecretString: {
requireEachIncludedType: true,
},
removalPolicy: cdk.RemovalPolicy.DESTROY, // TODO: Change to RETAIN for production
});
return jwtSecret;
};
};

export const getJwtSecret = buildGetJwtSecret();
56 changes: 56 additions & 0 deletions deployment/resources/cloudfront-distribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Fn, Stack } from 'aws-cdk-lib';
import type { HttpApi } from 'aws-cdk-lib/aws-apigatewayv2';
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cfOrigins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';

type DistributionSettings = {
indexDotHtml: s3.Bucket;
apiMain: HttpApi;
hasCustomDomain: boolean;
customDomainName?: string;
customDomainCertificateArn?: string;
};

export const instantiateCloudFrontDistribution = (stack: Stack, params: DistributionSettings) => {
const { indexDotHtml, apiMain, hasCustomDomain, customDomainName, customDomainCertificateArn } =
params;

const apiDomain = Fn.select(2, Fn.split('/', apiMain.apiEndpoint));
const cloudfrontDistribution = new cloudfront.Distribution(stack, 'cloudfrontDistribution', {
defaultBehavior: {
origin: new cfOrigins.S3StaticWebsiteOrigin(indexDotHtml, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
}),
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
compress: true,
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
},
additionalBehaviors: {
'/api/*': {
origin: new cfOrigins.HttpOrigin(apiDomain, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
}),
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
compress: true,
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
},
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
domainNames: hasCustomDomain ? [customDomainName!] : undefined,
certificate: hasCustomDomain
? Certificate.fromCertificateArn(
stack,
'CustomDomainCertificate',
customDomainCertificateArn!,
)
: undefined,
});

return cloudfrontDistribution;
};
29 changes: 29 additions & 0 deletions deployment/resources/index-dot-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
export const instantiateIndexDotHtml = (stack: cdk.Stack) => {
const indexDotHtml = new s3.Bucket(stack, 'indexDotHtml', {
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: true,
restrictPublicBuckets: false,
}),
websiteErrorDocument: 'index.html',
websiteIndexDocument: 'index.html',
publicReadAccess: true,
removalPolicy: cdk.RemovalPolicy.DESTROY, // TODO: Change to RETAIN for production
autoDeleteObjects: true, // TODO: Change to false for production
});

indexDotHtml.addToResourcePolicy(
new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [indexDotHtml.arnForObjects('*')],
effect: iam.Effect.ALLOW,
principals: [new iam.AnyPrincipal()],
}),
);

return indexDotHtml;
};
46 changes: 46 additions & 0 deletions deployment/resources/tallyup-github-actions-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Stack } from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';

export const instantiateTallyUpGithubActionsRole = (stack: Stack, branch: 'main' | 'staging') => {
const tallyupGithubActionsRole = new iam.Role(stack, 'tallyupGithubActionsRole', {
assumedBy: new iam.WebIdentityPrincipal(
'arn:aws:iam::253016134262:oidc-provider/token.actions.githubusercontent.com',
{
StringLike: {
'token.actions.githubusercontent.com:sub': [
`repo:codeforsanjose/TallyUp:ref:refs/heads/${branch}`,
],
},
'ForAllValues:StringEquals': {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
'token.actions.githubusercontent.com:iss': 'https://token.actions.githubusercontent.com',
},
},
),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')],
inlinePolicies: {
DenyExpensiveServiceAccess: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.DENY,
actions: [
'ec2:*',
'glue:*',
'sagemaker:*',
'athena:*',
'emr:*',
'redshift:*',
'kinesis:*',
'redshift:*',
'rds:*',
'dynamodb:*',
],
resources: ['*'],
}),
],
}),
},
});

return tallyupGithubActionsRole;
};
Loading