Skip to content

Commit

Permalink
serverless initial commit (#44)
Browse files Browse the repository at this point in the history
* serverless initial commit

* update secret name to normal follow convention

* missed removing caller when removed target
  • Loading branch information
davidcheung authored Jan 31, 2022
1 parent eee0fa1 commit 3697c39
Show file tree
Hide file tree
Showing 22 changed files with 2,464 additions and 28 deletions.
2 changes: 1 addition & 1 deletion scripts/setup-stripe-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ else
echo 'Must specify $ENVIRONMENT (stage/prod) to create stripe secret'>&2; exit 1;
fi

SECRET_NAME=${PROJECT_NAME}/kubernetes/${ENVIRONMENT}/${PROJECT_NAME}
SECRET_NAME=${PROJECT_NAME}/application/${ENVIRONMENT}/${PROJECT_NAME}

# Modify existing application secret to add stripe api key
UPDATED_SECRET=$(aws secretsmanager get-secret-value --region ${REGION} --secret=${SECRET_NAME} --query "SecretString" --output text | \
Expand Down
6 changes: 5 additions & 1 deletion templates/.dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
node_modules
node_modules
kubernetes
auth
events
.env
8 changes: 8 additions & 0 deletions templates/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@
# - run smoke test against staging enviroment
# - deploy to production
name: CI Pipeline
<% if eq (index .Params `backendApplicationHosting`) "serverless" -%>
on:
workflow_dispatch # Manual dispatch, when CI flow is disabled for syntax completeness
# # uncomment `push` to use CI with Kubernetes Cluster
# push:
# branches: [master, main]
<% else %>
on:
push:
branches: [master, main]
<% end -%>
env:
# Environment variables shared across jobs and doesn't change per environment
region: <% index .Params `region` %>
Expand Down
107 changes: 107 additions & 0 deletions templates/.github/workflows/sam.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Pipeline

on:
push:
branches:
- 'main'
- 'master'
- 'feature**'

env:
REGION: <% index .Params `region` %>
CI_USER_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
CI_USER_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
SAM_TEMPLATE: template.yaml
STAGE_STACK_NAME: <% .Name %>-stage
PROD_STACK_NAME: <% .Name %>-prod

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
# trigger the tests here
build-and-deploy-feature-stage:
# this stage is triggered only for feature branches (feature*),
# which will build the stack and deploy to a stack named with branch name.
# if: startsWith(github.ref, 'refs/heads/feature')
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/setup-node@v2
# Modules get packaged into lambda functions with the node_modules folder
# so we must obtain the modules inside CI
- name: Install Lambda Authorizer modules
run: |
cd ./auth &&
npm install --production
- uses: aws-actions/setup-sam@v1
- run: |
sam build \
--template ${SAM_TEMPLATE} \
--use-container
- name: Assume the testing pipeline user role
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ env.CI_USER_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ env.CI_USER_SECRET_ACCESS_KEY }}
aws-region: ${{ env.REGION }}
- name: Deploy to feature stack in the testing account
shell: bash
run: |
sam deploy \
--template ${SAM_TEMPLATE} \
--config-file ./config.toml \
--config-env stage \
--region ${REGION} \
--no-fail-on-empty-changeset
integration-test:
needs: [build-and-deploy-feature-stage]
runs-on: ubuntu-latest
steps:
## Example of smoke test against staging env before deploying to production
## To be enhanced to more sophisicated checks
- run: echo "TEST_RESPONSE_COD=$(curl -o /dev/null -s -w \"%{http_code}\" https://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/status/ready)" >> $GITHUB_ENV
- if: env.TEST_RESPONSE_COD >= 400
run: exit 1

build-and-deploy-feature-prod:
# this stage is triggered only for feature branches (feature*),
# which will build the stack and deploy to a stack named with branch name.
# if: startsWith(github.ref, 'refs/heads/feature')
needs: [integration-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/setup-node@v2
# Modules get packaged into lambda functions with the node_modules folder
# so we must obtain the modules inside CI
- name: Install Lambda Authorizer modules
run: |
cd ./auth &&
npm install --production
- uses: aws-actions/setup-sam@v1
- run: |
sam build \
--template ${SAM_TEMPLATE} \
--use-container
- name: Assume the testing pipeline user role
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ env.CI_USER_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ env.CI_USER_SECRET_ACCESS_KEY }}
aws-region: ${{ env.REGION }}
- name: Deploy to feature stack in Production
shell: bash
run: |
sam deploy \
--template ${SAM_TEMPLATE} \
--config-file ./config.toml \
--config-env prod \
--region ${REGION} \
--no-fail-on-empty-changeset
5 changes: 4 additions & 1 deletion templates/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,7 @@ $RECYCLE.BIN/

# application environment config
.env
node_modules
node_modules

# Serverless build artifacts
.aws-sam
8 changes: 8 additions & 0 deletions templates/Dockerfile.sam
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM public.ecr.aws/lambda/nodejs:14

COPY . .

RUN touch .env
RUN npm i --production

CMD ["src/app.lambdaHandler"]
20 changes: 19 additions & 1 deletion templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@
You now have a repo to start writing your backend logic! The Go api comes with an endpoint returning the status of the app.

# Deployment

