Update on 2021-10-30
You should also check out my SvelteKit-CDK adapter.
It covers everything discussed in this article. While not even close to stable, it's cleaner and has better structure. And has a wrapper for Lambda@Edge, too.
Below are notes of me putting together sveltekit and AWS CDK. Explanations are minimal. Familiarity with both SvelteKit and CDK is probably required to follow.
At the moment @sveltejs/kit version is 1.0.0-next.71, and the adapter interface is not stable, so this solution is likely to break as time passes.
1. Init SvelteKit project
~$ mkdir sveltekit-cdk
~$ cd sveltekit-cdk/
~/sveltekit-cdk$ npm init svelte@next
...
(I chose typescript/CSS/no eslint/no prettier)
...
~/sveltekit-cdk$ npm install
~/sveltekit-cdk$ git init
~/sveltekit-cdk$ git add .
~/sveltekit-cdk$ git commit -m "init svelte"
2. Init CDK project for adapter
~/sveltekit-cdk$ mkdir adapter
~/sveltekit-cdk$ cd adapter
~/sveltekit-cdk/adapter$ npx cdk init --language typescript
~/sveltekit-cdk/adapter$ git add .
~/sveltekit-cdk/adapter$ git commit -m "init CDK"
3. Replace node adapter with a dummy adapter
This is our dummy adapter (adapter/adapter.ts)
import type { Adapter } from '@sveltejs/kit'
export const adapter: Adapter = {
name: 'MAGIC',
async adapt(utils): Promise<void> {
console.log('TODO...')
}
}
To be able to use typescript for the adapter without extra compilation steps, lets add a little ts-node wrapper (adapter/index.js)
require('ts-node').register()
module.exports = require('./adapter.ts').adapter
Finally, node adapter is replaced in svelte.config.js with our adapter
- const node = require('@sveltejs/adapter-node');
+ const cdkAdapter = require('./adapter/index.js')
- // By default, `npm run build` will create a standard Node app.
- // You can create optimized builds for different platforms by
- // specifying a different adapter
- adapter: node(),
+ adapter: cdkAdapter,
To make ts-node wrapper work, I had to comment out "module": "es2020",
from tsconfig.json. Full commit here.
And try it out
$ npm run build
> sveltekit-cdk@0.0.1 build /workspaces/sveltekit-cdk
> svelte-kit build
vite v2.1.5 building for production...
✓ 18 modules transformed.
.svelte/output/client/_app/manifest.json 0.67kb
.svelte/output/client/_app/assets/start-d4cd1237.css 0.29kb / brotli: 0.18kb
.svelte/output/client/_app/assets/pages/index.svelte-27172613.css 0.69kb / brotli: 0.26kb
.svelte/output/client/_app/pages/index.svelte-f28bc36b.js 1.58kb / brotli: 0.68kb
.svelte/output/client/_app/chunks/vendor-57a96aae.js 5.14kb / brotli: 2.00kb
.svelte/output/client/_app/start-ff890ac9.js 15.52kb / brotli: 5.29kb
vite v2.1.5 building SSR bundle for production...
✓ 16 modules transformed.
.svelte/output/server/app.js 70.57kb
Run npm start to try your app locally.
> Using MAGIC
TODO...
✔ done
4. Capture Svelte output
Svelte files are copied to a place that is easy to include to CDK stack.
export const adapter: Adapter = {
name: 'MAGIC',
async adapt(utils): Promise<void> {
- console.log('TODO...')
+ const contentPath = path.join(__dirname, 'content')
+ rmRecursive(contentPath)
+ const serverPath = path.join(contentPath, 'server')
+ const staticPath = path.join(contentPath, 'static')
+ utils.copy_server_files(serverPath)
+ utils.copy_client_files(staticPath)
+ utils.copy_static_files(staticPath)
}
}
Svelte kit provides nice utilities for storing the files to desired folders. Here, the SSR implementation is put to server folder, and will be later deployed to a lambda function. And client and static files are stored to static folder, and will be later deployed to S3.
5. Create a Lambda wrapper for SSR
This lambda handler maps the API gateway request and response to what SSR implementation expects.
import { URLSearchParams } from 'url';
import { render } from '../content/server/app.js'
export async function handler(event) {
const { path, headers, multiValueQueryStringParameters } = event;
const query = new URLSearchParams();
if (multiValueQueryStringParameters) {
Object.keys(multiValueQueryStringParameters).forEach(k => {
const vs = multiValueQueryStringParameters[k]
vs.forEach(v => {
query.append(k, v)
})
})
}
const rendered = await render({
host: event.requestContext.domainName,
method: event.httpMethod,
body: JSON.parse(event.body), // TODO: other payload types
headers,
query,
path,
})
if (rendered) {
const resp = {
headers: {},
multiValueHeaders: {},
body: rendered.body,
statusCode: rendered.status
}
Object.keys(rendered.headers).forEach(k => {
const v = rendered.headers[k]
if (v instanceof Array) {
resp.multiValueHeaders[k] = v
} else {
resp.headers[k] = v
}
})
return resp
}
return {
statusCode: 404,
body: 'Not found.'
}
}
And it needs to be bundled for deployment to lambda
export const adapter: Adapter = {
name: 'MAGIC',
async adapt(utils): Promise<void> {
const contentPath = path.join(__dirname, 'content')
rmRecursive(contentPath)
const serverPath = path.join(contentPath, 'server')
const staticPath = path.join(contentPath, 'static')
utils.copy_server_files(serverPath)
utils.copy_client_files(staticPath)
utils.copy_static_files(staticPath)
+
+ const bundler = new ParcelBundler(
+ [path.join(__dirname, 'lambda', 'index.js')],
+ {
+ outDir: path.join(contentPath, 'server-bundle'),
+ bundleNodeModules: true,
+ target: 'node',
+ sourceMaps: false,
+ minify: false,
+ },
+ )
+ await bundler.bundle()
}
}
6. Create CDK Stack
This is a barebones stack for deploying the site. The only non-trivial parts are the routing configuration that is generated from the contents of the static folder, and that I configured CDN to pass session cookies through to SSR handler.
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
import * as gw from '@aws-cdk/aws-apigatewayv2'
import * as s3 from '@aws-cdk/aws-s3'
import * as s3depl from '@aws-cdk/aws-s3-deployment'
import { LambdaProxyIntegration } from '@aws-cdk/aws-apigatewayv2-integrations'
import * as cdn from '@aws-cdk/aws-cloudfront'
import * as fs from 'fs';
import * as path from 'path';
interface AdapterProps extends cdk.StackProps {
serverPath: string
staticPath: string
}
export class AdapterStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: AdapterProps) {
super(scope, id, props);
const handler = new lambda.Function(this, 'handler', {
code: new lambda.AssetCode(props?.serverPath),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_14_X,
})
const api = new gw.HttpApi(this, 'api')
api.addRoutes({
path: '/{proxy+}',
methods: [gw.HttpMethod.ANY],
integration: new LambdaProxyIntegration({
handler,
payloadFormatVersion: gw.PayloadFormatVersion.VERSION_1_0,
})
})
const staticBucket = new s3.Bucket(this, 'staticBucket')
const staticDeployment = new s3depl.BucketDeployment(this, 'staticDeployment', {
destinationBucket: staticBucket,
sources: [s3depl.Source.asset(props.staticPath)]
})
const staticID = new cdn.OriginAccessIdentity(this, 'staticID')
staticBucket.grantRead(staticID)
const distro = new cdn.CloudFrontWebDistribution(this, 'distro', {
priceClass: cdn.PriceClass.PRICE_CLASS_100,
defaultRootObject: '',
originConfigs: [
{
customOriginSource: {
domainName: cdk.Fn.select(1, cdk.Fn.split('://', api.apiEndpoint)),
originProtocolPolicy: cdn.OriginProtocolPolicy.HTTPS_ONLY,
},
behaviors: [
{
allowedMethods: cdn.CloudFrontAllowedMethods.ALL,
forwardedValues: {
queryString: false,
cookies: {
forward: 'whitelist',
whitelistedNames: ['sid', 'sid.sig']
}
},
isDefaultBehavior: true
}
]
},
{
s3OriginSource: {
s3BucketSource: staticBucket,
originAccessIdentity: staticID,
},
behaviors: mkStaticRoutes(props.staticPath)
}
]
})
}
}
function mkStaticRoutes(staticPath: string): cdn.Behavior[] {
return fs.readdirSync(staticPath).map(f => {
const fullPath = path.join(staticPath, f)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
return {
pathPattern: `/${f}/*`,
}
}
return { pathPattern: `/${f}` }
})
}
Check full commit to see how cdk deploy is invoked and parameters passed to the stack
$ export ACCOUNT=234......23
$ export REGION=eu-north-1
$ npm run build
> sveltekit-cdk@0.0.1 build /workspaces/sveltekit-cdk> svelte-kit build
vite v2.1.5 building for production...
✓ 18 modules transformed.
.svelte/output/client/_app/manifest.json 0.67kb
.svelte/output/client/_app/assets/start-d4cd1237.css 0.29kb / brotli: 0.18kb
.svelte/output/client/_app/assets/pages/index.svelte-27172613.css 0.69kb / brotli: 0.26kb
.svelte/output/client/_app/pages/index.svelte-f28bc36b.js 1.58kb / brotli: 0.68kb
.svelte/output/client/_app/chunks/vendor-57a96aae.js 5.14kb / brotli: 2.00kb
.svelte/output/client/_app/start-ff890ac9.js 15.52kb / brotli: 5.29kb
vite v2.1.5 building SSR bundle for production...
✓ 16 modules transformed.
.svelte/output/server/app.js 70.57kb
Run npm start to try your app locally.
> Using MAGIC
✨ Built in 1.45s.
adapter/content/server-bundle/index.js 83.97 KB 1.14s
✔ done
AdapterStack: deploying...
[0%] start: Publishing 77fee73b537c2786a56a5ae730eecc3d1121be2512b210c0ad7f92a87ba52125:current
[25%] success: Published 77fee73b537c2786a56a5ae730eecc3d1121be2512b210c0ad7f92a87ba52125:current
[25%] start: Publishing e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68:current
[50%] success: Published e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68:current
[50%] start: Publishing c24b999656e4fe6c609c31bae56a1cf4717a405619c3aa6ba1bc686b8c2c86cf:current
[75%] success: Published c24b999656e4fe6c609c31bae56a1cf4717a405619c3aa6ba1bc686b8c2c86cf:current
[75%] start: Publishing 9ab01d8ba7648b72beed50ec3fad310aef1e1af2bf56f3912d53a56e03579ece:current
[100%] success: Published 9ab01d8ba7648b72beed50ec3fad310aef1e1af2bf56f3912d53a56e03579ece:current
AdapterStack: creating CloudFormation changeset...
0/18 | 11:07:37 PM | REVIEW_IN_PROGRESS | AWS::CloudFormation::Stack | AdapterStack User Initiated
0/18 | 11:07:43 PM | CREATE_IN_PROGRESS | AWS::CloudFormation::Stack | AdapterStack User Initiated
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | handler/ServiceRole (handlerServiceRole187D5A5A)
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::S3::Bucket | staticBucket (staticBucket49CE0992)
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | handler/ServiceRole (handlerServiceRole187D5A5A) Resource creation Initiated
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::CloudFront::CloudFrontOriginAccessIdentity | staticID (staticID76F07208)
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Api | api (apiC8550315)
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata/Default (CDKMetadata)
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265)
1/18 | 11:08:16 PM | CREATE_IN_PROGRESS | AWS::Lambda::LayerVersion | staticDeployment/AwsCliLayer (staticDeploymentAwsCliLayerCF83B634)
1/18 | 11:08:17 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265) Resource creation Initiated
1/18 | 11:08:17 PM | CREATE_IN_PROGRESS | AWS::S3::Bucket | staticBucket (staticBucket49CE0992) Resource creation Initiated
1/18 | 11:08:18 PM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata/Default (CDKMetadata) Resource creation Initiated
1/18 | 11:08:18 PM | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata/Default (CDKMetadata)
1/18 | 11:08:18 PM | CREATE_IN_PROGRESS | AWS::CloudFront::CloudFrontOriginAccessIdentity | staticID (staticID76F07208) Resource creation Initiated
4/18 | 11:08:18 PM | CREATE_COMPLETE | AWS::CloudFront::CloudFrontOriginAccessIdentity | staticID (staticID76F07208)
4/18 | 11:08:18 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Api | api (apiC8550315) Resource creation Initiated
4/18 | 11:08:18 PM | CREATE_COMPLETE | AWS::ApiGatewayV2::Api | api (apiC8550315)
4/18 | 11:08:20 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Stage | api/DefaultStage (apiDefaultStage04B80AC9)
4/18 | 11:08:22 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Stage | api/DefaultStage (apiDefaultStage04B80AC9) Resource creation Initiated
4/18 | 11:08:22 PM | CREATE_COMPLETE | AWS::ApiGatewayV2::Stage | api/DefaultStage (apiDefaultStage04B80AC9)
5/18 | 11:08:32 PM | CREATE_IN_PROGRESS | AWS::Lambda::LayerVersion | staticDeployment/AwsCliLayer (staticDeploymentAwsCliLayerCF83B634) Resource creation Initiated
5/18 | 11:08:33 PM | CREATE_COMPLETE | AWS::Lambda::LayerVersion | staticDeployment/AwsCliLayer (staticDeploymentAwsCliLayerCF83B634)
9/18 | 11:08:34 PM | CREATE_COMPLETE | AWS::IAM::Role | handler/ServiceRole (handlerServiceRole187D5A5A)
9/18 | 11:08:35 PM | CREATE_COMPLETE | AWS::IAM::Role | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265)
9/18 | 11:08:37 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | handler (handlerE1533BD5)
9/18 | 11:08:37 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | handler (handlerE1533BD5) Resource creation Initiated
9/18 | 11:08:37 PM | CREATE_COMPLETE | AWS::Lambda::Function | handler (handlerE1533BD5)
9/18 | 11:08:38 PM | CREATE_COMPLETE | AWS::S3::Bucket | staticBucket (staticBucket49CE0992)
12/18 | 11:08:39 PM | CREATE_IN_PROGRESS | AWS::Lambda::Permission | api/ANY--{proxy+}/AdapterStackapiANYproxy1E757BCE-Permission (apiANYproxyAdapterStackapiANYproxy1E757BCEPermission4DD3BE97)
12/18 | 11:08:39 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Integration | api/ANY--{proxy+}/HttpIntegration-addabd80f5f992d479db94f5bda52ee5 (apiANYproxyHttpIntegrationaddabd80f5f992d479db94f5bda52ee5449897FD)
12/18 | 11:08:40 PM | CREATE_IN_PROGRESS | AWS::IAM::Policy | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF)
12/18 | 11:08:40 PM | CREATE_IN_PROGRESS | AWS::Lambda::Permission | api/ANY--{proxy+}/AdapterStackapiANYproxy1E757BCE-Permission (apiANYproxyAdapterStackapiANYproxy1E757BCEPermission4DD3BE97) Resource creation Initiated
12/18 | 11:08:40 PM | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | staticBucket/Policy (staticBucketPolicyA47383C0)
12/18 | 11:08:41 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Integration | api/ANY--{proxy+}/HttpIntegration-addabd80f5f992d479db94f5bda52ee5 (apiANYproxyHttpIntegrationaddabd80f5f992d479db94f5bda52ee5449897FD) Resource creation Initiated
12/18 | 11:08:41 PM | CREATE_COMPLETE | AWS::ApiGatewayV2::Integration | api/ANY--{proxy+}/HttpIntegration-addabd80f5f992d479db94f5bda52ee5 (apiANYproxyHttpIntegrationaddabd80f5f992d479db94f5bda52ee5449897FD)
12/18 | 11:08:41 PM | CREATE_IN_PROGRESS | AWS::IAM::Policy | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF) Resource creation Initiated
12/18 | 11:08:41 PM | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | staticBucket/Policy (staticBucketPolicyA47383C0) Resource creation Initiated
12/18 | 11:08:41 PM | CREATE_COMPLETE | AWS::S3::BucketPolicy | staticBucket/Policy (staticBucketPolicyA47383C0)
12/18 | 11:08:43 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Route | api/ANY--{proxy+} (apiANYproxy1413EA65)
12/18 | 11:08:43 PM | CREATE_IN_PROGRESS | AWS::ApiGatewayV2::Route | api/ANY--{proxy+} (apiANYproxy1413EA65) Resource creation Initiated
12/18 | 11:08:44 PM | CREATE_COMPLETE | AWS::ApiGatewayV2::Route | api/ANY--{proxy+} (apiANYproxy1413EA65)
13/18 | 11:08:50 PM | CREATE_COMPLETE | AWS::Lambda::Permission | api/ANY--{proxy+}/AdapterStackapiANYproxy1E757BCE-Permission (apiANYproxyAdapterStackapiANYproxy1E757BCEPermission4DD3BE97)
14/18 | 11:08:58 PM | CREATE_IN_PROGRESS | AWS::CloudFront::Distribution | distro/CFDistribution (distroCFDistributionB272DD5C)
14/18 | 11:08:58 PM | CREATE_COMPLETE | AWS::IAM::Policy | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF)
14/18 | 11:09:00 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536)
14/18 | 11:09:02 PM | CREATE_IN_PROGRESS | AWS::CloudFront::Distribution | distro/CFDistribution (distroCFDistributionB272DD5C) Resource creation Initiated
15/18 | 11:09:04 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536) Resource creation Initiated
15/18 | 11:09:05 PM | CREATE_COMPLETE | AWS::Lambda::Function | Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C (CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536)
15/18 | 11:09:07 PM | CREATE_IN_PROGRESS | Custom::CDKBucketDeployment | staticDeployment/CustomResource/Default (staticDeploymentCustomResource41B995BA)
16/18 | 11:09:39 PM | CREATE_IN_PROGRESS | Custom::CDKBucketDeployment | staticDeployment/CustomResource/Default (staticDeploymentCustomResource41B995BA) Resource creation Initiated
16/18 | 11:09:39 PM | CREATE_COMPLETE | Custom::CDKBucketDeployment | staticDeployment/CustomResource/Default (staticDeploymentCustomResource41B995BA)
16/18 Currently in progress: AdapterStack, distroCFDistributionB272DD5C
18/18 | 11:11:19 PM | CREATE_COMPLETE | AWS::CloudFront::Distribution | distro/CFDistribution (distroCFDistributionB272DD5C)
18/18 | 11:11:20 PM | CREATE_COMPLETE | AWS::CloudFormation::Stack | AdapterStack
✅ AdapterStack
Stack ARN:
arn:aws:cloudformation:eu-north-1:3123....123:stack/AdapterStack/b653b540-9663-11eb-b7b2-0eab76d49478
Hurrah!!
juranki / diy-sveltekit-cdk-adapter
An exercise on deploying SvelteKit with CDK
Cover photo by Alistair MacRobert on Unsplash
Top comments (2)
Fantastic this was really helpful for me today thank you 👏
Sir, you saved me.