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
32 changes: 32 additions & 0 deletions .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Pre-Release

on:
workflow_dispatch:

jobs:
pre-release:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
registry-url: 'https://npm.pkg.github.com'
scope: '@${{ github.repository_owner }}'

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Compile and test
run: npm run ci

- name: Prepare pre release
run: |
git_sha=$(git rev-parse --short HEAD)
mv package.json /tmp/package.json
jq --arg git_sha ${git_sha} 'del(.repository) | .version = .version + "-" + $git_sha' /tmp/package.json > package.json

- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,21 @@ export default () => ({
});
```

### S3 bucket Enryption

```js
export default () => ({
hostedZoneName: `example.com`,
s3: {
// <==
encryption: `KMS_MANAGED`, // default: S3_MANAGED
encryptionKeyArn: `arn:aws:kms:${aws_region}:${aws_accound}:key/${id}`, // only for type KMS, if missing AWS will generate a key
bucketKeyEnabled: true, // bucket key reduces encryption costs by lowering calls to AWS KMS
},
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```

### Source maps

#### Enabling source maps for a Lambda function on AWS
Expand Down Expand Up @@ -622,6 +637,12 @@ Note: The `onStart` hook is called before the routes are registered.
"Effect": "Allow",
"Action": ["iam:ListAttachedRolePolicies", "iam:DetachRolePolicy", "iam:DeleteRole"],
"Resource": "arn:aws:iam::*:role/aws-simple-*"
},
{
"Sid": "AwsSimple10",
"Effect": "Allow",
"Action": ["kms:GenerateDataKey", "kms:Decrypt"],
"Resource": "arn:aws:kms:::key/"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/cdk/add-s3-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { aws_iam, aws_s3 } from 'aws-cdk-lib';

import { addCorsPreflight } from './add-cors-preflight.js';
import { aws_apigateway } from 'aws-cdk-lib';
import { join } from 'path';
import { join } from 'node:path';

export interface S3ResourceConstructDependencies {
readonly bucket: aws_s3.IBucket;
Expand Down
10 changes: 7 additions & 3 deletions src/cdk/create-bucket.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { Stack } from 'aws-cdk-lib';

import { CfnOutput, RemovalPolicy, aws_s3 } from 'aws-cdk-lib';

export function createBucket(stack: Stack): aws_s3.IBucket {
import type { StackConfig } from '../parse-stack-config.js';
import { mapS3Encryption } from '../utils/map-s3-encryption.js';

export function createBucket(stackConfig: StackConfig, stack: Stack): aws_s3.IBucket {
const s3BucketEncryption = mapS3Encryption(stackConfig.s3, stack);

const bucket = new aws_s3.Bucket(stack, `Bucket`, {
...s3BucketEncryption,
blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL,
encryption: aws_s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
Expand Down
17 changes: 17 additions & 0 deletions src/parse-stack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ export type S3Route = z.TypeOf<typeof S3RouteSchema>;
const LambdaRuntimeSchema = z.enum([`20.x`, `22.x`, `24.x`, `LATEST`]).optional();
export type LambdaRuntime = z.TypeOf<typeof LambdaRuntimeSchema>;

const S3BucketEncryptionSchema = z
.discriminatedUnion(`encryption`, [
z.object({ encryption: z.literal('S3_MANAGED') }),
z.object({
encryption: z.literal('KMS_MANAGED'),
bucketKeyEnabled: z.boolean().default(true).optional(),
}),
z.object({
encryption: z.literal('KMS'),
bucketKeyEnabled: z.boolean().default(true).optional(),
encryptionKeyArn: z.string().optional(),
}),
])
.optional();
export type S3BucketEncryption = z.TypeOf<typeof S3BucketEncryptionSchema>;

const LambdaRouteSchema = z.object({
type: z.literal(`function`),
httpMethod: z.enum([`DELETE`, `GET`, `HEAD`, `PATCH`, `POST`, `PUT`]),
Expand Down Expand Up @@ -105,6 +121,7 @@ const StackConfigSchema = DomainNamePartsSchema.extend({
.optional(),
tags: z.record(z.string()).optional(),
routes: z.array(z.union([LambdaRouteSchema, S3RouteSchema])).min(1),
s3: S3BucketEncryptionSchema,
onSynthesize: z.function().optional(),
onStart: z.function().optional(),
});
Expand Down
2 changes: 1 addition & 1 deletion src/synthesize-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const synthesizeCommand: CommandModule<{}, {}> = {
const stackConfig = parseStackConfig(await readStackConfig());
const stack = createStack(stackConfig);
const restApi = createRestApi(stackConfig, stack);
const bucket = createBucket(stack);
const bucket = createBucket(stackConfig, stack);
const bucketReadRole = createBucketReadRole(stack, bucket);
const requestAuthorizer = createRequestAuthorizer(stackConfig, stack);
const lambdaServiceRole = createLambdaServiceRole(stack);
Expand Down
76 changes: 76 additions & 0 deletions src/utils/map-s3-encryption.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, test } from '@jest/globals';

import { mapS3Encryption } from './map-s3-encryption.js';
import { aws_s3, Stack } from 'aws-cdk-lib';

describe(`mapS3Encryption()`, () => {
test(`returns S3_MANAGED encryption when no encryption is provided`, () => {
const result = mapS3Encryption(undefined as any, {} as Stack);
expect(result).toEqual({ encryption: aws_s3.BucketEncryption.S3_MANAGED });
});

test(`returns S3_MANAGED encryption when S3_MANAGED is provided`, () => {
const result = mapS3Encryption({ encryption: 'S3_MANAGED' }, {} as Stack);
expect(result).toEqual({ encryption: aws_s3.BucketEncryption.S3_MANAGED });
});

test(`returns KMS_MANAGED encryption when KMS_MANAGED is provided`, () => {
const result = mapS3Encryption({ encryption: 'KMS_MANAGED' }, {} as Stack);
expect(result).toEqual({
encryption: aws_s3.BucketEncryption.KMS_MANAGED,
bucketKeyEnabled: true,
});
});

test(`returns KMS_MANAGED encryption with bucketKeyEnabled when KMS_MANAGED and bucketKeyEnabled are provided`, () => {
const result = mapS3Encryption(
{ encryption: 'KMS_MANAGED', bucketKeyEnabled: false },
{} as Stack,
);
expect(result).toEqual({
encryption: aws_s3.BucketEncryption.KMS_MANAGED,
bucketKeyEnabled: false,
});
});

test(`returns KMS encryption with key when KMS and encryptionKeyArn are provided`, () => {
const stack = new Stack();
expect(Stack.isStack(stack)).toBeTruthy();

const keyArn = 'arn:aws:kms:region:account-id:key/key-id';
const result = mapS3Encryption({ encryption: 'KMS', encryptionKeyArn: keyArn }, stack);
expect(result.encryption).toEqual(aws_s3.BucketEncryption.KMS);
expect(result.encryptionKey).toBeDefined();
});

test(`returns KMS encryption without key when KMS is provided without encryptionKeyArn`, () => {
const result = mapS3Encryption({ encryption: 'KMS' }, {} as Stack);
expect(result).toEqual({
encryption: aws_s3.BucketEncryption.KMS,
encryptionKey: undefined,
bucketKeyEnabled: true,
});
});

test(`returns KMS encryption with bucketKeyEnabled when KMS, bucketKeyEnabled and encryptionKeyArn are provided`, () => {
const stack = new Stack();
expect(Stack.isStack(stack)).toBeTruthy();

const keyArn = 'arn:aws:kms:region:account-id:key/key-id';
const result = mapS3Encryption(
{ encryption: 'KMS', encryptionKeyArn: keyArn, bucketKeyEnabled: false },
stack,
);
expect(result).toEqual({
encryption: aws_s3.BucketEncryption.KMS,
encryptionKey: expect.any(Object),
bucketKeyEnabled: false,
});
});

test(`throws an error for unsupported encryption types`, () => {
expect(() => mapS3Encryption({ encryption: 'UNSUPPORTED' } as any, {} as Stack)).toThrow(
'Unsupported encryption type',
);
});
});
36 changes: 36 additions & 0 deletions src/utils/map-s3-encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { S3BucketEncryption } from '../parse-stack-config.js';

import { aws_s3, aws_kms, type Stack } from 'aws-cdk-lib';

export type BucketEncryption = {
encryption: aws_s3.BucketEncryption;
encryptionKey?: aws_kms.IKey;
bucketKeyEnabled?: boolean;
};

export function mapS3Encryption(s3encryption: S3BucketEncryption, stack: Stack): BucketEncryption {
if (!s3encryption || s3encryption.encryption === `S3_MANAGED`) {
return { encryption: aws_s3.BucketEncryption.S3_MANAGED };
}

const bucketKeyEnabled = s3encryption.bucketKeyEnabled ?? true;

if (s3encryption.encryption === `KMS_MANAGED`) {
return { encryption: aws_s3.BucketEncryption.KMS_MANAGED, bucketKeyEnabled };
}

if (s3encryption.encryption === `KMS`) {
let encryptionKey: aws_kms.IKey | undefined = undefined;

if (s3encryption.encryptionKeyArn) {
encryptionKey = aws_kms.Key.fromKeyArn(
stack,
`S3EncryptionKey`,
s3encryption.encryptionKeyArn,
);
}
return { encryptionKey, encryption: aws_s3.BucketEncryption.KMS, bucketKeyEnabled };
}

throw new Error(`Unsupported encryption type`);
}
Loading