Juhani Ränkimies
Posted on April 5, 2021
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
Posted on April 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.