├── .gitignore ├── LICENSE ├── README.md ├── lerna.json ├── libs └── index.js ├── package.json ├── packages └── sample-package │ ├── index.js │ └── package.json ├── seed.yml ├── services ├── service1 │ ├── handler.js │ ├── package.json │ ├── serverless.yml │ └── tests │ │ └── handler.test.js └── service2 │ ├── handler.js │ ├── package.json │ └── serverless.yml └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | .webpack 8 | 9 | .*.sw* 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anomaly Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Lerna + Yarn Workspaces Starter [![Seed Status](https://api.seed.run/serverless-stack/serverless-lerna-yarn-starter/stages/dev/build_badge)](https://console.seed.run/serverless-stack/serverless-lerna-yarn-starter) 2 | 3 | A Serverless monorepo starter that uses [Lerna](https://lerna.js.org) and [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). 4 | 5 | - Designed to scale for larger projects 6 | - Maintains internal dependencies as packages 7 | - Uses Lerna to figure out which services have been updated 8 | - Supports publishing dependencies as private NPM packages 9 | - Uses [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle) to generate optimized Lambda packages 10 | - Uses Yarn Workspaces to hoist packages to the root `node_modules/` directory 11 | 12 | ----- 13 | 14 | ## Installation 15 | 16 | To create a new Serverless project 17 | 18 | ``` bash 19 | $ git clone https://github.com/AnomalyInnovations/serverless-lerna-yarn-starter my-project 20 | ``` 21 | 22 | Enter the new directory 23 | 24 | ``` bash 25 | $ cd my-project 26 | ``` 27 | 28 | Install NPM packages for the entire project 29 | 30 | ``` bash 31 | $ yarn 32 | ``` 33 | 34 | ## How It Works 35 | 36 | The directory structure roughly looks like: 37 | 38 | ``` 39 | package.json 40 | /libs 41 | /packages 42 | /sample-package 43 | index.js 44 | package.json 45 | /services 46 | /service1 47 | handler.js 48 | package.json 49 | serverless.yml 50 | /service2 51 | handler.js 52 | package.json 53 | serverless.yml 54 | ``` 55 | 56 | This repo is split into 3 directories. Each with a different purpose: 57 | 58 | - packages 59 | 60 | These are internal packages that are used in our services. Each contains a `package.json` and can be optionally published to NPM. Any changes to a package should only deploy the service that depends on it. 61 | 62 | - services 63 | 64 | These are Serverless services that are deployed. Has a `package.json` and `serverless.yml`. There are two sample services. 65 | 66 | 1. `service1`: Depends on the `sample-package`. This means that if it changes, we want to deploy `service1`. 67 | 2. `service2`: Does not depend on any internal packages. 68 | 69 | More on deployments below. 70 | 71 | - libs 72 | 73 | Any common code that you might not want to maintain as a package. Does NOT have a `package.json`. Any changes here should redeploy all our services. 74 | 75 | The `packages/` and `services/` directories are Yarn Workspaces. 76 | 77 | ### Services 78 | 79 | The Serverless services are meant to be managed on their own. Each service is based on our [Serverless Node.js Starter](https://github.com/AnomalyInnovations/serverless-nodejs-starter). It uses the [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle) plugin (based on [Webpack](https://webpack.js.org)) to create optimized Lambda packages. 80 | 81 | This is good for keeping your Lambda packages small. But it also ensures that you can have Yarn hoist all your NPM packages to the project root. Without Webpack, you'll need to disable hoisting since Serverless Framework does not package the dependencies of a service correctly on its own. 82 | 83 | Install an NPM package inside a service. 84 | 85 | ``` bash 86 | $ yarn add some-npm-package 87 | ``` 88 | 89 | Run a function locally. 90 | 91 | ``` bash 92 | $ serverless invoke local -f get 93 | ``` 94 | 95 | Run tests in a service. 96 | 97 | ``` bash 98 | $ yarn test 99 | ``` 100 | 101 | Deploy the service. 102 | 103 | ``` bash 104 | $ serverless deploy 105 | ``` 106 | 107 | Deploy a single function. 108 | 109 | ``` bash 110 | $ serverless deploy function -f get 111 | ``` 112 | 113 | To add a new service. 114 | 115 | ``` bash 116 | $ cd services/ 117 | $ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name new-service 118 | $ cd new-service 119 | $ yarn 120 | ``` 121 | 122 | ### Packages 123 | 124 | Since each package has its own `package.json`, you can manage it just like you would any other NPM package. 125 | 126 | To add a new package: 127 | 128 | ``` bash 129 | $ mkdir packages/new-package 130 | $ yarn init 131 | ``` 132 | 133 | Packages can also be optionally published to NPM. 134 | 135 | To use a package: 136 | 137 | ```bash 138 | $ yarn add new-package@1.0.0 139 | ``` 140 | 141 | Note that packages should be added by specifying the version number declared in their `package.json`. Otherwise, yarn tries to find the dependency in the registry. 142 | 143 | ### Libs 144 | 145 | If you need to add any other common code in your repo that won't be maintained as a package, add it to the `libs/` directory. It does not contain a `package.json`. This means that you'll need to install any NPM packages as dependencies in the root. 146 | 147 | To install an NPM package at the root. 148 | 149 | ``` bash 150 | $ yarn add -W some-npm-package 151 | ``` 152 | 153 | ## Deployment 154 | 155 | We want to ensure that only the services that have been updated get deployed. This means that, if a change is made to: 156 | 157 | - services 158 | 159 | Only the service that has been changed should be deployed. For ex, if you change any code in `service1`, then `service2` should not be deployed. 160 | 161 | - packages 162 | 163 | If a package is changed, then only the service that depends on this package should be deployed. For ex, if `sample-package` is changed, then `service1` should be deployed. 164 | 165 | - libs 166 | 167 | If any of the libs are changed, then all services will get deployed. 168 | 169 | ### Deployment Algorithm 170 | 171 | To implement the above, use the following algorithm in your CI: 172 | 173 | 1. Run `lerna ls --since ${prevCommitSHA} -all` to list all packages that have changed since the last successful deployment. If this list includes one of the services, then deploy it. 174 | 2. Run `git diff --name-only ${prevCommitSHA} ${currentCommitSHA}` to get a list of all the updated files. If they don't belong to any of your Lerna packages (`lerna ls -all`), deploy all the services. 175 | 3. Otherwise skip the deployment. 176 | 177 | 178 | ## Deploying Through Seed 179 | 180 | [Seed](https://seed.run) supports deploying Serverless monorepo projects that use Lerna and Yarn Workspaces. To enable it, add the following to the `seed.yml` in your repo root: 181 | 182 | ``` yaml 183 | check_code_change: lerna 184 | ``` 185 | 186 | To test this: 187 | 188 | **Add the App** 189 | 190 | 1. Fork this repo and add it to [your Seed account](https://console.seed.run). 191 | 2. Add both of the services. 192 | 3. Deploy your app once. 193 | 194 | **Update a Service** 195 | 196 | - Make a change in `services/service2/handler.js` and git push. 197 | - Notice that `service2` has been deployed while `service1` was skipped. 198 | 199 | **Update a Package** 200 | 201 | - Make a change in `packages/sample-package/index.js` and git push. 202 | - Notice that `service1` should be deployed while `service2` will have been skipped. 203 | 204 | **Update a Lib** 205 | 206 | - Finally, make a change in `libs/index.js` and git push. 207 | - Both `service1` and `service2` should've been deployed. 208 | 209 | ------- 210 | 211 | This repo is maintained by [Serverless Stack](https://serverless-stack.com/). 212 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /libs/index.js: -------------------------------------------------------------------------------- 1 | export default function lib() { 2 | return "library"; 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-lerna-yarn-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*", 9 | "services/*" 10 | ], 11 | "devDependencies": { 12 | "lerna": "^3.22.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/sample-package/index.js: -------------------------------------------------------------------------------- 1 | export default function sample() { 2 | return "sample package"; 3 | } 4 | -------------------------------------------------------------------------------- /packages/sample-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /seed.yml: -------------------------------------------------------------------------------- 1 | check_code_change: lerna 2 | -------------------------------------------------------------------------------- /services/service1/handler.js: -------------------------------------------------------------------------------- 1 | import sample from "sample"; 2 | import lib from "../../libs"; 3 | 4 | export async function main(event, context) { 5 | return { 6 | statusCode: 200, 7 | body: `Hello World! Via ${sample()} and ${lib()}.`, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /services/service1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service1", 3 | "version": "0.0.1", 4 | "main": "handler.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "test": "serverless-bundle test" 8 | }, 9 | "dependencies": { 10 | "sample": "^0.0.1" 11 | }, 12 | "devDependencies": { 13 | "serverless-bundle": "^1.9.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/service1/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-lerna-yarn-service1 2 | 3 | plugins: 4 | - serverless-bundle 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs12.x 9 | region: us-east-1 10 | stage: dev 11 | 12 | functions: 13 | get: 14 | handler: handler.main 15 | events: 16 | - http: GET / 17 | 18 | -------------------------------------------------------------------------------- /services/service1/tests/handler.test.js: -------------------------------------------------------------------------------- 1 | import * as handler from '../handler'; 2 | 3 | test('main', async () => { 4 | const response = await handler.main('event', 'context'); 5 | 6 | expect(response.statusCode).toEqual(200); 7 | expect(typeof response.body).toBe("string"); 8 | }); 9 | -------------------------------------------------------------------------------- /services/service2/handler.js: -------------------------------------------------------------------------------- 1 | export async function main(event, context) { 2 | return { 3 | statusCode: 200, 4 | body: `Hello World! Via service2.`, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /services/service2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service2", 3 | "version": "0.0.1", 4 | "main": "handler.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "test": "serverless-bundle test" 8 | }, 9 | "devDependencies": { 10 | "serverless-bundle": "^5.2.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/service2/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-lerna-yarn-service2 2 | 3 | plugins: 4 | - serverless-bundle 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs12.x 9 | region: us-east-1 10 | stage: dev 11 | 12 | functions: 13 | get: 14 | handler: handler.main 15 | events: 16 | - http: GET / 17 | --------------------------------------------------------------------------------