Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serverless initial commit #44

Merged
merged 7 commits into from
Jan 31, 2022
Merged
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
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