<%- if eq (index .Params `backendApplicationHosting`) "serverless" %>
## AWS Serverless Application Model
Your application is deployed to AWS using [SAM](https://aws.amazon.com/serverless/sam/). The main building blocks includes
- API Gateway
- Lambda Authorizer
- Lambda container Application (Your application)

### Configuring
The application is configured by `template.yaml` and declares the API gateway, Authorizer, and Application. The config file is also responsible for per environment and environment variables and invoke Lambda roles and Domain setup with API gateway.

There are also a Parameters file with your repository `.sam-params-stage` and `.sam-params-prod`, they are used for CI to pick up these values, consist of `Environment`, `HostedZoneId` and `GatewayCertificateArn`. This is created during the `init` step and committed to your repo, The cert ARN and hostedZoneID are not sensitive but requires list all permissions in IAM, therefore are created here instead of granting the CI user those permission to fetch on the fly, and they should not change often.

### Deployment
The deployment is configured from `config.toml`, and triggered from Github Actions `.github/workflows/sam.yml`, this declares the staging and production SAM deployment artifacts destination and settings.

<%- else %>
## Kubernetes
Your application is deployed on your EKS cluster through circleCI, you can see the pod status on kubernetes in your application namespace:
```
Expand Down Expand Up @@ -126,7 +143,8 @@ ALTER TABLE address
ADD COLUMN city VARCHAR(30) AFTER street_name,
ADD COLUMN province VARCHAR(30) AFTER city
```
<%if eq (index .Params `billingEnabled`) "yes" %>
<% end %>
<% if eq (index .Params `billingEnabled`) "yes" %>
## Billing example
A subscription and checkout example using [Stripe](https://stripe.com), coupled with the frontend repository to provide an end-to-end checkout example for you to customize. We also setup a webhook and an endpoint in the backend to receive webhook when events occur.

Expand Down
9 changes: 9 additions & 0 deletions templates/auth/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
NODE_ENV=development
ISSUER_BASE_URL=
CLIENT_ID=
CLIENT_SECRET=
COOKIE_SIGNING_SECRET=
COOKIE_DOMAIN="127.0.0.1"
AUTH_ENDPOINT="http://127.0.0.1:9000"
FRONTEND_URL="http://127.0.0.1:3000"
SERVER_PORT=9000
1 change: 1 addition & 0 deletions templates/auth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
124 changes: 124 additions & 0 deletions templates/auth/oidc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
var serverless = require("serverless-http");
var express = require('express');
const { auth } = require('express-openid-connect');


// OIDC settings
const issuerBaseURL = process.env.ISSUER_BASE_URL;
const clientID = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
// Redirect URL settings
const authorizerEndpoint = process.env.AUTH_ENDPOINT;
const frontendURL = process.env.FRONTEND_URL;
const authScope = process.env.AUTH_SCOPE || 'openid profile email';
// Cookie settings
const cookieDomain = process.env.COOKIE_DOMAIN;
const cookieAllowInsecure = process.env.ALLOW_INSECURE_COOKIES === "true";
const cookieSigningSecret = process.env.COOKIE_SIGNING_SECRET;
const jwtCookieKey = process.env.JWT_COOKIE_KEY;

const app = express();

// For development you may need to run this function locally
const lambdaRuntime = process.env.NODE_ENV === 'development' ? false : true;
// If request is from Authorize request must use context.succeed instead of res.json()
// this is for API gateway to parse versus it going to the end user
const isAuthorizerRequest = (req) => lambdaRuntime && (req.apiGateway.event.type === 'REQUEST');

const cookieParamsOverride = cookieAllowInsecure ? { secure: false, sameSite: 'Lax' } : {};

// By Default, this OIDC Authorizer sets an encrypted JWE, you can use this when migration to EKS
// to also set a JWT token on client cookie for JWT authorizer middleware, defaults to undefined
const afterCallback = jwtCookieKey ? (req, res, session) => {
res.cookie(jwtCookieKey, session.id_token, {
domain: cookieDomain,
path: "/",
...cookieParamsOverride,
});
return session
} : undefined;

if (!lambdaRuntime) {
const cors = require("cors");
app.use(cors({
origin: frontendURL,
credentials: true,
}));
}

app.use((req, res, next) => {
auth({
issuerBaseURL: issuerBaseURL,
baseURL: authorizerEndpoint,
routes: {
postLogoutRedirect: frontendURL,
},
afterCallback,
session: {
cookie: {
domain: cookieDomain,
path: "/",
...cookieParamsOverride,
},
},
getLoginState(req, options) {
if (req.originalUrl === "/login") {
return {
returnTo: frontendURL || options.returnTo || req.originalUrl,
};
} else if (!req.oidc.isAuthenticated()) {
const response = {
isAuthorized: false,
context: {
loginUrl: `${authorizerEndpoint}/login`,
},
};

if (isAuthorizerRequest(req)) {
res.simpleResponse = response;
}
res.status(401).json(response);
return {};
}
},
clientID,
clientSecret,
secret: cookieSigningSecret,
idpLogout: true,
authorizationParams: {
response_type: 'code',
scope: authScope,
}
})(req, res, next);
});

app.use('/*', (req, res) => {
const response = {
isAuthorized: true,
context: { ...req.oidc.user }
};
if (isAuthorizerRequest(req)) {
res.simpleResponse = response;
}
res.json(response);
});

app.use((err, req, res) => {
console.error(req.originalUrl, err);
});

// In lambda runtime it exports the lambdaHandler instead of http server listening
if (!lambdaRuntime) {
const port = process.env.SERVER_PORT || 80;
app.listen(port, () => {
console.log(`Authorizer listening at http://localhost:${port}`);
});
}

module.exports.lambdaHandler = serverless(app, {
response(response, event, context) {
if (response.simpleResponse) {
return context.succeed({ ...response.simpleResponse })
}
}
});
Loading

0 comments on commit 3697c39

Please sign in to comment.