https://github.com/clebert/aws-simple
Production-ready AWS website deployment with minimal configuration.
https://github.com/clebert/aws-simple
Last synced: about 1 year ago
JSON representation
Production-ready AWS website deployment with minimal configuration.
- Host: GitHub
- URL: https://github.com/clebert/aws-simple
- Owner: clebert
- License: mit
- Created: 2019-09-24T19:51:30.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2024-02-29T16:25:35.000Z (over 2 years ago)
- Last Synced: 2025-04-09T10:04:11.376Z (about 1 year ago)
- Language: TypeScript
- Homepage:
- Size: 4.13 MB
- Stars: 14
- Watchers: 5
- Forks: 9
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# aws-simple
> Production-ready AWS website deployment with minimal configuration.
## Installation
```
npm install aws-simple aws-cdk
```
## Getting started
The following are the steps to deploy a website using `aws-simple` and the AWS CDK.
### 1. Create a config file
Create a config file named `aws-simple.config.mjs`, which exports a function that describes a
website stack:
```js
// @ts-check
/** @type {import('aws-simple').ConfigFileDefaultExport} */
export default (port) => ({
hostedZoneName: `example.com`,
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```
The exported function optionally gets a DEV server `port` argument when called in the context of the
`aws-simple start [options]` CLI command.
### 2. Create a public hosted zone on AWS Route 53
Create a **public** hosted zone on AWS Route 53 to make a website available under a particular
domain. The required certificate is created automatically by `aws-simple` during deployment.
### 3. Create an AWS IAM user
Create an AWS IAM user with programmatic access and an [AWS IAM policy](#aws-iam-policy-example)
with sufficient permissions.
### 4. Set the credentials
Set the credentials of the AWS IAM user using the two environment variables, `AWS_ACCESS_KEY_ID` and
`AWS_SECRET_ACCESS_KEY`. Alternatively, the credentials are retrieved using the AWS profile.
### 5. Set the AWS region
Set the AWS region using either the environment variable `AWS_REGION` or `AWS_DEFAULT_REGION`
evaluated in the specified order. Alternatively, the region is retrieved using the AWS profile.
### 6. Bootstrap the AWS environment
```
npx cdk bootstrap --app 'npx aws-simple synthesize'
```
### 7. Deploy a website to AWS
```
npx cdk deploy --app 'npx aws-simple synthesize' && npx aws-simple upload
```
### 8. Optional: Start a local DEV server
```
npx aws-simple start
```
## CLI usage
```
Usage: aws-simple [options]
Commands:
aws-simple synthesize [options] Synthesize the configured stack using the CDK. [aliases: synth]
aws-simple upload [options] Upload all referenced files to the S3 bucket of the configured stack.
aws-simple list [options] List all deployed stacks filtered by the specified hosted zone name.
aws-simple tag [options] Update the tags of the specified stack.
aws-simple delete [options] Delete the specified stack.
aws-simple purge [options] Delete all expired stacks filtered by the specified hosted zone name.
aws-simple flush-cache [options] Flush the REST API cache of the specified stack.
aws-simple redeploy [options] Redeploy the REST API of the specified stack.
aws-simple cleanup [options] Deletes unused account-wide resources created by aws-simple.
aws-simple start [options] Start a local DEV server.
Options:
--version Show version number [boolean]
-h, --help Show help [boolean]
```
## Configuration
### Alias record name
```js
export default () => ({
hostedZoneName: `example.com`,
aliasRecordName: `stage`, // <==
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```
An optional alias record name allows multiple website variants to be deployed and operated
simultaneously. Example: `stage.example.com`, `test.example.com`
Except for the specified hosted zone, the website variants do not share any infrastructure. For the
management of multiple website variants, there are the following two CLI commands:
- `aws-simple list [options]`
- `aws-simple purge [options]`
### S3 file routes
```js
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `file`, // <==
publicPath: `/`,
path: `dist/index.html`,
// optional
responseHeaders: { 'cache-control': `max-age=157680000` },
},
],
});
```
### Lambda function routes
```js
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `function`, // <==
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`, // must be unique per stack and as short as possible
// optional
memorySize: 1769, // default: `128` MB
timeoutInSeconds: 3, // default: `28` seconds (this is the maximum timeout)
environment: { FOO: `bar` },
requestParameters: { foo: {}, bar: { cacheKey: true, required: true } },
},
],
});
```
```cjs
// dist/hello.js
exports.handler = async () => ({
statusCode: 200,
body: JSON.stringify({ hello: `world` }),
});
```
### Wildcard file/function routes
```js
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `file`,
publicPath: `/*`, // <== matches '/', '/foo', '/foo/bar'
path: `dist/index.html`,
},
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello/*`, // <== matches '/hello', '/hello/world'
path: `dist/hello.js`,
functionName: `hello`,
},
],
});
```
### S3 folder routes
```js
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `folder`, // <==
publicPath: `/*`, // matches '/foo' and '/foo/bar' but not '/'
path: `dist`,
// optional
responseHeaders: { 'cache-control': `max-age=157680000` },
},
],
});
```
### Caching
```js
export default () => ({
hostedZoneName: `example.com`,
cachingEnabled: true, // <==
routes: [
{
type: `file`,
publicPath: `/`,
path: `dist/index.html`,
cacheTtlInSeconds: 3600, // default: `300` seconds (if caching is enabled)
},
{
type: `folder`,
publicPath: `/*`,
path: `dist`,
cacheTtlInSeconds: 3600, // default: `300` seconds (if caching is enabled)
},
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
cacheTtlInSeconds: 3600, // default: `300` seconds (if caching is enabled)
},
],
});
```
### Authentication
```js
export default () => ({
hostedZoneName: `example.com`,
authentication: {
username: `johndoe`, // <==
password: `123456`, // <==
// optional
cacheTtlInSeconds: 3600, // default: `300` seconds (if caching is enabled)
realm: `foo`,
},
routes: [
{
type: `file`,
publicPath: `/`,
path: `dist/index.html`,
authenticationEnabled: true, // <==
},
{
type: `folder`,
publicPath: `/*`,
path: `dist`,
authenticationEnabled: true, // <==
},
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
authenticationEnabled: true, // <==
},
],
});
```
### CORS
```js
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `file`,
publicPath: `/`,
path: `dist/index.html`,
corsEnabled: true, // <==
},
{
type: `folder`,
publicPath: `/*`,
path: `dist`,
corsEnabled: true, // <==
},
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
corsEnabled: true, // <==
},
],
});
```
```cjs
// dist/hello.js
exports.handler = async () => ({
statusCode: 200,
body: JSON.stringify({ hello: `world` }),
headers: {
'access-control-allow-origin': `*`, // <==
},
});
```
### Monitoring
```js
export default () => ({
hostedZoneName: `example.com`,
monitoring: {
accessLoggingEnabled: true, // <==
lambdaInsightsEnabled: true, // <==
loggingEnabled: true, // <==
metricsEnabled: true, // <==
tracingEnabled: true, // <==
},
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```
```js
export default () => ({
hostedZoneName: `example.com`,
monitoring: true, // <== shorthand form
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```
### Throttling
```js
// @ts-check
/** @type {import('aws-simple').Throttling} */
const throttling = {
rateLimit: 100, // default: `10000` requests per second
burstLimit: 50, // default: `5000` requests
};
/** @type {import('aws-simple').ConfigFileDefaultExport} */
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `file`,
publicPath: `/`,
path: `dist/index.html`,
throttling, // <==
},
{
type: `folder`,
publicPath: `/*`,
path: `dist`,
throttling, // <==
},
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
throttling, // <==
},
],
});
```
### Tagging
```js
export default () => ({
hostedZoneName: `example.com`,
tags: { foo: `bar`, baz: `qux` }, // <==
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```
### Termination protection
```js
export default () => ({
hostedZoneName: `example.com`,
terminationProtectionEnabled: true, // <==
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
});
```
### Source maps
#### Enabling source maps for a Lambda function on AWS
```js
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
environment: { NODE_OPTIONS: `--enable-source-maps` }, // <==
},
],
});
```
#### Enabling source maps for a local DEV Server
```
node --enable-source-maps $(npm bin)/aws-simple start
```
### `onSynthesize` hooks
To implement advanced features, `onSynthesize` hooks can be used. Below are two examples.
#### Configuring a firewall
```js
import { aws_wafv2 } from 'aws-cdk-lib';
export default () => ({
hostedZoneName: `example.com`,
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
onSynthesize: ({ stack, restApi }) => {
const myWebAclArn = `...`;
new aws_wafv2.CfnWebACLAssociation(stack, `WebACLAssociation`, {
resourceArn: restApi.deploymentStage.stageArn,
webAclArn: myWebAclArn,
});
},
});
```
#### Allowing a Lambda function read-only access to S3 buckets
```js
import { aws_iam } from 'aws-cdk-lib';
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
onSynthesize: ({ stack, restApi, lambdaFunction }) => {
lambdaFunction.role.addManagedPolicy(
aws_iam.ManagedPolicy.fromAwsManagedPolicyName(`AmazonS3ReadOnlyAccess`),
);
},
},
],
});
```
#### Allowing a Lambda function to access a secret in the AWS Secret Manager
```js
import { aws_iam } from 'aws-cdk-lib';
export default () => ({
hostedZoneName: `example.com`,
routes: [
{
type: `function`,
httpMethod: `GET`,
publicPath: `/hello`,
path: `dist/hello.js`,
functionName: `hello`,
onSynthesize: ({ stack, restApi, lambdaFunction }) => {
const mySecretId = `...`;
const secretsManagerPolicyStatement = new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
actions: [`secretsmanager:GetSecretValue`],
resources: [
`arn:aws:secretsmanager:${stack.region}:${stack.account}:secret:${mySecretId}`,
],
});
lambdaFunction.addToRolePolicy(secretsManagerPolicyStatement);
},
},
],
});
```
### `onStart` hook
The `onStart` hook can be used to customize the DEV server's
[Express app](https://expressjs.com/en/5x/api.html#app), e.g. to configure a proxy middleware:
```js
import { createProxyMiddleware } from 'http-proxy-middleware';
export default () => ({
hostedZoneName: `example.com`,
routes: [{ type: `file`, publicPath: `/`, path: `dist/index.html` }],
onStart: (app) => {
app.use(
`/some-external-api`,
createProxyMiddleware({
target: `http://www.example.org`,
changeOrigin: true,
}),
);
},
});
```
Note: The `onStart` hook is called before the routes are registered.
## AWS IAM policy example
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Bootstrap0",
"Effect": "Allow",
"Action": "cloudformation:*",
"Resource": "arn:aws:cloudformation:*:*:stack/CDKToolkit/*"
},
{
"Sid": "Bootstrap1",
"Effect": "Allow",
"Action": "iam:*",
"Resource": "arn:aws:iam::*:role/cdk-*"
},
{
"Sid": "Bootstrap2",
"Effect": "Allow",
"Action": "ssm:*",
"Resource": "arn:aws:ssm:*:*:parameter/cdk-bootstrap/*"
},
{
"Sid": "Bootstrap3",
"Effect": "Allow",
"Action": "ecr:*",
"Resource": "arn:aws:ecr:*:*:repository/cdk-*"
},
{
"Sid": "Bootstrap4",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::cdk-*"
},
{
"Sid": "AwsSimple0",
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
},
{
"Sid": "AwsSimple1",
"Effect": "Allow",
"Action": "cloudformation:*",
"Resource": "arn:aws:cloudformation:*:*:stack/aws-simple-*"
},
{
"Sid": "AwsSimple2",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::aws-simple-*"
},
{
"Sid": "AwsSimple3",
"Effect": "Allow",
"Action": "apigateway:POST",
"Resource": "arn:aws:apigateway:*::/restapis/*/deployments"
},
{
"Sid": "AwsSimple4",
"Effect": "Allow",
"Action": "apigateway:PATCH",
"Resource": "arn:aws:apigateway:*::/restapis/*/stages/prod"
},
{
"Sid": "AwsSimple5",
"Effect": "Allow",
"Action": "cloudformation:DescribeStacks",
"Resource": "*"
},
{
"Sid": "AwsSimple6",
"Effect": "Allow",
"Action": "apigateway:DELETE",
"Resource": "arn:aws:apigateway:*::/restapis/*/stages/prod/cache/data"
},
{
"Sid": "AwsSimple7",
"Effect": "Allow",
"Action": "apigateway:GET",
"Resource": "arn:aws:apigateway:*::/account"
},
{
"Sid": "AwsSimple8",
"Effect": "Allow",
"Action": "iam:ListRoles",
"Resource": "arn:aws:iam::*:role/"
},
{
"Sid": "AwsSimple9",
"Effect": "Allow",
"Action": ["iam:ListAttachedRolePolicies", "iam:DetachRolePolicy", "iam:DeleteRole"],
"Resource": "arn:aws:iam::*:role/aws-simple-*"
}
]
}
```