diff --git a/.bundle/config b/.bundle/config index 2369228816..9bc01b4c32 100644 --- a/.bundle/config +++ b/.bundle/config @@ -1,2 +1,3 @@ --- BUNDLE_PATH: "vendor/bundle" +BUNDLE_DISABLE_SHARED_GEMS: "true" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 5dcf83668b..f0cfb5b45e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,5 @@ # These are supported funding model platforms # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: jayair patreon: serverless_stack diff --git a/.github/workflows/ebook.yml b/.github/workflows/ebook.yml new file mode 100644 index 0000000000..1d081ec3e7 --- /dev/null +++ b/.github/workflows/ebook.yml @@ -0,0 +1,89 @@ +name: Ebook + +on: + push: + branches: [ master ] + tags: [ v* ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0.7' + - name: Get git tag + id: get_ref + run: echo ::set-output name=REF::${GITHUB_REF/refs\/tags\//} + + - name: Assign book version + id: get_book_version + run: echo ::set-output name=BOOK_VERSION::${{ steps.get_ref.outputs.REF }} + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - uses: actions/checkout@v2 + - run: sudo apt-get update + - run: sudo apt-get install -y wget texlive-xetex texlive-fonts-extra + - run: sudo wget https://github.com/jgm/pandoc/releases/download/2.10.1/pandoc-2.10.1-linux-amd64.tar.gz + - run: sudo tar xvzf pandoc-2.10.1-linux-amd64.tar.gz --strip-components 1 -C /usr + - run: sudo mkdir -p ~/.fonts + - run: sudo cp etc/ebook/fonts/SourceSansPro/*.ttf ~/.fonts + - run: sudo cp etc/ebook/fonts/RobotoSlab/*.ttf ~/.fonts + - run: sudo cp etc/ebook/fonts/SourceCodePro/*.ttf ~/.fonts + - run: cd etc/ebook && make prepare + + # Merge PDF + - run: cd etc/ebook && make merge-pdf + env: + BOOK_VERSION: ${{ steps.get_book_version.outputs.BOOK_VERSION }} + - name: Archive pdf md + uses: actions/upload-artifact@v4 + with: + name: pdf.md + path: etc/ebook/output/pdf.md + + # Merge EPUB + - run: cd etc/ebook && make merge-epub + env: + BOOK_VERSION: ${{ steps.get_book_version.outputs.BOOK_VERSION }} + - name: Archive epub md + uses: actions/upload-artifact@v4 + with: + name: epub.md + path: etc/ebook/output/epub.md + + # Generate PDF + - run: cd etc/ebook && make build-pdf + env: + BOOK_VERSION: ${{ steps.get_book_version.outputs.BOOK_VERSION }} + - name: Archive pdf + uses: actions/upload-artifact@v4 + with: + name: ebook.pdf + path: etc/ebook/output/ebook.pdf + + # Generate EPUB + - run: cd etc/ebook && make build-epub + env: + BOOK_VERSION: ${{ steps.get_book_version.outputs.BOOK_VERSION }} + - name: Archive epub + uses: actions/upload-artifact@v4 + with: + name: ebook.epub + path: etc/ebook/output/ebook.epub + + # Upload PDF + - name: Upload book to S3 + run: sudo apt-get install -y awscli && aws s3 cp etc/ebook/output/ebook.pdf s3://anomaly/SST/guide/ebook-${{ steps.get_book_version.outputs.BOOK_VERSION }}.pdf --acl public-read --content-disposition 'attachment; filename="sst-guide-${{ steps.get_book_version.outputs.BOOK_VERSION }}.pdf"' + if: ${{ startsWith(steps.get_ref.outputs.REF, 'v' ) }} + + # Upload EPUB + - name: Upload book to S3 + run: sudo apt-get install -y awscli && aws s3 cp etc/ebook/output/ebook.epub s3://anomaly/SST/guide/ebook-${{ steps.get_book_version.outputs.BOOK_VERSION }}.epub --acl public-read --content-disposition 'attachment; filename="sst-guide-${{ steps.get_book_version.outputs.BOOK_VERSION }}.epub"' + if: ${{ startsWith(steps.get_ref.outputs.REF, 'v' ) }} diff --git a/.gitignore b/.gitignore index 10b5574a3e..bef1f633c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ Session.vim +.sst/ +node_modules/ _site .sw* .*.sw* .sass-cache .jekyll-metadata +.jekyll-cache/ .DS_Store -vendor/ \ No newline at end of file +vendor/ +.idea/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1890a7ae93..b772a8ed4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ # Contributing -Thank you for considering to contribute. Here is what Serverless Stack is trying to accomplish and how you can help. We use our [Gitter chat room][Gitter] for our contributors, feel free to join us there. +Thank you for considering to contribute. Here is what SST Guide is trying to accomplish and how you can help. We use [Discord][discord] for our contributors, feel free to join us there. ## Project Goals We are trying to create a really comprehensive and up to date guide to help people build production ready full-stack serverless applications. To start, we are focussing on building a CRUD API backend with Serverless Framework on AWS and a single-page app web frontend using React. -We want Serverless Stack to cover a very broad collection of technologies, services, and concepts that gives people the confidence to use this guide to build their next project. Too often we come across tutorials or blog posts that sound very promising but leave us hanging once we get to the complicated bits. +We want SST Guide to cover a very broad collection of technologies, services, and concepts that gives people the confidence to use this guide to build their next project. Too often we come across tutorials or blog posts that sound very promising but leave us hanging once we get to the complicated bits. However while we want to be as comprehensive as possible, we also want to make sure that you can get started easily. To achieve this we are using the following structure and layout. @@ -14,18 +14,22 @@ However while we want to be as comprehensive as possible, we also want to make s The guide is split up in the following way: -The Core: -- Part I: The basic aspects of creating a full-stack serverless app -- Part II: Some advanced concepts when you are getting your app ready for production +The Basics: + +- The basic aspects of creating a full-stack serverless app. And deploying your app to production. + +Best Practices: + +- Everything you need to run your app in production. The Extensions -- Extra Credits: Standalone chapters/articles intended to supplement the first two parts and to extend some functionality of the demo app + +- Extra Credits: Standalone chapters/articles intended to supplement the first two parts and to extend some functionality of the demo app. Additionally, the demo app that people build as a part of the tutorial is split into the backend (a Serverless Framework project) and the frontend (a React app). Both these are in their separate Git repos. Key chapters in the Core part of the guide are accompanied with branches in their respective demo app repos. This is used to track progress and as a reference as you work through the tutorial. - ## How to Help There are quite a few ways to help. @@ -39,10 +43,9 @@ There are quite a few ways to help. Additionally, you are welcome to provide general feedback and suggestions via our forums. - ### Keep the core guide updated -Serverless Stack is reliant on a large number of services and open source libraries and projects. Here is what needs updating: +The SST Guide is reliant on a large number of services and open source libraries and projects. Here is what needs updating: ### Updating Screenshots @@ -69,12 +72,11 @@ For the steps in the tutorial: Once all the PRs are merged, we'll tag the repo (tutorial & demo app) with the new version number and update the Changelog chapter. - ### Add an Extra Credit Chapter -The core chapters are missing some extra details (for the sake of simplicity) that are necessary once you start customizing the Serverless Stack setup. Additionally, there are cases that we just don't handle in the core chapters. [Here is a rough list of topics that have been requested](https://github.com/AnomalyInnovations/serverless-stack-com/projects/1#column-2785572). This is not an exhaustive list. If you have some ideas to extend some of the demo app functionality, feel free to get in touch with us. Here is how to go about adding a new extra credit chapter: +The core chapters are missing some extra details (for the sake of simplicity) that are necessary once you start customizing SST setup. Additionally, there are cases that we just don't handle in the core chapters. [Here is a rough list of topics that have been requested](https://github.com/AnomalyInnovations/serverless-stack-com/projects/1#column-2785572). This is not an exhaustive list. If you have some ideas to extend some of the demo app functionality, feel free to get in touch with us. Here is how to go about adding a new extra credit chapter: -- Let us know via [Gitter][Gitter] that you are planning to work on it +- Let us know via [Discord][discord] that you are planning to work on it - Create a new issue in GitHub to track progress - Fork the tutorial repo - Copy the `_drafts/template.md` as a starting point for the chapter @@ -93,23 +95,18 @@ For any changes to the demo app: Finally, submit a PR to the tutorial repo with the new changes. We'll review it, maybe suggest some edits or give you some feedback. Once everything looks okay we'll merge with master and publish it. We'll also create comments threads for your chapter in the forums and link to it. - ### Improve Tooling Currently we do a lot of manual work to publish updates and maintain the tutorial. You can help by contributing to improve the process. Feel free to get in touch if you're interested in helping out. Here is roughly what we need help with: -- Generating the Ebook - - The PDF version of Serverless Stack is very popular. Unfortunately it is generated manually using a set of AppleScripts stored in the `etc/` directory. It opens up Safari and prints to PDF. It would be much better if we could use a headless Chrome script to generate this. In addition to the PDF we need to figure out how to generate the EPUB format. - -- Creating a pipeline - - We would like to create a Circle CI setup that automatically generates the PDF and uploads the latest version to S3 (where it is hosted) every time a new release is created to the tutorial. We would also like to run a simple suite of tests to ensure that the changes to the demo app repos are correct. - - Compress screenshots The images for the screenshots are quite large. It would be ideal if they can be compressed as a part of the build process. +- ~~Generating the Ebook~~ + +- ~~Creating a pipeline~~ + ### Translating to Other Languages We currently have translation efforts for Spanish and Portuguese underway. If you'd like to get involved [refer to this thread](https://github.com/AnomalyInnovations/serverless-stack-com/issues/271). @@ -118,12 +115,12 @@ To translate a chapter follow these steps: 1. Add the following to the frontmatter of the chapter you intend to translate. - ``` yml + ```yml ref: uri-of-the-chapter lang: en ``` - - Here `uri-of-the-chapter` is the part of the url that represents the name of the chapter. For example, the [What is Serverless](https://serverless-stack.com/chapters/what-is-serverless.html) has a URI `what-is-serverless`. + + Here `uri-of-the-chapter` is the part of the url that represents the name of the chapter. For example, the [What is Serverless](https://sst.dev/chapters/what-is-serverless.html) has a URI `what-is-serverless`. 2. Copy the file to `_chapters/[language-code]/[filename].md` @@ -131,21 +128,19 @@ To translate a chapter follow these steps: 3. Change the frontmatter to. - ``` yml + ```yml lang: language-code ``` - + Again the `language-code` is either `pt` or `es`. - Note that the only thing linking the translation with the original is the `ref:` attribute in the frontmatter. Make sure that it is the same for both the files. -As an example, compare the [What is Serverless](https://serverless-stack.com/chapters/what-is-serverless.html) chapter: +As an example, compare the [What is Serverless](https://sst.dev/chapters/what-is-serverless.html) chapter: - English version: https://github.com/AnomalyInnovations/serverless-stack-com/blob/master/_chapters/what-is-serverless.md - Spanish version: https://github.com/AnomalyInnovations/serverless-stack-com/blob/master/_chapters/es/what-is-serverless.md Feel free to [contact us](mailto:contact@anoma.ly) if you have any questions. - -[Gitter]: https://gitter.im/serverless-stack/Lobby +[discord]: https://sst.dev/discord diff --git a/Gemfile b/Gemfile index 7bdeaba10d..ce74304526 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ ruby RUBY_VERSION # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! -gem "jekyll", "3.8.1" +gem "jekyll", "4.3.2" # This is the default theme for new Jekyll sites. You may change this to anything you like. gem "minima", "~> 2.0" @@ -21,7 +21,8 @@ gem "minima", "~> 2.0" # If you have any plugins, put them here! group :jekyll_plugins do - gem "jekyll-seo-tag" gem "jekyll-sitemap" gem "jekyll-redirect-from" end + +gem "webrick", "~> 1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 9a3cb10438..7f561fa3a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,77 +1,94 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) colorator (1.1.0) - concurrent-ruby (1.0.5) - em-websocket (0.5.1) + concurrent-ruby (1.2.2) + em-websocket (0.5.3) eventmachine (>= 0.12.9) - http_parser.rb (~> 0.6.0) + http_parser.rb (~> 0) eventmachine (1.2.7) - ffi (1.9.23) + ffi (1.16.3) forwardable-extended (2.6.0) - http_parser.rb (0.6.0) - i18n (0.9.5) + google-protobuf (3.24.4-arm64-darwin) + google-protobuf (3.24.4-x86_64-darwin) + http_parser.rb (0.8.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) - jekyll (3.8.1) + jekyll (4.3.2) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (~> 0.7) - jekyll-sass-converter (~> 1.0) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) jekyll-watch (~> 2.0) - kramdown (~> 1.14) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (~> 0.3.3) + mercenary (>= 0.3.6, < 0.5) pathutil (~> 0.9) - rouge (>= 1.7, < 4) + rouge (>= 3.0, < 5.0) safe_yaml (~> 1.0) - jekyll-redirect-from (0.11.0) - jekyll (>= 2.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) - jekyll-seo-tag (2.1.0) - jekyll (~> 3.3) - jekyll-sitemap (0.12.0) - jekyll (~> 3.3) - jekyll-watch (2.0.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-feed (0.16.0) + jekyll (>= 3.7, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-watch (2.2.1) listen (~> 3.0) - kramdown (1.16.2) - liquid (4.0.0) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - mercenary (0.3.6) - minima (2.0.0) - pathutil (0.16.1) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (3.0.2) - rb-fsevent (0.10.3) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) - rouge (3.1.1) - ruby_dep (1.5.0) - safe_yaml (1.0.4) - sass (3.5.6) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) + public_suffix (5.0.3) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.6) + rouge (4.1.3) + safe_yaml (1.0.5) + sass-embedded (1.69.4-arm64-darwin) + google-protobuf (~> 3.23) + sass-embedded (1.69.4-x86_64-darwin) + google-protobuf (~> 3.23) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.5.0) + webrick (1.7.0) PLATFORMS - ruby + arm64-darwin-21 + arm64-darwin-23 + x86_64-darwin-22 DEPENDENCIES - jekyll (= 3.8.1) + jekyll (= 4.3.2) jekyll-redirect-from - jekyll-seo-tag jekyll-sitemap minima (~> 2.0) + webrick (~> 1.7) RUBY VERSION - ruby 2.4.0p0 + ruby 3.0.7p220 BUNDLED WITH - 1.15.4 + 2.3.20 diff --git a/README.md b/README.md index 38ddc6d88a..43934f9a9b 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,71 @@

- - Serverless Stack + + SST

-

- Learn to Build Full-Stack Apps with Serverless and React -

- Discourse posts - Twitter follow - Chat on Gitter + Discord + Twitter + Seed Status

------------------------------------------------------------------------------------- +--- + +## SST Guide + +This repo for [**guide.sst.dev**](https://guide.sst.dev), the SST Guide. If you are looking for the SST repo, [head over here](https://github.com/sst/sst). -[Serverless Stack](https://serverless-stack.com) is an open source guide for building and deploying full-stack apps using Serverless and React on AWS. +SST.dev is built with [SST](https://sst.dev) and deployed with [Seed](https://seed.run). -We are going to create a [note taking app](https://demo2.serverless-stack.com) from scratch using React.js, AWS Lambda, API Gateway, DynamoDB, and Cognito. +## About the Guide + +The guide is a comprehensive open source tutorial for building and deploying full-stack apps using Serverless and React on AWS. + +We are going to create a [note taking app](https://demo.sst.dev) from scratch using React.js, AWS Lambda, API Gateway, DynamoDB, and Cognito. ![Demo App](assets/completed-app-desktop.png) It is a single-page React app powered by a serverless CRUD API. We also cover how add user authentication and handle file uploads. -The entire guide is hosted on GitHub and we use [Discourse][Discourse] for our comments. With the help of the community we add more detail to the guide and keep it up to date. - -## Project Goals +### Project Goals - Provide a free comprehensive resource - Add more content to build on core concepts - Keep the content accurate and up to date - Help people resolve their issues -## Getting Help +### Getting Help -- If you are running into issues with a specific chapter, post in the comments for that [chapter][Discourse]. +- If you are running into issues with a specific chapter, join us on [Discord][discord]. - Open a [new issue](../../issues/new) if you've found a bug -- Or if you have a suggestion create a [new topic][Discourse] in our forums -- If you've found a typo, edit the chapter and submit a [pull request][PR]. +- If you've found a typo, edit the chapter and submit a [pull request][pr]. -## Source for the Demo App +#### Source for the Demo App -- [Backend Serverless API](https://github.com/AnomalyInnovations/serverless-stack-demo-api) -- [Frontend React app](https://github.com/AnomalyInnovations/serverless-stack-demo-client) +- [Demo Notes App](https://github.com/sst/demo-notes-app) ## Contributing -Thank you for your considering to contribute. [Read more about how you can contribute to Serverless Stack][Contributing]. +[Read more about how you can contribute to the guide][contributing]. -## Running Locally +### Running Locally -Serverless Stack is built using [Jekyll](https://jekyllrb.com). [Follow these steps to install Jekyll](https://jekyllrb.com/docs/installation/). +SST.dev is a [Jekyll](https://jekyllrb.com) site. [Follow these steps to install Jekyll](https://jekyllrb.com/docs/installation/). -#### Viewing Locally +### Viewing Locally To install, run the following in the root of the project. -``` bash +```bash +$ pnpm install $ bundle install ``` And to view locally. -``` bash +```bash $ bundle exec jekyll serve ``` @@ -71,36 +73,42 @@ You can now view the guide locally by visiting `http://localhost:4000/`. You can also turn on live reloading and incremental builds while editing. -``` bash +```bash $ bundle exec jekyll serve --incremental --livereload ``` -#### Generating the PDF +#### Generating the eBook -You can generate the PDF locally on macOS by following these steps. +We use [Pandoc](https://pandoc.org) to create the eBook. You can generate it locally by following these steps. -1. Generate a `Cover.pdf` with latest version and date - 1. Create an `ebook` folder in `~/Downloads` (for example). - 2. Update the date and version in the `etc/cover.html` - 3. Open the cover page locally in Safari by going to `file:///Users/frank/Sites/ServerlessStackCom/etc/cover.html`. - 4. Hit the **Export to PDF…** button. - 5. Place `Cover.pdf` in the `~/Downloads/ebook` folder. -2. Ensure `ebook` folder is an option when hitting the **Export to PDF…** button in Safari. -3. In the terminal, run `osascript pdf.scpt` in the `etc/` directory of this repository. +```bash +$ cd ~/Sites/sst.dev/etc/ebook +$ make start +``` + +This'll start a Docker instance. Inside the Docker run: + +```bash +$ make pdf +$ make epub +``` -We are looking for a better way to generate the PDF (and other eBook) formats. If you've got any ideas [consider contributing][Contributing]. +The above are run automatically through [Github Actions](https://github.com/sst/sst.dev/actions) in this repo: -## Sponsors +- When a new commit is pushed to master +- And when a new tag is pushed, the generated eBook is uploaded to S3 -[**Sponsor Serverless Stack on Patreon**](https://www.patreon.com/serverless_stack) if you've found this guide useful or would like to be an official supporter. [A big thanks to our supporters](https://serverless-stack.com/sponsors.html#sponsors)! +### Deploying Locally -## Maintainers +To deploy this site. Run: -Serverless Stack is maintained by [Anomaly Innovations](https://anoma.ly/). [**Subscribe to our newsletter**](https://emailoctopus.com/lists/1c11b9a8-1500-11e8-a3c9-06b79b628af2/forms/subscribe) for updates on Serverless Stack. +```bash +$ npx sst deploy +``` ## Contributors -Thanks to these folks for their contributions to the content of Serverless Stack. +Thanks to these folks for their contributions to the content of SST. - [Peter Eman Paver Abastillas](https://github.com/jatazoulja): Social login chapters - [Bernardo Bugmann](https://github.com/bernardobugmann): Translating chapters to Portuguese @@ -110,8 +118,15 @@ Thanks to these folks for their contributions to the content of Serverless Stack - [Vieko Franetovic](https://github.com/vieko): Translating chapters to Spanish - [Christian Kaindl](https://github.com/christiankaindl): Translating chapters to German - [Jae Chul Kim](https://github.com/bsg-bob): Translating chapters to Korean +- [Ben Force](https://x.com/theBenForce): Extra credit chapters +- [Eze Sunday](https://x.com/ezesundayeze): Extra credit chapters +- [Maniteja Pratha](https://x.com/PrataManitej): Vue.js example + +--- +**Join our community** [Discord][discord] | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev) | [Contribute][contributing] -[Discourse]: https://discourse.serverless-stack.com -[Contributing]: CONTRIBUTING.md -[PR]: ../../compare +[discourse]: https://discourse.sst.dev +[discord]: https://sst.dev/discord +[contributing]: CONTRIBUTING.md +[pr]: ../../compare diff --git a/_chapters/add-a-billing-api.md b/_archives/add-a-billing-api.md similarity index 67% rename from _chapters/add-a-billing-api.md rename to _archives/add-a-billing-api.md index d5292d0fbe..6d61fbfd2a 100644 --- a/_chapters/add-a-billing-api.md +++ b/_archives/add-a-billing-api.md @@ -4,29 +4,29 @@ title: Add a Billing API date: 2018-03-07 00:00:00 lang: en description: We are going to create a Lambda function for our serverless billing API. It will take the Stripe token that is passed in from our app and use the Stripe JS SDK to process the payment. -context: true ref: add-a-billing-api comments_id: add-a-billing-api/170 +redirect_from: /chapters/add-a-billing-api.html --- Now let's get started with creating our billing API. It is going to take a Stripe token and the number of notes the user wants to store. ### Add a Billing Lambda -Start by installing the Stripe NPM package. Run the following in the root of our project. +{%change%} Start by installing the Stripe NPM package. Run the following in the root of our project. ``` bash $ npm install --save stripe ``` -Next, add the following to `billing.js`. +{%change%} Create a new file called `billing.js` with the following. ``` js import stripePackage from "stripe"; +import handler from "./libs/handler-lib"; import { calculateCost } from "./libs/billing-lib"; -import { success, failure } from "./libs/response-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const { storage, source } = JSON.parse(event.body); const amount = calculateCost(storage); const description = "Scratch charge"; @@ -34,18 +34,15 @@ export async function main(event, context) { // Load our secret key from the environment variables const stripe = stripePackage(process.env.stripeSecretKey); - try { - await stripe.charges.create({ - source, - amount, - description, - currency: "usd" - }); - return success({ status: true }); - } catch (e) { - return failure({ message: e.message }); - } -} + await stripe.charges.create({ + source, + amount, + description, + currency: "usd", + }); + + return { status: true }; +}); ``` Most of this is fairly straightforward but let's go over it quickly: @@ -58,11 +55,13 @@ Most of this is fairly straightforward but let's go over it quickly: - Finally, we use the `stripe.charges.create` method to charge the user and respond to the request if everything went through successfully. +Note, if you are testing this from India, you'll need to add some shipping information as well. Check out the [details from our forums](https://discourse.sst.dev/t/test-the-billing-api/172/20). + ### Add the Business Logic Now let's implement our `calculateCost` method. This is primarily our *business logic*. -Create a `libs/billing-lib.js` and add the following. +{%change%} Create a `libs/billing-lib.js` and add the following. ``` js export function calculateCost(storage) { @@ -76,34 +75,28 @@ export function calculateCost(storage) { } ``` -This is basically saying that if a user wants to store 10 or fewer notes, we'll charge them $4 per note. For 100 or fewer, we'll charge $2 and anything more than a 100 is $1 per note. Clearly, our serverless infrastructure might be cheap but our service isn't! +This is basically saying that if a user wants to store 10 or fewer notes, we'll charge them $4 per note. For 11 to 100 notes, we'll charge $2 and any more than 100 is $1 per note. Since Stripe expects us to provide the amount in pennies (the currency’s smallest unit) we multiply the result by 100. Clearly, our serverless infrastructure might be cheap but our service isn't! ### Configure the API Endpoint Let's add a reference to our new API and Lambda function. -Add the following above the `resources:` block in the `serverless.yml`. +{%change%} Open the `serverless.yml` file and append the following to it. ``` yml billing: + # Defines an HTTP API endpoint that calls the main function in billing.js + # - path: url path is /billing + # - method: POST request handler: billing.main events: - http: path: billing - method: post cors: true + method: post authorizer: aws_iam ``` Make sure this is **indented correctly**. This block falls under the `functions` block. -### Commit Our Changes - -Let's quickly commit these to Git. - -``` bash -$ git add . -$ git commit -m "Adding a billing API" -``` - Now before we can test our API we need to load our Stripe secret key in our environment. diff --git a/_archives/add-a-create-note-api.md b/_archives/add-a-create-note-api.md new file mode 100644 index 0000000000..4e88e67402 --- /dev/null +++ b/_archives/add-a-create-note-api.md @@ -0,0 +1,314 @@ +--- +layout: post +title: Add a Create Note API +date: 2016-12-30 00:00:00 +lang: en +ref: add-a-create-note-api +description: To allow users to create notes in our note taking app, we are going to add a create note POST API. To do this we are going to add a new Lambda function to our Serverless Framework project. The Lambda function will save the note to our DynamoDB table and return the newly created note. +comments_id: add-a-create-note-api/125 +redirect_from: /chapters/add-a-create-note-api.html +--- + +Let's get started on our backend by first adding an API to create a note. This API will take the note object as the input and store it in the database with a new id. The note object will contain the `content` field (the content of the note) and an `attachment` field (the URL to the uploaded file). + +### Add the Function + +Let's add our first function. + +{%change%} Create a new file called `create.js` in our project root with the following. + +```js +import * as uuid from "uuid"; +import AWS from "aws-sdk"; + +const dynamoDb = new AWS.DynamoDB.DocumentClient(); + +export async function main(event, context) { + // Request body is passed in as a JSON encoded string in 'event.body' + const data = JSON.parse(event.body); + + const params = { + TableName: process.env.tableName, + Item: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + attachment: data.attachment, // Parsed from request body + createdAt: Date.now(), // Current Unix timestamp + }, + }; + + // Set response headers to enable CORS (Cross-Origin Resource Sharing) + const headers = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }; + + try { + await dynamoDb.put(params).promise(); + + return { + statusCode: 200, + headers: headers, + body: JSON.stringify(params.Item), + }; + } catch (e) { + return { + statusCode: 500, + headers: headers, + body: JSON.stringify({ error: e.message }), + }; + } +} +``` + +There are some helpful comments in the code but we are doing a few simple things here. + +- The AWS JS SDK assumes the region based on the current region of the Lambda function. So if your DynamoDB table is in a different region, make sure to set it by calling `AWS.config.update({ region: "my-region" });` before initializing the DynamoDB client. +- Parse the input from the `event.body`. This represents the HTTP request body. +- It contains the contents of the note, as a string — `content`. +- It also contains an `attachment`, if one exists. It's the filename of file that has been uploaded to [our S3 bucket]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}). +- We read the name of our DynamoDB table from the environment variable using `process.env.tableName`. We'll be setting this in our `serverless.yml` below. We do this so we won't have to hardcode it in every function. +- The `userId` is the id for the author of the note. For now we are hardcoding it to `123`. Later we'll be setting this based on the authenticated user. +- Make a call to DynamoDB to put a new object with a generated `noteId` and the current date as the `createdAt`. +- And if the DynamoDB call fails then return an error with the HTTP status code `500`. + +### Configure the API Endpoint + +Now let's define the API endpoint for our function. + +{%change%} Open the `serverless.yml` file and replace it with the following. + +```yaml +service: notes-api + +# Create an optimized package for our functions +package: + individually: true + +plugins: + - serverless-bundle # Package our functions with Webpack + - serverless-offline + - serverless-dotenv-plugin # Load .env as environment variables + +provider: + name: aws + runtime: nodejs12.x + stage: prod + region: us-east-1 + + # These environment variables are made available to our functions + # under process.env. + environment: + tableName: notes + + # 'iamRoleStatements' defines the permission policy for the Lambda function. + # In this case Lambda functions are granted with permissions to access DynamoDB. + iamRoleStatements: + - Effect: Allow + Action: + - dynamodb:Scan + - dynamodb:Query + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:UpdateItem + - dynamodb:DeleteItem + - dynamodb:DescribeTable + Resource: "arn:aws:dynamodb:us-east-1:*:*" + +functions: + # Defines an HTTP API endpoint that calls the main function in create.js + # - path: url path is /notes + # - method: POST request + create: + handler: create.main + events: + - http: + path: notes + cors: true + method: post +``` + +Here we are adding our newly added create function to the configuration. We specify that it handles `post` requests at the `/notes` endpoint. This pattern of using a single Lambda function to respond to a single HTTP event is very much like the [Microservices architecture](https://en.wikipedia.org/wiki/Microservices). We discuss this and a few other patterns in the chapter on [organizing Serverless Framework projects]({% link _archives/organizing-serverless-projects.md %}). + +The `environment:` block allows us to define environment variables for our Lambda function. These are made available under the `process.env` Node.js variable. In our specific case, we are using `process.env.tableName` to access the name of our DynamoDB table. + +The `iamRoleStatements` section is telling AWS which resources our Lambda functions have access to. In this case we are saying that our Lambda functions can carry out the above listed actions on DynamoDB. We specify DynamoDB using `arn:aws:dynamodb:us-east-1:*:*`. This is roughly pointing to every DynamoDB table in the `us-east-1` region. We can be more specific here by specifying the table name. We'll be doing this later in the guide when we define our infrastructure as code. For now, just make sure to use the region that the DynamoDB table was created in, as this can be a common source of issues later on. For us the region is `us-east-1`. + +### Test + +Now we are ready to test our new API. To be able to test it on our local we are going to mock the input parameters. + +{%change%} In our project root, create a `mocks/` directory. + +```bash +$ mkdir mocks +``` + +{%change%} Create a `mocks/create-event.json` file and add the following. + +```json +{ + "body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}" +} +``` + +The `body` here corresponds to the `event.body` that we reference in our function. We are passing it in as a JSON encoded string. Note that, for the `attachment` we are just pretending that there is a file called `hello.jpg` that has already been uploaded. + +And to invoke our function we run the following in the root directory. + +```bash +$ serverless invoke local --function create --path mocks/create-event.json +``` + +If you have multiple profiles for your AWS SDK credentials, you will need to explicitly pick one. Use the following command instead: + +```bash +$ AWS_PROFILE=myProfile serverless invoke local --function create --path mocks/create-event.json +``` + +Where `myProfile` is the name of the AWS profile you want to use. If you need more info on how to work with AWS profiles in Serverless, refer to our [Configure multiple AWS profiles]({% link _archives/configure-multiple-aws-profiles.md %}) chapter. + +The response should look similar to this. + +```bash +{ + "statusCode": 200, + "body": "{\"userId\":\"123\",\"noteId\":\"bf586970-1007-11eb-a17f-a5105a0818d3\",\"content\":\"hello world\",\"attachment\":\"hello.jpg\",\"createdAt\":1602891102599}" +} +``` + +Make a note of the `noteId` in the response. We are going to use this newly created note in the next chapter. + +### Refactor Our Code + +Before we move on to the next chapter, let's quickly refactor the code since we are going to be doing much of the same for all of our APIs. + +{%change%} Start by replacing our `create.js` with the following. + +```js +import * as uuid from "uuid"; +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; + +export const main = handler(async (event, context) => { + const data = JSON.parse(event.body); + const params = { + TableName: process.env.tableName, + Item: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + attachment: data.attachment, // Parsed from request body + createdAt: Date.now(), // Current Unix timestamp + }, + }; + + await dynamoDb.put(params); + + return params.Item; +}); +``` + +This code doesn't work just yet but it shows you what we want to accomplish: + +- We want to make our Lambda function `async`, and simply return the results. +- We want to simplify how we make calls to DynamoDB. We don't want to have to create a `new AWS.DynamoDB.DocumentClient()`. +- We want to centrally handle any errors in our Lambda functions. +- Finally, since all of our Lambda functions will be handling API endpoints, we want to handle our HTTP responses in one place. + +To do all of this let's first create our `dynamodb-lib`. + +{%change%} In our project root, create a `libs/` directory. + +```bash +$ mkdir libs +$ cd libs +``` + +{%change%} Create a `libs/dynamodb-lib.js` file with: + +```js +import AWS from "aws-sdk"; + +const client = new AWS.DynamoDB.DocumentClient(); + +export default { + get: (params) => client.get(params).promise(), + put: (params) => client.put(params).promise(), + query: (params) => client.query(params).promise(), + update: (params) => client.update(params).promise(), + delete: (params) => client.delete(params).promise(), +}; +``` + +Here we are creating a convenience object that exposes the DynamoDB client methods that we are going to need in this guide. + +{%change%} Also create a `libs/handler-lib.js` file with the following. + +```js +export default function handler(lambda) { + return async function (event, context) { + let body, statusCode; + + try { + // Run the Lambda + body = await lambda(event, context); + statusCode = 200; + } catch (e) { + body = { error: e.message }; + statusCode = 500; + } + + // Return HTTP response + return { + statusCode, + body: JSON.stringify(body), + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, + }; + }; +} +``` + +Let's go over this in detail. + +- We are creating a `handler` function that we'll use as a wrapper around our Lambda functions. +- It takes our Lambda function as the argument. +- We then run the Lambda function in a `try/catch` block. +- On success, we `JSON.stringify` the result and return it with a `200` status code. +- If there is an error then we return the error message with a `500` status code. + +It's **important to note** that the `handler-lib.js` needs to be **imported before we import anything else**. This is because we'll be adding some error handling to it later that needs to be initialized when our Lambda function is first invoked. + +### Remove Template Files + +{%change%} Also, let's remove the starter files by running the following command in the root of our project. + +```bash +$ rm handler.js +``` + +Next, we are going to add the API to get a note given its id. + +--- + +#### Common Issues + +- Response `statusCode: 500` + + If you see a `statusCode: 500` response when you invoke your function, here is how to debug it. The error is generated by our code in the `catch` block. Adding a `console.log` in our `libs/handler-lib.js`, should give you a clue about what the issue is. + + ```js + } catch (e) { + // Print out the full error + console.log(e); + + body = { error: e.message }; + statusCode = 500; + } + ``` diff --git a/_archives/add-a-delete-note-api.md b/_archives/add-a-delete-note-api.md new file mode 100644 index 0000000000..38dda7fad9 --- /dev/null +++ b/_archives/add-a-delete-note-api.md @@ -0,0 +1,88 @@ +--- +layout: post +title: Add a Delete Note API +date: 2017-01-03 00:00:00 +lang: en +ref: add-a-delete-note-api +description: To allow users to delete their notes in our note taking app, we are going to add a DELETE note API. To do this we will add a new Lambda function to our Serverless Framework project. The Lambda function will delete a user’s note in the DynamoDB table. +comments_id: add-a-delete-note-api/153 +redirect_from: /chapters/add-a-delete-note-api.html +--- + +Finally, we are going to create an API that allows a user to delete a given note. + +### Add the Function + +{%change%} Create a new file `delete.js` and paste the following code + +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; + +export const main = handler(async (event, context) => { + const params = { + TableName: process.env.tableName, + // 'Key' defines the partition key and sort key of the item to be removed + Key: { + userId: "123", // The id of the author + noteId: event.pathParameters.id, // The id of the note from the path + }, + }; + + await dynamoDb.delete(params); + + return { status: true }; +}); +``` + +This makes a DynamoDB `delete` call with the `userId` & `noteId` key to delete the note. + +### Configure the API Endpoint + +{%change%} Open the `serverless.yml` file and append the following to it. + +```yaml +delete: + # Defines an HTTP API endpoint that calls the main function in delete.js + # - path: url path is /notes/{id} + # - method: DELETE request + handler: delete.main + events: + - http: + path: notes/{id} + cors: true + method: delete +``` + +This adds a DELETE request handler to the `/notes/{id}` endpoint. + +### Test + +{%change%} Create a `mocks/delete-event.json` file and add the following. + +Just like before we'll use the `noteId` of our note in place of the `id` in the `pathParameters` block. + +```json +{ + "pathParameters": { + "id": "578eb840-f70f-11e6-9d1a-1359b3b22944" + } +} +``` + +Invoke our newly created function from the root directory. + +```bash +$ serverless invoke local --function delete --path mocks/delete-event.json +``` + +And the response should look similar to this. + +```bash +{ + "statusCode": 200, + "body": "{\"status\":true}" +} +``` + +Now that our APIs are complete, let's deploy them next! diff --git a/_archives/add-a-get-note-api.md b/_archives/add-a-get-note-api.md new file mode 100644 index 0000000000..10b2b61654 --- /dev/null +++ b/_archives/add-a-get-note-api.md @@ -0,0 +1,94 @@ +--- +layout: post +title: Add a Get Note API +date: 2016-12-31 00:00:00 +lang: en +ref: add-a-get-note-api +description: To allow users to retrieve a note in our note taking app, we are going to add a GET note API. To do this we will add a new Lambda function to our Serverless Framework project. The Lambda function will retrieve the note from our DynamoDB table. +comments_id: add-a-get-note-api/132 +redirect_from: /chapters/add-a-get-note-api.html +--- + +Now that we created a note and saved it to our database. Let's add an API to retrieve a note given its id. + +### Add the Function + +{%change%} Create a new file `get.js` in your project root and paste the following code: + +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; + +export const main = handler(async (event, context) => { + const params = { + TableName: process.env.tableName, + // 'Key' defines the partition key and sort key of the item to be retrieved + Key: { + userId: "123", // The id of the author + noteId: event.pathParameters.id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; +}); +``` + +This follows exactly the same structure as our previous `create.js` function. The major difference here is that we are doing a `dynamoDb.get(params)` to get a note object given the `userId` (still hardcoded) and `noteId` that is passed in through the request. + +### Configure the API Endpoint + +{%change%} Open the `serverless.yml` file and append the following to it. + +```yaml +get: + # Defines an HTTP API endpoint that calls the main function in get.js + # - path: url path is /notes/{id} + # - method: GET request + handler: get.main + events: + - http: + path: notes/{id} + cors: true + method: get +``` + +Make sure that this block is indented exactly the same way as the preceding `create` block. + +This defines our get note API. It adds a GET request handler with the endpoint `/notes/{id}`. The `{id}` here translates to the `event.pathParameters.id` that we used in our function above. + +### Test + +To test our get note API we need to mock passing in the `noteId` parameter. We are going to use the `noteId` of the note we created in the previous chapter and add in a `pathParameters` block to our mock. So it should look similar to the one below. Replace the value of `id` with the id you received when you invoked the previous `create.js` function. + +{%change%} Create a `mocks/get-event.json` file and add the following. + +```json +{ + "pathParameters": { + "id": "a63c5450-1274-11eb-81db-b9d1e2c85f15" + } +} +``` + +And invoke our newly created function from the root directory of the project. + +```bash +$ serverless invoke local --function get --path mocks/get-event.json +``` + +The response should look similar to this. + +```bash +{ + "statusCode": 200, + "body": "{\"attachment\":\"hello.jpg\",\"content\":\"hello world\",\"createdAt\":1603157777941,\"noteId\":\"a63c5450-1274-11eb-81db-b9d1e2c85f15\",\"userId\":\"123\"}" +} +``` + +Next, let's create an API to list all the notes a user has. diff --git a/_archives/add-a-list-all-the-notes-api.md b/_archives/add-a-list-all-the-notes-api.md new file mode 100644 index 0000000000..2cd4df56b8 --- /dev/null +++ b/_archives/add-a-list-all-the-notes-api.md @@ -0,0 +1,91 @@ +--- +layout: post +title: Add a List All the Notes API +date: 2017-01-01 00:00:00 +lang: en +ref: add-a-list-all-the-notes-api +description: To allow users to retrieve their notes in our note taking app, we are going to add a list note GET API. To do this we will add a new Lambda function to our Serverless Framework project. The Lambda function will retrieve all the user’s notes from the DynamoDB table. +comments_id: add-a-list-all-the-notes-api/147 +redirect_from: /chapters/add-a-list-all-the-notes-api.html +--- + +Now we are going to add an API that returns a list of all the notes a user has. + +### Add the Function + +{%change%} Create a new file called `services/functions/list.js` with the following. + +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; + +export const main = handler(async (event, context) => { + const params = { + TableName: process.env.tableName, + // 'KeyConditionExpression' defines the condition for the query + // - 'userId = :userId': only return items with matching 'userId' + // partition key + KeyConditionExpression: "userId = :userId", + // 'ExpressionAttributeValues' defines the value in the condition + // - ':userId': defines 'userId' to be the id of the author + ExpressionAttributeValues: { + ":userId": "123", + }, + }; + + const result = await dynamoDb.query(params); + + // Return the matching list of items in response body + return result.Items; +}); +``` + +This is pretty much the same as our `get.js` except we use a condition to only return the items that have the same `userId` as the one we are passing in. In our case, it's still hardcoded to `123`. + +### Configure the API Endpoint + +{%change%} Open the `serverless.yml` file and append the following. + +```yaml +list: + # Defines an HTTP API endpoint that calls the main function in list.js + # - path: url path is /notes + # - method: GET request + handler: list.main + events: + - http: + path: notes + cors: true + method: get +``` + +This defines the `/notes` endpoint that takes a GET request. + +### Test + +{%change%} Create a `mocks/list-event.json` file and add the following. + +```json +{} +``` + +We are still adding an empty mock event because we are going to replace this later on in the guide. + +And invoke our function from the root directory of the project. + +```bash +$ serverless invoke local --function list --path mocks/list-event.json +``` + +The response should look similar to this. + +```bash +{ + "statusCode": 200, + "body": "[{\"attachment\":\"hello.jpg\",\"content\":\"hello world\",\"createdAt\":1602891322039,\"noteId\":\"42244c70-1008-11eb-8be9-4b88616c4b39\",\"userId\":\"123\"}]" +} +``` + +Note that this API returns an array of note objects as opposed to the `get.js` function that returns just a single note object. + +Next we are going to add an API to update a note. diff --git a/_chapters/add-an-update-note-api.md b/_archives/add-an-update-note-api.md similarity index 57% rename from _chapters/add-an-update-note-api.md rename to _archives/add-an-update-note-api.md index cae08b033d..44140e9497 100644 --- a/_chapters/add-an-update-note-api.md +++ b/_archives/add-an-update-note-api.md @@ -5,112 +5,96 @@ date: 2017-01-02 00:00:00 lang: en ref: add-an-update-note-api description: To allow users to update their notes in our note taking app, we are going to add an update note PUT API. To do this we will add a new Lambda function to our Serverless Framework project. The Lambda function will update a user’s note in the DynamoDB table. -context: true -code: backend comments_id: add-an-update-note-api/144 +redirect_from: /chapters/add-an-update-note-api.html --- Now let's create an API that allows a user to update a note with a new note object given its id. ### Add the Function -Create a new file `update.js` and paste the following code +{%change%} Create a new file `update.js` and paste the following code -``` javascript -import * as dynamoDbLib from "./libs/dynamodb-lib"; -import { success, failure } from "./libs/response-lib"; +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const data = JSON.parse(event.body); const params = { - TableName: "notes", + TableName: process.env.tableName, // 'Key' defines the partition key and sort key of the item to be updated - // - 'userId': Identity Pool identity id of the authenticated user - // - 'noteId': path parameter Key: { - userId: event.requestContext.identity.cognitoIdentityId, - noteId: event.pathParameters.id + userId: "123", // The id of the author + noteId: event.pathParameters.id, // The id of the note from the path }, // 'UpdateExpression' defines the attributes to be updated // 'ExpressionAttributeValues' defines the value in the update expression UpdateExpression: "SET content = :content, attachment = :attachment", ExpressionAttributeValues: { ":attachment": data.attachment || null, - ":content": data.content || null + ":content": data.content || null, }, // 'ReturnValues' specifies if and how to return the item's attributes, // where ALL_NEW returns all attributes of the item after the update; you // can inspect 'result' below to see how it works with different settings - ReturnValues: "ALL_NEW" + ReturnValues: "ALL_NEW", }; - try { - await dynamoDbLib.call("update", params); - return success({ status: true }); - } catch (e) { - return failure({ status: false }); - } -} + await dynamoDb.update(params); + + return { status: true }; +}); ``` This should look similar to the `create.js` function. Here we make an `update` DynamoDB call with the new `content` and `attachment` values in the `params`. ### Configure the API Endpoint -Open the `serverless.yml` file and append the following to it. - -``` yaml - update: - # Defines an HTTP API endpoint that calls the main function in update.js - # - path: url path is /notes/{id} - # - method: PUT request - handler: update.main - events: - - http: - path: notes/{id} - method: put - cors: true - authorizer: aws_iam +{%change%} Open the `serverless.yml` file and append the following to it. + +```yaml +update: + # Defines an HTTP API endpoint that calls the main function in update.js + # - path: url path is /notes/{id} + # - method: PUT request + handler: update.main + events: + - http: + path: notes/{id} + cors: true + method: put ``` Here we are adding a handler for the PUT request to the `/notes/{id}` endpoint. ### Test -Create a `mocks/update-event.json` file and add the following. +{%change%} Create a `mocks/update-event.json` file and add the following. Also, don't forget to use the `noteId` of the note we have been using in place of the `id` in the `pathParameters` block. -``` json +```json { "body": "{\"content\":\"new world\",\"attachment\":\"new.jpg\"}", "pathParameters": { "id": "578eb840-f70f-11e6-9d1a-1359b3b22944" - }, - "requestContext": { - "identity": { - "cognitoIdentityId": "USER-SUB-1234" - } } } ``` And we invoke our newly created function from the root directory. -``` bash +```bash $ serverless invoke local --function update --path mocks/update-event.json ``` The response should look similar to this. -``` bash +```bash { - statusCode: 200, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Credentials': true - }, - body: '{"status":true}' + "statusCode": 200, + "body": "{\"status\":true}" } ``` diff --git a/_archives/add-support-for-es6-and-typescript.md b/_archives/add-support-for-es6-and-typescript.md new file mode 100644 index 0000000000..7a1cdf4305 --- /dev/null +++ b/_archives/add-support-for-es6-and-typescript.md @@ -0,0 +1,112 @@ +--- +layout: post +title: Add Support for ES6 and TypeScript +date: 2016-12-29 12:00:00 +lang: en +ref: add-support-for-es6-and-typescript +description: AWS Lambda supports Node.js v10.x and v12.x. However, to use ES 6 features or TypeScript in our Serverless Framework project we need to use Babel, Webpack 5, and a ton of other packages. We can do this by using the serverless-bundle plugin to our project. +comments_id: add-support-for-es6-es7-javascript/128 +redirect_from: + - /chapters/add-support-for-es6-javascript.html + - /chapters/add-support-for-es6-es7-javascript.html + - /chapters/add-support-for-es6-and-typescript.html +--- + +AWS Lambda supports Node.js v10.x, v12.x, and v14.x. However, the supported syntax is a little different when compared to the more advanced ECMAScript flavor of JavaScript that our frontend React app supports. It makes sense to use similar ES features across both parts of the project – specifically, we'll be relying on ES imports/exports in our handler functions. + +Additionally, our frontend React app automatically supports TypeScript, via [Create React App](https://create-react-app.dev). And while we are not using TypeScript in this guide, it makes sense to have a similar setup for your backend Lambda functions. So you can use it in your future projects. + +To do this we typically need to install [Babel](https://babeljs.io), [TypeScript](https://www.typescriptlang.org), [Webpack](https://webpack.js.org), and a long list of other packages. This can add a ton of extra config and complexity to your project. + +To help with this we created, [`serverless-bundle`](https://github.com/AnomalyInnovations/serverless-bundle). This is a Serverless Framework plugin that has a few key advantages: + +- Only one dependency +- Supports ES6 and TypeScript +- Generates optimized packages +- Linting Lambda functions using [ESLint](https://eslint.org) +- Supports transpiling unit tests with [babel-jest](https://github.com/facebook/jest/tree/master/packages/babel-jest) +- Source map support for proper error messages + +It's automatically included in the starter project we used in the previous chapter — [`serverless-nodejs-starter`]({% link _archives/serverless-nodejs-starter.md %}). For TypeScript, we have a starter for that as well — [`serverless-typescript-starter`](https://github.com/AnomalyInnovations/serverless-typescript-starter). + +However, if you are looking to add ES6 and TypeScript support to your existing Serverless Framework projects, you can do this by installing [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle): + +``` bash +$ npm install --save-dev serverless-bundle +``` + +And including it in your `serverless.yml` using: + +``` yml +plugins: + - serverless-bundle +``` + +To run your tests, add this to your `package.json`. + +``` json +"scripts": { + "test": "serverless-bundle test" +} +``` + +### ES6 Lambda Functions + +Let's look at the Lambda function that comes with our starter project. + +Your `handler.js` should look like this. + +``` js +export const hello = async (event, context) => { + return { + statusCode: 200, + body: JSON.stringify({ + message: `Go Serverless v2.0! ${(await message({ time: 1, copy: 'Your function executed successfully!'}))}`, + }), + }; +}; + +const message = ({ time, ...rest }) => new Promise((resolve, reject) => + setTimeout(() => { + resolve(`${rest.copy} (with a delay)`); + }, time * 1000) +); +``` + +Let's run this. In your project root run: + +``` bash +$ serverless invoke local --function hello +``` + +You should see something like this in your terminal. + +``` bash +{ + "statusCode": 200, + "body": "{\"message\":\"Go Serverless v2.0! Your function executed successfully! (with a delay)\"}" +} +``` + +In the above command we are asking Serverless Framework to (locally) invoke a Lambda function called `hello`. This in turn will run the `hello` method that we are exporting in our `handler.js`. + +Here we are directly invoking the Lambda function. Though once deployed, we'll be invoking this function through the `/hello` API endpoint (as we [talked about in the last chapter]({% link _archives/setup-the-serverless-framework.md %})). + +Now we are almost ready to deploy our Lambda function and API. But before we do that let's quickly look at one of the other things that's been set up for us in this starter project. + +### Optimized Packages + +By default Serverless Framework creates a single package for all your Lambda functions. This means that when a Lambda function is invoked, it'll load all the code in your app. Including all the other Lambda functions. This negatively affects performance as your app grows in size. The larger your Lambda function packages, the longer [the cold starts]({% link _chapters/what-is-serverless.md %}#cold-starts). + +To turn this off and it to ensure that Serverless Framework is packaging our functions individually, add the following to your `serverless.yml`. + +``` yml +package: + individually: true +``` + +This should be on by default in our starter project. + +Note that, with the above option enabled, serverless-bundle can use Webpack to generate optimized packages using a [tree shaking algorithm](https://webpack.js.org/guides/tree-shaking/). It'll only include the code needed to run your Lambda function and nothing else! + +Now we are ready to deploy our backend API. diff --git a/_archives/allow-users-to-change-passwords.md b/_archives/allow-users-to-change-passwords.md new file mode 100644 index 0000000000..123f80fd86 --- /dev/null +++ b/_archives/allow-users-to-change-passwords.md @@ -0,0 +1,222 @@ +--- +layout: post +title: Allow Users to Change Passwords +description: Use the AWS Amplify Auth.changePassword method to support change password functionality in our Serverless React app. This triggers Cognito to help our users change their password. +date: 2018-04-15 00:00:00 +code: user-management +comments_id: allow-users-to-change-passwords/507 +redirect_from: /chapters/allow-users-to-change-passwords.html +--- + +For our [Serverless notes app](https://demo.sst.dev), we want to allow our users to change their password. Recall that we are using Cognito to manage our users and AWS Amplify in our React app. In this chapter we will look at how to do that. + +For reference, we are using a forked version of the notes app with: + +- A separate GitHub repository: [**{{ site.frontend_user_mgmt_github_repo }}**]({{ site.frontend_user_mgmt_github_repo }}) + +Let's start by editing our settings page so that our users can use to change their password. + +### Add a Settings Page + +{%change%} Replace the `return` statement in `src/containers/Settings.js` with. + +```jsx +return ( +
+ + + Change Email + + + + + Change Password + + +
+ + + +
+); +``` + +{%change%} And import the following as well. + +```jsx +import { LinkContainer } from "react-router-bootstrap"; +import LoaderButton from "../components/LoaderButton"; +``` + +All this does is add two links to a page that allows our users to change their password and email. + +{%change%} Replace our `src/containers/Settings.css` with the following. + +```css +@media all and (min-width: 480px) { + .Settings { + padding: 60px 0; + margin: 0 auto; + max-width: 480px; + } + + .Settings > .LoaderButton:first-child { + margin-bottom: 15px; + } +} +``` + +![Settings page screenshot](/assets/user-management/settings-page.png) + +### Change Password Form + +Now let's create the form that allows our users to change their password. + +{%change%} Add the following to `src/containers/ChangePassword.js`. + +```jsx +import React, { useState } from "react"; +import { Auth } from "aws-amplify"; +import { useNavigate } from "react-router-dom"; +import { FormGroup, FormControl, FormLabel } from "react-bootstrap"; +import LoaderButton from "../components/LoaderButton"; +import { useFormFields } from "../lib/hooksLib"; +import { onError } from "../lib/errorLib"; +import "./ChangePassword.css"; + +export default function ChangePassword() { + const nav = useNavigate(); + const [fields, handleFieldChange] = useFormFields({ + password: "", + oldPassword: "", + confirmPassword: "", + }); + const [isChanging, setIsChanging] = useState(false); + + function validateForm() { + return ( + fields.oldPassword.length > 0 && + fields.password.length > 0 && + fields.password === fields.confirmPassword + ); + } + + async function handleChangeClick(event) { + event.preventDefault(); + + setIsChanging(true); + + try { + const currentUser = await Auth.currentAuthenticatedUser(); + await Auth.changePassword( + currentUser, + fields.oldPassword, + fields.password + ); + + nav("/settings"); + } catch (error) { + onError(error); + setIsChanging(false); + } + } + + return ( +
+
+ + Old Password + + +
+ + New Password + + + + Confirm Password + + + + Change Password + +
+
+ ); +} +``` + +Most of this should be very straightforward. The key part of the flow here is that we ask the user for their current password along with their new password. Once they enter it, we can call the following: + +```jsx +const currentUser = await Auth.currentAuthenticatedUser(); +await Auth.changePassword(currentUser, fields.oldPassword, fields.password); +``` + +The above snippet uses the `Auth` module from Amplify to get the current user. And then uses that to change their password by passing in the old and new password. Once the `Auth.changePassword` method completes, we redirect the user to the settings page. + +{%change%} Let's also add a couple of styles. + +```css +@media all and (min-width: 480px) { + .ChangePassword { + padding: 60px 0; + } + + .ChangePassword form { + margin: 0 auto; + max-width: 320px; + } +} +``` + +{%change%} Let's add our new page to `src/Routes.js`. + +```html + + + + } +/> +``` + +{%change%} And import it. + +```jsx +import ChangePassword from "./containers/ChangePassword"; +``` + +That should do it. The `/settings/password` page should allow us to change our password. + +![Change password page screenshot](/assets/user-management/change-password-page.png) + +Next, let's look at how to implement a change email form for our users. diff --git a/_chapters/allow-users-to-change-their-email.md b/_archives/allow-users-to-change-their-email.md similarity index 50% rename from _chapters/allow-users-to-change-their-email.md rename to _archives/allow-users-to-change-their-email.md index 97244ce4df..cc8efe3bc5 100644 --- a/_chapters/allow-users-to-change-their-email.md +++ b/_archives/allow-users-to-change-their-email.md @@ -3,157 +3,145 @@ layout: post title: Allow Users to Change Their Email description: Use the AWS Amplify Auth.updateUserAttributes method to support change email functionality in our Serverless React app. This triggers Cognito to help our users change their email. date: 2018-04-16 00:00:00 -context: true code: user-management comments_id: allow-users-to-change-their-email/508 +redirect_from: /chapters/allow-users-to-change-their-email.html --- -We want the users of our [Serverless notes app](https://demo.serverless-stack.com) to be able to change their email. Recall that we are using Cognito to manage our users and AWS Amplify in our React app. In this chapter we will look at how to do that. +We want the users of our [Serverless notes app](https://demo.sst.dev) to be able to change their email. Recall that we are using Cognito to manage our users and AWS Amplify in our React app. In this chapter we will look at how to do that. For reference, we are using a forked version of the notes app with: - A separate GitHub repository: [**{{ site.frontend_user_mgmt_github_repo }}**]({{ site.frontend_user_mgmt_github_repo }}) -- And it can be accessed through: [**https://demo-user-mgmt.serverless-stack.com**](https://demo-user-mgmt.serverless-stack.com) +- And it can be accessed through: [**https://demo-user-mgmt.sst.dev**](https://demo-user-mgmt.sst.dev) In the previous chapter we created a settings page that links to `/settings/email`. Let's implement that. ### Change Email Form -Add the following to `src/containers/ChangeEmail.js`. +{%change%} Add the following to `src/containers/ChangeEmail.js`. -``` coffee -import React, { Component } from "react"; +```coffee +import React, { useState } from "react"; import { Auth } from "aws-amplify"; +import { useNavigate } from "react-router-dom"; import { - HelpBlock, + FormText, FormGroup, FormControl, - ControlLabel + FormLabel, } from "react-bootstrap"; import LoaderButton from "../components/LoaderButton"; +import { useFormFields } from "../lib/hooksLib"; +import { onError } from "../lib/errorLib"; import "./ChangeEmail.css"; -export default class ChangeEmail extends Component { - constructor(props) { - super(props); - - this.state = { - code: "", - email: "", - codeSent: false, - isConfirming: false, - isSendingCode: false - }; +export default function ChangeEmail() { + const nav = useNavigate(); + const [codeSent, setCodeSent] = useState(false); + const [fields, handleFieldChange] = useFormFields({ + code: "", + email: "", + }); + const [isConfirming, setIsConfirming] = useState(false); + const [isSendingCode, setIsSendingCode] = useState(false); + + function validateEmailForm() { + return fields.email.length > 0; } - validatEmailForm() { - return this.state.email.length > 0; + function validateConfirmForm() { + return fields.code.length > 0; } - validateConfirmForm() { - return this.state.code.length > 0; - } - - handleChange = event => { - this.setState({ - [event.target.id]: event.target.value - }); - }; - - handleUpdateClick = async event => { + async function handleUpdateClick(event) { event.preventDefault(); - this.setState({ isSendingCode: true }); + setIsSendingCode(true); try { const user = await Auth.currentAuthenticatedUser(); - await Auth.updateUserAttributes(user, { email: this.state.email }); - - this.setState({ codeSent: true }); - } catch (e) { - alert(e.message); - this.setState({ isSendingCode: false }); + await Auth.updateUserAttributes(user, { email: fields.email }); + setCodeSent(true); + } catch (error) { + onError(error); + setIsSendingCode(false); } - }; + } - handleConfirmClick = async event => { + async function handleConfirmClick(event) { event.preventDefault(); - this.setState({ isConfirming: true }); + setIsConfirming(true); try { - await Auth.verifyCurrentUserAttributeSubmit("email", this.state.code); + await Auth.verifyCurrentUserAttributeSubmit("email", fields.code); - this.props.history.push("/settings"); - } catch (e) { - alert(e.message); - this.setState({ isConfirming: false }); + nav("/settings"); + } catch (error) { + onError(error); + setIsConfirming(false); } - }; + } - renderUpdateForm() { + function renderUpdateForm() { return ( -
+ - Email + Email + isLoading={isSendingCode} + disabled={!validateEmailForm()} + > + Update Email +
); } - renderConfirmationForm() { + function renderConfirmationForm() { return ( -
+ - Confirmation Code + Confirmation Code - - Please check your email ({this.state.email}) for the confirmation - code. - + + Please check your email ({fields.email}) for the confirmation code. + + isLoading={isConfirming} + disabled={!validateConfirmForm()} + > + Confirm +
); } - render() { - return ( -
- {!this.state.codeSent - ? this.renderUpdateForm() - : this.renderConfirmationForm()} -
- ); - } + return ( +
+ {!codeSent ? renderUpdateForm() : renderConfirmationForm()} +
+ ); } ``` @@ -163,24 +151,24 @@ The flow for changing a user's email is pretty similar to how we sign a user up. 2. Cognito sends them a verification code. 3. They enter the code and we confirm that their email has been changed. -We start by rendering a form that asks our user to enter their new email in `this.renderUpdateForm()`. Once the user submits this form, we call: +We start by rendering a form that asks our user to enter their new email in `renderUpdateForm()`. Once the user submits this form, we call: -``` js +```js const user = await Auth.currentAuthenticatedUser(); -Auth.updateUserAttributes(user, { email: this.state.email }); +Auth.updateUserAttributes(user, { email: fields.email }); ``` -This gets the current user and updates their email using the `Auth` module from Amplify. Next we render the form where they can enter the code in `this.renderConfirmationForm()`. Upon submitting this form we call: +This gets the current user and updates their email using the `Auth` module from Amplify. Next we render the form where they can enter the code in `renderConfirmationForm()`. Upon submitting this form we call: -``` js -Auth.verifyCurrentUserAttributeSubmit("email", this.state.code); +```js +Auth.verifyCurrentUserAttributeSubmit("email", fields.code); ``` This confirms the change on Cognito's side. Finally, we redirect the user to the settings page. -Let's add a couple of styles to `src/containers/ChangeEmail.css`. +{%change%} Let's add a couple of styles to `src/containers/ChangeEmail.css`. -``` css +```css @media all and (min-width: 480px) { .ChangeEmail { padding: 60px 0; @@ -193,20 +181,17 @@ This confirms the change on Cognito's side. Finally, we redirect the user to the } ``` -Finally, let's add our new page to `src/Routes.js`. +{%change%} Finally, let's add our new page to `src/Routes.js`. -``` html - +```html + + + ``` -And import it in the header. +{%change%} And import it in the header. -``` coffee +```coffee import ChangeEmail from "./containers/ChangeEmail"; ``` @@ -222,6 +207,6 @@ You might notice that the change email flow is interrupted if the user does not - In this case show a simple sign that allows users to resend the verification code. You can do this by calling `Auth.verifyCurrentUserAttribute("email")`. -- Next you can simply display the confirm code form from above and follow the same flow by calling `Auth.verifyCurrentUserAttributeSubmit("email", this.state.code)`. +- Next you can simply display the confirm code form from above and follow the same flow by calling `Auth.verifyCurrentUserAttributeSubmit("email", fields.code)`. This can make your change email flow more robust and handle the case where a user forgets to verify their new email. diff --git a/_chapters/api-gateway-and-lambda-logs.md b/_archives/api-gateway-and-lambda-logs.md similarity index 95% rename from _chapters/api-gateway-and-lambda-logs.md rename to _archives/api-gateway-and-lambda-logs.md index 400a8b3750..874a8aae4a 100644 --- a/_chapters/api-gateway-and-lambda-logs.md +++ b/_archives/api-gateway-and-lambda-logs.md @@ -3,13 +3,12 @@ layout: post title: API Gateway and Lambda Logs description: To view logs for your serverless APIs on AWS, CloudWatch needs to be enabled for API Gateway and Lambda. CloudWatch logs are ordered by Log Groups and Log Stream. Lambda CloudWatch logs can also be viewed using the Serverless CLI with the “serverless logs” command. date: 2018-04-03 00:00:00 -context: true comments_id: api-gateway-and-lambda-logs/31 +redirect_from: /chapters/api-gateway-and-lambda-logs.html --- Logging is an essential part of building backends and it is no different for a serverless API. It gives us visibility into how we are processing and responding to incoming requests. - ### Types of Logs There are 2 types of logs we usually take for granted in a monolithic environment. @@ -102,21 +101,19 @@ In the **Logs** tab: Scroll to the bottom of the page and click **Save Changes**. Now our API Gateway requests should be logged via CloudWatch. -Note that, the execution logs can generate a ton of log data and it's not recommended to leave them on. They are much better for debugging. API Gateway does have support for access logs, which we recomend leaving on. Here is [how to enable access logs for your API Gateway project](https://seed.run/blog/how-to-enable-access-logs-for-api-gateway). - +Note that, the execution logs can generate a ton of log data and it's not recommended to leave them on. They are much better for debugging. API Gateway does have support for access logs, which we recommend leaving on. Here is [how to enable access logs for your API Gateway project](https://seed.run/blog/how-to-enable-access-logs-for-api-gateway). ### Enable Lambda CloudWatch Logs Lambda CloudWatch logs are enabled by default. It tracks the duration and max memory usage for each execution. You can write additional information to CloudWatch via `console.log`. For example: -``` javascript +```js export function main(event, context, callback) { - console.log('Hello world'); - callback(null, { body: '' }); + console.log("Hello world"); + callback(null, { body: "" }); } ``` - ### Viewing API Gateway CloudWatch Logs CloudWatch groups log entries into **Log Groups** and then further into **Log Streams**. Log Groups and Log Streams can mean different things for different AWS services. For API Gateway, when logging is first enabled in an API project's stage, API Gateway creates 1 log group for the stage, and 300 log streams in the group ready to store log entries. API Gateway picks one of these streams when there is an incoming request. @@ -129,7 +126,7 @@ Select **Logs** from the left panel. ![Select CloudWatch Logs Screenshot](/assets/logging/select-cloudwatch-logs.png) -Select the log group prefixed with **API-Gateway-Execution-Logs_** followed by the API Gateway id. +Select the log group prefixed with **API-Gateway-Execution-Logs\_** followed by the API Gateway id. ![Select CloudWatch API Gateway Log Group Screenshot](/assets/logging/select-cloudwatch-api-gateway-log-group.png) @@ -143,7 +140,6 @@ This shows you the log entries grouped by request. Note that two consecutive groups of logs are not necessarily two consecutive requests in real time. This is because there might be other requests that are processed in between these two that were picked up by one of the other log streams. - ### Viewing Lambda CloudWatch Logs For Lambda, each function has its own log group. And the log stream rotates if a new version of the Lambda function has been deployed or if it has been idle for some time. @@ -164,17 +160,16 @@ You can also use the Serverless CLI to view CloudWatch logs for a Lambda functio From your project root run the following. -``` bash +```bash $ serverless logs -f ``` Where the `` is the name of the Lambda function you are looking for. Additionally, you can use the `--tail` flag to stream the logs automatically to your console. -``` bash +```bash $ serverless logs -f --tail ``` This can be very helpful during development when trying to debug your functions using the `console.log` call. - Hopefully, this has helped you set up CloudWatch logging for your API Gateway and Lambda projects. And given you a quick idea of how to read your serverless logs using the AWS Console. diff --git a/_chapters/backups-in-dynamodb.md b/_archives/backups-in-dynamodb.md similarity index 92% rename from _chapters/backups-in-dynamodb.md rename to _archives/backups-in-dynamodb.md index eebed72339..bb5420fa30 100644 --- a/_chapters/backups-in-dynamodb.md +++ b/_archives/backups-in-dynamodb.md @@ -3,13 +3,13 @@ layout: post title: Backups in DynamoDB date: 2018-04-06 12:00:00 description: Amazon DynamoDB allows you to create On-demand backups and enable Point-in-time recovery with a single click. Backups are fully-managed, extremely fast, and do not impact performance. In this chapter we look at the two ways to backup and restore DynamoDB tables. -context: true comments_id: backups-in-dynamodb/705 +redirect_from: /chapters/backups-in-dynamodb.html --- An important (yet overlooked) aspect of having a database powering your web application are, backups! In this chapter we are going to take a look at how to configure backups for your DynamoDB tables. -For [our demo notes app](https://demo.serverless-stack.com), we are using a DynamoDB table to store all our user's notes. DynamoDB achieves a high degree of data availability and durability by replicating your data across three different facilities within a given region. However, DynamoDB does not provide an SLA for the data durability. This means that you should backup your database tables. +For [our demo notes app](https://demo.sst.dev), we are using a DynamoDB table to store all our user's notes. DynamoDB achieves a high degree of data availability and durability by replicating your data across three different facilities within a given region. However, DynamoDB does not provide an SLA for the data durability. This means that you should backup your database tables. Let's start by getting a quick background on how backups work in DynamoDB. diff --git a/_archives/best-practices-for-building-serverless-apps.md b/_archives/best-practices-for-building-serverless-apps.md new file mode 100644 index 0000000000..715477a841 --- /dev/null +++ b/_archives/best-practices-for-building-serverless-apps.md @@ -0,0 +1,85 @@ +--- +layout: post +title: Best Practices for Building Serverless Apps +description: In this section of the guide we'll be covering the best practices for developing and maintaining large Serverless applications. It builds on what we've covered so far and it extends the demo notes app that we built in the first section. It's intended for teams as opposed to individual developers. It's meant to give you a foundation that scales as your app (and team) grows. +date: 2019-10-03 00:00:00 +comments_id: best-practices-for-building-serverless-apps/1315 +redirect_from: /chapters/best-practices-for-building-serverless-apps.html +--- + +In this section of the guide we'll be covering the best practices for developing and maintaining large Serverless applications. It builds on what we've covered so far and it extends the [demo notes app]({{ site.demo_url }}) that we built in the first section. It's intended for teams as opposed to individual developers. It's meant to give you a foundation that scales as your app (and team) grows. + +### Background + +SST was launched back in March 2017. Since then thousands of folks have used the guide to build their first full-stack Serverless app. Many of you have used this as a starting point to build really large applications. Applications that are made up of scores of services worked on by a team of developers. + +However, the challenges that teams face while developing large scale Serverless applications are very different from the one an individual faces while building his or her first app. You've to deal with architectural design decisions and questions that can be hard to answer if you haven't built and managed a large scale Serverless app before. Questions like: + +- How should my project be structured when I have dozens of interdependent services? +- How should I manage my environments? +- What is the best practice for storing secrets? +- How do I make sure my production environments are completely secure? +- What does the workflow look like for the developers on my team? +- How do I debug large Serverless applications? + +Some of these are not exclusive to folks working on large scale apps, but they are very common once your app grows to a certain size. We hear most of these through our readers, our users, and our [Serverless Toronto Meetup](http://serverlesstoronto.org) members. + +While there are tons of blog posts out there that answer some of these questions, it requires you to piece them together to figure out what the best practices are. + +### A new perspective + +Now nearly 3 years into working on SST and building large scale Serverless applications, there are some common design patterns that we can confidently share with our readers. Additionally, Serverless as a technology and community has also matured to the point where there are reasonable answers for the above questions. + +This new addition to the guide is designed to lay out some of the best practices and give you a solid foundation to use Serverless at your company. You can be confident that as your application and team grows, you'll be on the right track for building something that scales. + +### Who is this for + +While the topics covered in this section can be applied to any project you are working on, they are far better suited for larger scale ones. For example, we talk about using multiple AWS accounts to configure your environments. This works well when you have multiple developers on your team but isn't worth the overhead when you are the only one working on the project. + +### What is covered in this section + +Here is a rough rundown of the topics covered in this section of the guide. + +We are covering primarily the backend Serverless portion. The frontend flow works relatively the same way as what we covered in the first section. We also found that there is a distinct lack of best practices for building Serverless backends as opposed to React apps. + +- [Organizing large Serverless apps]({% link _archives/organizing-serverless-projects.md %}) + - [Sharing resources using cross-stack references]({% link _archives/cross-stack-references-in-serverless.md %}) + - [Sharing code between services]({% link _archives/share-code-between-services.md %}) + - [Sharing API endpoints across services]({% link _archives/share-an-api-endpoint-between-services.md %}) +- [Configuring environments]({% link _archives/environments-in-serverless-apps.md %}) + - [Using separate AWS accounts to manage environments ]({% link _archives/structure-environments-across-aws-accounts.md %}) + - [Parameterizing resource names]({% link _archives/parameterize-serverless-resources-names.md %}) + - [Managing environment specific configs]({% link _archives/manage-environment-related-config.md %}) + - [Best practices for handling secrets]({% link _archives/storing-secrets-in-serverless-apps.md %}) + - [Sharing domains across environments]({% link _archives/share-route-53-domains-across-aws-accounts.md %}) +- [Development lifecycle]({% link _archives/working-on-serverless-apps.md %}) + - [Working locally]({% link _archives/invoke-api-gateway-endpoints-locally.md %}) + - [Creating feature environments]({% link _archives/creating-feature-environments.md %}) + - [Creating pull request environments]({% link _archives/creating-pull-request-environments.md %}) + - [Promoting to production]({% link _archives/promoting-to-production.md %}) + - [Handling rollbacks]({% link _archives/rollback-changes.md %}) +- [Using AWS X-Ray to trace Lambda functions]({% link _archives/tracing-serverless-apps-with-x-ray.md %}) + +We think these concepts should be a good starting point for your projects and you should be able to adapt them to fit your use case! + +### How this new section is structured + +This section of the guide has a fair bit of _theory_ when compared to the first section. However, we try to take a similar approach. We'll slowly introduce these concepts as we work through the chapters. + +The following repos will serve as the centerpiece of this section: + +1. [**Serverless Infrastructure**]({{ site.backend_ext_resources_github_repo }}) + + A repo containing all the main infrastructure resources of our extended notes application. We are creating a DynamoDB table to store all the notes related info, an S3 bucket for uploading attachments, and a Cognito User Pool and Identity Pool to authenticate users. We'll be using [AWS CDK]({% link _archives/what-is-aws-cdk.md %}) with [SST](https://github.com/sst/sst). + +2. [**Serverless Services**]({{ site.backend_ext_api_github_repo }}) + + A monorepo containing all the services in our extended notes application. We have three different services here. The `notes-api` service that powers the notes CRUD API endpoint, the `billing-api` service that processes payment information through Stripe and publishes a message on an SNS topic. Finally, we have a `notify-job` service that listens to the SNS topic and sends us a text message when somebody makes a purchase. We'll be using [Serverless Framework](https://github.com/serverless/serverless) for our services. + +We'll start by forking these repos but unlike the first section we won't be directly working on the code. Instead as we work through the sections we'll point out the key aspects of the codebase. + +We'll then go over step by step how to configure the environments through AWS. We'll use [Seed](https://seed.run) to illustrate how to deploy our application to our environments. Note that, you do not need Seed to configure your own setup. We'll only be using it as an example. Once you complete the guide you should be able to use your favorite CI/CD service to build a pipeline that follows the best practices. Finally, we'll go over the development workflow for you and your team. + +The end result of this will be that you'll have a fully functioning Serverless backend, hosted in your own GitHub repo, and deployed to your AWS environments. We want to make sure that you'll have a working setup in place, so you can always refer back to it when you need to! + +Let's get started. diff --git a/_chapters/code-splitting-in-create-react-app.md b/_archives/code-splitting-in-create-react-app.md similarity index 88% rename from _chapters/code-splitting-in-create-react-app.md rename to _archives/code-splitting-in-create-react-app.md index 08c2b7e9ad..ce8723dbd7 100644 --- a/_chapters/code-splitting-in-create-react-app.md +++ b/_archives/code-splitting-in-create-react-app.md @@ -1,11 +1,10 @@ --- layout: post title: Code Splitting in Create React App -description: Code splitting in Create React App is an easy way to reduce the size of your React.js app bundle. To do this in an app using React Router v4, we can asynchronously load our routes using the dynamic import() method that Create React App supports. +description: Code splitting in Create React App is an easy way to reduce the size of your React.js app bundle. To do this in an app using React Router v5, we can asynchronously load our routes using the dynamic import() method that Create React App supports. date: 2018-04-17 00:00:00 -code: frontend -context: true comments_id: code-splitting-in-create-react-app/98 +redirect_from: /chapters/code-splitting-in-create-react-app.html --- Code Splitting is not a necessary step for building React apps. But feel free to follow along if you are curious about what Code Splitting is and how it can help larger React apps. @@ -20,23 +19,24 @@ Create React App (from 1.0 onwards) allows us to dynamically import parts of our While, the dynamic `import()` can be used for any component in our React app; it works really well with React Router. Since, React Router is figuring out which component to load based on the path; it would make sense that we dynamically import those components only when we navigate to them. -### Code Splitting and React Router v4 +### Code Splitting and React Router v5 The usual structure used by React Router to set up routing for your app looks something like this. -``` coffee +```jsx /* Import the components */ import Home from "./containers/Home"; import Posts from "./containers/Posts"; import NotFound from "./containers/NotFound"; /* Use components to define routes */ -export default () => +export default () => ( - ; + +); ``` We start by importing the components that will respond to our routes. And then use them to define our routes. The `Switch` component renders the route that matches the path. @@ -47,9 +47,9 @@ However, we import all of the components in the route statically at the top. Thi To do this we are going to dynamically import the required component. -Add the following to `src/components/AsyncComponent.js`. +{%change%} Add the following to `src/components/AsyncComponent.js`. -``` coffee +```jsx import React, { Component } from "react"; export default function asyncComponent(importComponent) { @@ -58,7 +58,7 @@ export default function asyncComponent(importComponent) { super(props); this.state = { - component: null + component: null, }; } @@ -66,7 +66,7 @@ export default function asyncComponent(importComponent) { const { default: component } = await importComponent(); this.setState({ - component: component + component: component, }); } @@ -91,13 +91,13 @@ We are doing a few things here: Now let's use this component in our routes. Instead of statically importing our component. -``` coffee +```jsx import Home from "./containers/Home"; ``` We are going to use the `asyncComponent` to dynamically import the component we want. -``` coffee +```jsx const AsyncHome = asyncComponent(() => import("./containers/Home")); ``` @@ -107,15 +107,15 @@ Also, it might seem weird that we are passing a function here. Why not just pass We are then going to use the `AsyncHome` component in our routes. React Router will create the `AsyncHome` component when the route is matched and that will in turn dynamically import the `Home` component and continue just like before. -``` coffee +```jsx ``` Now let's go back to our Notes project and apply these changes. -Your `src/Routes.js` should look like this after the changes. +{%change%} Your `src/Routes.js` should look like this after the changes. -``` coffee +```jsx import React from "react"; import { Route, Switch } from "react-router-dom"; import asyncComponent from "./components/AsyncComponent"; @@ -130,14 +130,9 @@ const AsyncSignup = asyncComponent(() => import("./containers/Signup")); const AsyncNewNote = asyncComponent(() => import("./containers/NewNote")); const AsyncNotFound = asyncComponent(() => import("./containers/NotFound")); -export default ({ childProps }) => +export default ({ childProps }) => ( - + {/* Finally, catch all unmatched routes */} -; +); ``` It is pretty cool that with just a couple of changes, our app is all set up for code splitting. And without adding a whole lot more complexity either! Here is what our `src/Routes.js` looked like before. -``` coffee +```jsx import React from "react"; import { Route, Switch } from "react-router-dom"; import AppliedRoute from "./components/AppliedRoute"; @@ -184,14 +179,9 @@ import Signup from "./containers/Signup"; import NewNote from "./containers/NewNote"; import NotFound from "./containers/NotFound"; -export default ({ childProps }) => +export default ({ childProps }) => ( - + {/* Finally, catch all unmatched routes */} -; +); ``` Notice that instead of doing the static imports for all the containers at the top, we are creating these functions that are going to do the dynamic imports for us when necessary. @@ -228,9 +218,9 @@ Now if you build your app using `npm run build`; you'll see the code splitting i ![Create React App Code Splitting build screenshot](/assets/create-react-app-code-splitting-build.png) -Each of those `.chunk.js` files are the different dynamic `import()` calls that we have. Of course, our app is quite small and the various parts that are split up are not significant at all. However, if the page that we use to edit our note included a rich text editor; you can imagine how that would grow in size. And it would unfortunately affect the initial load time of our app. +Each of those `.chunk.js` files are the different dynamic `import()` calls that we have. Of course, our app is quite small and the various parts that are split up are not significant at all. However, if the page that we use to edit our note included a rich text editor; you can imagine how that would grow in size. And it would unfortunately affect the initial load time of our app. -Now if we deploy our app using `npm run deploy`; you can see the browser load the different chunks on-demand as we browse around in the [demo](https://demo.serverless-stack.com). +Now if we deploy our app using `npx sst deploy`; you can see the browser load the different chunks on-demand as we browse around in the [demo](https://demo.sst.dev). ![Create React App loading Code Splitting screenshot](/assets/create-react-app-loading-code-splitting.png) @@ -244,23 +234,23 @@ It was mentioned above that you can add a loading spinner while the import is in All you need to do to use it is install it. -``` bash +```bash $ npm install --save react-loadable ``` Use it instead of the `asyncComponent` that we had above. -``` coffee +```jsx const AsyncHome = Loadable({ loader: () => import("./containers/Home"), - loading: MyLoadingComponent + loading: MyLoadingComponent, }); ``` And `AsyncHome` is used exactly as before. Here the `MyLoadingComponent` would look something like this. -``` coffee -const MyLoadingComponent = ({isLoading, error}) => { +```jsx +const MyLoadingComponent = ({ isLoading, error }) => { // Handle the loading state if (isLoading) { return
Loading...
; @@ -268,8 +258,7 @@ const MyLoadingComponent = ({isLoading, error}) => { // Handle the error state else if (error) { return
Sorry, there was a problem loading the page.
; - } - else { + } else { return null; } }; diff --git a/_chapters/cognito-user-pool-vs-identity-pool.md b/_archives/cognito-user-pool-vs-identity-pool.md similarity index 98% rename from _chapters/cognito-user-pool-vs-identity-pool.md rename to _archives/cognito-user-pool-vs-identity-pool.md index 5fd9994079..3ac0a71ef3 100644 --- a/_chapters/cognito-user-pool-vs-identity-pool.md +++ b/_archives/cognito-user-pool-vs-identity-pool.md @@ -5,8 +5,8 @@ date: 2017-01-05 12:00:00 lang: en ref: cognito-user-pool-vs-identity-pool description: Amazon Cognito User Pool is a service that helps manage your users and the sign-up and sign-in functionality for your mobile or web app. Cognito Identity Pool or Cognito Federated Identities is a service that uses identity providers (like Google, Facebook, or Cognito User Pool) to secure access to other AWS resources. -context: true comments_id: cognito-user-pool-vs-identity-pool/146 +redirect_from: /chapters/cognito-user-pool-vs-identity-pool.html --- We often get questions about the differences between the Cognito User Pool and the Identity Pool, so it is worth covering in detail. The two can seem a bit similar in function and it is not entirely clear what they are for. Let's first start with the official definitions. diff --git a/_chapters/configure-cognito-identity-pool-in-serverless.md b/_archives/configure-cognito-identity-pool-in-serverless.md similarity index 76% rename from _chapters/configure-cognito-identity-pool-in-serverless.md rename to _archives/configure-cognito-identity-pool-in-serverless.md index d794984598..0211e8cba3 100644 --- a/_chapters/configure-cognito-identity-pool-in-serverless.md +++ b/_archives/configure-cognito-identity-pool-in-serverless.md @@ -1,19 +1,21 @@ --- layout: post -title: Configure Cognito Identity Pool in Serverless +title: Configure Cognito Identity Pool in serverless date: 2018-03-02 00:00:00 lang: en description: We can define our Cognito Identity Pool using the Infrastructure as Code pattern by using CloudFormation in our serverless.yml. We are going to set the User Pool as the Cognito Identity Provider. And define the Auth Role with a policy allowing access to our S3 Bucket and API Gateway endpoint. -context: true ref: configure-cognito-identity-pool-in-serverless comments_id: configure-cognito-identity-pool-in-serverless/165 +redirect_from: + - /chapters/configure-cognito-identity-pool-in-serverless.html + - /chapters/cognito-as-a-serverless-service.html --- -If you recall from the first part of tutorial, we use the Cognito Identity Pool as a way to control which AWS resources our logged in users will have access to. And also tie in our Cognito User Pool as our authentication provider. +If you recall from the earlier part of this section, we used the Cognito Identity Pool as a way to control which AWS resources our logged-in users will have access to. We also tie in our Cognito User Pool as our authentication provider. ### Create the Resource -Add the following to `resources/cognito-identity-pool.yml`. +{%change%} Add the following to `resources/cognito-identity-pool.yml`. ``` yml Resources: @@ -110,19 +112,19 @@ Outputs: Ref: CognitoIdentityPool ``` -Now it looks like there is a whole lot going on here. But it is pretty much exactly what we did back in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}) chapter. It's just that CloudFormation can be a bit verbose and can end up looking a bit intimidating. +While it looks like there's a whole lot going on here, it's pretty much exactly what we did back in the [Create a Cognito identity pool]({% link _archives/create-a-cognito-identity-pool.md %}) chapter. It's just that CloudFormation can be a bit verbose and can end up looking a bit intimidating. Let's quickly go over the various sections of this configuration: 1. First we name our Identity Pool based on the stage name using `${self:custom.stage}`. -2. We set that we only want logged in users by adding `AllowUnauthenticatedIdentities: false`. +2. We specify that we only want logged in users by adding `AllowUnauthenticatedIdentities: false`. -3. Next we state that we want to use our User Pool as the identity provider. We are doing this specifically using the `Ref: CognitoUserPoolClient` line. If you refer back to the [Configure Cognito User Pool in Serverless]({% link _chapters/configure-cognito-user-pool-in-serverless.md %}) chapter, you'll notice we have a block under `CognitoUserPoolClient` that we are referencing here. +3. Next we state that we want to use our User Pool as the identity provider. We are doing this specifically using the `Ref: CognitoUserPoolClient` line. If you refer back to the [Configure Cognito User Pool in Serverless]({% link _archives/configure-cognito-user-pool-in-serverless.md %}) chapter, you'll notice we have a block under `CognitoUserPoolClient` that we are referencing here. -4. We then attach a IAM role to our authenticated users. +4. We then attach an IAM role to our authenticated users. -5. We add the various parts to this role. This is exactly what we use in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}) chapter. It just needs to be formatted this way to work with CloudFormation. +5. We add the various parts to this role. This is exactly what we use in the [Create a Cognito identity pool]({% link _archives/create-a-cognito-identity-pool.md %}) chapter. It just needs to be formatted this way to work with CloudFormation. 6. The `ApiGatewayRestApi` ref that you might notice is generated by Serverless Framework when you define an API endpoint in your `serverless.yml`. So in this case, we are referencing the API resource that we are creating. @@ -132,13 +134,11 @@ Let's quickly go over the various sections of this configuration: ### Add the Resource -Let's reference the resource in our `serverless.yml`. Replace your `resources:` block with the following. +{%change%} Let's reference the resource in our `serverless.yml`. Replace your `resources:` block with the following. ``` yml # Create our resources with separate CloudFormation templates resources: - # API Gateway Errors - - ${file(resources/api-gateway-errors.yml)} # DynamoDB - ${file(resources/dynamodb-table.yml)} # S3 @@ -148,13 +148,4 @@ resources: - ${file(resources/cognito-identity-pool.yml)} ``` -### Commit Your Code - -Let's commit the changes we've made so far. - -``` bash -$ git add . -$ git commit -m "Adding our Cognito Identity Pool resource" -``` - -Next, let's quickly reference our DynamoDB table in our Lambda functions using environment variables. +Now we are almost ready to deploy our new serverless infrastructure. We are going to add one more resource to the mix. It'll make it easier for us to debug CORS errors on the frontend. diff --git a/_chapters/configure-cognito-user-pool-in-serverless.md b/_archives/configure-cognito-user-pool-in-serverless.md similarity index 78% rename from _chapters/configure-cognito-user-pool-in-serverless.md rename to _archives/configure-cognito-user-pool-in-serverless.md index 2fa357d14a..72336f2aaf 100644 --- a/_chapters/configure-cognito-user-pool-in-serverless.md +++ b/_archives/configure-cognito-user-pool-in-serverless.md @@ -1,19 +1,19 @@ --- layout: post -title: Configure Cognito User Pool in Serverless +title: Configure Cognito User Pool in serverless date: 2018-03-01 00:00:00 lang: en description: We can define our Cognito User Pool using the Infrastructure as Code pattern by using CloudFormation in our serverless.yml. We are going to set the User Pool and App Client name based on the stage we are deploying to. We will also output the User Pool and App Client Id. -context: true ref: configure-cognito-user-pool-in-serverless comments_id: configure-cognito-user-pool-in-serverless/164 +redirect_from: /chapters/configure-cognito-user-pool-in-serverless.html --- -Now let's look into setting up Cognito User Pool through the `serverless.yml`. It should be very similar to the one we did by hand in the [Create a Cognito user pool]({% link _chapters/create-a-cognito-user-pool.md %}) chapter. +Now let's look into setting up Cognito User Pool through the `serverless.yml`. It should be very similar to the one we did by hand in the [Create a Cognito user pool]({% link _archives/create-a-cognito-user-pool.md %}) chapter. ### Create the Resource -Add the following to `resources/cognito-user-pool.yml`. +{%change%} Add the following to `resources/cognito-user-pool.yml`. ``` yml Resources: @@ -60,13 +60,11 @@ Let's quickly go over what we are doing here: ### Add the Resource -Let's reference the resource in our `serverless.yml`. Replace your `resources:` block with the following. +{%change%} Let's reference the resource in our `serverless.yml`. Replace your `resources:` block with the following. ``` yml # Create our resources with separate CloudFormation templates resources: - # API Gateway Errors - - ${file(resources/api-gateway-errors.yml)} # DynamoDB - ${file(resources/dynamodb-table.yml)} # S3 @@ -75,13 +73,4 @@ resources: - ${file(resources/cognito-user-pool.yml)} ``` -### Commit Your Code - -Let's commit the changes we've made so far. - -``` bash -$ git add . -$ git commit -m "Adding our Cognito User Pool resource" -``` - And next let's tie all of this together by configuring our Cognito Identity Pool. diff --git a/_chapters/configure-dynamodb-in-serverless.md b/_archives/configure-dynamodb-in-serverless.md similarity index 72% rename from _chapters/configure-dynamodb-in-serverless.md rename to _archives/configure-dynamodb-in-serverless.md index e42ade215b..8e5ecbcaef 100644 --- a/_chapters/configure-dynamodb-in-serverless.md +++ b/_archives/configure-dynamodb-in-serverless.md @@ -1,19 +1,29 @@ --- layout: post -title: Configure DynamoDB in Serverless +title: Configure DynamoDB in serverless date: 2018-02-27 00:00:00 lang: en description: We can define our DynamoDB table using the Infrastructure as Code pattern by using CloudFormation in our serverless.yml. We are going to define the AttributeDefinitions, KeySchema, and ProvisionedThroughput. -context: true ref: configure-dynamodb-in-serverless comments_id: configure-dynamodb-in-serverless/162 +redirect_from: + - /chapters/configure-dynamodb-in-serverless.html + - /chapters/dynamodb-as-a-serverless-service.html --- -We are now going to start creating our resources through our `serverless.yml`. Starting with DynamoDB. +For our Serverless Framework app, we had [previously created our DynamoDB table through the AWS console]({% link _archives/create-a-dynamodb-table.md %}). This can be hard to do when you are creating multiple apps or environments. Ideally, we want to be able to do this programmatically. In this section we'll look at how to use [infrastructure as code]({% link _chapters/what-is-infrastructure-as-code.md %}) to do just that. ### Create the Resource -Add the following to `resources/dynamodb-table.yml`. +[Serverless Framework](https://serverless.com) supports [CloudFormation](https://aws.amazon.com/cloudformation/) to help us configure our infrastructure through code. CloudFormation is a way to define our AWS resources using YAML or JSON, instead of having to use the AWS Console. We'll go into this in more detail later in this section. + +{%change%} Let’s create a directory to add our resources. + +``` bash +$ mkdir resources/ +``` + +{%change%} Add the following to `resources/dynamodb-table.yml`. ``` yml Resources: @@ -39,9 +49,9 @@ Let's quickly go over what we are doing here. 1. We are describing a DynamoDB table resource called `NotesTable`. -2. The table we get from a custom variable `${self:custom.tableName}`. This is generated dynamically in our `serverless.yml`. We will look at this in detail below. +2. We get the table name from the custom variable `${self:custom.tableName}`. This is generated dynamically in our `serverless.yml`. We will look at this in detail below. -3. We are also configuring the two attributes of our table as `userId` and `noteId`. +3. We are also configuring the two attributes of our table as `userId` and `noteId`and specifying them as our primary key. 4. Finally, we are provisioning the read/write capacity for our table through a couple of custom variables as well. We will be defining this shortly. @@ -49,18 +59,16 @@ Let's quickly go over what we are doing here. Now let's add a reference to this resource in our project. -Replace the `resources:` block at the bottom of our `serverless.yml` with the following: +{%change%} Add the following `resources:` block to the bottom of our `serverless.yml` with the following: ``` yml # Create our resources with separate CloudFormation templates resources: - # API Gateway Errors - - ${file(resources/api-gateway-errors.yml)} # DynamoDB - ${file(resources/dynamodb-table.yml)} ``` -Add the following `custom:` block at the top of our `serverless.yml` above the `provider:` block. +{%change%} Add the following `custom:` block at the top of our `serverless.yml` above the `provider:` block. ``` yml custom: @@ -73,24 +81,29 @@ custom: We added a couple of things here that are worth spending some time on: -- We first create a custom variable called `stage`. You might be wondering why we need a custom variable for this when we already have `stage: dev` in the `provider:` block. This is because we want to set the current stage of our project based on what is set through the `serverless deploy --stage $STAGE` command. And if a stage is not set when we deploy, we want to fallback to the one we have set in the provider block. So `${opt:stage, self:provider.stage}`, is telling Serverless to first look for the `opt:stage` (the one passed in through the command line), and then fallback to `self:provider.stage` (the one in the provider block. +- We first create a custom variable called `stage`. You might be wondering why we need a custom variable for this when we already have `stage: dev` in the `provider:` block. This is because we want to set the current stage of our project based on what is set through the `serverless deploy --stage $STAGE` command. And if a stage is not set when we deploy, we want to fallback to the one we have set in the provider block. So `${opt:stage, self:provider.stage}`, is telling Serverless Framework to first look for the `opt:stage` (the one passed in through the command line), and then fallback to `self:provider.stage` (the one in the provider block). - The table name is based on the stage we are deploying to - `${self:custom.stage}-notes`. The reason this is dynamically set is because we want to create a separate table when we deploy to a new stage (environment). So when we deploy to `dev` we will create a DynamoDB table called `dev-notes` and when we deploy to `prod`, it'll be called `prod-notes`. This allows us to clearly separate the resources (and data) we use in our various environments. -- Finally, we are using the `PAY_PER_REQUEST` setting for the `BillingMode`. This tells DynamoDB that we want to pay per request and use the [On-Demand Capacity](https://aws.amazon.com/dynamodb/pricing/on-demand/) option. With DynamoDB in On-Demand mode, our database is now truly Serverless. This option can be very cost-effective, especially if you are just starting out and your workloads are not very predictable or stable. On the other hand, if you know exactly how much capacity you need, the [Provisioned Capacity](https://aws.amazon.com/dynamodb/pricing/provisioned/) mode would work out to be cheaper. +- Finally, we are using the `PAY_PER_REQUEST` setting for the `BillingMode`. This tells DynamoDB that we want to pay per request and use the [On-Demand Capacity](https://aws.amazon.com/dynamodb/pricing/on-demand/) option. With DynamoDB in On-Demand mode, our database is now truly serverless. This option can be very cost-effective, especially if you are just starting out and your workloads are not very predictable or stable. On the other hand, if you know exactly how much capacity you need, the [Provisioned Capacity](https://aws.amazon.com/dynamodb/pricing/provisioned/) mode would work out to be cheaper. A lot of the above might sound tricky and overly complicated right now. But we are setting it up so that we can automate and replicate our entire setup with ease. Note that, Serverless Framework (and CloudFormation behind the scenes) will be completely managing our resources based on the `serverless.yml`. This means that if you have a typo in your table name, the old table will be removed and a new one will be created in place. To prevent accidentally deleting serverless resources (like DynamoDB tables), you need to set the `DeletionPolicy: Retain` flag. We have a [detailed post on this over on the Seed blog](https://seed.run/blog/how-to-prevent-accidentally-deleting-serverless-resources). We are also going to make a quick tweak to reference the DynamoDB resource that we are creating. -Replace the `iamRoleStatements:` block in your `serverless.yml` with the following. +{%change%} Update our environment variables with the new generated table name. Replace the `environment:` block with the following: ``` yml # These environment variables are made available to our functions # under process.env. environment: tableName: ${self:custom.tableName} + stripeSecretKey: ${env:STRIPE_SECRET_KEY} +``` + +{%change%} Replace the `iamRoleStatements:` block in your `serverless.yml` with the following. +``` yml iamRoleStatements: - Effect: Allow Action: @@ -117,13 +130,4 @@ A couple of interesting things we are doing here: 3. For the case of our `iamRoleStatements:` we are now specifically stating which table we want to connect to. This block is telling AWS that these are the only resources that our Lambda functions have access to. -### Commit Your Code - -Let's commit the changes we've made so far. - -``` bash -$ git add . -$ git commit -m "Adding our DynamoDB resource" -``` - Next, let's add our S3 bucket for file uploads. diff --git a/_chapters/configure-multiple-aws-profiles.md b/_archives/configure-multiple-aws-profiles.md similarity index 96% rename from _chapters/configure-multiple-aws-profiles.md rename to _archives/configure-multiple-aws-profiles.md index 71ac31e5d2..8f8e903a9b 100644 --- a/_chapters/configure-multiple-aws-profiles.md +++ b/_archives/configure-multiple-aws-profiles.md @@ -1,10 +1,10 @@ --- layout: post title: Configure Multiple AWS Profiles -description: To use multiple IAM credentials to deploy your Serverless application you need to create a new AWS CLI profile. On local set the default AWS profile using the AWS_PROFILE bash variable. To deploy using your new profile use the "--aws-profile" option for the "serverless deploy" command. Alternatively, you can use the "profile:" setting in your serverless.yml. +description: To use multiple IAM credentials to deploy your serverless application you need to create a new AWS CLI profile. On local set the default AWS profile using the AWS_PROFILE bash variable. To deploy using your new profile use the "--aws-profile" option for the "serverless deploy" command. Alternatively, you can use the "profile:" setting in your serverless.yml. date: 2018-04-07 00:00:00 -context: true comments_id: configure-multiple-aws-profiles/21 +redirect_from: /chapters/configure-multiple-aws-profiles.html --- When we configured our AWS CLI in the [Configure the AWS CLI]({% link _chapters/configure-the-aws-cli.md %}) chapter, we used the `aws configure` command to set the IAM credentials of the AWS account we wanted to use to deploy our serverless application to. @@ -74,7 +74,7 @@ provider: profile: newAccount ``` -Note the `profile: newAccount` line here. This is telling Serverless to use the `newAccount` profile while running `serverless deploy`. +Note the `profile: newAccount` line here. This is telling Serverless Framework to use the `newAccount` profile while running `serverless deploy`. ### Set Profiles per Stage @@ -118,7 +118,7 @@ There are a couple of things happening here. - We also defined `custom.myProfile`, which contains the AWS profiles we want to use to deploy for each stage. Just as before we want to use the `prodAccount` profile if we are deploying to stage `prod` and the `devAccount` profile if we are deploying to stage `dev`. - Finally, we set the `provider.profile` to `${self:custom.myProfile.${self:custom.myStage}}`. This picks the value of our profile depending on the current stage defined in `custom.myStage`. -We used the concept of variables in Serverless Framework in this example. You can read more about this in the chapter on [Serverless Environment Variables]({% link _chapters/serverless-environment-variables.md %}). +We used the concept of variables in Serverless Framework in this example. You can read more about this in the chapter on [Serverless Environment Variables]({% link _archives/serverless-environment-variables.md %}). Now, when you deploy to production, Serverless Framework is going to use the `prodAccount` profile. And the resources will be provisioned inside `prodAccount` profile user's AWS account. diff --git a/_chapters/configure-s3-in-serverless.md b/_archives/configure-s3-in-serverless.md similarity index 64% rename from _chapters/configure-s3-in-serverless.md rename to _archives/configure-s3-in-serverless.md index 5ff663e02f..e906553239 100644 --- a/_chapters/configure-s3-in-serverless.md +++ b/_archives/configure-s3-in-serverless.md @@ -1,19 +1,21 @@ --- layout: post -title: Configure S3 in Serverless +title: Configure S3 in serverless date: 2018-02-28 00:00:00 lang: en description: We can define our S3 Buckets using the Infrastructure as Code pattern by using CloudFormation in our serverless.yml. We are going to set the CORS policy and output the name of the bucket that's created. -context: true ref: configure-s3-in-serverless comments_id: configure-s3-in-serverless/163 +redirect_from: + - /chapters/configure-s3-in-serverless.html + - /chapters/s3-as-a-serverless-service.html --- Now that we have DynamoDB configured, let's look at how we can configure the S3 file uploads bucket through our `serverless.yml`. ### Create the Resource -Add the following to `resources/s3-bucket.yml`. +{%change%} Add the following to `resources/s3-bucket.yml`. ``` yml Resources: @@ -43,32 +45,21 @@ Outputs: Ref: AttachmentsBucket ``` -If you recall from the [Create an S3 bucket for file uploads]({% link _chapters/create-an-s3-bucket-for-file-uploads.md %}) chapter, we had created a bucket and configured the CORS policy for it. We needed to do this because we are going to be uploading directly from our frontend client. We configure the same policy here. +If you recall from the [Create an S3 bucket for file uploads]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}) chapter, we had created a bucket and configured the CORS policy for it. We needed to do this because we are going to be uploading directly from our frontend client. We configure the same policy here. -S3 buckets (unlike DynamoDB tables) are globally named. So it is not really possible for us to know what it is going to be called before hand. Hence, we let CloudFormation generate the name for us and we just add the `Outputs:` block to tell it to print it out so we can use it later. +S3 buckets (unlike DynamoDB tables) are globally named, so it is not really possible for us to know what our bucket is going to be called beforehand. Hence, we let CloudFormation generate the name for us and we just add the `Outputs:` block to tell it to print it out so we can use it later. ### Add the Resource -Let's reference the resource in our `serverless.yml`. Replace your `resources:` block with the following. +{%change%} Let's reference the resource in our `serverless.yml`. Replace your `resources:` block with the following. ``` yml # Create our resources with separate CloudFormation templates resources: - # API Gateway Errors - - ${file(resources/api-gateway-errors.yml)} # DynamoDB - ${file(resources/dynamodb-table.yml)} # S3 - ${file(resources/s3-bucket.yml)} ``` -### Commit Your Code - -Let's commit the changes we've made so far. - -``` bash -$ git add . -$ git commit -m "Adding our S3 resource" -``` - And that's it. Next let's look into configuring our Cognito User Pool. diff --git a/_chapters/connect-to-api-gateway-with-iam-auth.md b/_archives/connect-to-api-gateway-with-iam-auth.md similarity index 90% rename from _chapters/connect-to-api-gateway-with-iam-auth.md rename to _archives/connect-to-api-gateway-with-iam-auth.md index 78def1853f..b37b50b439 100644 --- a/_chapters/connect-to-api-gateway-with-iam-auth.md +++ b/_archives/connect-to-api-gateway-with-iam-auth.md @@ -3,12 +3,11 @@ layout: post title: Connect to API Gateway with IAM Auth description: For our React.js app to make requests to a serverless backend API secured using AWS IAM, we need to sign our requests using Signature Version 4. But to be able to do that we need to use our User Pool user token and get temporary IAM credentials from our Identity Pool. Using these temporary IAM credentials we can then generate the Signature Version 4 security headers and make a request using HTTP fetch. date: 2018-04-11 00:00:00 -context: true comments_id: 113 comments_id: comments-signup-with-aws-cognito/113 +redirect_from: /chapters/connect-to-api-gateway-with-iam-auth.html --- - Connecting to an API Gateway endpoint secured using AWS IAM can be challenging. You need to sign your requests using [Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). You can use: - [Generated API Gateway SDK](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-generate-sdk.html) @@ -18,7 +17,7 @@ The generated SDK can be hard to use since you need to re-generate it every time However if you are looking to simply connect to API Gateway using the AWS JS SDK, we've create a standalone [**`sigV4Client.js`**](https://github.com/AnomalyInnovations/sigV4Client) that you can use. It is based on the client that comes pre-packaged with the generated SDK. -In this chapter we'll go over how to use the the `sigV4Client.js`. The basic flow looks like this: +In this chapter we'll go over how to use the `sigV4Client.js`. The basic flow looks like this: 1. Authenticate a user with Cognito User Pool and acquire a user token. 2. With the user token get temporary IAM credentials from the Identity Pool. @@ -28,11 +27,11 @@ In this chapter we'll go over how to use the the `sigV4Client.js`. The basic flo The following method can authenticate a user to Cognito User Pool. -``` js +```js function login(username, password) { const userPool = new CognitoUserPool({ UserPoolId: USER_POOL_ID, - ClientId: APP_CLIENT_ID + ClientId: APP_CLIENT_ID, }); const user = new CognitoUser({ Username: username, Pool: userPool }); const authenticationData = { Username: username, Password: password }; @@ -40,8 +39,8 @@ function login(username, password) { return new Promise((resolve, reject) => user.authenticateUser(authenticationDetails, { - onSuccess: result => resolve(), - onFailure: err => reject(err) + onSuccess: (result) => resolve(), + onFailure: (err) => reject(err), }) ); } @@ -49,18 +48,18 @@ function login(username, password) { Ensure to use your `USER_POOL_ID` and `APP_CLIENT_ID`. And given their Cognito `username` and `password` you can log a user in by calling: -``` js -await login('my_username', 'my_password'); +```js +await login("my_username", "my_password"); ``` ### Generate Temporary IAM Credentials Once your user is authenticated you can generate a set of temporary credentials. To do so you need to first get their JWT user token using the following: -``` js +```js function getUserToken(currentUser) { return new Promise((resolve, reject) => { - currentUser.getSession(function(err, session) { + currentUser.getSession(function (err, session) { if (err) { reject(err); return; @@ -73,11 +72,11 @@ function getUserToken(currentUser) { Where you can get the current logged in user using: -``` js +```js function getCurrentUser() { const userPool = new CognitoUserPool({ UserPoolId: config.cognito.USER_POOL_ID, - ClientId: config.cognito.APP_CLIENT_ID + ClientId: config.cognito.APP_CLIENT_ID, }); return userPool.getCurrentUser(); } @@ -85,18 +84,17 @@ function getCurrentUser() { And with the JWT token you can generate their temporary IAM credentials using: -``` coffee +```jsx function getAwsCredentials(userToken) { - const authenticator = `cognito-idp.${config.cognito - .REGION}.amazonaws.com/${config.cognito.USER_POOL_ID}`; + const authenticator = `cognito-idp.${config.cognito.REGION}.amazonaws.com/${config.cognito.USER_POOL_ID}`; AWS.config.update({ region: config.cognito.REGION }); AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: config.cognito.IDENTITY_POOL_ID, Logins: { - [authenticator]: userToken - } + [authenticator]: userToken, + }, }); return AWS.config.credentials.getPromise(); @@ -109,7 +107,7 @@ The `sigV4Client.js` needs [**crypto-js**](https://github.com/brix/crypto-js) in Install it by running the following in your project root. -``` bash +```bash $ npm install crypto-js --save ``` @@ -119,7 +117,7 @@ And to use the `sigV4Client.js` simply copy it over to your project. This file can look a bit intimidating at first but it is just using the temporary credentials and the request parameters to create the necessary signed headers. To create a new `sigV4Client` we need to pass in the following: -``` javascript +```js // Pseudocode sigV4Client.newClient({ @@ -132,13 +130,13 @@ sigV4Client.newClient({ // API Gateway region region, // API Gateway URL - endpoint + endpoint, }); ``` And to sign a request you need to use the `signRequest` method and pass in: -``` javascript +```js // Pseudocode const signedRequest = client.signRequest({ @@ -151,7 +149,7 @@ const signedRequest = client.signRequest({ // The request query parameters queryParams, // The request body - body + body, }); ``` @@ -161,7 +159,7 @@ And `signedRequest.headers` should give you the signed headers that you need to Let's put it all together. The following gives you a simple helper function to call an API Gateway endpoint. -``` js +```js function invokeApig({ path, method = "GET", diff --git a/_chapters/create-a-cloudfront-distribution.md b/_archives/create-a-cloudfront-distribution.md similarity index 97% rename from _chapters/create-a-cloudfront-distribution.md rename to _archives/create-a-cloudfront-distribution.md index b465db71a1..aad08dcecd 100644 --- a/_chapters/create-a-cloudfront-distribution.md +++ b/_archives/create-a-cloudfront-distribution.md @@ -4,9 +4,9 @@ title: Create a CloudFront Distribution date: 2017-02-08 00:00:00 lang: en description: To server out our React.js app hosted on Amazon S3 through a CDN we are going to use CloudFront. We will create a CloudFront Distribution and point it to our S3 Bucket. We are also going to enable Gzip compression using the “Compress Objects Automatically” setting in the AWS console. And to ensure that our React.js app responds with the right HTTP headers, we will create a Custom Error Response. -context: true comments_id: create-a-cloudfront-distribution/104 ref: create-a-cloudfront-distribution +redirect_from: /chapters/create-a-cloudfront-distribution.html --- Now that we have our app up and running on S3, let's serve it out globally through CloudFront. To do this we need to create an AWS CloudFront Distribution. @@ -73,4 +73,4 @@ Pick **404** for the **HTTP Error Code** and select **Customize Error Response** And hit **Create**. This is basically telling CloudFront to respond to any 404 responses from our S3 bucket with the `index.html` and a 200 status code. Creating a custom error response should take a couple of minutes to complete. -Next up, let's point our domain to our CloudFront Distribution. +Next up, let's point a custom domain to our CloudFront Distribution. And setup SSL. diff --git a/_chapters/create-a-cognito-identity-pool.md b/_archives/create-a-cognito-identity-pool.md similarity index 79% rename from _chapters/create-a-cognito-identity-pool.md rename to _archives/create-a-cognito-identity-pool.md index 2563610a10..62c4d064d3 100644 --- a/_chapters/create-a-cognito-identity-pool.md +++ b/_archives/create-a-cognito-identity-pool.md @@ -5,8 +5,8 @@ date: 2017-01-05 00:00:00 lang: en ref: create-a-cognito-identity-pool description: Amazon Cognito Federated Identities helps us secure our AWS resources. We can use the Cognito User Pool as an identity provider for our serverless backend. To allow users to be able to upload files to our S3 bucket and connect to API Gateway we need to create an Identity Pool. We will assign it an IAM Policy with the name of our S3 bucket and prefix our files with the cognito-identity.amazonaws.com:sub. And we’ll add our API Gateway endpoint as a resource as well. -context: true comments_id: create-a-cognito-identity-pool/135 +redirect_from: /chapters/create-a-cognito-identity-pool.html --- Now that we have deployed our backend API; we almost have all the pieces we need for our backend. We have the User Pool that is going to store all of our users and help sign in and sign them up. We also have an S3 bucket that we will use to help our users upload files as attachments for their notes. The final piece that ties all these services together in a secure way is called Amazon Cognito Federated Identities. @@ -31,7 +31,7 @@ Enter an **Identity pool name**. If you have any existing Identity Pools, you'll ![Fill Cognito Identity Pool Info Screenshot](/assets/cognito-identity-pool/fill-identity-pool-info.png) -Select **Authentication providers**. Under **Cognito** tab, enter **User Pool ID** and **App Client ID** of the User Pool created in the [Create a Cognito user pool]({% link _chapters/create-a-cognito-user-pool.md %}) chapter. Select **Create Pool**. +Select **Authentication providers**. Under **Cognito** tab, enter **User Pool ID** and **App Client ID** of the User Pool created in the [Create a Cognito user pool]({% link _archives/create-a-cognito-user-pool.md %}) chapter. Select **Create Pool**. ![Fill Authentication Provider Info Screenshot](/assets/cognito-identity-pool/fill-authentication-provider-info.png) @@ -47,7 +47,7 @@ It will warn you to read the documentation. Select **Ok** to edit. ![Select Confirm Edit Policy Screenshot](/assets/cognito-identity-pool/select-confirm-edit-policy.png) -Add the following policy into the editor. Replace `YOUR_S3_UPLOADS_BUCKET_NAME` with the **bucket name** from the [Create an S3 bucket for file uploads]({% link _chapters/create-an-s3-bucket-for-file-uploads.md %}) chapter. And replace the `YOUR_API_GATEWAY_REGION` and `YOUR_API_GATEWAY_ID` with the ones that you get after you deployed your API in the last chapter. +{%change%} Add the following policy into the editor. Replace `YOUR_S3_UPLOADS_BUCKET_NAME` with the **bucket name** from the [Create an S3 bucket for file uploads]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}) chapter. And replace the `YOUR_API_GATEWAY_REGION` and `YOUR_API_GATEWAY_ID` with the ones that you got back in the [Deploy the APIs]({% link _archives/deploy-the-apis.md %}) chapter. In our case `YOUR_S3_UPLOADS_BUCKET_NAME` is `notes-app-uploads`, `YOUR_API_GATEWAY_ID` is `ly55wbovq4`, and `YOUR_API_GATEWAY_REGION` is `us-east-1`. @@ -88,16 +88,18 @@ In our case `YOUR_S3_UPLOADS_BUCKET_NAME` is `notes-app-uploads`, `YOUR_API_GATE } ``` -A quick note on the block that relates to the S3 Bucket. In the above policy we are granting our logged in users access to the path `private/${cognito-identity.amazonaws.com:sub/}/`. Where `cognito-identity.amazonaws.com:sub` is the authenticated user's federated identity ID (their user id). So a user has access to only their folder within the bucket. This is how we are securing the uploads for each user. +Once a user has been authenticated with our User Pool and verified with our Identity Pool, he/she is assigned this [IAM role]({% link _archives/what-is-iam.md %}). This role limits what our user has access to in our AWS account. + +A quick note on the block that relates to the S3 Bucket. In the above policy we are granting our logged in users access to the path `private/${cognito-identity.amazonaws.com:sub}/`. Where `cognito-identity.amazonaws.com:sub` is the authenticated user's federated identity ID (their user id). So a user has access to only their folder within the bucket. This is how we are securing the uploads for each user. So in summary we are telling AWS that an authenticated user has access to two resources. 1. Files in the S3 bucket that are inside a folder with their federated identity id as the name of the folder. 2. And, the APIs we deployed using API Gateway. -One other thing to note is that the federated identity id is a UUID that is assigned by our Identity Pool. This is the id (`event.requestContext.identity.cognitoIdentityId`) that we were using as our user id back when we were creating our APIs. +One other thing to note is that the federated identity id is a UUID that is assigned by our Identity Pool. This id is different from the one that a user is assigned in a User Pool. This is because you can have multiple authentication providers. The Identity Pool _federates_ these identities and gives each user a unique id. -Select **Allow**. +Next, select **Allow**. ![Submit Cognito Identity Pool Policy Screenshot](/assets/cognito-identity-pool/submit-identity-pool-policy.png) @@ -111,4 +113,4 @@ Take a note of the **Identity pool ID** which will be required in the later chap ![Cognito Identity Pool Created Screenshot](/assets/cognito-identity-pool/identity-pool-id.png) -Now before we test our serverless API let's take a quick look at the Cognito User Pool and Cognito Identity Pool and make sure we've got a good idea of the two concepts and the differences between them. +Now we are ready to use what we've created so far to secure access to our APIs. diff --git a/_chapters/create-a-cognito-test-user.md b/_archives/create-a-cognito-test-user.md similarity index 82% rename from _chapters/create-a-cognito-test-user.md rename to _archives/create-a-cognito-test-user.md index 3f6b0e4de7..f37f621180 100644 --- a/_chapters/create-a-cognito-test-user.md +++ b/_archives/create-a-cognito-test-user.md @@ -5,8 +5,8 @@ date: 2016-12-28 12:00:00 lang: en ref: create-a-cognito-test-user description: To test using the Cognito User Pool as an authorizer for our serverless API backend, we are going to create a test user. We can create a user from the AWS CLI using the aws cognito-idp sign-up and admin-confirm-sign-up command. -context: true comments_id: create-a-cognito-test-user/126 +redirect_from: /chapters/create-a-cognito-test-user.html --- In this chapter, we are going to create a test user for our Cognito User Pool. We are going to need this user to test the authentication portion of our app later. @@ -15,7 +15,7 @@ In this chapter, we are going to create a test user for our Cognito User Pool. W First, we will use AWS CLI to sign up a user with their email and password. -In your terminal, run. +{%change%} In your terminal, run. ``` bash $ aws cognito-idp sign-up \ @@ -27,7 +27,7 @@ $ aws cognito-idp sign-up \ Now, the user is created in Cognito User Pool. However, before the user can authenticate with the User Pool, the account needs to be verified. Let's quickly verify the user using an administrator command. -In your terminal, run. +{%change%} In your terminal, run. ``` bash $ aws cognito-idp admin-confirm-sign-up \ @@ -36,4 +36,4 @@ $ aws cognito-idp admin-confirm-sign-up \ --username admin@example.com ``` -Now our test user is ready. Next, let's set up the Serverless Framework to create our backend APIs. +Now that our User Pool has been configured and ready to use, let's create our Identity Pool to manage access to our AWS resources. diff --git a/_chapters/create-a-cognito-user-pool.md b/_archives/create-a-cognito-user-pool.md similarity index 87% rename from _chapters/create-a-cognito-user-pool.md rename to _archives/create-a-cognito-user-pool.md index 8227f01c44..afe3d65a0c 100644 --- a/_chapters/create-a-cognito-user-pool.md +++ b/_archives/create-a-cognito-user-pool.md @@ -5,8 +5,8 @@ date: 2016-12-28 00:00:00 lang: en ref: create-a-cognito-user-pool description: Amazon Cognito User Pool handles sign-up and sign-in functionality for web and mobile apps. We are going to create a Cognito User Pool to store and manage the users for our serverless app. We'll use the email address as username option since we want our users to login with their email. We are also going to set up our app as an App Client for our Cognito User Pool. -context: true comments_id: create-a-cognito-user-pool/148 +redirect_from: /chapters/create-a-cognito-user-pool.html --- Our notes app needs to handle user accounts and authentication in a secure and reliable way. To do this we are going to use [Amazon Cognito](https://aws.amazon.com/cognito/). @@ -69,11 +69,15 @@ Select **Add an app client**. Enter **App client name**, un-select **Generate client secret**, select **Enable sign-in API for server-based authentication**, then select **Create app client**. -- **Generate client secret**: user pool apps with a client secret are not supported by the JavaScript SDK. We need to un-select the option. -- **Enable sign-in API for server-based authentication**: required by AWS CLI when managing the pool users via command line interface. We will be creating a test user through the command line interface in the next chapter. +- **DISABLE client secret generation**: user pool apps with a client secret are not supported by the JavaScript SDK. We need to un-select the option. +- **Enable username password auth for admin APIs for authentication**: required by AWS CLI when managing the pool users via command line interface. We will be creating a test user through the command line interface in the next chapter. ![Fill Cognito User Pool App Info Screenshot](/assets/cognito-user-pool/fill-user-pool-app-info.png) +Now select **Create app client**. + +![Fill Cognito User Pool Create App Client Screenshot](/assets/cognito-user-pool/fill-user-pool-create-app-client.png) + Your app client has been created. Take note of the **App client id** which will be required in the later chapters. ![Cognito User Pool App Created Screenshot](/assets/cognito-user-pool/user-pool-app-created.png) diff --git a/_chapters/create-a-dynamodb-table.md b/_archives/create-a-dynamodb-table.md similarity index 62% rename from _chapters/create-a-dynamodb-table.md rename to _archives/create-a-dynamodb-table.md index 0643de40c0..27e45597a0 100644 --- a/_chapters/create-a-dynamodb-table.md +++ b/_archives/create-a-dynamodb-table.md @@ -5,15 +5,24 @@ date: 2016-12-27 00:00:00 lang: en ref: create-a-dynamodb-table description: Amazon DynamoDB is a fully managed NoSQL database that we are going to use to power our serverless API backend. DynamoDB stores data in tables and each table has a primary key that cannot be changed once set. We are also going to provision the throughput capacity by setting reads and writes for our DynamoDB table. -context: true comments_id: create-a-dynamodb-table/139 +redirect_from: /chapters/create-a-dynamodb-table.html --- -To build the backend for our notes app, it makes sense that we first start by thinking about how the data is going to be stored. We are going to use [DynamoDB](https://aws.amazon.com/dynamodb/) to do this. +We are going to build a REST API for our notes app. It's a simple [CRUD (create, read, update, and delete)](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) API. Meaning that we'll be performing these operations on our database. We also want our users to be able to upload files as attachments to their notes. + +So in this section we'll be creating a couple of AWS resources: + +1. A database +2. And a file storage service + +Let's first start by thinking about how the data is going to be stored. We are going to use [DynamoDB](https://aws.amazon.com/dynamodb/) to do this. ### About DynamoDB -Amazon DynamoDB is a fully managed NoSQL database that provides fast and predictable performance with seamless scalability. Similar to other databases, DynamoDB stores data in tables. Each table contains multiple items, and each item is composed of one or more attributes. We are going to cover some basics in the following chapters. But to get a better feel for it, here is a [great guide on DynamoDB](https://www.dynamodbguide.com). +Amazon DynamoDB is a fully managed NoSQL database that provides fast and predictable performance with seamless scalability. DynamoDB is also a serverless database, which means (as you guessed) it'll scale automatically and you only pay for what you use. + +Similar to other databases, DynamoDB stores data in tables. Each table contains multiple items, and each item is composed of one or more attributes. We are going to cover some basics in the following chapters. But to get a better feel for it, here is a [great guide on DynamoDB](https://www.dynamodbguide.com). ### Create Table @@ -23,7 +32,7 @@ First, log in to your [AWS Console](https://console.aws.amazon.com) and select * Select **Create table**. -![Create DynamoDB Table screenshot](/assets/dynamodb/create-dynamodb-table.png) +![Create DynamoDB Table screenshot](/assets/dynamodb/dynamodb-dashboard-create-table.png) Enter the **Table name** and **Primary key** info as shown below. Just make sure that `userId` and `noteId` are in camel case. @@ -42,12 +51,14 @@ Next scroll down and deselect **Use default settings**. ![Deselect Use default settings screenshot](/assets/dynamodb/deselect-use-default-settings.png) -Scroll down further and **On-demand** instead of **Provisioned**. +Scroll down further and select **On-demand** instead of **Provisioned**. ![Select On-Demand Capacity screenshot](/assets/dynamodb/select-on-demand-capacity.png) [On-Demand Capacity](https://aws.amazon.com/dynamodb/pricing/on-demand/) is DynamoDB's pay per request mode. For workloads that are not predictable or if you are just starting out, this ends up being a lot cheaper than the [Provisioned Capacity](https://aws.amazon.com/dynamodb/pricing/provisioned/) mode. +If the On-Demand Capacity option is missing and there is an info box containing the message "You do not have the required role to enable Auto Scaling by default", you can create the table and afterwards modify the setting from the "Capacity" tab of the table settings page. The role mentioned by the info box is automatically created by the table creation process. + Finally, scroll down and hit **Create**. ![Create DynamoDB table screenshot](/assets/dynamodb/create-dynamodb-table.png) @@ -56,8 +67,8 @@ The `notes` table has now been created. If you find yourself stuck with the **Ta ![Select DynamoDB Service screenshot](/assets/dynamodb/dynamodb-table-created.png) -It is also a good idea to set up backups for your DynamoDB table, especially if you are planning to use it in production. We cover this in an extra-credit chapter, [Backups in DynamoDB]({% link _chapters/backups-in-dynamodb.md %}). +It is also a good idea to set up backups for your DynamoDB table, especially if you are planning to use it in production. We cover this in an extra-credit chapter, [Backups in DynamoDB]({% link _archives/backups-in-dynamodb.md %}). -Next we'll set up an S3 bucket to handle file uploads. +Next let's look at how we are going to store the files that our users upload. [dynamodb-components]: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html diff --git a/_archives/create-a-netlify-build-script.md b/_archives/create-a-netlify-build-script.md new file mode 100644 index 0000000000..eb70738cbe --- /dev/null +++ b/_archives/create-a-netlify-build-script.md @@ -0,0 +1,57 @@ +--- +layout: post +title: Create a Netlify Build Script +date: 2018-03-26 00:00:00 +lang: en +description: To configure our Create React App with Netlify, we need to add a build script to our project root. To make sure that we return a HTTP status code of 200 for our React Router routes we will be adding a redirects rule. +ref: create-a-netlify-build-script +comments_id: create-a-build-script/189 +redirect_from: + - /chapters/create-a-netlify-build-script.html + - /chapters/create-a-build-script.html +--- + +To automate our React.js deployments with [Netlify](https://www.netlify.com) we just need to set up a build script. If you recall from the [previous chapter]({% link _archives/manage-environments-in-create-react-app.md %}), we had configured our app to use the `REACT_APP_STAGE` build environment variable. We are going to create a build script to tell Netlify to set this variable up for the different deployment cases. + +### Add the Netlify Build Script + +{%change%} Start by adding the following to a file called `netlify.toml` to your project root. + +``` toml +# Global settings applied to the whole site. +# “base” is directory to change to before starting build, and +# “publish” is the directory to publish (relative to root of your repo). +# “command” is your build command. + +[build] + base = "" + publish = "build" + command = "REACT_APP_STAGE=dev npm run build" + +# Production context: All deploys to the main +# repository branch will inherit these settings. +[context.production] + command = "REACT_APP_STAGE=prod npm run build" + +# Deploy Preview context: All Deploy Previews +# will inherit these settings. +[context.deploy-preview] + command = "REACT_APP_STAGE=dev npm run build" + +# Branch Deploy context: All deploys that are not in +# an active Deploy Preview will inherit these settings. +[context.branch-deploy] + command = "REACT_APP_STAGE=dev npm run build" +``` + +The build script is configured based on contexts. There is a default one right up top. There are three parts to this: + +1. The `base` is the directory where Netlify will run our build commands. In our case it is in the project root. So this is left empty. + +2. The `publish` option points to where our build is generated. In the case of Create React App it is the `build` directory in our project root. + +3. The `command` option is the build command that Netlify will use. If you recall the [Manage environments in Create React App]({% link _archives/manage-environments-in-create-react-app.md %}) chapter, this will seem familiar. In the default context the command is `REACT_APP_STAGE=dev npm run build`. + +The production context labelled, `context.production` is the only one where we set the `REACT_APP_STAGE` variable to `prod`. This is when we push to `master`. The `branch-deploy` is what we will be using when we push to any other non-production branch. The `deploy-preview` is for pull requests. + +If you commit and push your changes to Git, you'll see Netlify pick up your build script. diff --git a/_archives/create-an-s3-bucket-for-file-uploads.md b/_archives/create-an-s3-bucket-for-file-uploads.md new file mode 100644 index 0000000000..1ebcaddbec --- /dev/null +++ b/_archives/create-an-s3-bucket-for-file-uploads.md @@ -0,0 +1,85 @@ +--- +layout: post +title: Create an S3 Bucket for File Uploads +date: 2016-12-27 00:00:00 +lang: en +ref: create-an-s3-bucket-for-file-uploads +description: To allow users to upload files to our serverless app we are going to use Amazon S3 (Simple Storage Service). S3 allows you to store files and organize them into buckets. +comments_id: create-an-s3-bucket-for-file-uploads/150 +redirect_from: + - /chapters/create-an-s3-bucket-for-file-uploads.html + - /chapters/create-a-s3-bucket-for-file-uploads.html +--- + +Now that we have [our database table]({% link _archives/create-a-dynamodb-table.md %}) ready; let's get things set up for handling file uploads. We need to handle file uploads because each note can have an uploaded file as an attachment. + +[Amazon S3](https://aws.amazon.com/s3/) (Simple Storage Service) provides storage service through web services interfaces like REST. You can store any object in S3 including images, videos, files, etc. Objects are organized into buckets, and identified within each bucket by a unique, user-assigned key. + +In this chapter, we are going to create an S3 bucket which will be used to store user uploaded files from our notes app. + +### Create Bucket + +First, log in to your [AWS Console](https://console.aws.amazon.com) and select **S3** from the list of services. + +![Select S3 Service screenshot](/assets/s3/select-s3-service.png) + +Select **Create bucket**. + +![Select Create Bucket screenshot](/assets/s3/select-create-bucket.png) + +Pick a name of the bucket and select a region. Then select **Create**. + +- **Bucket names** are globally unique, which means you cannot pick the same name as this tutorial. +- **Region** is the physical geographical region where the files are stored. We will use **US East (N. Virginia)** for this guide. + +Make a note of the name and region as we'll be using it later in the guide. + +![Enter S3 Bucket Info screenshot](/assets/s3/enter-s3-bucket-info.png) + +Then scroll all the way down and click **Create bucket**. + +![Click create S3 Bucket screenshot](/assets/s3/click-create-s3-bucket.png) + +This should create your new S3 bucket. Before we move on, we need to make sure that our React.js frontend will be able to upload files to this bucket. Since it'll be hosted on a different domain, we need to enable [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing). + +Select the newly created bucket from the list. + +![Select Created S3 Bucket screenshot](/assets/s3/select-created-s3-bucket.png) + +Select the **Permissions** tab + +![Select S3 Bucket Permissions tab](/assets/s3/select-s3-bucket-permissions-tab.png) + +Then scroll down to the **Cross-origin resource sharing (CORS)** section and hit **Edit**. + +![Scroll to S3 Bucket CORS Configuration screenshot](/assets/s3/scroll-to-s3-bucket-cors-configuration.png) + +Paste the following CORS configuration into the editor, then hit **Save changes**. + +``` json +[ + { + "AllowedHeaders": [ + "*" + ], + "AllowedMethods": [ + "GET", + "PUT", + "POST", + "HEAD", + "DELETE" + ], + "AllowedOrigins": [ + "*" + ], + "ExposeHeaders": [], + "MaxAgeSeconds": 3000 + } +] +``` + +![Save S3 Bucket CORS Configuration screenshot](/assets/s3/save-s3-bucket-cors-configuration.png) + +Note that, you can customize this configuration to use your own domain or a list of domains when you use this in production. + +Next we are going to start working on our serverless API backend. diff --git a/_chapters/create-an-s3-bucket.md b/_archives/create-an-s3-bucket.md similarity index 93% rename from _chapters/create-an-s3-bucket.md rename to _archives/create-an-s3-bucket.md index bda5904197..48c645c1e5 100644 --- a/_chapters/create-an-s3-bucket.md +++ b/_archives/create-an-s3-bucket.md @@ -3,11 +3,12 @@ layout: post title: Create an S3 Bucket date: 2017-02-06 00:00:00 lang: en -redirect_from: /chapters/create-a-s3-bucket.html description: We are going to use S3 to host our React.js app on AWS. We first need to configure our S3 bucket with the correct Bucket Policy and enable Static Web Hosting through the AWS console before we can upload our app. -context: true comments_id: create-an-s3-bucket/48 ref: create-an-s3-bucket +redirect_from: + - /chapters/create-an-s3-bucket.html + - /chapters/create-a-s3-bucket.html --- To be able to host our note taking app, we need to upload the assets that are going to be served out statically on S3. S3 has a concept of buckets (or folders) to separate different types of files. @@ -46,9 +47,9 @@ Buckets by default are not publicly accessible, so we need to change the S3 Buck ![Add AWS S3 Bucket permission screenshot](/assets/add-bucket-policy.png) -Add the following bucket policy into the editor. Where `notes-app-client` is the name of our S3 bucket. Make sure to use the name of your bucket here. +{%change%} Add the following bucket policy into the editor. Where `notes-app-client` is the name of our S3 bucket. Make sure to use the name of your bucket here. -``` json +```json { "Version":"2012-10-17", "Statement":[{ diff --git a/_archives/creating-a-ci-cd-pipeline-for-react.md b/_archives/creating-a-ci-cd-pipeline-for-react.md new file mode 100644 index 0000000000..2980707e7f --- /dev/null +++ b/_archives/creating-a-ci-cd-pipeline-for-react.md @@ -0,0 +1,28 @@ +--- +layout: post +title: Creating a CI/CD pipeline for React +date: 2020-11-04 00:00:00 +lang: en +description: In this chapter we are going to look at how to create a CI/CD pipeline for our React app. We'll be using a service called Netlify for this. And we'll be using a branch based Git workflow. +ref: creating-a-ci-cd-pipeline-for-react +comments_id: creating-a-ci-cd-pipeline-for-react/188 +redirect_from: + - /chapters/creating-a-ci-cd-pipeline-for-react.html + - /chapters/automating-react-deployments.html +--- + +In the last couple of chapters, we [set our React.js app up in Netlify]({% link _archives/setting-up-your-project-on-netlify.md %}) and [added a custom domain to it]({% link _archives/custom-domain-in-netlify.md %}). In this chapter we'll look at how to use Netlify to create a CI/CD pipeline for our React app. + +Here's what the CI/CD pipeline for our React app will look like. + +![React CI/CD pipeline](/assets/diagrams/react-ci-cd-pipeline.png) + +Let's go over the workflow. + +- Our repo will be connected to our CI/CD service. +- Any commits that are pushed to the `master` branch will be automatically built and deployed under our production url. +- While any other commits that are pushed to a non-master branch, will be built and deployed to a unique development url. + +The one thing we haven't talked about for our React app is, environments. We want our development branches to connect to our development backend resources. And likewise for our production branch. We'll need to do this before we can automate our deployments. + +So let's do that next! diff --git a/_archives/creating-feature-environments.md b/_archives/creating-feature-environments.md new file mode 100644 index 0000000000..3e3c6048e9 --- /dev/null +++ b/_archives/creating-feature-environments.md @@ -0,0 +1,251 @@ +--- +layout: post +title: Creating Feature Environments +description: In this chapter we look at how to create feature environments for your serverless app. We'll go over the process of creating a new feature branch in Git and adding a new Serverless service using Seed. +date: 2019-10-02 00:00:00 +comments_id: creating-feature-environments/1317 +redirect_from: /chapters/creating-feature-environments.html +--- + +Over the last couple of chapters we looked at how to work on Lambda and API Gateway locally. However, besides Lambda and API Gateway, your project will have other AWS services. To run your code locally, you have to simulate all the AWS services. Similar to [serverless-offline](https://www.github.com/dherault/serverless-offline), there are plugins like [serverless-dynamodb-local](https://www.github.com/99xt/serverless-dynamodb-local) and [serverless-offline-sns](https://github.com/mj1618/serverless-offline-sns) that can simulate DynamoDB and SNS. However, mocking only takes you so far since they do not simulate IAM permissions and they are not always up to date with the services' latest changes. You want to test your code with the real resources. + +Serverless is really good at creating ephemeral environments. Let's look at what the workflow looks like when you are trying to add a new feature to your app. + +As an example we'll add a feature that lets you _like_ a note. We will add a new API endpoint `/notes/{id}/like`. We are going to work on this in a new feature branch and then deploy this using Seed. + +### Create a feature branch + +We will create a new feature branch called `like`. + +```bash +$ git checkout -b like +``` + +Since we are going to be using `/notes/{id}/like` as our endpoint we need to first export the `/notes/{id}` API path. Open the `serverless.yml` in the `services/notes-api` service, and append to the resource outputs. + +```yaml +ApiGatewayResourceNotesIdVarId: + Value: + Ref: ApiGatewayResourceNotesIdVar + Export: + Name: ${self:custom.stage}-ExtApiGatewayResourceNotesIdVarId +``` + +Our resource outputs should now look like: + +```yaml + +--- +- Outputs: + ApiGatewayRestApiId: + Value: + Ref: ApiGatewayRestApi + Export: + Name: ${self:custom.stage}-ExtApiGatewayRestApiId + + ApiGatewayRestApiRootResourceId: + Value: + Fn::GetAtt: + - ApiGatewayRestApi + - RootResourceId + Export: + Name: ${self:custom.stage}-ExtApiGatewayRestApiRootResourceId + + ApiGatewayResourceNotesIdVarId: + Value: + Ref: ApiGatewayResourceNotesIdVar + Export: + Name: ${self:custom.stage}-ExtApiGatewayResourceNotesIdVarId +``` + +Let's create the `like-api` service. + +```bash +$ cd services +$ mkdir like-api +$ cd like-api +``` + +Add a `serverless.yml`. + +```yaml +service: notes-app-ext-like-api + +plugins: + - serverless-bundle + - serverless-offline + +custom: ${file(../../serverless.common.yml):custom} + +package: + individually: true + +provider: + name: aws + runtime: nodejs12.x + stage: dev + region: us-east-1 + tracing: + lambda: true + + apiGateway: + restApiId: !ImportValue ${self:custom.stage}-ExtApiGatewayRestApiId + restApiRootResourceId: !ImportValue ${self:custom.stage}-ExtApiGatewayRestApiRootResourceId + restApiResources: + /notes/{id}: !ImportValue ${self:custom.stage}-ExtApiGatewayResourceNotesIdVarId + + environment: + stage: ${self:custom.stage} + + iamRoleStatements: + - ${file(../../serverless.common.yml):lambdaPolicyXRay} + +functions: + like: + handler: like.main + events: + - http: + path: /notes/{id}/like + method: post + cors: true + authorizer: aws_iam +``` + +Again, the `like-api` will share the same API endpoint as the `notes-api` service. + +Add the handler file `like.js`. + +```js +import { success } from "../../libs/response-lib"; + +export async function main(event, context) { + // Business logic code for liking a post + + return success({ status: true }); +} +``` + +Now before we push our Git branch, let's enable the branch workflow in Seed. + +### Enable branch workflow in Seed + +Go to your app on Seed and head over to the **Pipeline** tab and hit **Edit Pipeline**. + +![Select edit pipeline in Seed](/assets/best-practices/creating-feature-environments/select-edit-pipeline-in-seed.png) + +Enable **Auto-deploy branches**. + +![Select Enable Auto-Deploy Branches](/assets/best-practices/creating-feature-environments/select-enable-auto-deploy-branches.png) + +Select the **dev** stage, since we want the stage to be deployed into the **Development** AWS account. Click **Enable**. + +![Select Enable Auto-Deploy](/assets/best-practices/creating-feature-environments/select-enable-auto-deploy.png) + +Click **Pipeline** to go. + +![Head back to pipeline](/assets/best-practices/creating-feature-environments/head-back-to-pipeline.png) + +### Add the new service to Seed + +Click on **Add a Service**. + +![Select Add a service](/assets/best-practices/creating-feature-environments/select-add-a-service.png) + +Enter the path to the service `services/like-api` and click **Search**. + +![Select search new service path](/assets/best-practices/creating-feature-environments/select-search-new-service-path.png) + +Since the code has not been committed to Git yet, Seed is not able to find the `serverless.yml` of the service. That's totally fine. We'll specify a name for the service `like-api`. Then hit **Add Service**. + +![Set new service name](/assets/best-practices/creating-feature-environments/set-new-service-name.png) + +This should add the new service across all your stages. + +![Added new service in Seed](/assets/best-practices/creating-feature-environments/added-new-service-in-seed.png) + +By default, the new service is added to the last deploy phase. Let's click on **Manage Deploy Phases**, and move it to Phase 2. This is because it's dependent on the API Gateway resources exported by `notes-api`. + +![Show default Deploy Phase](/assets/best-practices/creating-feature-environments/show-default-deploy-phase.png) + +### Git push to deploy new feature + +Now we are ready to create our new feature environment. Go back to our command line, and then push the code to the `like` branch. + +```bash +$ git add . +$ git commit -m "Add like API" +$ git push --set-upstream origin like +``` + +Back in Seed, a new stage called **like** is created and is being deployed automatically. + +![Show new feature stage created](/assets/best-practices/creating-feature-environments/show-new-feature-stage-created.png) + +After the new stage successfully deploys, you can get the API endpoint in the stage's resources page. Head over to the **Resources** tab. + +![Select Resources tab in Seed](/assets/best-practices/creating-feature-environments/select-resources-tab-in-seed.png) + +And select the **like** stage. + +![Select feature stage](/assets/best-practices/creating-feature-environments/select-feature-stage.png) + +You will see the API Gateway endpoint for the **like** stage and the API path for the **like** handler. + +![Show API Gateway endpoint in feature stage](/assets/best-practices/creating-feature-environments/show-api-gateway-endpoint-in-feature-stage.png) + +You can now use the endpoint in your frontend for further testing and development. + +Now that our new feature environment has been created, let's quickly look at the flow for working on your new feature. + +### Working on new feature environments locally + +Once the environment has been created, we want to continue working on the feature. A common problem people run into is that `serverless deploy` takes very long to execute. And running `serverless deploy` for every change just does not work. + +#### Why is 'serverless deploy' slow? + +When you run `serverless deploy`, Serverless Framework does two things: + +1. Package the Lambda code into zip files. +2. Build a CloudFormation template with all the resources defined in `serverless.yml`. + +The code is uploaded to S3 and the template is submitted to CloudFormation. + +There are a couple of things that are causing the slowness here: + +- When working on a feature, most of the changes are code changes. It is not necessary to rebuild and resubmit the CloudFormation template for every code change. +- When making a code change, a lot of the times you are only changing one Lambda function. In this case, it's not necessary to repackage the code for all Lambda functions in the service. + +#### Deploying individual functions + +Fortunately, there is a way to deploy individual functions using the `serverless deploy -f` command. Let's take a look at an example. + +Say we change our new `like.js` code to: + +```js +import { success } from "../../libs/response-lib"; + +export async function main(event, context) { + // Business logic code for liking a post + + console.log("adding some debug code to test"); + + return success({ status: true }); +} +``` + +To deploy the code for this function, run: + +```bash +$ cd services/like-api +$ serverless deploy -f like -s like +``` + +Deploying an individual function should be much quicker than deploying the entire stack. + +#### Deploy multiple functions + +Sometimes a code change can affect multiple functions at the same time. For example, if you changed a shared library, you have to redeploy all the services importing the library. + +However, there isn't a convenient way to deploy multiple Lambda functions. If you can easily tell which Lambda functions are affected, deploy them individually. If there are many functions involved, run `serverless deploy -s like` to deploy all of them. Just to be on the safe side. + +Now let's assume we are done working on our new feature and we want our team lead to review our code before we promote it to production. To do this we are going to create a pull request environment. Let's look at how to do that next. diff --git a/_archives/creating-pull-request-environments.md b/_archives/creating-pull-request-environments.md new file mode 100644 index 0000000000..1fb0f902cb --- /dev/null +++ b/_archives/creating-pull-request-environments.md @@ -0,0 +1,76 @@ +--- +layout: post +title: Creating Pull Request Environments +description: In this chapter we'll look at the workflow for creating new pull request based environments for your serverless app using Seed. +date: 2019-10-02 00:00:00 +comments_id: creating-pull-request-environments/1318 +redirect_from: /chapters/creating-pull-request-environments.html +--- + +Now that we are done working on our new feature, we would like our team lead to review our work before promoting it to production. To do that we are going to create a pull request and Seed will automatically create an ephemeral environment for it. + +### Enable pull request workflow on Seed + +To enable auto-deploying pull requests, head over to your app on Seed. Click **Settings**. + +![Select edit pipeline in Seed](/assets/best-practices/creating-pull-request-environments/select-edit-pipeline-in-seed.png) + +And **Enable auto-deploy pull requests**. + +![Select Enable Auto-Deploy PRs](/assets/best-practices/creating-pull-request-environments/select-enable-auto-deploy-prs.png) + +Select the **dev** stage, since we want the stage to be deployed into the **Development** AWS account. Click **Enable**. + +![Select Enable Auto-Deploy](/assets/best-practices/creating-pull-request-environments/select-enable-auto-deploy.png) + +### Create a pull request + +Go to GitHub, and select the **like** branch. Then hit **New pull request**. + +![Select New pull requests in GitHub](/assets/best-practices/creating-pull-request-environments/select-new-pull-requests-in-github.png) + +Click **Create pull request**. + +![Select Create pull request in GitHub](/assets/best-practices/creating-pull-request-environments/select-create-pull-request-in-github.png) + +Now back in Seed, a new stage (in this case **pr2**) should be created and is being deployed automatically. + +![Shoow pull request stage created](/assets/best-practices/creating-pull-request-environments/show-pull-request-stage-created.png) + +After the **pr2** stage successfully deploys, you can see the deployed API endpoint on the PR page. You can give the endpoint to your frontend team for testing. + +![Show API endpoint in GitHub PR page](/assets/best-practices/creating-pull-request-environments/show-api-endpoint-in-github-pr-page.png) + +You can also access the **pr2** stage and the upstream **like** stage on Seed via the **View deployment** button. And you can see the deployment status for each service under the **checks** section. + +![Show pull request checks in GitHub](/assets/best-practices/creating-pull-request-environments/show-pull-request-checks-in-github.png) + +Now that our new feature has been reviewed, we are ready to merge it to master. + +### Merge to master + +Once your final test looks good, you are ready to merge the pull request. Go to GitHub's pr page and click **Merge pull request**. + +![Select Merge pull request](/assets/best-practices/creating-pull-request-environments/select-merge-pull-request.png) + +Back in Seed, this will trigger a deployment in the **dev** stage automatically, since the stage auto-deploys changes in the **master** branch. Also, since merging the pull request closes it, this will automatically remove the **pr2** stage. + +![Show dev stage auto deploying](/assets/best-practices/creating-pull-request-environments/show-dev-stage-auto-deploying.png) + +After the deployment completes and the **pr2** stage is removed, this is what your pipeline should look like: + +![Show pull request stage removed](/assets/best-practices/creating-pull-request-environments/show-pull-request-stage-removed.png) + +From GitHub's pull request screen, we can remove the **like** branch. + +![Select remove branch in GitHub](/assets/best-practices/creating-pull-request-environments/select-remove-branch-in-github.png) + +Back in Seed, this will trigger the **like** stage to be automatically removed. + +![Show branch stage removed](/assets/best-practices/creating-pull-request-environments/show-branch-stage-removed.png) + +After the removal is completed, your pipeline should now look like this. + +![Show feature merged in dev stage](/assets/best-practices/creating-pull-request-environments/show-feature-merged-in-dev-stage.png) + +Next, we are ready to promote our new feature to production. diff --git a/_archives/cross-stack-references-in-serverless.md b/_archives/cross-stack-references-in-serverless.md new file mode 100644 index 0000000000..8055b5961f --- /dev/null +++ b/_archives/cross-stack-references-in-serverless.md @@ -0,0 +1,90 @@ +--- +layout: post +title: Cross-Stack References in serverless +description: AWS CloudFormation allows us to link multiple serverless services using cross-stack references. To create a cross-stack reference, export a value using the "Export:" option in CloudFormation or CfnOutput construct in CDK. To import it in your serverless.yml, use "Fn::ImportValue". +date: 2018-04-02 13:00:00 +ref: cross-stack-references-in-serverless +comments_id: cross-stack-references-in-serverless/405 +redirect_from: /chapters/cross-stack-references-in-serverless.html +--- + +In the previous chapter we looked at [some of the most common patterns for organizing your serverless applications]({% link _archives/organizing-serverless-projects.md %}). Now let's look at how to work with multiple services in your Serverless application. + +You might recall that a Serverless Framework service is where a single `serverless.yml` is used to define the project. And the `serverless.yml` file is converted into a [CloudFormation template](https://aws.amazon.com/cloudformation/aws-cloudformation-templates/) using Serverless Framework. This means that in the case of multiple services you might need to reference a resource that is available in a different service. + +You also might be defining your AWS infrastructure using [AWS CDK]({% link _archives/what-is-aws-cdk.md %}). And you want to make sure your serverless API is connected to those resources. + +For example, you might have your DynamoDB tables created in CDK and your APIs (as a Serverless Framework service) need to refer to them. Of course you don't want to hard code this. To do this we are going to be using cross-stack references. + +A cross-stack reference is a way for one CloudFormation template to refer to the resource in another CloudFormation template. + +- Cross-stack references have a name and value. +- Cross-stack references only apply within the same region. +- The name needs to be unique for a given region in an AWS account. + +A reference is created when one stack creates a CloudFormation export and another imports it. So for example, our `DynamoDBStack.js` is exporting the name of our DynamoDB table, and our `notes-api` service is importing it. Once the reference has been created, you cannot remove the DynamoDB stack without first removing the stack that is referencing it (the `notes-api`). + +The above relationship between two stacks means that they need to be deployed and removed in a specific order. We'll be looking at this later. + +### CloudFormation Export in CDK + +To create a cross-stack reference, we first create a CloudFormation export. + +```js +new CfnOutput(this, "TableName", { + value: table.tableName, + exportName: app.logicalPrefixedName("ExtTableName"), +}); +``` + +The above is a CDK example from our `DynamoDBStack.js`. + +Here the `exportName` is the name of the CloudFormation export. We use a convenience method from [SST](https://github.com/sst/sst) called `app.logicalPrefixedName` that prefixes our export name with the name of the stage we are deploying to, and the name of our SST app. This ensures that our export name is unique when we deploy our stack across multiple environments. + +### CloudFormation Export in Serverless Framework + +Similarly, we can create a CloudFormation export in Serverless Framework by adding the following + +```yml +resources: + Outputs: + NotePurchasedTopicArn: + Value: + Ref: NotePurchasedTopic + Export: + Name: ${self:custom.stage}-ExtNotePurchasedTopicArn +``` + +The above is an example from the `serverless.yml` of our `billing-api`. We can add a `resources:` section to our `serverless.yml` and the `Outputs:` allows us to add CloudFormation exports. + +Just as above we need to name our CloudFormation export. We do it using the `Name:` property. Here we are prefixing our export name with the stage name (`${self:custom.stage}`) to make it unique across environments. + +The `${self:custom.stage}` is a custom variable that we define at the top of our `serverless.yml`. + +```yml +# Our stage is based on what is passed in when running serverless +# commands. Or falls back to what we have set in the provider section. +stage: ${opt:stage, self:provider.stage} +``` + +### Importing a CloudFormation Export + +Now once we've created a CloudFormation export, we need to import it in our `serverless.yml`. To do so, we'll use the `Fn::ImportValue` CloudFormation function. + +For example, in our `notes-api/serverless.yml`. + +```yml +provider: + environment: + tableName: !ImportValue ${self:custom.sstApp}-ExtTableName +``` + +We import the name of the DynamoDB table that we created and exported in `DynamoDBStack.js`. + +### Advantages of Cross-Stack References + +As your application grows, it can become hard to track the dependencies between the services in the application. And cross-stack references can help with that. It creates a strong link between the services. As a comparison, if you were to refer to the linked resource by hard coding the value, it'll be difficult to keep track of it as your application grows. + +The other advantage is that you can easily recreate the entire application (say for testing) with ease. This is because none of the services of your application are statically linked to each other. + +In the next chapter let's look at how we share code between our services. diff --git a/_chapters/custom-domain-in-netlify.md b/_archives/custom-domain-in-netlify.md similarity index 60% rename from _chapters/custom-domain-in-netlify.md rename to _archives/custom-domain-in-netlify.md index 9737f83559..a0fb05fd1c 100644 --- a/_chapters/custom-domain-in-netlify.md +++ b/_archives/custom-domain-in-netlify.md @@ -4,32 +4,14 @@ title: Custom Domains in Netlify date: 2018-03-28 00:00:00 lang: en description: To configure your React app with custom domains on Netlify and AWS, you need to point the Route 53 DNS to Netlify. Create a new Record set, add an A Record, and a CNAME for your new Netlify project. -context: true ref: custom-domains-in-netlify comments_id: custom-domains-in-netlify/191 +redirect_from: /chapters/custom-domain-in-netlify.html --- -Now that we have our first deployment, let's configure a custom domain for our app through Netlify. +Now that we [have our React app hosted on Netlify]({% link _archives/setting-up-your-project-on-netlify.md %}), let's configure a custom domain. ---- - -**A Note to Readers from Part I** - -The following section is assuming that you completed [Part I](/#part-1) independently and that the custom domains below are being set up from scratch. However, if you've just completed Part I, then you have a couple of options: - -1. Configure a new custom domain - - Let's say your domain from Part I is something like `https://notes-app.my-domain.com`. Then for the following section you can configure something like `https://notes-app-2.my-domain.com`. This is the preferred option since it does not interfere with what you had configured previously. This is what we do for the tutorial demo app as well. You can see the [Part I version here](https://demo.serverless-stack.com) and the [Part II version here](https://demo2.serverless-stack.com). The downside is that you'll have two different versions of the frontend React app. - -2. Replace the old domain - - You might be working through this guide to build an app as opposed to learning how to build one. If that's the case, it doesn't make sense that you have two versions of the frontend floating around. You'll need to disconnect the domain from Part I. To do that, remove the Route 53 records sets that we created for the [apex domain]({% link _chapters/setup-your-domain-with-cloudfront.md %}#point-domain-to-cloudfront-distribution) and the [www domain]({% link _chapters/setup-www-domain-redirect.md %}#point-www-domain-to-cloudfront-distribution) in Part I. - -If you are not sure about the two above options or have any questions, post a comment in the discussion thread that we link to at the bottom of the chapter. - ---- - -Let's get started! +Before we get started, make sure to follow this chapter to [purchase a domain on Amazon Route 53]({% link _chapters/purchase-a-domain-with-route-53.md %}). ### Pick a Netlify Site Name @@ -57,7 +39,7 @@ And hit **Add custom domain**. ![Click Add custom domain screenshot](/assets/part2/click-add-custom-domain.png) -Type in the name of our domain, for example it might be `demo-serverless-stack.com`. And hit **Save**. +Type in the name of our domain, for example it might be `demo-sst.dev`. And hit **Save**. ![Enter custom domain screenshot](/assets/part2/enter-custom-domain.png) @@ -69,15 +51,15 @@ Next hit **Check DNS configuration**. ![Hit check DNS configuration screenshot](/assets/part2/hit-check-dns-configuration.png) -This will show you the instructions for setting up your domain through Route 53. +This will show you the instructions for setting up your domain through Route 53. ![DNS configuration dialog screenshot](/assets/part2/dns-configuration-dialog.png) ### DNS Settings in Route 53 -To do this we need to head back to the [AWS Console](https://console.aws.amazon.com/). and search for Route 53 as the service. +To do this we need to go to the [AWS Console](https://console.aws.amazon.com/). and search for Route 53 as the service. -![Select Route 53 service screenshot](/assets/part2/select-route-53-service.png) +![Select Route 53 service screenshot](/assets/select-route-53-service.png) Click on **Hosted zones**. @@ -109,11 +91,11 @@ Back in Netlify, hit **HTTPS** in the side panel. And it should say that it is w ![Waiting on DNS propagation screenshot](/assets/part2/waiting-on-dns-propagation.png) -Once that is complete, Netlify will automatically provision your SSL ceritificate using Let's Encrypt. +Once that is complete, Netlify will automatically provision your SSL certificate using Let's Encrypt. ![Provisioning Let's Encrypt Certificate screenshot](/assets/part2/provisioning-lets-encrypt-certificate.png) -Wait a few seconds for the ceritificate to be provisioned. +Wait a few seconds for the certificate to be provisioned. ![SSL certificate provisioned screenshot](/assets/part2/ssl-certificate-provisioned.png) @@ -121,4 +103,4 @@ Now if you head over to your browser and go to your custom domain, your notes ap ![Notes app on custom domain screenshot](/assets/part2/notes-app-on-custom-domain.png) -We have our app in production but we haven't had a chance to go through our workflow just yet. Let's take a look at that next. +Now our React.js app is hosted on Netlify with a custom domain! Next, let's look at how to use Netlify as a CI/CD pipeline for our React app. diff --git a/_chapters/customize-the-serverless-iam-policy.md b/_archives/customize-the-serverless-iam-policy.md similarity index 74% rename from _chapters/customize-the-serverless-iam-policy.md rename to _archives/customize-the-serverless-iam-policy.md index 3e47f98f83..16a8ffd037 100644 --- a/_chapters/customize-the-serverless-iam-policy.md +++ b/_archives/customize-the-serverless-iam-policy.md @@ -1,10 +1,10 @@ --- layout: post -title: Customize the Serverless IAM Policy +title: Customize the serverless IAM Policy description: Serverless Framework deploys using the policy attached to the IAM credentials in your AWS CLI profile. To customize the IAM Policy used, access can be restricted to the services that Serverless Framework needs, and to the project that is being deployed. date: 2018-04-08 00:00:00 -context: true comments_id: customize-the-serverless-iam-policy/18 +redirect_from: /chapters/customize-the-serverless-iam-policy.html --- Serverless Framework deploys using the policy attached to the IAM credentials in your AWS CLI profile. Back in the [Create an IAM User]({% link _chapters/create-an-iam-user.md %}) chapter we created a user that the Serverless Framework will use to deploy our project. This user was assigned **AdministratorAccess**. This means that Serverless Framework and your project has complete access to your AWS account. This is fine in trusted environments but if you are working as a part of a team you might want to fine-tune the level of access based on who is using your project. @@ -19,10 +19,10 @@ The permissions required can be categorized into the following areas: Granting **AdministratorAccess** policy ensures that your project will always have the necessary permissions. But if you want to create an IAM policy that grants the minimal set of permissions, you need to customize your IAM policy. -A basic Serverless project needs permissions to the following AWS services: +A basic serverless project needs permissions to the following AWS services: - **CloudFormation** to create change set and update stack -- **S3** to upload and store Serverless artifacts and Lambda source code +- **S3** to upload and store serverless artifacts and Lambda source code - **CloudWatch Logs** to store Lambda execution logs - **IAM** to manage policies for the Lambda IAM Role - **API Gateway** to manage API endpoints @@ -80,7 +80,7 @@ This policy grants your Serverless Framework project access to all the resources ### An advanced IAM Policy template -Below is a more nuanced policy template that restricts access to the Serverless project that is being deployed. Make sure to replace ``, `` and `` for your specific project. +Below is a more nuanced policy template that restricts access to the serverless project that is being deployed. Make sure to replace ``, `` and `` for your specific project. ``` json { @@ -89,144 +89,142 @@ Below is a more nuanced policy template that restricts access to the Serverless { "Effect": "Allow", "Action": [ - "cloudformation:Describe*", - "cloudformation:List*", - "cloudformation:Get*", - "cloudformation:CreateStack", - "cloudformation:UpdateStack", - "cloudformation:DeleteStack" + "apigateway:GET", + "apigateway:PATCH", + "apigateway:POST", + "apigateway:PUT" ], - "Resource": "arn:aws:cloudformation:::stack/*/*" + "Resource": [ + "arn:aws:apigateway:::/apis" + ] }, { "Effect": "Allow", "Action": [ - "cloudformation:ValidateTemplate" + "apigateway:GET", + "apigateway:PATCH", + "apigateway:POST", + "apigateway:PUT" ], - "Resource": "*" + "Resource": [ + "arn:aws:apigateway:::/apis/*" + ] }, { "Effect": "Allow", "Action": [ - "s3:CreateBucket", - "s3:DeleteBucket", - "s3:Get*", - "s3:List*" + "cloudformation:CancelUpdateStack", + "cloudformation:ContinueUpdateRollback", + "cloudformation:CreateChangeSet", + "cloudformation:CreateStack", + "cloudformation:CreateUploadBucket", + "cloudformation:DeleteStack", + "cloudformation:Describe*", + "cloudformation:EstimateTemplateCost", + "cloudformation:ExecuteChangeSet", + "cloudformation:Get*", + "cloudformation:List*", + "cloudformation:UpdateStack", + "cloudformation:UpdateTerminationProtection" ], - "Resource": [ - "arn:aws:s3:::*" - ] + "Resource": "arn:aws:cloudformation:::stack/*/*" }, { "Effect": "Allow", "Action": [ - "s3:*" + "cloudformation:ValidateTemplate" ], - "Resource": [ - "arn:aws:s3:::*/*" - ] + "Resource": "*" }, { "Effect": "Allow", "Action": [ - "logs:DescribeLogGroups" + "ec2:Describe*" ], - "Resource": "arn:aws:logs:::log-group::log-stream:*" + "Resource": [ + "*" + ] }, { + "Effect": "Allow", "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:DeleteLogGroup", - "logs:DeleteLogStream", - "logs:DescribeLogStreams", - "logs:FilterLogEvents" + "events:Put*", + "events:Describe*", + "events:List*" ], - "Resource": "arn:aws:logs:::log-group:/aws/lambda/*:log-stream:*", - "Effect": "Allow" + "Resource": "arn:aws:events:::rule/*" }, { "Effect": "Allow", "Action": [ - "iam:GetRole", - "iam:PassRole", + "iam:AttachRolePolicy", "iam:CreateRole", "iam:DeleteRole", + "iam:DeleteRolePolicy", "iam:DetachRolePolicy", - "iam:PutRolePolicy", - "iam:AttachRolePolicy", - "iam:DeleteRolePolicy" + "iam:GetRole", + "iam:PassRole", + "iam:PutRolePolicy" ], "Resource": [ - "arn:aws:iam:::role/*-lambdaRole" + "arn:aws:iam::*:role/*-lambdaRole" ] }, { "Effect": "Allow", "Action": [ - "apigateway:GET", - "apigateway:POST", - "apigateway:PUT", - "apigateway:DELETE" + "lambda:*" ], "Resource": [ - "arn:aws:apigateway:::/restapis" + "arn:aws:lambda:*:*:function:*" ] }, { "Effect": "Allow", "Action": [ - "apigateway:GET", - "apigateway:POST", - "apigateway:PUT", - "apigateway:DELETE" + "logs:DescribeLogGroups" ], - "Resource": [ - "arn:aws:apigateway:::/restapis/*" - ] + "Resource": "arn:aws:logs:::log-group::log-stream:*" }, { - "Effect": "Allow", "Action": [ - "lambda:GetFunction", - "lambda:CreateFunction", - "lambda:DeleteFunction", - "lambda:UpdateFunctionConfiguration", - "lambda:UpdateFunctionCode", - "lambda:ListVersionsByFunction", - "lambda:PublishVersion", - "lambda:CreateAlias", - "lambda:DeleteAlias", - "lambda:UpdateAlias", - "lambda:GetFunctionConfiguration", - "lambda:AddPermission", - "lambda:RemovePermission", - "lambda:InvokeFunction" + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:FilterLogEvents" ], - "Resource": [ - "arn:aws:lambda:*::function:*" - ] + "Resource": "arn:aws:logs:::log-group:/aws/lambda/*:log-stream:*", + "Effect": "Allow" }, { "Effect": "Allow", "Action": [ - "ec2:DescribeSecurityGroups", - "ec2:DescribeSubnets", - "ec2:DescribeVpcs" + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:DeleteBucketPolicy", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:Get*", + "s3:List*", + "s3:PutBucketNotification", + "s3:PutBucketPolicy", + "s3:PutBucketTagging", + "s3:PutBucketWebsite", + "s3:PutEncryptionConfiguration", + "s3:PutObject" ], "Resource": [ - "*" + "arn:aws:s3:::*" ] }, { "Effect": "Allow", "Action": [ - "events:Put*", - "events:Remove*", - "events:Delete*", - "events:Describe*" + "s3:*" ], - "Resource": "arn:aws:events:::rule/*" + "Resource": [ + "arn:aws:s3:::*/*" + ] } ] } @@ -246,6 +244,6 @@ provider: In the above `serverless.yml`, the `` is `us-east-1` and the `` is `my-service`. -The above IAM policy template restricts access to the AWS services based on the name of your Serverless project and the region it is deployed in. +The above IAM policy template restricts access to the AWS services based on the name of your serverless project and the region it is deployed in. -It provides sufficient permissions for a minimal Serverless project. However, if you provision any additional resources in your **serverless.yml**, or install Serverless plugins, or invoke any AWS APIs in your application code; you would need to update the IAM policy to accommodate for those changes. If you are looking for details on where this policy comes from; here is an in-depth discussion on the minimal [Serverless IAM Deployment Policy](https://github.com/serverless/serverless/issues/1439) required for a Serverless project. +It provides sufficient permissions for a minimal Serverless Framework project. However, if you provision any additional resources in your **serverless.yml**, or install Serverless plugins, or invoke any AWS APIs in your application code; you would need to update the IAM policy to accommodate for those changes. If you are looking for details on where this policy comes from; here is an in-depth discussion on the minimal [Serverless IAM Deployment Policy](https://github.com/serverless/serverless/issues/1439) required for a Serverless project. diff --git a/_chapters/debugging-serverless-api-issues.md b/_archives/debugging-serverless-api-issues.md similarity index 96% rename from _chapters/debugging-serverless-api-issues.md rename to _archives/debugging-serverless-api-issues.md index 5ce3e69ce7..9c51864534 100644 --- a/_chapters/debugging-serverless-api-issues.md +++ b/_archives/debugging-serverless-api-issues.md @@ -3,13 +3,13 @@ layout: post title: Debugging Serverless API Issues description: Debugging serverless APIs can be tricky because there isn’t enough visibility on all the steps a request goes through. A very common issue is an invalid or missing IAM Role while using aws_iam as an authorizer for API Gateway and Lambda. To fix this use the Policy Simulator to ensure that your IAM Role has access to API Gateway. date: 2018-04-04 00:00:00 -context: true comments_id: debugging-serverless-api-issues/143 +redirect_from: /chapters/debugging-serverless-api-issues.html --- In this chapter we are going to take a brief look at some common API Gateway and Lambda issues we come across and how to debug them. -We've also compiled a list of some of the most common Serverless errors over on [Seed](https://seed.run). Check out [Common Serverless Errors](https://seed.run/docs/serverless-errors/) and do a quick search for your error message and see if it has a solution. +We've also compiled a list of some of the most common serverless errors over on [Seed](https://seed.run). Check out [Common Serverless Errors](https://seed.run/docs/serverless-errors/) and do a quick search for your error message and see if it has a solution. When a request is made to your serverless API, it starts by hitting API Gateway and makes its way through to Lambda and invokes your function. It takes quite a few hops along the way and each hop can be a point of failure. And since we don't have great visibility over each of the specific hops, pinpointing the issue can be a bit tricky. We are going to take a look at the following issues: @@ -18,14 +18,13 @@ When a request is made to your serverless API, it starts by hitting API Gateway - [Lambda Function Error](#lambda-function-error) - [Lambda Function Timeout](#lambda-function-timeout) -This chapter assumes you have turned on CloudWatch logging for API Gateway and that you know how to read both the API Gateway and Lambda logs. If you have not done so, start by taking a look at the chapter on [API Gateway and Lambda Logs]({% link _chapters/api-gateway-and-lambda-logs.md %}). - +This chapter assumes you have turned on CloudWatch logging for API Gateway and that you know how to read both the API Gateway and Lambda logs. If you have not done so, start by taking a look at the chapter on [API Gateway and Lambda Logs]({% link _archives/api-gateway-and-lambda-logs.md %}). ### Invalid API Endpoint The first and most basic issue we see is when the API Gateway endpoint that is requested is invalid. An API Gateway endpoint usually looks something like this: -``` +```txt https://API_ID.execute-api.REGION.amazonaws.com/STAGE/PATH ``` @@ -42,15 +41,14 @@ An API request will fail if: In all of these cases, the error does not get logged to CloudWatch since the request does not hit your API Gateway project. - ### Missing IAM Policy -This happens when your API endpoint uses **aws_iam** as the authorizer, and the IAM role +This happens when your API endpoint uses **aws_iam** as the authorizer, and the IAM role assigned to the Cognito Identity Pool has not been granted the **execute-api:Invoke** permission for your API Gateway resource. This is a tricky issue to debug because the request still has not reached API Gateway, and hence the error is not logged in the API Gateway CloudWatch logs. But we can perform a check to ensure that our Cognito Identity Pool users have the required permissions, using the [IAM policy Simulator](https://policysim.aws.amazon.com). -Before we can use the simulator we first need to find out the name of the IAM role that we are using to connect to API Gateway. We had created this role back in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}) chapter. +Before we can use the simulator we first need to find out the name of the IAM role that we are using to connect to API Gateway. We had created this role back in the [Create a Cognito identity pool]({% link _archives/create-a-cognito-identity-pool.md %}) chapter. Select **Cognito** from your [AWS Console](https://console.aws.amazon.com). @@ -88,7 +86,7 @@ Select **API Gateway** as the service and select the **Invoke** action. ![Select IAM Service Simulator Action](/assets/debugging/select-iam-policy-simulator-action.png) -Expand the service and enter the API Gateway endpoint ARN, then select **Run Simulation**. The format here is the same one we used back in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}) chapter; `arn:aws:execute-api:YOUR_API_GATEWAY_REGION:*:YOUR_API_GATEWAY_ID/*`. In our case this looks like `arn:aws:execute-api:us-east-1:*:ly55wbovq4/*`. +Expand the service and enter the API Gateway endpoint ARN, then select **Run Simulation**. The format here is the same one we used back in the [Create a Cognito identity pool]({% link _archives/create-a-cognito-identity-pool.md %}) chapter; `arn:aws:execute-api:YOUR_API_GATEWAY_REGION:*:YOUR_API_GATEWAY_ID/*`. In our case this looks like `arn:aws:execute-api:us-east-1:*:ly55wbovq4/*`. ![Enter API Gateway Endpoint ARN](/assets/debugging/enter-api-gateway-endpoint-arn.png) @@ -122,7 +120,7 @@ Click **Edit policy**. Here you can edit the policy to ensure that it has the right permission to invoke API Gateway. Ensure that there is a block in your policy like the one below. -``` coffee +```coffee ... { "Effect": "Allow", @@ -142,28 +140,25 @@ Finally, hit **Save** to update the policy. Now if you test your policy, it should show that you are allowed to invoke your API Gateway endpoint. - ### Lambda Function Error Now if you are able to invoke your Lambda function but it fails to execute properly due to uncaught exceptions, it'll error out. These are pretty straightforward to debug. When this happens, AWS Lambda will attempt to convert the error object to a string, and then send it to CloudWatch along with the stacktrace. This can be observed in both Lambda and API Gateway CloudWatch log groups. - ### Lambda Function Timeout Sometimes we might run into a case where the Lambda function just times out. Normally, a Lambda function will end its execution by invoking the **callback** function that was passed in. By default, the callback will wait until the Node.js runtime event loop is empty before returning the results to the caller. If the Lambda function has an open connection to, let's say a database server, the event loop is not empty, and the callback will wait indefinitely until the connection is closed or the Lambda function times out. To get around this issue, you can set this **callbackWaitsForEmptyEventLoop** property to false to request AWS Lambda to freeze the process as soon as the callback is called, even if there are events in the event loop. -``` javascript +```js export async function handler(event, context, callback) { context.callbackWaitsForEmptyEventLoop = false; - + ... }; ``` This effectively allows a Lambda function to return its result to the caller without requiring that the database connection be closed. This allows the Lambda function to reuse the same connection across calls, and it reduces the execution time as well. - These are just a few of the common issues we see folks running into while working with serverless APIs. Feel free to let us know via the comments if there are any other issues you'd like us to cover. diff --git a/_archives/deploy-a-serverless-app-with-dependencies.md b/_archives/deploy-a-serverless-app-with-dependencies.md new file mode 100644 index 0000000000..26095ba422 --- /dev/null +++ b/_archives/deploy-a-serverless-app-with-dependencies.md @@ -0,0 +1,82 @@ +--- +layout: post +title: Deploy a Serverless App with Dependencies +description: In this chapter we go over how to locally deploy a serverless app with multiple interdependent services. So you'll need to ensure that you deploy the service that is exporting the reference before deploying the one that imports it. You'll only need to do this for the first time. +date: 2019-09-29 00:00:00 +comments_id: deploying-multiple-services-in-serverless/410 +redirect_from: + - /chapters/deploy-a-serverless-app-with-dependencies.html + - /chapters/deploying-multiple-services-in-serverless.html +--- + +So now that we have a couple of downstream services that are referencing a resource deployed in an upstream service; let's look at how this dependency affects the way we deploy our app. + +The short version is that: + +- When you introduce a new dependency in your app you cannot deploy all the services concurrently. +- However, once these services have been deployed, you can do so. + +#### First deployment + +In our [resources repo]({{ site.backend_ext_resources_github_repo }}) we are using SST to deploy our CDK app. CDK internally keeps track of the dependencies between stacks. + +Our `stacks/index.js` looks like this. + +```js +export default function main(app) { + new DynamoDBStack(app, "dynamodb"); + + const s3 = new S3Stack(app, "s3"); + + new CognitoStack(app, "cognito", { bucketArn: s3.bucket.bucketArn }); +} +``` + +Here CDK knows that the `CognitoStack` depends on the `S3Stack`. And it needs to wait for the `S3Stack` to complete first. + +[SST](https://github.com/sst/sst) will deploy the stacks in our CDK app concurrently while ensuring that the dependencies are respected. + +Next for the [API repo]({{ site.backend_ext_api_github_repo }}) for the first time, you have to: + +- Deploy the `notes-api` first. This will export the value `dev-ExtApiGatewayRestApiId` and `dev-ExtApiGatewayRestApiRootResourceId`. +- Then deploy the `billing-api` next. This will export the value `dev-ExtNotePurchasedTopicArn`. +- Then deploy the `notify-job`. + +Assuming that you are deploying to the `dev` stage. + +If you were to deploy `billing-api` and `notify-job` concurrently, the `notify-job` will fail with the following CloudFormation error: + +```txt +notify-job - No export named dev-ExtNotePurchasedTopicArn found. +``` + +This error is basically saying that the ARN referenced in its `serverless.yml` does not exist. This makes sense because we haven’t created it yet! + +#### Subsequent deployments + +Once all the services have been successfully deployed, you can deploy them all concurrently. This is because the referenced ARN has already been created. + +#### Adding new dependencies + +Say you add a new SNS topic in `billing-api` service and you want the `notify-job` service to subscribe to that topic. The first deployment after the change, will again fail if all the services are deployed concurrently. You need to deploy the `billing-api` service first, and then deploy the `notify-job` service. + +We are almost ready to deploy our extended notes app. But before we can do that, let's configure our environments. + +### Deploying through a CI + +If you are using a CI, you'll need to deploy the above in phases. With [Seed](https://seed.run), we handle this using a concept of [Deploy Phases](https://seed.run/docs/configuring-deploy-phases). + +### Managing deployment in phases for api + +For our api repo, the dependencies look like: + +```txt +notes-api > billing-api > notify-job +``` + +To break it down in detail: + +- The `billing-api` service relies on the `notes-api` service for the API Gateway export. +- The `notify-job` service relies on the `billing-api` service for the SNS Topic export. + +That covers our section on organizing your serverless app. Next, let's configure our environments. diff --git a/_archives/deploy-the-api-services-repo.md b/_archives/deploy-the-api-services-repo.md new file mode 100644 index 0000000000..b3bfd8b24f --- /dev/null +++ b/_archives/deploy-the-api-services-repo.md @@ -0,0 +1,64 @@ +--- +layout: post +title: Deploy the API Services Repo +description: In this chapter we'll deploy our demo API services GitHub repo of our serverless app to multiple AWS environments. We'll be using Seed to manage our deployments and environments. +date: 2019-10-08 00:00:00 +comments_id: deploy-the-api-services-repo/1319 +redirect_from: /chapters/deploy-the-api-services-repo.html +--- + +Just as the previous chapter we'll add the API repo on Seed and deploy it to our environments. + +Click **Add an App** again, and select your Git provider. This time, select the API repo. + +![Select Add an App in Seed](/assets/best-practices/deploy-api-services-repo-to-seed/select-add-an-app-in-seed.png) + +Select the **notes-api** service from the list of services. + +![Select serverless service to add](/assets/best-practices/deploy-api-services-repo-to-seed/select-serverless-service-to-add.png) + +The environments for our API repo are identical to our resources repo. So instead of manually configuring them, we'll copy the settings. + +Select **Copy Settings** tab, and select the resources app. Then hit **Add a New App**. + +![Set app settings from resources](/assets/best-practices/deploy-api-services-repo-to-seed/set-app-settings-from-resources.png) + +The API app has been created. Now, let's add the other services. Head over to the **Pipeline** tab. + +![Create an App in Seed](/assets/best-practices/deploy-api-services-repo-to-seed/create-an-app-in-seed.png) + +Click **Add a service** to add the **billing-api** service at the `services/billing-api` path. And then repeat the step to add the **notify-job** service at the `services/notify-job` path. + +![Added all services in Seed](/assets/best-practices/deploy-api-services-repo-to-seed/added-all-services-in-seed.png) + +Next, click on **Manage Deploy Phases**. + +![Hit Manage Deploy Phases screenshot](/assets/best-practices/deploy-api-services-repo-to-seed/hit-manage-deploy-phases-screenshot.png) + +Again you'll notice that by default all the services are deployed concurrently. + +![Default Deploy Phase screenshot](/assets/best-practices/deploy-api-services-repo-to-seed/default-deploy-phase-screenshot.png) + +Since the **billing-api** service depends on the **notes-api** service, and in turn the **notify-job** service depends on the **billing-api** service, we are going too add 2 phases. And move the **billing-api** service to **Phase 2**, and the **notify-job** service to **Phase 3**. Finally, click **Update Phases**. + +![Edit Deploy Phase screenshot](/assets/best-practices/deploy-api-services-repo-to-seed/edit-deploy-phase-screenshot.png) + +Now let's make our first deployment. + +![Show services are deploying in dev stage](/assets/best-practices/deploy-api-services-repo-to-seed/show-services-are-deploying-in-dev-stage.png) + +You can see the deployments were carried out according to the specified deploy phases. + +Just as before, promote **dev** to **prod**. + +![Select Promote in dev stage](/assets/best-practices/deploy-api-services-repo-to-seed/select-promote-in-dev-stage.png) + +Hit **Promote to Production**. + +![Promote dev stage to prod stage](/assets/best-practices/deploy-api-services-repo-to-seed/promote-dev-stage-to-prod-stage.png) + +Now we have the API deployed to both **dev** and **prod**. + +![Show services are deployed in prod stage](/assets/best-practices/deploy-api-services-repo-to-seed/show-services-are-deployed-in-prod-stage.png) + +Now that our entire app has been deployed, let's look at how we are sharing environment specific configs across our services. diff --git a/_chapters/deploy-the-apis.md b/_archives/deploy-the-apis.md similarity index 50% rename from _chapters/deploy-the-apis.md rename to _archives/deploy-the-apis.md index 62c1c09630..3dd8aba0a4 100644 --- a/_chapters/deploy-the-apis.md +++ b/_archives/deploy-the-apis.md @@ -1,17 +1,17 @@ --- layout: post title: Deploy the APIs -date: 2017-01-04 00:00:00 +date: 2020-10-20 00:00:00 lang: en ref: deploy-the-apis description: Use the “serverless deploy” command to deploy to AWS Lambda and API Gateway using the Serverless Framework. Running this command will display the list of deployed API endpoints and the AWS region it was deployed to. And we can run the "serverless deploy function" command when we want to update an individual Lambda function. -context: true comments_id: deploy-the-apis/121 +redirect_from: /chapters/deploy-the-apis.html --- -Now that our APIs are complete, let's deploy them. +So far we've been working on our Lambda functions locally. In this chapter we are going to deploy them. -Run the following in your working directory. +{%change%} Run the following in your working directory. ``` bash $ serverless deploy @@ -23,34 +23,38 @@ If you have multiple profiles for your AWS SDK credentials, you will need to exp $ serverless deploy --aws-profile myProfile ``` -Where `myProfile` is the name of the AWS profile you want to use. If you need more info on how to work with AWS profiles in Serverless, refer to our [Configure multiple AWS profiles]({% link _chapters/configure-multiple-aws-profiles.md %}) chapter. +Where `myProfile` is the name of the AWS profile you want to use. If you need more info on how to work with AWS profiles in serverless, refer to our [Configure multiple AWS profiles]({% link _archives/configure-multiple-aws-profiles.md %}) chapter. Near the bottom of the output for this command, you will find the **Service Information**. ``` bash Service Information -service: notes-app-api +service: notes-api stage: prod region: us-east-1 +stack: notes-api-prod +resources: 32 api keys: None endpoints: - POST - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes - GET - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes/{id} - GET - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes - PUT - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes/{id} - DELETE - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes/{id} + POST - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes + GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id} + GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes + PUT - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id} + DELETE - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id} functions: - notes-app-api-prod-create - notes-app-api-prod-get - notes-app-api-prod-list - notes-app-api-prod-update - notes-app-api-prod-delete + create: notes-api-prod-create + get: notes-api-prod-get + list: notes-api-prod-list + update: notes-api-prod-update + delete: notes-api-prod-delete +layers: + None ``` -This has a list of the API endpoints that were created. Make a note of these endpoints as we are going to use them later while creating our frontend. Also make a note of the region and the id in these endpoints, we are going to use them in the coming chapters. In our case, `us-east-1` is our API Gateway Region and `ly55wbovq4` is our API Gateway ID. +This has a list of the API endpoints that were created. Make a note of these endpoints as we are going to use them later while creating our frontend. Also make a note of the region and the id in these endpoints, we are going to use them in the coming chapters. In our case, `us-east-1` is our API Gateway Region and `0f7jby961h` is our API Gateway ID. -If you are running into some issues while deploying your app, we have [a compilation of some of the most common Serverless errors](https://seed.run/docs/serverless-errors/) over on [Seed](https://seed.run). +If you are running into some issues while deploying your app, we have [a compilation of some of the most common serverless errors](https://seed.run/docs/serverless-errors/) over on [Seed](https://seed.run). ### Deploy a Single Function @@ -62,4 +66,22 @@ For example, to deploy the list function again, we can run the following. $ serverless deploy function -f list ``` -Now before we test our APIs we have one final thing to set up. We need to ensure that our users can securely access the AWS resources we have created so far. Let's look at setting up a Cognito Identity Pool. +### Test the APIs + +So far we've been testing our Lambda functions locally using the `serverless invoke local` command. Now that we've deployed our APIs, we can test it through their endpoints. + +So if you head over to our list notes API endpoint. In our case it is: + +``` text +https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes +``` + +You should see something like this. + +``` json +[{"attachment":"hello.jpg","content":"hello world","createdAt":1487800950620,"noteId":"578eb840-f70f-11e6-9d1a-1359b3b22944","userId":"123"}] +``` + +This is a JSON encoded array of notes objects that we stored in DynamoDB. + +So our API is publicly available, this means that anybody can access it and create notes. And it's always connecting to the `123` user id. Let's fix these next by handling users and authentication. diff --git a/_archives/deploy-the-resources-repo.md b/_archives/deploy-the-resources-repo.md new file mode 100644 index 0000000000..1b62ac92dc --- /dev/null +++ b/_archives/deploy-the-resources-repo.md @@ -0,0 +1,62 @@ +--- +layout: post +title: Deploy the Resources Repo +description: In this chapter we'll deploy our demo resources GitHub repo of our serverless app to multiple AWS environments. We'll be using Seed to manage our deployments and environments. +date: 2019-10-08 00:00:00 +comments_id: deploy-the-resources-repo/1320 +redirect_from: /chapters/deploy-the-resources-repo.html +--- + +First, add the resources repo on Seed. If you haven't yet, you can create a free account [here](https://console.seed.run/signup). + +Go in to your [Seed account](https://console.seed.run), add a new app, authenticate with GitHub, search for the resources repo, and select it. + +![Search for Git repository](/assets/best-practices/deploy-resources-repo-to-seed/search-for-git-repository.png) + +Seed will now automatically detect the SST service in the repo. After detection, select **Add Service**. + +![Select serverless service to add](/assets/best-practices/deploy-resources-repo-to-seed/select-serverless-service-to-add.png) + +By default, Seed lets you configure two stages out of the box, a **Development** and a **Production** stage. Serverless Framework has a concept of stages. They are synonymous with environments. Recall that in the previous chapter we used this stage name to parameterize our resource names. + +Let's first configure the **Development** stage. Enter: +- **Stage Name**: dev +- **AWS IAM Access Key** and **AWS IAM Secret Key**: the IAM credentials of the IAM user you created in your **Development** AWS account above. + +![Set dev stage IAM credentials](/assets/best-practices/deploy-resources-repo-to-seed/set-dev-stage-iam-credentials.png) + +Next, let's configure the **Production** stage. Uncheck **Use the same IAM credentials as the dev stage** checkbox since we want to use a different AWS account for **Production**. Then enter: +- **Stage Name**: prod +- **AWS IAM Access Key** and **AWS IAM Secret Key**: the IAM credentials of the IAM user you created in your **Production** AWS account above. + +Finally hit **Add a New App**. + +![Create an App in Seed](/assets/best-practices/deploy-resources-repo-to-seed/create-an-app-in-seed.png) + +Now let's make our first deployment. Click **Trigger Deployment** under the **dev** stage. + +![Select Deploy in dev stage](/assets/best-practices/deploy-resources-repo-to-seed/select-deploy-in-dev-stage.png) + +We are deploying the `master` branch here. Confirm this by clicking **Deploy**. + +![Select master branch to deploy](/assets/best-practices/deploy-resources-repo-to-seed/select-master-branch-to-deploy.png) + +You'll notice that the service is being deployed. + +![Show service is deploying in dev stage](/assets/best-practices/deploy-resources-repo-to-seed/show-service-is-deploying-in-dev-stage.png) + +After the service is successfully deployed. Click **Promote** to deploy this to the **prod** stage. + +![Select Promote in dev stage](/assets/best-practices/deploy-resources-repo-to-seed/select-promote-in-dev-stage.png) + +You will see a list of changes in resources. Since this is the first time we are deploying to the `prod` stage, the change list shows all the resources that will be created. We'll take a look at this in detail later in the [Promoting to production]({% link _archives/promoting-to-production.md %}) chapter. + +Click **Promote to Production**. + +![Promote dev stage to prod stage](/assets/best-practices/deploy-resources-repo-to-seed/promote-dev-stage-to-prod-stage.png) + +Now our resources have been deployed to both **dev** and **prod**. + +![Show service is deployed in prod stage](/assets/best-practices/deploy-resources-repo-to-seed/show-service-is-deployed-in-prod-stage.png) + +Next, let's deploy our API services repo. diff --git a/_chapters/deploy-to-s3.md b/_archives/deploy-to-s3.md similarity index 93% rename from _chapters/deploy-to-s3.md rename to _archives/deploy-to-s3.md index a45183c21f..c350dc417d 100644 --- a/_chapters/deploy-to-s3.md +++ b/_archives/deploy-to-s3.md @@ -4,9 +4,9 @@ title: Deploy to S3 date: 2017-02-07 00:00:00 lang: en description: To use our React.js app in production we are going to use Create React App’s build command to create a production build of our app. And to upload our React.js app to an S3 Bucket on AWS, we are going to use the AWS CLI s3 sync command. -context: true comments_id: deploy-to-s3/134 ref: deploy-to-s3 +redirect_from: /chapters/deploy-to-s3.html --- Now that our S3 Bucket is created we are ready to upload the assets of our app. @@ -23,7 +23,7 @@ This packages all of our assets and places them in the `build/` directory. ### Upload to S3 -Now to deploy simply run the following command; where `YOUR_S3_DEPLOY_BUCKET_NAME` is the name of the S3 Bucket we created in the [Create an S3 bucket]({% link _chapters/create-an-s3-bucket.md %}) chapter. +Now to deploy simply run the following command; where `YOUR_S3_DEPLOY_BUCKET_NAME` is the name of the S3 Bucket we created in the [Create an S3 bucket]({% link _archives/create-an-s3-bucket.md %}) chapter. ``` bash $ aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME diff --git a/_chapters/deploy-again.md b/_archives/deploy-updates.md similarity index 75% rename from _chapters/deploy-again.md rename to _archives/deploy-updates.md index 15f298eb6a..80132e31e4 100644 --- a/_chapters/deploy-again.md +++ b/_archives/deploy-updates.md @@ -1,22 +1,31 @@ --- layout: post -title: Deploy Again -date: 2017-02-14 00:00:00 +title: Deploy Updates +date: 2017-02-12 00:00:00 lang: en -description: To be able to deploy updates to our React.js app hosted on S3 and CloudFront, we need to uploads our app to S3 and invalidate the CloudFront cache. We can do this using the “aws cloudfront create-invalidation” command in our AWS CLI. To automate these steps by running “npm run deploy”, we will add these commands to predeploy, deploy, and postdeploy scripts in our package.json. -context: true -code: frontend_part1 -comments_id: deploy-again/138 -ref: deploy-again +description: To be able to deploy updates to our React.js app hosted on S3 and CloudFront, we need to uploads our app to S3 and invalidate the CloudFront cache. We can do this using the “aws cloudfront create-invalidation” command in our AWS CLI. To automate these steps by running “npx sst deploy”, we will add these commands to predeploy, deploy, and postdeploy scripts in our package.json. +comments_id: deploy-updates/16 +ref: deploy-updates +redirect_from: + - /chapters/deploy-updates.html + - /chapters/deploy-again.html --- -Now that we've made some changes to our app, let's deploy the updates. This is the process we are going to repeat every time we need to deploy any updates. +Now let's look at how we make changes and update our app. The process is very similar to how we deployed our code to S3 but with a few changes. Here is what it looks like. + +1. Build our app with the changes +2. Deploy to the main S3 Bucket +3. Invalidate the cache in both our CloudFront Distributions + +We need to do the last step since CloudFront caches our objects in its edge locations. So to make sure that our users see the latest version, we need to tell CloudFront to invalidate it's cache in the edge locations. + +Let's assume you've made some changes to your app; you'll need to build these changes first. ### Build Our App First let's prepare our app for production by building it. Run the following in your working directory. -``` bash +```bash $ npm run build ``` @@ -24,17 +33,13 @@ Now that our app is built and ready in the `build/` directory, let's deploy to S ### Upload to S3 -Run the following from our working directory to upload our app to our main S3 Bucket. Make sure to replace `YOUR_S3_DEPLOY_BUCKET_NAME` with the S3 Bucket we created in the [Create an S3 bucket]({% link _chapters/create-an-s3-bucket.md %}) chapter. +Run the following from our working directory to upload our app to our main S3 Bucket. Make sure to replace `YOUR_S3_DEPLOY_BUCKET_NAME` with the S3 Bucket we created in the [Create an S3 bucket]({% link _archives/create-an-s3-bucket.md %}) chapter. -``` bash +```bash $ aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME --delete ``` -Note the `--delete` flag here; this is telling S3 to delete all the files that are in the bucket that we aren't uploading this time around. Create React App generates unique bundles when we build it and without this flag we'll end up retaining all the files from the previous builds. - -Our changes should be live on S3. - -![App updated live on S3 screenshot](/assets/app-updated-live-on-s3.png) +Note the `--delete` flag here; this is telling S3 to delete all the files that are in the bucket that we aren't uploading this time around. Create React App generates unique bundles when we build it and without this flag we'll end up retaining all the files from the previous builds. Our changes should be live on S3. Now to ensure that CloudFront is serving out the updated version of our app, let's invalidate the CloudFront cache. @@ -48,7 +53,7 @@ To do this we'll need the **Distribution ID** of **both** of our CloudFront Dist Now we can use the AWS CLI to invalidate the cache of the two distributions. Make sure to replace `YOUR_CF_DISTRIBUTION_ID` and `YOUR_WWW_CF_DISTRIBUTION_ID` with the ones from above. -``` bash +```bash $ aws cloudfront create-invalidation --distribution-id YOUR_CF_DISTRIBUTION_ID --paths "/*" $ aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ID --paths "/*" ``` @@ -59,17 +64,15 @@ This invalidates our distribution for both the www and non-www versions of our d It can take a few minutes to complete. But once it is done, the updated version of our app should be live. -![App update live screenshot](/assets/app-update-live.png) - And that’s it! We now have a set of commands we can run to deploy our updates. Let's quickly put them together so we can do it with one command. ### Add a Deploy Command NPM allows us to add a `deploy` command in our `package.json`. -Add the following in the `scripts` block above `eject` in the `package.json`. +{%change%} Add the following in the `scripts` block above `eject` in the `package.json`. -``` coffee +```coffee "predeploy": "npm run build", "deploy": "aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME --delete", "postdeploy": "aws cloudfront create-invalidation --distribution-id YOUR_CF_DISTRIBUTION_ID --paths '/*' && aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ID --paths '/*'", @@ -79,20 +82,20 @@ Make sure to replace `YOUR_S3_DEPLOY_BUCKET_NAME`, `YOUR_CF_DISTRIBUTION_ID`, an For Windows users, if `postdeploy` returns an error like. -``` +```txt An error occurred (InvalidArgument) when calling the CreateInvalidation operation: Your request contains one or more invalid invalidation paths. ``` Make sure that there is no quote in the `/*`. -``` coffee +```coffee "postdeploy": "aws cloudfront create-invalidation --distribution-id YOUR_CF_DISTRIBUTION_ID --paths /* && aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ID --paths /*", ``` Now simply run the following command from your project root when you want to deploy your updates. It'll build your app, upload it to S3, and invalidate the CloudFront cache. -``` bash -$ npm run deploy +```bash +$ npx sst deploy ``` -Our app is now complete. And this is the end of Part I. Next we'll be looking at how to automate this stack so we can use it for our future projects. You can also take a look at how to add a Login with Facebook option in the [Facebook Login with Cognito using AWS Amplify]({% link _chapters/facebook-login-with-cognito-using-aws-amplify.md %}) chapter. It builds on what we have covered in Part I so far. +And that's it! Now you have a workflow for deploying and updating your React app on AWS using S3 and CloudFront. diff --git a/_archives/deploy-your-hello-world-api.md b/_archives/deploy-your-hello-world-api.md new file mode 100644 index 0000000000..b842987e05 --- /dev/null +++ b/_archives/deploy-your-hello-world-api.md @@ -0,0 +1,57 @@ +--- +layout: post +title: Deploy your Hello World API +date: 2020-10-16 00:00:00 +lang: en +ref: deploy-your-first-serverless-api +description: In this chapter we'll be deploying our first Hello World serverless API. We'll be using the `serverless deploy` command to deploy it to AWS. +comments_id: deploy-your-hello-world-api/2173 +redirect_from: /chapters/deploy-your-hello-world-api.html +--- + +So far we've [created our Serverless Framework]({% link _archives/setup-the-serverless-framework.md %}) app. Now we are going to deploy it to AWS. Make sure you've [configured your AWS account]({% link _chapters/create-an-aws-account.md %}) and [AWS CLI]({% link _chapters/configure-the-aws-cli.md %}). + +A big advantage with serverless is that there isn't any infrastructure or servers to provision. You can simply deploy your app directly and it's ready to serve (millions of) users right away. + +Let's do a quick deploy to see how this works. + +{%change%} In your project root, run the following. + +``` bash +$ serverless deploy +``` + +The first time your serverless app is deployed, it creates a S3 bucket (to store your Lambda function code), Lambda, API Gateway, and a few other resources. This can take a minute or two. + +Once complete, you should see something like this: + +``` bash +Service Information +service: notes-api +stage: prod +region: us-east-1 +stack: notes-api-prod +resources: 11 +api keys: + None +endpoints: + GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/hello +functions: + hello: notes-api-prod-hello +layers: + None +``` + +Notice that we have a new GET endpoint created. In our case it points to — [https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/hello](https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/hello) + +If you head over to that URL, you should see something like this: + +``` bash +{"message":"Go Serverless v2.0! Your function executed successfully! (with a delay)"} +``` + +You'll recall that this is the same output that we received when we invoked our Lambda function locally in the last chapter. In this case we are invoking that function through the `/hello` API endpoint. + +We now have a serverless API endpoint. You only pay per request to this endpoint and it scales automatically. That's a great first step! + +Now we are ready to create our infrastructure. Starting with our database. diff --git a/_archives/deploy-your-serverless-infrastructure.md b/_archives/deploy-your-serverless-infrastructure.md new file mode 100644 index 0000000000..026d81f685 --- /dev/null +++ b/_archives/deploy-your-serverless-infrastructure.md @@ -0,0 +1,78 @@ +--- +layout: post +title: Deploy Your Serverless Infrastructure +date: 2018-03-04 00:00:00 +lang: en +description: In this chapter we'll be deploying our entire serverless infrastructure. We are using CDK to define our resources and we are deploying it using SST. Our API on the other hand is deployed using Serverless Framework. +code: backend_full +ref: deploy-your-serverless-infrastructure +comments_id: deploy-your-serverless-infrastructure/167 +redirect_from: /chapters/deploy-your-serverless-infrastructure.html +--- + +Now that we have all our resources configured, let's go ahead and deploy our entire infrastructure. + +Note that, this deployment will create a new set of resources (DynamoDB table, S3 bucket, etc.). You can remove the ones that we had previously created. We're leaving this as an exercise for you. + +### Deploy Your Serverless App + +Let's deploy our Serverless Framework app. + +{%change%} From your project root, run the following. + +``` bash +$ serverless deploy -v +``` + +Your output should look something like this: + +``` bash +Service Information +service: notes-api +stage: dev +region: us-east-1 +stack: notes-api-dev +resources: 44 +api keys: + None +endpoints: + POST - https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev/notes + GET - https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev/notes/{id} + GET - https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev/notes + PUT - https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev/notes/{id} + DELETE - https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev/notes/{id} + POST - https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev/billing +functions: + create: notes-api-dev-create + get: notes-api-dev-get + list: notes-api-dev-list + update: notes-api-dev-update + delete: notes-api-dev-delete + billing: notes-api-dev-billing +layers: + None + +Stack Outputs +DeleteLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:087220554750:function:notes-api-dev-delete:3 +CreateLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:087220554750:function:notes-api-dev-create:3 +GetLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:087220554750:function:notes-api-dev-get:3 +UpdateLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:087220554750:function:notes-api-dev-update:3 +BillingLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:087220554750:function:notes-api-dev-billing:1 +ListLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:087220554750:function:notes-api-dev-list:3 +ServiceEndpoint: https://5opmr1alga.execute-api.us-east-1.amazonaws.com/dev +ServerlessDeploymentBucketName: notes-api-dev-serverlessdeploymentbucket-1323e6pius3a +``` + +And there you have it! Your entire serverless app has been created completely programmatically. + +### Next Steps + +You can also deploy your app to production by running. + +``` bash +$ serverless deploy --stage prod +``` + +Note that, production in this case is just an environment with a stage called `prod`. You can call it anything you like. Serverless Framework will simply create another version of your app with a completely new set of resources. You can learn more about this in our chapter on [Stages in Serverless Framework]({% link _archives/stages-in-serverless-framework.md %}). + +Next, you can go to our main guide and [follow the frontend section]({% link _chapters/create-a-new-reactjs-app.md %}). Just remember to use the resources that were created here in your React.js app config! diff --git a/_chapters/deploy-the-frontend.md b/_archives/deploying-a-react-app-to-aws.md similarity index 79% rename from _chapters/deploy-the-frontend.md rename to _archives/deploying-a-react-app-to-aws.md index d024c07a7e..0830005e0f 100644 --- a/_chapters/deploy-the-frontend.md +++ b/_archives/deploying-a-react-app-to-aws.md @@ -1,14 +1,17 @@ --- layout: post -title: Deploy the Frontend +title: Deploy a React App to AWS date: 2017-02-05 00:00:00 lang: en description: Tutorial on how to host a React.js single page application on AWS S3 and CloudFront. comments_id: deploy-the-frontend/39 ref: deploy-the-frontend +redirect_from: + - /chapters/deploying-a-react-app-to-aws.html + - /chapters/deploy-the-frontend.html --- -Now that we have our setup working in our local environment, let's do our first deploy and look into what we need to do to host our serverless application. +In this section we'll be looking at how to deploy your React app as a static website on AWS. The basic setup we are going to be using will look something like this: diff --git a/_archives/deploying-a-react-app-to-netlify.md b/_archives/deploying-a-react-app-to-netlify.md new file mode 100644 index 0000000000..c35d9ed2c6 --- /dev/null +++ b/_archives/deploying-a-react-app-to-netlify.md @@ -0,0 +1,26 @@ +--- +layout: post +title: Deploying a React App to Netlify +date: 2020-11-03 00:00:00 +lang: en +description: In this chapter we'll be looking at hosting your React app on Netlify. Our React app is a static site and it's pretty simple to host them. +ref: deploying-a-react-app-to-netlify +comments_id: hosting-your-react-app/2177 +redirect_from: + - /chapters/deploying-a-react-app-to-netlify.html + - /chapters/hosting-your-react-app.html +--- + +In this section we'll be looking at how to deploy your React.js app as a static website to [Netlify](https://www.netlify.com). You'll recall that in the [main part of the guide]({% link _chapters/create-a-new-reactjs-app.md %}) we used the SST [`ReactStaticSite`]({{ site.v2_url }}/constructs/ReactStaticSite) construct to deploy our React app to AWS. + +Netlify allows you to [host your React app for free](https://www.netlify.com/pricing/) and it allows your to `git push` to deploy your apps. + +The basic setup we are going to be using will look something like this: + +1. Setup our project on Netlify +2. Configure custom domains +3. Create a CI/CD pipeline for our app + +We also have another alternative version of this where we deploy our React app to S3 and we use CloudFront as a CDN in front of it. We use Route 53 to configure our custom domain. We also need to configure the www version of our domain and this needs another S3 and CloudFront distribution. The entire process can be a bit cumbersome. But if you are looking for a way to deploy and host the React app in your AWS account, we have an Extra Credit chapter on this — [Deploying a React app on AWS]({% link _archives/deploying-a-react-app-to-aws.md %}). + +Let's get started by creating our project on Netlify. diff --git a/_archives/deploying-only-updated-services.md b/_archives/deploying-only-updated-services.md new file mode 100644 index 0000000000..87d4f1b1a7 --- /dev/null +++ b/_archives/deploying-only-updated-services.md @@ -0,0 +1,56 @@ +--- +layout: post +title: Deploying Only Updated Services +description: In this chapter we look at how to speed up deployments to our monorepo serverless app by only redeploying the services that have been updated. We can do this by relying on the check Serverless Framework does. Or by looking at the Git log for the directories that have been updated. +date: 2019-10-02 00:00:00 +comments_id: deploying-only-updated-services/1321 +redirect_from: /chapters/deploying-only-updated-services.html +--- + +Once you are repeatedly deploying your serverless application, you might notice that the Serverless deployments are not very fast. This is especially true if your app has a ton of service. There are a couple of things you can do here to speed up your builds. One of them is to only deploy the services that've been updated. + +In this chapter we'll look at how to do that. + +Note that, we are doing this by default in Seed. Recall that when we merged the **like** branch to the **master** branch, only the `like-api` service and the `notes-api` showed a solid check. The other two services showed a greyed out check mark. This means that there were no changes to be deployed for this service. + +![Show deployment skipped in Seed](/assets/best-practices/deploy-only-changed-services/show-deployment-skipped-in-seed.png) + +In a serverless app with a single service, the deployment strategy in your CI/CD pipeline is straight forward: deploy my app on every git push. + +However in a monorepo setup, an app is made up of many [Serverless Framework](https://serverless.com/) services. It’s not uncommon for teams to have apps with over 40 services in a single repo on [Seed](https://seed.run/). For these setups, it does not make sense to deploy all the services when all you are trying to do is fix a typo! You only want to deploy the services that have been updated because deploying all your services on every commit is: + +1. Slow: deploying all services can take very long, especially when you are not deploying them concurrently. +2. Expensive: traditional CI services charge extra for concurrency. As of writing this chapter, it costs $50 for each added level of concurrency on [CircleCI](https://circleci.com/), hence it can be very costly to deploy all your services concurrently. + +There are a couple of ways to only deploy the services that have been updated in your serverless CI/CD pipeline. + +### Strategy 1: Skip deployments in Serverless Framework + +The `serverless deploy` command has built-in support to skip a deployment if the deployment package has not changed. + +A bit of a background, when you run `serverless deploy`, two things are done behind the scenes. It first does a `serverless package` to generate a deployment package. This includes the CloudFormation template and the zipped Lambda code. Next, it does a `serverless deploy -p path/to/package` to deploy the package that was created. Before Serverless deploys the package in the second step, it first computes the hash of the package and compares it with that of the previous deployment. If the hash is the same, the deployment is skipped. We are simplifying the process here but that’s the basic idea. + +However, there are two downsides to this. + +1. Serverless Framework still has to generate the deployment package first. For a Node.js application, this could mean installing the dependencies, linting, running Webpack, and finally packaging the code. Meaning that the entire process can still be pretty slow even if you skip the deployment. +2. If your previous deployment had failed due to an external cause, after you fix the issue and re-run `serverless deploy`, the deployment will be skipped. For example, you tried to create an S3 bucket in your `serverless.yml`, but you hit the 100 S3 buckets per account limit. You talked to AWS support and had the limit lifted. Now you re-run `serverless deploy` , but since neither the CloudFormation template or the Lambda code changed, the deployment will be skipped. To fix this, you need to use the `--force` flag to skip the check and force a deployment. + +### Strategy 2: Check the Git log for changes + +A better approach here is to check if there are any commits in a service directory before deploying that service. + +When some code is pushed, you can run the following command to get a list of updated files: + +``` bash +$ git diff --name-only ${prevCommitSHA} ${currentCommitSHA} +``` + +This will give you a list of files that have changed between the two commits. With the list of changed files, there are three scenarios from the perspective of a given service. We are going to use `notes-api` as an example: + +1. A file was changed in my service directory (ie. `services/notes-api`) ⇒ we deploy the `notes-api` service +2. A file was changed in another service's directory (ie. `services/like-api`) ⇒ we do not deploy the `notes-api` service +3. Or, a file was changed in `libs/` ⇒ we deploy the `notes-api` service + +Your repo setup can look different, but the general concept still holds true. You have to figure out which file change affects an individual service, and which affects all the services. The advantage of this strategy is that you know upfront which services can be skipped, allowing you to skip a portion of the entire build process! + +And this concludes our section on the development workflow! Next, we'll look at how to trace our serverless applications using AWS X-Ray. diff --git a/_archives/deploying-to-multiple-aws-accounts.md b/_archives/deploying-to-multiple-aws-accounts.md new file mode 100644 index 0000000000..d93cb0caab --- /dev/null +++ b/_archives/deploying-to-multiple-aws-accounts.md @@ -0,0 +1,54 @@ +--- +layout: post +title: Deploying to Multiple AWS Accounts +description: Once you've configured the environments in your serverless app across multiple AWS accounts, you'll want to deploy them. In this chapter, we look at how to create the AWS credentials and manage the environments using Seed. +date: 2019-09-30 00:00:00 +comments_id: deploying-to-multiple-aws-accounts/1322 +redirect_from: /chapters/deploying-to-multiple-aws-accounts.html +--- + +Now that you have a couple of AWS accounts created and your resources have been parameterized, let's look at how to deploy them. In this chapter, we'll deploy the following: + +1. The [resources repo]({{ site.backend_ext_resources_github_repo }}) will be deployed in phases to the `dev` and `prod` stage. These two stages are configured in our `Development` and `Production` AWS accounts respectively. + +2. Then we'll do the same with the [APIs repo]({{ site.backend_ext_api_github_repo }}). + +### Configure AWS Profiles + +Follow the [setup up IAM users]({% link _chapters/create-an-iam-user.md %}) chapter to create an IAM user in your `Development` AWS account. And take a note of the **Access key ID** and **Secret access key** for the user. + +Next, set these credentials in your local machine using the AWS CLI: + +``` bash +$ aws configure --profile default +``` + +This sets the default IAM credentials to those of the Development account. Meaning when you run `serverless deploy`, a service will get deployed into the Development account. + +Repeat the step to create an IAM user in your `Production` account. And make a note of the credentials. We will not add the IAM credentials for the Production account on our local machine. This is because we do not want to be able to deploy code to the Production environment EVER from our local machine. + +Production deployments should always go through our CI/CD pipeline. + +Next we are going to deploy our two repos to our environments. We want you to follow along so you can get a really good sense of what the workflow is like. + +So let's start by using the demo repo templates from GitHub. + +### Create demo repos + +Let's first create [the resources repo]({{ site.backend_ext_resources_github_repo }}). Click **Use this template**. + +![Use demo resources repo template](/assets/best-practices/deploy-environments-to-multiple-aws-accounts/use-demo-resources-repo-template.png) + +Enter Repository name **serverless-stack-demo-ext-resources** and click **Create repository from template**. + +![Create demo resources repo on GitHub](/assets/best-practices/deploy-environments-to-multiple-aws-accounts/create-demo-resources-repo-on-github.png) + +And do the same for [the API services repo]({{ site.backend_ext_api_github_repo }}). + +![Create demo API services repo template](/assets/best-practices/deploy-environments-to-multiple-aws-accounts/use-demo-api-services-repo-template.png) + +Enter Repository name **serverless-stack-demo-ext-api** and click **Create repository from template**. + +![Create demo API services repo on GitHub](/assets/best-practices/deploy-environments-to-multiple-aws-accounts/create-demo-api-services-repo-on-github.png) + +Now that we've forked these repos, let's deploy them to our environments. We are going to use [Seed](https://seed.run) to do this but you can set this up later with your favorite CI/CD tool. diff --git a/_archives/dynamically-generate-social-share-images-with-serverless.md b/_archives/dynamically-generate-social-share-images-with-serverless.md new file mode 100644 index 0000000000..80da3e4936 --- /dev/null +++ b/_archives/dynamically-generate-social-share-images-with-serverless.md @@ -0,0 +1,781 @@ +--- +layout: post +title: Dynamically generate social share images with serverless +date: 2021-06-25 00:00:00 +lang: en +description: In this chapter we'll look at how to create a serverless service that dynamically generates social cards for our website or blog. It uses SST and Puppeteer to generate these images. +repo: https://github.com/sst/social-cards +ref: dynamically-generate-social-share-images-with-serverless +comments_id: dynamically-generate-social-share-images-with-serverless/2419 +redirect_from: /chapters/dynamically-generate-social-share-images-with-serverless.html +--- + +In this chapter we'll look at how to dynamically generate social share images or open graph (OG) images with serverless. + +Social cards or social share images or open graph images are preview images that are displayed in social media sites like Facebook, Twitter, etc. when a link is posted. These images give users better context on what the link is about. They also look nicer than a logo or favicon. + +However, creating a unique image for each blog post or page of your website can be time consuming and impractical. So we ideally want to be able to generate these images dynamically based on the title of the blog post or page and some other accompanying information. + +We wanted to do something like this for [SST](/). And this was a perfect use case for [serverless]({% link _chapters/what-is-serverless.md %}). These images will be generated when your website is shared and it doesn't make sense to run a server to serve these out. So we built our own social cards service with [SST](/)! It's also deployed and managed with [Seed](https://seed.run). + +For instance, here is what the social card for one of our chapters looks like. + +![Social card for SST chapter](/assets/dynamically-generate-social-share-images-with-serverless/social-card-for-serverless-stack-chapter.png) + +We also have multiple templates to generate these social cards. Here's one for our blog. + +![Social card for sample blog post](/assets/dynamically-generate-social-share-images-with-serverless/social-card-for-sample-blog-post.png) + +These images are served out of our social cards service. It's built using [SST](/) and is hosted on AWS: + +```bash +https://social-cards.sst.dev +``` + +In this chapter we'll look at how we created this service and how you can do the same! + +The entire social cards service is open source and available on GitHub. So you can fork it and play around with everything that will be talked about in this chapter. + +  [SST Social Cards Service on GitHub]({{ page.repo }}) + +### Table of Contents + +In this chapter we'll be looking at: + +1. [The architecture of our social cards service](#the-architecture) +1. [Create a serverless app with SST](#create-an-sst-app) +1. [Design templates for our social cards in the browser](#design-social-card-templates) +1. [Use Puppeteer to take screenshots of the templates](#take-screenshots-with-puppeteer) +1. [Support non-Latin fonts in Lambda](#support-non-latin-fonts-in-lambda) +1. [Cache the images in an S3 bucket](#cache-images-in-s3) +1. [Use CloudFront as a CDN to serve the images](#use-cloudfront-as-a-cdn) +1. [Add a custom domain for our social cards service](#adding-a-custom-domain) +1. [Integrate with static site generators](#integrate-with-static-site-generators) + +Let's start by taking a step back and getting a sense of the architecture of our social card service. + +### The Architecture + +Our social cards service is a serverless app deployed to AWS. + +![Social card service architecture diagram](/assets/dynamically-generate-social-share-images-with-serverless/social-card-service-architecture-diagram.png) + +There are a couple of key parts to this. So let's look at it in detail. + +1. We have [CloudFront](https://aws.amazon.com/cloudfront/) as our CDN to serve out images. +2. CloudFront connects to our serverless API. +3. The API is powered by a Lambda function. +4. The Lambda function generating these images will: + - Include the templates that we'll be using. These are HTML files that are included in our Lambda function. + - Run a headless Chrome instance with [Puppeteer](https://developers.google.com/web/tools/puppeteer) and pass in the parameters for the templates. + - Load these templates and take a screenshot. + - Store these images in an S3 bucket. + - Check the S3 bucket first to see if we've previously generated these images. + +### Create an SST App + +We'll start by creating a new SST app. + +```bash +$ npx create-sst@one --template=minimal/javascript-starter social-cards +$ cd social-cards +``` + +The infrastructure in our app is defined using [CDK]({% link _archives/what-is-aws-cdk.md %}). Currently we just have a simple API that invokes a Lambda function. + +You can see this in `stacks/MyStack.js`. + +```js +// Create a HTTP API +const api = new Api(stack, "Api", { + routes: { + "GET /": "functions/lambda.handler", + }, +}); +``` + +For now our Lambda function in `functions/lambda.js` just prints out _"Hello World"_. + +### Design Social Card Templates + +The first step is to create a template for our social share images. These HTML files will be loaded locally and we'll pass in the parameters for our template via the query string. + +Let's look at the blog template that we use in SST as an example. + +![Template for social card running locally](/assets/dynamically-generate-social-share-images-with-serverless/template-for-social-card-running-locally.png) + +The HTML that generates this page looks like: + +```html + + + + + + + + + + Blog +
+

+
+ + +
+
+ Read Post + + + +``` + +Note the ` +``` + A couple of notes here. - Amplify refers to Cognito as `Auth`, S3 as `Storage`, and API Gateway as `API`. @@ -112,6 +115,16 @@ A couple of notes here. - The `name: "notes"` is basically telling Amplify that we want to name our API. Amplify allows you to add multiple APIs that your app is going to work with. In our case our entire backend is just one single API. -- The `Amplify.configure()` is just setting the various AWS resources that we want to interact with. It isn't doing anything else special here beside configuration. So while this might look intimidating, just remember this is only setting things up. +- The `Amplify.configure()` is just setting the various AWS resources that we want to interact with. It isn't doing anything else special here beside configuration. So while this might look intimidating, just remember this is only setting things up. + +### Commit the Changes + +{%change%} Let's commit our code so far and push it to GitHub. + +```bash +$ git add . +$ git commit -m "Setting up our React app" +$ git push +``` Next up, we are going to work on creating our login and sign up forms. diff --git a/_chapters/configure-secrets-in-seed.md b/_chapters/configure-secrets-in-seed.md index 8b24134aa0..e94c35f1a7 100644 --- a/_chapters/configure-secrets-in-seed.md +++ b/_chapters/configure-secrets-in-seed.md @@ -3,36 +3,49 @@ layout: post title: Configure Secrets in Seed lang: en date: 2018-03-13 00:00:00 -description: To automate our Serverless deployments with Seed (https://seed.run), we will need to set our secrets in the Seed console. Move the environment variables from your env.yml to the stage we are deploying to. -context: true +description: To automate our serverless deployments with Seed, we will need to set our secrets in the Seed console. Move the environment variables from `sst secrets` to the stage we are deploying to. ref: configure-secrets-in-seed comments_id: configure-secrets-in-seed/176 --- -Before we can do our first deployment, we need to make sure to configure our secret environment variables. If you'll recall, we have explicitly not stored these in our code (or in Git). This means that if somebody else on our team needs to deploy, we'll need to pass the `env.yml` file around. Instead we'll configure [Seed](https://seed.run) to deploy with our secrets for us. +Before we can make our first deployment, we need to make sure to configure our secret environment variables. If you'll recall, we have explicitly [not stored these in our code (or in Git)]({% link _chapters/handling-secrets-in-sst.md %}). This means that if somebody else on our team needs to deploy, we'll need to pass the information around. Instead we'll configure [Seed](https://seed.run) to deploy with our secrets for us. -To do that, hit the **Settings** button in our **dev** stage. +We are also going to configure Seed to deploy our app to production when we push any changes to the `main` branch. -![Select Settings in dev stage screenshot](/assets/part2/select-settings-in-dev-stage.png) +By default, Seed sets you up with two environments, `dev` and `prod`. Where pushing to the `main` branch would deploy to `dev`. And you'll need to promote your changes to `prod`. To keep things simple, we are only going to use the `prod` stage here and deploy directly to it. -Here click **Show Env Variables**. +To configure the above, click **dev** in your app **Settings**. -![Show dev env variables settings screenshot](/assets/part2/show-dev-env-variables-settings.png) +![Select dev stage in Settings](/assets/part2/select-dev-stage-in-settings.png) -And type in `stripeSecretKey` as the **Key** and the value should be the `STRIPE_TEST_SECRET_KEY` back from the [Load secrets from env.yml]({% link _chapters/load-secrets-from-env-yml.md %}) chapter. Hit **Add** to save your secret key. +Here **turn off the Auto-deploy** setting. -![Add secret dev environment variable screenshot](/assets/part2/add-secret-dev-environment-variable.png) +![Turn off auto-deploy for dev](/assets/part2/turn-off-auto-deploy-for-dev.png) -Next we need to configure our secrets for the `prod` stage. Head over there and hit the **Settings** button. +Then head over to the **prod** stage in your app **Settings**. -![Select Settings in prod stage screenshot](/assets/part2/select-settings-in-prod-stage.png) +![Select prod stage in Settings](/assets/part2/select-prod-stage-in-settings.png) -Click **Show Env Variables**. +Here **turn on the Auto-deploy** setting. -![Show prod env variables settings screenshot](/assets/part2/show-prod-env-variables-settings.png) +![Turn on auto-deploy for prod](/assets/part2/turn-on-auto-deploy-for-prod.png) -And type in `stripeSecretKey` as the **Key** and the value should be the `STRIPE_PROD_SECRET_KEY` back from the [Load secrets from env.yml]({% link _chapters/load-secrets-from-env-yml.md %}) chapter. Hit **Add** to save your secret key. +You'll be prompted to select a branch. Select **main** and click **Enable**. -![Add secret prod environment variable screenshot](/assets/part2/add-secret-prod-environment-variable.png) +![Select branch to auto-deploy to prod](/assets/part2/select-branch-to-auto-deploy-to-prod.png) + +Next, scroll down and click **Show Env Variables**. + +![Show prod env variables settings](/assets/part2/show-prod-env-variables-settings.png) + +And type in `STRIPE_SECRET_KEY` as the **Key**. + +{%note%} +We saved this in as an sst secret back in the [Handling Secrets in SST]({% link _chapters/handling-secrets-in-sst.md %}) chapter. Remember, you can run `pnpm sst secrets list` to see the secrets for the current stage. +{%endnote%} + +Hit **Add** to save your secret key. + +![Add secret prod environment variable](/assets/part2/add-secret-prod-environment-variable.png) Next, we'll trigger our first deployment on Seed. diff --git a/_chapters/configure-the-aws-cli.md b/_chapters/configure-the-aws-cli.md index 2a65391b6b..c91d3db8a6 100644 --- a/_chapters/configure-the-aws-cli.md +++ b/_chapters/configure-the-aws-cli.md @@ -5,7 +5,6 @@ date: 2016-12-26 00:00:00 lang: en ref: configure-the-aws-cli description: To interact with AWS using the command line we need to install the AWS command line interface (or AWS CLI). It also needs to be configured with our IAM user Access key ID and Secret Access key from the AWS console. -context: true comments_id: configure-the-aws-cli/86 --- @@ -18,7 +17,7 @@ AWS CLI needs Python 2 version 2.6.5+ or Python 3 version 3.3+ and [Pip](https:/ - [Installing Python](https://www.python.org/downloads/) - [Installing Pip](https://pip.pypa.io/en/stable/installing/) -Now using Pip you can install the AWS CLI (on Linux, macOS, or Unix) by running: +{%change%} Now using Pip you can install the AWS CLI (on Linux, macOS, or Unix) by running: ``` bash $ sudo pip install awscli @@ -41,7 +40,7 @@ It should look something like this: - Access key ID **AKIAIOSFODNN7EXAMPLE** - Secret access key **wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY** -Simply run the following with your Secret Key ID and your Access Key. +{%change%} Simply run the following with your Secret Key ID and your Access Key. ``` bash $ aws configure diff --git a/_chapters/connect-the-billing-form.md b/_chapters/connect-the-billing-form.md index 8f3eb10e96..9e3f52d8b7 100644 --- a/_chapters/connect-the-billing-form.md +++ b/_chapters/connect-the-billing-form.md @@ -1,83 +1,83 @@ --- layout: post title: Connect the Billing Form -date: 2018-03-24 00:00:00 +date: 2017-01-31 18:00:00 lang: en -description: To add our Stripe billing form to our React app container we need to wrap it inside a StripeProvider component. We also need to include Stripe.js in our HTML page. -context: true +description: To add our Stripe billing form to our React app container we need to wrap it inside a StripeProvider component. ref: connect-the-billing-form comments_id: connect-the-billing-form/187 --- Now all we have left to do is to connect our billing form to our billing API. -Let's start by including Stripe.js in our HTML. +{%change%} Replace our `return` statement in `src/containers/Settings.tsx` with this. -Append the following the the `` block in our `public/index.html`. - -``` html - -``` - -Replace our `render` method in `src/containers/Settings.js` with this. - -``` coffee -handleFormSubmit = async (storage, { token, error }) => { - if (error) { - alert(error); +{% raw %} +```tsx +const handleFormSubmit: BillingFormType["onSubmit"] = async ( + storage, + info +) => { + if (info.error) { + onError(info.error); return; } - this.setState({ isLoading: true }); + setIsLoading(true); try { - await this.billUser({ + await billUser({ storage, - source: token.id + source: info.token?.id, }); alert("Your card has been charged successfully!"); - this.props.history.push("/"); + nav("/"); } catch (e) { - alert(e); - this.setState({ isLoading: false }); + onError(e); + setIsLoading(false); } -} - -render() { - return ( -
- - - - - -
- ); -} +}; + +return ( +
+ + + +
+); ``` +{% endraw %} -And add the following to the header. +{%change%} Add the following imports to the header. -``` js -import { Elements, StripeProvider } from "react-stripe-elements"; -import BillingForm from "../components/BillingForm"; -import config from "../config"; +```tsx +import { Elements } from "@stripe/react-stripe-js"; +import { BillingForm, BillingFormType } from "../components/BillingForm"; import "./Settings.css"; ``` -We are adding the `BillingForm` component that we previously created here and passing in the `loading` and `onSubmit` prop that we referenced in the last chapter. In the `handleFormSubmit` method, we are checking if the Stripe method from the last chapter returned an error. And if things looked okay then we call our billing API and redirect to the home page after letting the user know. +We are adding the `BillingForm` component that we previously created here and passing in the `isLoading` and `onSubmit` prop that we referenced in the previous chapter. In the `handleFormSubmit` method, we are checking if the Stripe method returned an error. And if things looked okay then we call our billing API and redirect to the home page after letting the user know. + +To initialize the Stripe Elements we pass in the Stripe.js object that we loaded [a couple of chapters ago]({% link _chapters/add-stripe-keys-to-config.md %}). This Elements component needs to wrap around any Stripe React components. -An important detail here is about the `StripeProvider` and the `Elements` component that we are using. The `StripeProvider` component let's the Stripe SDK know that we want to call the Stripe methods using `config.STRIPE_KEY`. And it needs to wrap around at the top level of our billing form. Similarly, the `Elements` component needs to wrap around any component that is going to be using the `CardElement` Stripe component. +The Stripe elements are loaded inside an [IFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe). So if we are using any custom fonts, we'll need to include them explicitly. As covered in the [Stripe docs](https://stripe.com/docs/js/elements_object){:target="_blank"}. Finally, let's handle some styles for our settings page as a whole. -Add the following to `src/containers/Settings.css`. +{%change%} Create a file named `src/containers/Settings.css` and add the following. -``` css +```css @media all and (min-width: 480px) { .Settings { padding: 60px 0; @@ -106,13 +106,8 @@ If everything is set correctly, you should see the success message and you'll be ![Settings screen billing success screenshot](/assets/part2/settings-screen-billing-success.png) -### Commit the Changes +Now with our app nearly complete, we'll look at securing some the pages of our app that require a login. Currently if you visit a note page while you are logged out, it throws an ugly error. -Let's quickly commit these to Git. - -``` bash -$ git add . -$ git commit -m "Connecting the billing form" -``` +![Note page logged out error screenshot](/assets/note-page-logged-out-error.png) -Next, we'll set up automatic deployments for our React app using a service called [Netlify](https://www.netlify.com). This will be fairly similar to what we did for our serverless backend API. +Instead, we would like it to redirect us to the login page and then redirect us back after we login. Let's look at how to do that next. diff --git a/_chapters/create-a-billing-form.md b/_chapters/create-a-billing-form.md index 5a6ce4d2e1..01a3970af0 100644 --- a/_chapters/create-a-billing-form.md +++ b/_chapters/create-a-billing-form.md @@ -1,175 +1,190 @@ --- layout: post title: Create a Billing Form -date: 2018-03-23 00:00:00 +date: 2017-01-31 12:00:00 lang: en description: We will create a billing form in our React app using the Stripe React SDK. We will use the CardElement to let the user input their credit card details and call the createToken method to generate a token that we can pass to our serverless billing API. -context: true ref: create-a-billing-form comments_id: create-a-billing-form/186 --- Now our settings page is going to have a form that will take a user's credit card details, get a stripe token and call our billing API with it. Let's start by adding the Stripe React SDK to our project. -From our project root, run the following. +{%change%} Run the following **in the `packages/frontend/` directory**. -``` bash -$ npm install --save react-stripe-elements +```bash +$ npm install @stripe/react-stripe-js ``` Next let's create our billing form component. -Add the following to a new file in `src/components/BillingForm.js`. +{%change%} Add the following to a new file in `src/components/BillingForm.tsx`. {% raw %} -``` coffee -import React, { Component } from "react"; -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; -import { CardElement, injectStripe } from "react-stripe-elements"; -import LoaderButton from "./LoaderButton"; + +```tsx +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import Stack from "react-bootstrap/Stack"; +import { useFormFields } from "../lib/hooksLib"; +import { Token, StripeError } from "@stripe/stripe-js"; +import LoaderButton from "../components/LoaderButton"; +import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js"; import "./BillingForm.css"; -class BillingForm extends Component { - constructor(props) { - super(props); +export interface BillingFormType { + isLoading: boolean; + onSubmit: ( + storage: string, + info: { token?: Token; error?: StripeError } + ) => Promise; +} - this.state = { - name: "", - storage: "", - isProcessing: false, - isCardComplete: false - }; - } +export function BillingForm({ isLoading, onSubmit }: BillingFormType) { + const stripe = useStripe(); + const elements = useElements(); + const [fields, handleFieldChange] = useFormFields({ + name: "", + storage: "", + }); + const [isProcessing, setIsProcessing] = useState(false); + const [isCardComplete, setIsCardComplete] = useState(false); - validateForm() { + isLoading = isProcessing || isLoading; + + function validateForm() { return ( - this.state.name !== "" && - this.state.storage !== "" && - this.state.isCardComplete + stripe && + elements && + fields.name !== "" && + fields.storage !== "" && + isCardComplete ); } - handleFieldChange = event => { - this.setState({ - [event.target.id]: event.target.value - }); - } + async function handleSubmitClick(event: React.FormEvent) { + event.preventDefault(); - handleCardFieldChange = event => { - this.setState({ - isCardComplete: event.complete - }); - } + if (!stripe || !elements) { + // Stripe.js has not loaded yet. Make sure to disable + // form submission until Stripe.js has loaded. + return; + } - handleSubmitClick = async event => { - event.preventDefault(); + if (!elements.getElement(CardElement)) { + return; + } - const { name } = this.state; + setIsProcessing(true); - this.setState({ isProcessing: true }); + const cardElement = elements.getElement(CardElement); - const { token, error } = await this.props.stripe.createToken({ name }); + if (!cardElement) { + return; + } - this.setState({ isProcessing: false }); + const { token, error } = await stripe.createToken(cardElement); - this.props.onSubmit(this.state.storage, { token, error }); - } + setIsProcessing(false); - render() { - const loading = this.state.isProcessing || this.props.loading; + onSubmit(fields.storage, { token, error }); + } - return ( -
- - Storage - - -
- - Cardholder's name - + + Storage + + +
+ + + Cardholder's name + -
- Credit Card Info - + +
+ Credit Card Info + setIsCardComplete(e.complete)} + options={{ + style: { + base: { + fontSize: "16px", + fontWeight: "400", + color: "#495057", + fontFamily: "'Open Sans', sans-serif", + }, + }, + }} + /> +
- - ); - } + isLoading={isLoading} + disabled={!validateForm()} + > + Purchase + + + + ); } - -export default injectStripe(BillingForm); ``` + {% endraw %} Let's quickly go over what we are doing here: -- To begin with we are going to wrap our component with a Stripe module using the `injectStripe` HOC. This gives our component access to the `this.props.stripe.createToken` method. +- To begin with we are getting a reference to the Stripe object by calling `useStripe`. -- As for the fields in our form, we have input field of type `number` that allows a user to enter the number of notes they want to store. We also take the name on the credit card. These are stored in the state through the `this.handleFieldChange` method. +- As for the fields in our form, we have input field of type `number` that allows a user to enter the number of notes they want to store. We also take the name on the credit card. These are stored in the state through the `handleFieldChange` method that we get from our `useFormFields` custom React Hook. - The credit card number form is provided by the Stripe React SDK through the `CardElement` component that we import in the header. -- The submit button has a loading state that is set to true when we call Stripe to get a token and when we call our billing API. However, since our Settings container is calling the billing API we use the `this.props.loading` to set the state of the button from the Settings container. +- The submit button has a loading state that is set to true when we call Stripe to get a token and when we call our billing API. However, since our Settings container is calling the billing API we use the `props.isLoading` to set the state of the button from the Settings container. -- We also validate this form by checking if the name, the number of notes, and the card details are complete. For the card details, we use the CardElement's `onChange` method. +- We also validate this form by checking if the name, the number of notes, and the card details are complete. For the card details, we use the `CardElement`'s `onChange` method. -- Finally, once the user completes and submits the form we make a call to Stripe by passing in the credit card name and the credit card details (this is handled by the Stripe SDK). We call the `this.props.stripe.createToken` method and in return we get the token or an error back. We simply pass this and the number of notes to be stored to the settings page via the `this.props.onSubmit` method. We will be setting this up shortly. +- Finally, once the user completes and submits the form we make a call to Stripe by passing in the `CardElement`. It uses this to generate a token for the specific call. We simply pass this and the number of notes to be stored to the settings page via the `onSubmit` method. We will be setting this up shortly. -You can read more about how to use the [React Stripe Elements here](https://github.com/stripe/react-stripe-elements). +You can read more about how to use the [React Stripe SDK here](https://github.com/stripe/react-stripe-js). Also, let's add some styles to the card field so it matches the rest of our UI. -Create a file at `src/components/BillingForm.css`. +{%change%} Create a file at `src/components/BillingForm.css`. -``` css +```css .BillingForm .card-field { - margin-bottom: 15px; - background-color: white; - padding: 11px 16px; - border-radius: 6px; - border: 1px solid #CCC; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - line-height: 1.3333333; + line-height: 1.5; + padding: 0.65rem 0.75rem; + background-color: var(--bs-body-bg); + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius-lg); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .BillingForm .card-field.StripeElement--focus { - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); - border-color: #66AFE9; + outline: 0; + border-color: #86B7FE; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, 0.25); } ``` -### Commit the Changes - -Let's quickly commit these to Git. - -``` bash -$ git add . -$ git commit -m "Adding a billing form" -``` +These styles might look complicated. But we are just copying them from the other form fields on the page to make sure that the card field looks like them. Next we'll plug our form into the settings page. diff --git a/_chapters/create-a-build-script.md b/_chapters/create-a-build-script.md deleted file mode 100644 index e7ddf33dc1..0000000000 --- a/_chapters/create-a-build-script.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -layout: post -title: Create a Build Script -date: 2018-03-26 00:00:00 -lang: en -code: frontend_full -description: To configure our Create React App with Netlify, we need to add a build script to our project root. To make sure that we return a HTTP status code of 200 for our React Router routes we will be adding a redirects rule. -ref: create-a-build-script -comments_id: create-a-build-script/189 ---- - -Before we can add our project to [Netlify](https://www.netlify.com) we just need to set up a build script. If you recall, we had configured our app to use the `REACT_APP_STAGE` build environment variable. We are going to create a build script to tell Netlify to set this variable up for the different deployment cases. - -### Add the Netlify Build Script - -Start by adding the following to a file called `netlify.toml` to your project root. - -``` toml -# Global settings applied to the whole site. -# “base” is directory to change to before starting build, and -# “publish” is the directory to publish (relative to root of your repo). -# “command” is your build command. - -[build] - base = "" - publish = "build" - command = "REACT_APP_STAGE=dev npm run build" - -# Production context: All deploys to the main -# repository branch will inherit these settings. -[context.production] - command = "REACT_APP_STAGE=prod npm run build" - -# Deploy Preview context: All Deploy Previews -# will inherit these settings. -[context.deploy-preview] - command = "REACT_APP_STAGE=dev npm run build" - -# Branch Deploy context: All deploys that are not in -# an active Deploy Preview will inherit these settings. -[context.branch-deploy] - command = "REACT_APP_STAGE=dev npm run build" -``` - -The build script is configured based on contexts. There is a default one right up top. There are three parts to this: - -1. The `base` is the directory where Netlify will run our build commands. In our case it is in the project root. So this is left empty. - -2. The `publish` option points to where our build is generated. In the case of Create React App it is the `build` directory in our project root. - -3. The `command` option is the build command that Netlify will use. If you recall the [Manage environments in Create React App]({% link _chapters/manage-environments-in-create-react-app.md %}) chapter, this will seem familiar. In the default context the command is `REACT_APP_STAGE=dev npm run build`. - -The production context labelled, `context.production` is the only one where we set the `REACT_APP_STAGE` variable to `prod`. This is when we push to `master`. The `branch-deploy` is what we will be using when we push to any other non-production branch. The `deploy-preview` is for pull requests. - -### Handle HTTP Status Codes - -Just as the first part of the tutorial, we'll need to handle requests to any non-root paths of our app. Our frontend is a single-page app and the routing is handled on the client side. We need to tell Netlify to always redirect any request to our `index.html` and return the 200 status code for it. - -To do this, add a redirects rule at the bottom of `netlify.toml`: - -``` toml -# Always redirect any request to our index.html -# and return the status code 200. -[[redirects]] - from = "/*" - to = "/index.html" - status = 200 -``` - -### Modify the Build Command - -To deploy our app to Netlify we need to modify the build commands in our `package.json`. - -Replace the `scripts` block in your `package.json` with this. - -``` coffee -"scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test --env=jsdom", - "eject": "react-scripts eject" -} -``` - -You'll notice we are getting rid of our old build and deploy scripts. We are not going to be deploying to S3. - -### Commit the Changes - -Let's quickly commit these to Git. - -``` bash -$ git add . -$ git commit -m "Adding a Netlify build script" -``` - -### Push the Changes - -We are pretty much done making changes to our project. So let's go ahead and push them to GitHub. - -``` bash -$ git push -``` - - Now we are ready to add our project to Netlify. diff --git a/_chapters/create-a-custom-react-hook-to-handle-form-fields.md b/_chapters/create-a-custom-react-hook-to-handle-form-fields.md new file mode 100644 index 0000000000..6e13e12303 --- /dev/null +++ b/_chapters/create-a-custom-react-hook-to-handle-form-fields.md @@ -0,0 +1,159 @@ +--- +layout: post +title: Create a Custom React Hook to Handle Form Fields +date: 2017-01-18 12:00:00 +lang: en +ref: create-a-custom-react-hook-to-handle-form-fields +description: In this chapter we are going to create a custom React Hook to make it easier to handle form fields in our React app. +comments_id: create-a-custom-react-hook-to-handle-form-fields/1316 +--- + +Now before we move on to creating our sign up page, we are going to take a short detour to simplify how we handle form fields in React. We built a form as a part of our login page and we are going to do the same for our sign up page. You'll recall that in our login component we were creating two state variables to store the username and password. + +```typescript +const [email, setEmail] = useState(""); +const [password, setPassword] = useState(""); +``` + +And we also use something like this to set the state: + +```tsx +onChange={(e) => setEmail(e.target.value)} +``` + +Now we are going to do something similar for our sign up page and it'll have a few more fields than the login page. So it makes sense to simplify this process and have some common logic that all our form related components can share. Plus this is a good way to introduce the biggest benefit of React Hooks — reusing stateful logic between components. + +### Creating a Custom React Hook + +{%change%} Add the following to `src/lib/hooksLib.ts`. + +```typescript +import { useState, ChangeEvent, ChangeEventHandler } from "react"; + +interface FieldsType { + [key: string | symbol]: string; +} + +export function useFormFields( + initialState: FieldsType +): [FieldsType, ChangeEventHandler] { + const [fields, setValues] = useState(initialState); + + return [ + fields, + function (event: ChangeEvent) { + setValues({ + ...fields, + [event.target.id]: event.target.value, + }); + return; + }, + ]; +} +``` + +Creating a custom hook is amazingly simple. In fact, we did this back when we created our app context. But let's go over in detail how this works: + +1. A custom React Hook starts with the word `use` in its name. So ours is called `useFormFields`. + +2. Our Hook takes the initial state of our form fields as an object and saves it as a state variable called `fields`. The initial state in our case is an object where the _keys_ are the ids of the form fields and the _values_ are what the user enters. + +3. So our hook returns an array with `fields` and a callback function that sets the new state based on the event object. The callback function takes the event object and gets the form field id from `event.target.id` and the value from `event.target.value`. In the case of our form elements, the `event.target.id` comes from the `controlId` that's set in the `Form.Group` element: + + ```tsx + + Email + setEmail(e.target.value)} + /> + + ``` + +4. The callback function is directly using `setValues`, the function that we get from `useState`. So `onChange` we take what the user has entered and call `setValues` to update the state of `fields`, `{ ...fields, [event.target.id]: event.target.value }`. This updated object is now set as our new form field state. + +And that's it! We can now use this in our Login component. + +### Using Our Custom Hook + +We need to make a couple of changes to the component to use our custom hook. + +{%change%} Let's start by importing it in `src/containers/Login.tsx`. + +```typescript +import { useFormFields } from "../lib/hooksLib"; +``` + +{%change%} Replace the variable declarations. + +```typescript +const [email, setEmail] = useState(""); +const [password, setPassword] = useState(""); +``` + +{%change%} With: + +```typescript +const [fields, handleFieldChange] = useFormFields({ + email: "", + password: "", +}); +``` + +{%change%} Replace the `validateForm` function with. + +```typescript +function validateForm() { + return fields.email.length > 0 && fields.password.length > 0; +} +``` + +{%change%} In the `handleSubmit` function, replace the `Auth.signIn` call with. + +```typescript +await Auth.signIn(fields.email, fields.password); +``` + +{%change%} Replace our two form fields, starting with the ``. + +```tsx + +``` + +{%change%} And finally the password ``. + +```tsx + +``` + +You'll notice that we are using our `useFormFields` hook. A good way to think about custom React Hooks is to simply replace the line where we use it, with the Hook code itself. So instead of this line: + +```typescript +const [fields, handleFieldChange] = useFormFields({ + email: "", + password: "", +}); +``` + +Simply imagine the code for the `useFormFields` function instead! + +Finally, we are setting our fields using the function our custom hook is returning. + +```tsx +onChange = { handleFieldChange } +``` + +Now we are ready to tackle our sign up page. diff --git a/_chapters/create-a-dynamodb-table-in-sst.md b/_chapters/create-a-dynamodb-table-in-sst.md new file mode 100644 index 0000000000..2695dbc9b0 --- /dev/null +++ b/_chapters/create-a-dynamodb-table-in-sst.md @@ -0,0 +1,59 @@ +--- +layout: post +title: Create a DynamoDB Table in SST +date: 2021-08-23 00:00:00 +lang: en +description: In this chapter we'll be using a higher-level SST component to configure a DynamoDB table. +redirect_from: /chapters/configure-dynamodb-in-cdk.html +ref: create-a-dynamodb-table-in-sst +comments_id: create-a-dynamodb-table-in-sst/2459 +--- + +We are now going to start creating our infrastructure in SST. Starting with DynamoDB. + +### Create a Table + +{%change%} Add the following to our `infra/storage.ts`. + +```typescript +// Create the DynamoDB table +export const table = new sst.aws.Dynamo("Notes", { + fields: { + userId: "string", + noteId: "string", + }, + primaryIndex: { hashKey: "userId", rangeKey: "noteId" }, +}); +``` + +Let's go over what we are doing here. + +We are using the [`Dynamo`]({{ site.sst_url }}/docs/component/aws/dynamo/){:target="_blank"} component to create our DynamoDB table. + +It has two fields: + +1. `userId`: The id of the user that the note belongs to. +2. `noteId`: The id of the note. + +We are then creating an index for our table. + +Each DynamoDB table has a primary key. This cannot be changed once set. The primary key uniquely identifies each item in the table, so that no two items can have the same key. DynamoDB supports two different kinds of primary keys: + +- Partition key +- Partition key and sort key (composite) + +We are going to use the composite primary key (referenced by `primaryIndex` in code block above) which gives us additional flexibility when querying the data. For example, if you provide only the value for `userId`, DynamoDB would retrieve all of the notes by that user. Or you could provide a value for `userId` and a value for `noteId`, to retrieve a particular note. + +### Deploy Changes + +After you make your changes, SST will automatically create the table. You should see something like this at the end of the deploy process. + +```bash +| Created Notes sst:aws:Dynamo +``` + +{%info%} +You'll need to make sure you have `sst dev` running, if not then restart it by running `npx sst dev`. +{%endinfo%} + +Now that our database has been created, let's create an S3 bucket to handle file uploads. diff --git a/_chapters/create-a-hello-world-api.md b/_chapters/create-a-hello-world-api.md new file mode 100644 index 0000000000..66ad6f97a4 --- /dev/null +++ b/_chapters/create-a-hello-world-api.md @@ -0,0 +1,121 @@ +--- +layout: post +title: Create a Hello World API +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we'll be creating a simple Hello World API using SST. +ref: create-a-hello-world-api +comments_id: create-a-hello-world-api/2460 +--- + +With our newly created SST app, we are ready to deploy a simple _Hello World_ API. Let's rename some of the files from our template. + +### Rename the Template + +{%change%} Replace our `infra/api.ts` with the following. + +```ts +import { bucket } from "./storage"; + +export const api = new sst.aws.ApiGatewayV2("Api"); + +api.route("GET /", { + link: [bucket], + handler: "packages/functions/src/api.handler", +}); +``` + +Here we are creating a simple API with one route, `GET /`. When this API is invoked, the function called `handler` in `packages/functions/src/api.ts` will be executed. + +We are also _linking_ an S3 Bucket to our API. This allows the functions in our API to access the bucket. We'll be using this bucket later to handle file uploads. For now let's quickly rename it. + +{%change%} Replace our `infra/storage.ts` with. + +```ts +// Create an S3 bucket +export const bucket = new sst.aws.Bucket("Uploads"); +``` + +Also let's rename how this bucket is accessed in our app code. We'll go into detail about this in the coming chapters. + +{%change%} Rename `Resource.MyBucket.name` line in `packages/functions/src/api.ts`. + +```ts +body: `${Example.hello()} Linked to ${Resource.Uploads.name}.`, +``` + +Given that we've renamed a few components, let's also make the change in our config. + +{%change%} Replace the `run` function in `sst.config.ts`. + +```ts +async run() { + await import("./infra/storage"); + await import("./infra/api"); +}, +``` + +{%note%} +By default SST sets you up with a TypeScript project. While the infrastructure is in TypeScript, you are free to use regular JavaScript in your application code. +{%endnote%} + +Let's go ahead and deploy this. + +### Start Your Dev Environment + +We'll do this by starting up our local development environment. SST's dev environment runs your functions [Live]({{ site.sst_url }}/docs/live){:target="_blank"}. It allows you to work on your serverless apps live. + +{%change%} Start your dev environment. + +```bash +$ npx sst dev +``` + +Running `sst dev` will take a minute or two to deploy your app and bootstrap your account for SST. + +```txt +SST 0.1.17 ready! + +➜ App: notes + Stage: jayair + Console: https://console.sst.dev/local/notes/jayair + + ... + ++ Complete + Api: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +The `Api` is the API we just created. Let's test our endpoint. If you open the endpoint URL in your browser, you should see _Hello World!_ being printed out. + +![Serverless Hello World API invoked](/assets/part2/sst-hello-world-api-invoked.png) + +You'll notice its also printing out the name of the bucket that it's linked to. + +### Deploying to Prod + +To deploy our API to prod, we'll need to stop our local development environment and run the following. + +```bash +$ npx sst deploy --stage production +``` + +We don't have to do this right now. We'll be doing it later once we are done working on our app. + +The idea here is that we are able to work on separate environments. So when we are working in our personal stage (`jayair`), it doesn't break the API for our users in `production`. The environment (or stage) names in this case are just strings and have no special significance. We could've called them `development` and `prod` instead. + +We are however creating completely new apps when we deploy to a different environment. This is another advantage of the SST workflow. The infrastructure as code idea makes it easy to replicate to new environments. And the pay per use model of serverless means that we are not charged for these new environments unless we actually use them. + +### Commit the Changes + +As we work through the guide we'll save our changes. + +{%change%} Commit what we have and push our changes to GitHub. + +```bash +$ git add . +$ git commit -m "Initial commit" +$ git push +``` + +Now we are ready to create the backend for our notes app. diff --git a/_chapters/create-a-login-page.md b/_chapters/create-a-login-page.md index 8378490cf6..bbf4497bcc 100644 --- a/_chapters/create-a-login-page.md +++ b/_chapters/create-a-login-page.md @@ -3,102 +3,88 @@ layout: post title: Create a Login Page date: 2017-01-13 00:00:00 lang: en -comments_id: create-a-login-page +ref: create-a-login-page description: We are going to add a login page to our React.js app. To create the login form we are using the FormGroup and FormControl React-Bootstrap components. -context: true comments_id: create-a-login-page/71 --- -Let's create a page where the users of our app can login with their credentials. When we created our User Pool we asked it to allow a user to sign in and sign up with their email as their username. We'll be touching on this further when we create the signup form. +Let's create a page where the users of our app can login with their credentials. When we [created our User Pool]({% link _archives/create-a-cognito-user-pool.md %}){:target="_blank"} we asked it to allow a user to sign in and sign up with their email as their username. We'll be touching on this further when we create the signup form. So let's start by creating the basic form that'll take the user's email (as their username) and password. ### Add the Container -Create a new file `src/containers/Login.js` and add the following. +{%change%} Create a new file `src/containers/Login.tsx` and add the following. -``` coffee -import React, { Component } from "react"; -import { Button, FormGroup, FormControl, ControlLabel } from "react-bootstrap"; +```tsx +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import Stack from "react-bootstrap/Stack"; +import Button from "react-bootstrap/Button"; import "./Login.css"; -export default class Login extends Component { - constructor(props) { - super(props); +export default function Login() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); - this.state = { - email: "", - password: "" - }; + function validateForm() { + return email.length > 0 && password.length > 0; } - validateForm() { - return this.state.email.length > 0 && this.state.password.length > 0; - } - - handleChange = event => { - this.setState({ - [event.target.id]: event.target.value - }); - } - - handleSubmit = event => { + function handleSubmit(event: React.FormEvent) { event.preventDefault(); } - render() { - return ( -
-
- - Email - + + + + Email + setEmail(e.target.value)} /> - - - Password - + + Password + setPassword(e.target.value)} /> - - -
-
- ); - } + + + + ); } ``` We are introducing a couple of new concepts in this. -1. In the constructor of our component we create a state object. This will be where we'll store what the user enters in the form. +1. Right at the top of our component, we are using the `useState` hook to store what the user enters in the form. The `useState` hook just gives you the current value of the variable you want to store in the state and a function to set the new value. -2. We then connect the state to our two fields in the form by setting `this.state.email` and `this.state.password` as the `value` in our input fields. This means that when the state changes, React will re-render these components with the updated value. +2. We then connect the state to our two fields in the form using the `setEmail` and `setPassword` functions to store what the user types in — `e.target.value`. Once we set the new state, our component gets re-rendered. The variables `email` and `password` now have the new values. -3. But to update the state when the user types something into these fields, we'll call a handle function named `handleChange`. This function grabs the `id` (set as `controlId` for the ``) of the field being changed and updates its state with the value the user is typing in. Also, to have access to the `this` keyword inside `handleChange` we store the reference to an anonymous function like so: `handleChange = (event) => { } `. +3. We are setting the form controls to show the value of our two state variables `email` and `password`. In React, this pattern of displaying the current form value as a state variable and setting the new one when a user types something, is called a Controlled Component. 4. We are setting the `autoFocus` flag for our email field, so that when our form loads, it sets focus to this field. 5. We also link up our submit button with our state by using a validate function called `validateForm`. This simply checks if our fields are non-empty, but can easily do something more complicated. -6. Finally, we trigger our callback `handleSubmit` when the form is submitted. For now we are simply suppressing the browsers default behavior on submit but we'll do more here later. +6. Finally, we trigger our callback `handleSubmit` when the form is submitted. For now we are simply suppressing the browser's default behavior on submit but we'll do more here later. -Let's add a couple of styles to this in the file `src/containers/Login.css`. +{%change%} Let's add a couple of styles to this in the file `src/containers/Login.css`. -``` css +```css @media all and (min-width: 480px) { .Login { padding: 60px 0; @@ -115,16 +101,16 @@ These styles roughly target any non-mobile screen sizes. ### Add the Route -Now we link this container up with the rest of our app by adding the following line to `src/Routes.js` below our home ``. +{%change%} Now we link this container up with the rest of our app by adding the following line to `src/Routes.tsx` below our `` route. -``` coffee - +```tsx +} /> ``` -And include our component in the header. +{%change%} And include our component in the header. -``` javascript -import Login from "./containers/Login"; +```tsx +import Login from "./containers/Login.tsx"; ``` Now if we switch to our browser and navigate to the login page we should see our newly created form. diff --git a/_chapters/create-a-new-reactjs-app.md b/_chapters/create-a-new-reactjs-app.md index 11c9f9ad83..52703d5886 100644 --- a/_chapters/create-a-new-reactjs-app.md +++ b/_chapters/create-a-new-reactjs-app.md @@ -4,48 +4,152 @@ title: Create a New React.js App date: 2017-01-06 00:00:00 lang: en ref: create-a-new-react-js-app -description: Create React App helps you build React.js app with no configuration. Install the Create React App CLI using the NPM package and use the command to start a new React.js project. -context: true +description: In this chapter we'll use Vite to create a new React.js app. We'll be deploying our React app to AWS using the SST StaticSite component. comments_id: create-a-new-react-js-app/68 --- -Let's get started with our frontend. We are going to create a single page app using [React.js](https://facebook.github.io/react/). We'll use the [Create React App](https://github.com/facebookincubator/create-react-app) project to set everything up. It is officially supported by the React team and conveniently packages all the dependencies for a React.js project. +We are now ready to work on our frontend. So far we've built and deployed our backend API and infrastructure. We are now going to build a web app that connects to our backend. -Move out of the directory that we were working in for the backend. +We are going to create a single page app using [React.js](https://facebook.github.io/react/). We'll use the [Vite](https://vitejs.dev) project to set everything up. -``` bash -$ cd ../ +### Create a New React App + +{%change%} Run the following command **in the `packages/` directory**. + +```bash +$ npm create vite@latest frontend -- --template react-ts ``` -### Create a New React App +{%note%} +Make sure you use the extra `--` in the command. +{%endnote%} + +This will create your new project in the `frontend/` directory. + +{%change%} Let's update the name of the package in the `packages/frontend/package.json`. Replace this: + +```diff +- "name": "frontend", ++ "name": "@notes/frontend", +``` + +Make sure to use the name of your app instead of `notes`. + +{%change%} Now install the dependencies. + +```bash +$ cd frontend +$ npm install +``` + +This should take a second to run. + +We also need to make a small change to our Vite config to bundle our frontend. + +{%change%} Add the following below the `plugins: [react()],` line in `packages/frontend/vite.config.ts`. + +```ts +build: { + // NOTE: Needed when deploying + chunkSizeWarningLimit: 800, +}, +``` + +### Add the React App to SST + +We are going to be deploying our React app to AWS. To do that we'll be using the SST [`StaticSite`]({{ site.sst_url }}/docs/component/aws/static-site/){:target="_blank"} component. + +{%change%} Create a new file in `infra/web.ts` and add the following. + +```ts +import { api } from "./api"; +import { bucket } from "./storage"; +import { userPool, identityPool, userPoolClient } from "./auth"; + +const region = aws.getRegionOutput().name; + +export const frontend = new sst.aws.StaticSite("Frontend", { + path: "packages/frontend", + build: { + output: "dist", + command: "npm run build", + }, + environment: { + VITE_REGION: region, + VITE_API_URL: api.url, + VITE_BUCKET: bucket.name, + VITE_USER_POOL_ID: userPool.id, + VITE_IDENTITY_POOL_ID: identityPool.id, + VITE_USER_POOL_CLIENT_ID: userPoolClient.id, + }, +}); +``` + +We are doing a couple of things of note here: + +1. We are pointing our `StaticSite` component to the `packages/frontend/` directory where our React app is. +2. We are passing in the outputs from our other components as [environment variables in Vite](https://vitejs.dev/guide/env-and-mode.html#env-variables){:target="_blank"}. This means that we won't have to hard code them in our React app. The `VITE_*` prefix is a convention Vite uses to say that we want to access these in our frontend code. + +### Adding to the app + +Let's add this to our config. + + +{%change%} Add this below the `await import("./infra/api");` line in your `sst.config.ts`. + +```ts +await import("./infra/web"); +``` -Run the following command to create the client for our notes app. +### Deploy Our Changes -``` bash -$ npx create-react-app notes-app-client +If you switch over to your terminal, you will notice that your changes are being deployed. + +{%info%} +You’ll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. +{%endinfo%} + +```bash ++ Complete + Api: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com + Frontend: https://d1wyq46yoha2b6.cloudfront.net + ... ``` -This should take a second to run, and it will create your new project and your new working directory. +### Starting the React App + +The `sst dev` CLI will automatically start our React frontend by running `npm run dev`. It also passes in the environment variables that we have configured above. + +![sst dev CLI starts frontend](/assets/part2/sst-dev-cli-starts-frontend.png) + +You can click on **Frontend** in the sidebar or navigate to it. -Now let's go into our working directory and run our project. +This should show where your frontend is running locally. -``` bash -$ cd notes-app-client -$ npm start +```bash +VITE v5.3.4 ready in 104 ms + +➜ Local: http://127.0.0.1:5173/ +➜ Network: use --host to expose +➜ press h + enter to show help ``` -This should fire up the newly created app in your browser. +{%info%} +SST doesn't deploy your frontend while you are working locally. This is because most frontends come with their own local dev environments. +{%endinfo%} + +If you head to that URL in your browser you should see. -![New Create React App screenshot](/assets/new-create-react-app.png) +![New Vite React App screenshot](/assets/part2/new-vite-react-app.png) ### Change the Title -Let's quickly change the title of our note taking app. Open up `public/index.html` and edit the `title` tag to the following: +Let's quickly change the title of our note taking app. -``` html +{%change%} Open up `packages/frontend/index.html` and edit the `title` tag to the following: + +```html Codestin Search App ``` -Create React App comes pre-loaded with a pretty convenient yet minimal development environment. It includes live reloading, a testing framework, ES6 support, and [much more](https://github.com/facebookincubator/create-react-app#why-use-this). - -Next, we are going to create our app icon and update the favicons. +Now we are ready to build our frontend! We are going to start by creating our app icon and updating the favicons. diff --git a/_chapters/create-a-route-that-redirects.md b/_chapters/create-a-route-that-redirects.md index af44e78998..87f22b3735 100644 --- a/_chapters/create-a-route-that-redirects.md +++ b/_chapters/create-a-route-that-redirects.md @@ -4,53 +4,75 @@ title: Create a Route That Redirects date: 2017-02-02 00:00:00 lang: en redirect_from: /chapters/create-a-hoc-that-checks-auth.html -description: In our React.js app we want to redirect users to the login page if they are not logged in and redirect them away from the login page if they are logged in. To do so we are going to use the Redirect component from React Router v4. -context: true +description: In our React.js app we want to redirect users to the login page if they are not logged in and redirect them away from the login page if they are logged in. To do so we are going to use the Redirect component and useLocation hook from React Router. While, the session will be stored in our app Context using the useContext hook. comments_id: create-a-route-that-redirects/47 ref: create-a-route-that-redirects --- Let's first create a route that will check if the user is logged in before routing. -Add the following to `src/components/AuthenticatedRoute.js`. - -``` coffee -import React from "react"; -import { Route, Redirect } from "react-router-dom"; - -export default ({ component: C, props: cProps, ...rest }) => - - cProps.isAuthenticated - ? - : } - />; +{%change%} Add the following file in components `src/components/AuthenticatedRoute.tsx`. + +```tsx +import { ReactElement } from "react"; +import { Navigate, useLocation } from "react-router-dom"; +import { useAppContext } from "../lib/contextLib"; + +export default function AuthenticatedRoute({ + children, +}: { + children: ReactElement; +}): ReactElement { + const { pathname, search } = useLocation(); + const { isAuthenticated } = useAppContext(); + + if (!isAuthenticated) { + return ; + } + + return children; +} ``` -This component is similar to the `AppliedRoute` component that we created in the [Add the session to the state]({% link _chapters/add-the-session-to-the-state.md %}) chapter. The main difference being that we look at the props that are passed in to check if a user is authenticated. If the user is authenticated, then we simply render the passed in component. And if the user is not authenticated, then we use the `Redirect` React Router v4 component to redirect the user to the login page. We also pass in the current path to the login page (`redirect` in the querystring). We will use this later to redirect us back after the user logs in. +This simple component creates a `Route` where its children are rendered only if the user is authenticated. If the user is not authenticated, then it redirects to the login page. Let's take a closer look at it: + +- Like all components in React, `AuthenticatedRoute` has a prop called `children` that represents all child components. Example child components in our case would be `NewNote`, `Notes` and `Settings`. + +- The `AuthenticatedRoute` component returns a React Router `Route` component. + +- We use the `useAppContext` hook to check if the user is authenticated. + +- If the user is authenticated, then we simply render the `children` component. And if the user is not authenticated, then we use the `Navigate` React Router component to redirect the user to the login page. + +- We also pass in the current path to the login page (`redirect` in the query string). We will use this later to redirect us back after the user logs in. We use the `useLocation` React Router hook to get this info. We'll do something similar to ensure that the user is not authenticated. -Add the following to `src/components/UnauthenticatedRoute.js`. +{%change%} Next, add the following file in components `src/components/UnauthenticatedRoute.tsx`. + +```tsx +import { cloneElement, ReactElement } from "react"; +import { Navigate } from "react-router-dom"; +import { useAppContext } from "../lib/contextLib"; -``` coffee -import React from "react"; -import { Route, Redirect } from "react-router-dom"; +interface Props { + children: ReactElement; +} -export default ({ component: C, props: cProps, ...rest }) => - - !cProps.isAuthenticated - ? - : } - />; +export default function UnauthenticatedRoute(props: Props): ReactElement { + const { isAuthenticated } = useAppContext(); + const { children } = props; + + if (isAuthenticated) { + return ; + } + + return cloneElement(children, props); +} ``` -Here we are checking to ensure that the user is not authenticated before we render the component that is passed in. And in the case where the user is authenticated, we use the `Redirect` component to simply send the user to the homepage. +Here we are checking to ensure that the user is **not** authenticated before we render the child components. Example child components here would be `Login` and `Signup`. And in the case where the user is authenticated, we use the `Navigate` component to simply send the user to the homepage. + +The `cloneElement` above makes sure that passed in `state` is handled correctly for child components of `UnauthenticatedRoute` routes. Next, let's use these components in our app. diff --git a/_chapters/create-a-settings-page.md b/_chapters/create-a-settings-page.md index 9904a1d970..fb7efd2434 100644 --- a/_chapters/create-a-settings-page.md +++ b/_chapters/create-a-settings-page.md @@ -1,10 +1,9 @@ --- layout: post title: Create a Settings Page -date: 2018-03-21 00:00:00 +date: 2017-01-31 06:00:00 lang: en description: Our notes app needs a settings page for our users to input their credit card details and sign up for a pricing plan. -context: true ref: create-a-settings-page comments_id: create-a-settings-page/184 --- @@ -18,118 +17,72 @@ We are going to add a settings page to our app. This is going to allow users to To get started let's add our settings page. -Create a new file in `src/containers/Settings.js` and add the following. +{%change%} Create a new file in `src/types/billing.ts` and add the following to define a type for our billing API. -``` coffee -import React, { Component } from "react"; -import { API } from "aws-amplify"; +```typescript +export interface BillingType { + storage: string; + source?: string; +} +``` -export default class Settings extends Component { - constructor(props) { - super(props); +{%change%} Create a new file in `src/containers/Settings.tsx` and add the following. - this.state = { - isLoading: false - }; - } +```tsx +import { useState } from "react"; +import config from "../config"; +import { API } from "aws-amplify"; +import { onError } from "../lib/errorLib"; +import { useNavigate } from "react-router-dom"; +import { BillingType } from "../types/billing"; - billUser(details) { +export default function Settings() { + const nav = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + + function billUser(details: BillingType) { return API.post("notes", "/billing", { - body: details + body: details, }); } - render() { - return ( -
-
- ); - } + return
; } ``` -Next import this component in the header of `src/Routes.js`. +{%change%} Next, add the following below the `/signup` route in our `` block in `src/Routes.tsx`. -``` js -import Settings from "./containers/Settings"; +```tsx +} /> ``` -And replace our `` block in `src/Routes.js` with this. - -``` coffee - - - - - - - - { /* Finally, catch all unmatched routes */ } - - -``` +{%change%} And import this component in the header of `src/Routes.js`. -Notice that we added a route for our new settings page. - -Next add a link to our settings page in the navbar by replacing the `render` method in `src/App.js` with this. - -``` coffee -render() { - const childProps = { - isAuthenticated: this.state.isAuthenticated, - userHasAuthenticated: this.userHasAuthenticated - }; - - return ( - !this.state.isAuthenticating && -
- - - - Scratch - - - - - - - - -
- ); -} +```tsx +import Settings from "./containers/Settings.tsx"; ``` -You'll notice that we added another link in the navbar for the case a user is logged in. +Next add a link to our settings page in the navbar. -Now if you head over to your app, you'll see a new **Settings** link at the top. Of course, the page is pretty empty right now. - -![Add empty settings page screenshot](/assets/part2/add-empty-settings-page.png) +{%change%} Replace the following line in the `return` statement `src/App.tsx`. -### Commit the Changes +```tsx +Logout +``` -Let's quickly commit these to Git. +{%change%} With. -``` bash -$ git add . -$ git commit -m "Adding settings page" +```tsx +<> + + Settings + + Logout + ``` +Now if you head over to your app, you'll see a new **Settings** link at the top. Of course, the page is pretty empty right now. + +![Add empty settings page screenshot](/assets/part2/add-empty-settings-page.png) + Next, we'll add our Stripe SDK keys to our config. diff --git a/_chapters/create-a-signup-page.md b/_chapters/create-a-signup-page.md index 256725c845..db6c20cd48 100644 --- a/_chapters/create-a-signup-page.md +++ b/_chapters/create-a-signup-page.md @@ -7,7 +7,7 @@ date: 2017-01-19 00:00:00 comments_id: create-a-signup-page/65 --- -The signup page is quite similar to the login page that we just created. But it has a couple of key differences. When we sign the user up, AWS Cognito sends them a confirmation code via email. We also need to authenticate the new user once they've confirmed their account. +The signup page is quite similar to the login page that we just created. But it has a couple of key differences. When we sign the user up, [AWS Cognito]({% link _archives/create-a-cognito-user-pool.md %}) sends them a confirmation code via email. We also need to authenticate the new user once they've confirmed their account. So the signup flow will look something like this: diff --git a/_chapters/create-an-aws-account.md b/_chapters/create-an-aws-account.md index df1bc6daa0..5ec172589c 100644 --- a/_chapters/create-an-aws-account.md +++ b/_chapters/create-an-aws-account.md @@ -5,12 +5,9 @@ date: 2016-12-25 00:00:00 lang: en ref: create-an-aws-account description: To create a serverless app using Lambda we are going to first need to create an AWS (Amazon Web Services) account. -context: true comments_id: create-an-aws-account/88 --- -Let's first get started by creating an AWS (Amazon Web Services) account. Of course you can skip this if you already have one. Head over to the [AWS homepage](https://aws.amazon.com) and hit the **Create a Free Account** and follow the steps to create your account. +Let's first get started by creating an AWS (Amazon Web Services) account. Of course you can skip this if you already have one. Head over to the [AWS homepage](https://aws.amazon.com){:target="_blank"} and create your account. -![Create an aws account Screenshot](/assets/create-an-aws-account.png) - -Next let's configure your account so it's ready to be used for the rest of our guide. +Next we'll configure your account so it's ready to be used for the rest of our guide. diff --git a/_chapters/create-an-iam-user.md b/_chapters/create-an-iam-user.md index 088802ac0b..be37c1833b 100644 --- a/_chapters/create-an-iam-user.md +++ b/_chapters/create-an-iam-user.md @@ -5,54 +5,79 @@ date: 2016-12-25 12:00:00 lang: en ref: create-an-iam-user description: To interact with AWS using some command line tools we need to create an IAM user through the AWS console. -context: true comments_id: create-an-iam-user/92 --- +Once we have an AWS account, we'll need to create an IAM user to programmatically interact with it. We'll be using this later to configure our AWS CLI (command-line interface). + Amazon IAM (Identity and Access Management) enables you to manage users and user permissions in AWS. You can create one or more IAM users in your AWS account. You might create an IAM user for someone who needs access to your AWS console, or when you have a new application that needs to make API calls to AWS. This is to add an extra layer of security to your AWS account. In this chapter, we are going to create a new IAM user for a couple of the AWS related tools we are going to be using later. ### Create User -First, log in to your [AWS Console](https://console.aws.amazon.com) and select IAM from the list of services. - -![Select IAM Service Screenshot](/assets/iam-user/select-iam-service.png) +First, log in to your [AWS Console](https://console.aws.amazon.com) and search for IAM in the search bar. Hover or focus on the **IAM card** and then select the **Users** link. -Select **Users**. +![Select IAM Service Screenshot](/assets/create-iam-user/search-to-iam-service.png) -![Select IAM Users Screenshot](/assets/iam-user/select-iam-users.png) +Select **Add Users**. -Select **Add User**. +![Add IAM User Screenshot](/assets/create-iam-user/add-iam-user-button.png) -![Add IAM User Screenshot](/assets/iam-user/add-iam-user.png) +Enter a **User name**, then select **Next**. -Enter a **User name** and check **Programmatic access**, then select **Next: Permissions**. +This account will be used by our [AWS CLI](https://aws.amazon.com/cli/) and [SST]({{ site.sst_github_repo }}). They will be connecting to the AWS API directly and will not be using the Management Console. -This account will be used by our [AWS CLI](https://aws.amazon.com/cli/) and [Serverless Framework](https://serverless.com). They'll be connecting to the AWS API directly and will not be using the Management Console. +{%note%} +The best practice is to avoid creating keys when possible. When using programmatic access keys, regularly rotate them. In most cases, there are alternative solutions, see the [AWS IAM User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_RotateAccessKey) for more information. +{%endnote%} -![Fill in IAM User Info Screenshot](/assets/iam-user/fill-in-iam-user-info.png) +![Fill in IAM User Info Screenshot](/assets/create-iam-user/fill-in-iam-user-details.png) Select **Attach existing policies directly**. -![Add IAM User Policy Screenshot](/assets/iam-user/add-iam-user-policy.png) +![Add IAM User Policy Screenshot](/assets/create-iam-user/add-iam-attach-policies-directly.png) -Search for **AdministratorAccess** and select the policy, then select **Next: Review**. +Search for **AdministratorAccess** and select the policy by checking the checkbox, then select **Next**. -We can provide a more fine-grained policy here and we cover this later in the [Customize the Serverless IAM Policy]({% link _chapters/customize-the-serverless-iam-policy.md %}) chapter. But for now, let's continue with this. +We can provide a more fine-grained policy here. We cover this later in the [Customize the Serverless IAM Policy]({% link _archives/customize-the-serverless-iam-policy.md %}) chapter. But for now, let's continue with this. -![Added Admin Policy Screenshot](/assets/iam-user/added-admin-policy.png) +![Added Admin Policy Screenshot](/assets/create-iam-user/iam-user-add-admin-policy.png) Select **Create user**. -![Reivew IAM User Screenshot](/assets/iam-user/review-iam-user.png) +![Reivew IAM User Screenshot](/assets/create-iam-user/iam-create-user.png) + +Select **View user**. + +![View IAM User Screenshot](/assets/create-iam-user/iam-success-view-user.png) + +Select **Security credentials** + +![IAM User Security Credentials Screenshot](/assets/create-iam-user/iam-user-security-credentials.png) + +Select **Create access key** + +![IAM User Create Access Key Screenshot](/assets/create-iam-user/iam-user-create-access-key.png) + +In keeping with the current guide instructions, we will choose other to generate an access key and secret. Select **Other** and select **Next** + +![IAM User Access Key Purpose](/assets/create-iam-user/iam-user-access-key-purpose.png) + +You could add a descriptive tag here, but we will skip that in this tutorial, select **Create access key** + +![IAM User Access Key Purpose](/assets/create-iam-user/iam-access-key-skip-tag-create.png) Select **Show** to reveal **Secret access key**. -![Added IAM User Screenshot](/assets/iam-user/added-iam-user.png) +![IAM User Access Key Show](/assets/create-iam-user/iam-access-key-secret-show.png) + +{%note%} +This is the only screen on which you will be able to access this key. Save it to a secure location using best practices to ensure the security of your application. +{%endnote%} -Take a note of the **Access key ID** and **Secret access key**. We will be needing this later. +Take a note of the **Access key** and **Secret access key**. We will be needing this in the next chapter. -![IAM User Credentials Screenshot](/assets/iam-user/iam-user-credentials.png) +![IAM Access Credentials Screenshot](/assets/create-iam-user/iam-access-credentials.png) -The concept of IAM pops up very frequently when working with AWS services. So it is worth taking a better look at what IAM is and how it can help us secure our serverless setup. +Now let's configure our AWS CLI. By configuring the AWS CLI, we can deploy our applications from our command line. diff --git a/_chapters/create-an-s3-bucket-for-file-uploads.md b/_chapters/create-an-s3-bucket-for-file-uploads.md deleted file mode 100644 index c54b1c2a4b..0000000000 --- a/_chapters/create-an-s3-bucket-for-file-uploads.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -layout: post -title: Create an S3 Bucket for File Uploads -date: 2016-12-27 00:00:00 -lang: en -ref: create-an-s3-bucket-for-file-uploads -description: To allow users to upload files to our serverless app we are going to use Amazon S3 (Simple Storage Service). S3 allows you to store files and organize them into buckets. We are going to create an S3 bucket and enable CORS (cross-origin resource sharing) to ensure that our React.js app can upload files to it. -redirect_from: /chapters/create-a-s3-bucket-for-file-uploads.html -context: true -comments_id: create-an-s3-bucket-for-file-uploads/150 ---- - -Now that we have our database table ready; let's get things set up for handling file uploads. We need to handle file uploads because each note can have an uploaded file as an attachment. - -[Amazon S3](https://aws.amazon.com/s3/) (Simple Storage Service) provides storage service through web services interfaces like REST. You can store any object in S3 including images, videos, files, etc. Objects are organized into buckets, and identified within each bucket by a unique, user-assigned key. - -In this chapter, we are going to create an S3 bucket which will be used to store user uploaded files from our notes app. - -### Create Bucket - -First, log in to your [AWS Console](https://console.aws.amazon.com) and select **S3** from the list of services. - -![Select S3 Service screenshot](/assets/s3/select-s3-service.png) - -Select **Create bucket**. - -![Select Create Bucket screenshot](/assets/s3/select-create-bucket.png) - -Pick a name of the bucket and select a region. Then select **Create**. - -- **Bucket names** are globally unique, which means you cannot pick the same name as this tutorial. -- **Region** is the physical geographical region where the files are stored. We will use **US East (N. Virginia)** for this guide. - -Make a note of the name and region as we'll be using it later in the guide. - -![Enter S3 Bucket Info screenshot](/assets/s3/enter-s3-bucket-info.png) - -Step through the next steps and leave the defaults by clicking **Next**, and then click **Create bucket** on the last step. - -![Set S3 Bucket Properties screenshot](/assets/s3/set-s3-bucket-properties.png) -![Set S3 Bucket Permissions screenshot](/assets/s3/set-s3-bucket-permissions.png) -![Review S3 Bucket screenshot](/assets/s3/review-s3-bucket.png) - -### Enable CORS - -In the notes app we'll be building, users will be uploading files to the bucket we just created. And since our app will be served through our custom domain, it'll be communicating across domains while it does the uploads. By default, S3 does not allow its resources to be accessed from a different domain. However, cross-origin resource sharing (CORS) defines a way for client web applications that are loaded in one domain to interact with resources in a different domain. Let's enable CORS for our S3 bucket. - -Select the bucket we just created. - -![Select Created S3 Bucket screenshot](/assets/s3/select-created-s3-bucket.png) - -Select the **Permissions** tab, then select **CORS configuration**. - -![Select S3 Bucket CORS Configuration screenshot](/assets/s3/select-s3-bucket-cors-configuration.png) - -Add the following CORS configuration into the editor, then hit **Save**. - -``` xml - - - * - GET - PUT - POST - HEAD - DELETE - 3000 - * - - -``` - -Note that you can edit this configuration to use your own domain or a list of domains when you use this in production. - -![Save S3 Bucket CORS Configuration screenshot](/assets/s3/save-s3-bucket-cors-configuration.png) - -Now that our S3 bucket is ready, let's get set up to handle user authentication. - diff --git a/_chapters/create-an-s3-bucket-in-sst.md b/_chapters/create-an-s3-bucket-in-sst.md new file mode 100644 index 0000000000..89c59ea349 --- /dev/null +++ b/_chapters/create-an-s3-bucket-in-sst.md @@ -0,0 +1,33 @@ +--- +layout: post +title: Create an S3 Bucket in SST +date: 2021-07-17 00:00:00 +lang: en +description: In this chapter we will be using a higher-level component to create an S3 bucket in our SST app. +redirect_from: /chapters/configure-s3-in-cdk.html +ref: create-an-s3-bucket-in-sst +comments_id: create-an-s3-bucket-in-sst/2461 +--- + +We'll be storing the files that's uploaded by our users to an S3 bucket. The template we are using comes with a bucket that we renamed back in the [Create a Hello World API]({% link _chapters/create-a-hello-world-api.md %}). + +Recall the following from `infra/storage.ts`. + +```ts +// Create an S3 bucket +export const bucket = new sst.aws.Bucket("Uploads"); +``` + +Here we are creating a new S3 bucket using the SST [`Bucket`]({{ site.sst_url }}/docs/component/aws/bucket){:target="_blank"} component. + +### Commit the Changes + +{%change%} Let's commit and push our changes to GitHub. + +```bash +$ git add . +$ git commit -m "Adding storage" +$ git push +``` + +Next, let's create the API for our notes app. diff --git a/_chapters/create-an-sst-app.md b/_chapters/create-an-sst-app.md new file mode 100644 index 0000000000..a4a2acbab8 --- /dev/null +++ b/_chapters/create-an-sst-app.md @@ -0,0 +1,95 @@ +--- +layout: post +title: Create an SST app +date: 2021-08-17 00:00:00 +lang: en +description: Create your first SST app by cloning the template from GitHub. +redirect_from: + - /chapters/building-a-cdk-app-with-sst.html + - /chapters/initialize-a-github-repo.html + - /chapters/initialize-the-backend-repo.html + - /chapters/initialize-the-frontend-repo.html +ref: create-an-sst-app +comments_id: create-an-sst-app/2462 +--- + +Now that we have some background on SST and _infrastructure as code_, we are ready to create our first SST app! + +We are going to use a [template SST project][Template]{:target="_blank"}, it comes with a good monorepo setup. It'll help us organize our frontend and APIs. + +Head over to — [**github.com/sst/monorepo-template**][Template]{:target="_blank"}, click **Use this template** > **Create a new repository**. + +![Use the SST monorepo GitHub template screenshot](/assets/part2/use-the-sst-monorepo-github-template-screenshot.png) + +Give your repository a name, in our case we are calling it `notes`. Next hit **Create repository**. + +![Name new GitHub repository screenshot](/assets/part2/name-new-github-repository.png) + +Once your repository is created, copy the repository URL. + +{%change%} Run the following in your working directory. + +```bash +$ git clone +$ cd notes +``` + +{%change%} Use your app name in the template. + +```bash +$ npx replace-in-file '/monorepo-template/g' 'notes' '**/*.*' --verbose +``` + +{%change%} Install the dependencies. + +```bash +$ npm install +``` + +By default, the template is creating an API. You can see this in the `sst.config.ts` in the root. + +```ts +/// + +export default $config({ + app(input) { + return { + name: "notes", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + await import("./infra/storage"); + const api = await import("./infra/api"); + + return { + api: api.myApi.url, + }; + }, +}); +``` + +{%caution%} +To rename an app, you’ll need to remove the resources from the old one and deploy to the new one. +{%endcaution%} + +The name of your app as you might recall is `notes`. A word of caution on IaC, if you rename your app after you deploy it, it doesn't rename the previously created resources in your app. You'll need to remove your old app and redeploy it again with the new name. To get a better sense of this, you can read more about the [SST workflow]({{ site.sst_url }}/docs/workflow){:target="_blank"}. + +## Project layout + +An SST app is made up of two parts. + +1. `infra/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `infra/` directory of your project. + +2. `packages/` — App Code + + The Lambda function code that's run when your API is invoked is placed in the `packages/functions` directory of your project, the `packages/core` contains our business logic, and the `packages/scripts` are for any one-off scripts we might create. + +Later on we'll be adding a `packages/frontend/` directory for our React app. + +The starter project that's created is defining a simple _Hello World_ API. In the next chapter, we'll be deploying it and running it locally. + +[Template]: https://github.com/sst/monorepo-template diff --git a/_chapters/create-containers.md b/_chapters/create-containers.md index 874e7be065..87b328c08a 100644 --- a/_chapters/create-containers.md +++ b/_chapters/create-containers.md @@ -4,46 +4,31 @@ title: Create Containers date: 2017-01-11 00:00:00 lang: en ref: create-containers -description: To split up our React.js app into different routes we are going to structure it using containers in React Router v4. We are also going to add the Navbar React-Bootstrap component to our App container. -context: true +description: To split up our React.js app into different routes we are going to structure it using containers in React Router v6. We are also going to add the Navbar React-Bootstrap component to our App container. comments_id: create-containers/62 --- -Currently, our app has a single component that renders our content. For creating our note taking app, we need to create a few different pages to load/edit/create notes. Before we can do that we will put the outer chrome of our app inside a component and render all the top level components inside them. These top level components that represent the various pages will be called containers. +Currently, our app has a single component that renders our content. For creating our note taking app, we need to create a few different pages to load/edit/create notes. Before we can do that we will put the outer "chrome" (or UI) of our app inside a component and render all the top level components inside them. We are calling the top level components that represent the various pages, containers. ### Add a Navbar Let's start by creating the outer chrome of our application by first adding a navigation bar to it. We are going to use the [Navbar](https://react-bootstrap.github.io/components/navbar/) React-Bootstrap component. -To start, you can go remove the `src/logo.svg` that is placed there by Create React App. +{%change%} Go ahead and remove the code inside `src/App.tsx` and replace it with the following. -``` bash -$ rm src/logo.svg -``` - -And go ahead and remove the code inside `src/App.js` and replace it with the following. - -``` coffee -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import { Navbar } from "react-bootstrap"; +```tsx +import Navbar from "react-bootstrap/Navbar"; import "./App.css"; -class App extends Component { - render() { - return ( -
- - - - Scratch - - - - -
- ); - } +function App() { + return ( +
+ + Scratch + + +
+ ); } export default App; @@ -52,62 +37,63 @@ export default App; We are doing a few things here: 1. Creating a fixed width container using Bootstrap in `div.container`. -2. Adding a Navbar inside the container that fits to its container's width using the attribute `fluid`. -3. Using `Link` component from the React-Router to handle the link to our app's homepage (without forcing the page to refresh). +1. Using a couple of [Bootstrap spacing utility classes](https://getbootstrap.com/docs/4.5/utilities/spacing/) (like `mb-#` and `py-#`) to add margin bottom (`mb`) and padding vertical (`py`). These use a proportional set of spacer units to give a more harmonious feel to our UI. -Let's also add a couple of line of styles to space things out a bit more. +Let's clear out the styles that came with our template. -Remove all the code inside `src/App.css` and replace it with the following: +{%change%} Remove all the code inside `src/App.css` and replace it with the following: -``` css +```css .App { - margin-top: 15px; } +``` -.App .navbar-brand { - font-weight: bold; -} +For now we don't have any styles to add but we'll leave this file around, in case you want to add to it later. + +Also, let's remove some unused template files. + +{%change%} Run the following **in the `packages/frontend/` directory**. + +```bash +$ rm -r public/vite.svg src/assets/ ``` ### Add the Home container -Now that we have the outer chrome of our application ready, let's add the container for the homepage of our app. It'll respond to the `/` route. +Now that we have the outer chrome of our application ready, let's add the container for the homepage of our app. It'll respond to the `/` route. -Create a `src/containers/` directory by running the following in your working directory. +{%change%} Create a `src/containers/` directory by running the following **in the `packages/frontend/` directory**. -``` bash +```bash $ mkdir src/containers/ ``` -We'll be storing all of our top level components here. These are components that will respond to our routes and make requests to our API. We will be calling them *containers* through the rest of this tutorial. +We'll be storing all of our top level components here. These are components that will respond to our routes and make requests to our API. We will be calling them _containers_ through the rest of this tutorial. -Create a new container and add the following to `src/containers/Home.js`. +{%change%} Create a new container and add the following to `src/containers/Home.tsx`. -``` coffee -import React, { Component } from "react"; +```tsx import "./Home.css"; -export default class Home extends Component { - render() { - return ( -
-
-

Scratch

-

A simple note taking app

-
+export default function Home() { + return ( +
+
+

Scratch

+

A simple note taking app

- ); - } +
+ ); } ``` -This simply renders our homepage given that the user is not currently signed in. +This renders our homepage given that the user is not currently signed in. Now let's add a few lines to style this. -Add the following into `src/containers/Home.css`. +{%change%} Add the following into `src/containers/Home.css`. -``` css +```css .Home .lander { padding: 80px 0; text-align: center; @@ -117,60 +103,54 @@ Now let's add a few lines to style this. font-family: "Open Sans", sans-serif; font-weight: 600; } - -.Home .lander p { - color: #999; -} ``` ### Set up the Routes Now we'll set up the routes so that we can have this container respond to the `/` route. -Create `src/Routes.js` and add the following into it. +{%change%} Create `src/Routes.tsx` and add the following into it. -``` coffee -import React from "react"; -import { Route, Switch } from "react-router-dom"; -import Home from "./containers/Home"; +```tsx +import { Route, Routes } from "react-router-dom"; +import Home from "./containers/Home.tsx"; -export default () => - - - ; +export default function Links() { + return ( + + } /> + + ); +} ``` -This component uses this `Switch` component from React-Router that renders the first matching route that is defined within it. For now we only have a single route, it looks for `/` and renders the `Home` component when matched. We are also using the `exact` prop to ensure that it matches the `/` route exactly. This is because the path `/` will also match any route that starts with a `/`. +This component uses this `Routes` component from React-Router that renders the first matching route that is defined within it. For now we only have a single route, it looks for `/` and renders the `Home` component when matched. We are also using the `exact` prop to ensure that it matches the `/` route exactly. This is because the path `/` will also match any route that starts with a `/`. ### Render the Routes Now let's render the routes into our App component. -Add the following to the header of your `src/App.js`. +{%change%} Add the following to the header of your `src/App.tsx`. -``` coffee -import Routes from "./Routes"; +```tsx +import Routes from "./Routes.tsx"; ``` -And add the following line below our `Navbar` component inside the `render` of `src/App.js`. +{%change%} And add the following line below our `Navbar` component inside `src/App.tsx`. -``` coffee +```tsx ``` -So the `render` method of our `src/App.js` should now look like this. +So the `App` function component of our `src/App.tsx` should now look like this. -``` coffee -render() { +```tsx +function App() { return ( -
- - - - Scratch - - - +
+ + Scratch +
@@ -182,6 +162,6 @@ This ensures that as we navigate to different routes in our app, the portion bel Finally, head over to your browser and your app should show the brand new homepage of your app. -![New homepage loaded screenshot](/assets/new-homepage-loaded.png) +![New homepage loaded screenshot](/assets/part2/new-homepage-loaded.png) Next we are going to add login and signup links to our navbar. diff --git a/_chapters/create-the-signup-form.md b/_chapters/create-the-signup-form.md index 978f639515..8dcd52fe2b 100644 --- a/_chapters/create-the-signup-form.md +++ b/_chapters/create-the-signup-form.md @@ -4,8 +4,7 @@ title: Create the Signup Form date: 2017-01-20 00:00:00 lang: en ref: create-the-signup-form -description: We are going to create a signup page for our React.js app. To sign up users with Amazon Cognito, we need to create a form that allows users to enter a cofirmation code that is emailed to them. -context: true +description: We are going to create a signup page for our React.js app. To sign up users with Amazon Cognito, we need to create a form that allows users to enter a confirmation code that is emailed to them. comments_id: create-the-signup-form/52 --- @@ -13,143 +12,136 @@ Let's start by creating the signup form that'll get the user's email and passwor ### Add the Container -Create a new container at `src/containers/Signup.js` with the following. +{%change%} Create a new container at `src/containers/Signup.tsx` with the following. -``` coffee -import React, { Component } from "react"; -import { - HelpBlock, - FormGroup, - FormControl, - ControlLabel -} from "react-bootstrap"; +```tsx +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import Stack from "react-bootstrap/Stack"; +import { useNavigate } from "react-router-dom"; +import { useFormFields } from "../lib/hooksLib"; +import { useAppContext } from "../lib/contextLib"; import LoaderButton from "../components/LoaderButton"; import "./Signup.css"; -export default class Signup extends Component { - constructor(props) { - super(props); - - this.state = { - isLoading: false, - email: "", - password: "", - confirmPassword: "", - confirmationCode: "", - newUser: null - }; - } - - validateForm() { +export default function Signup() { + const [fields, handleFieldChange] = useFormFields({ + email: "", + password: "", + confirmPassword: "", + confirmationCode: "", + }); + const nav = useNavigate(); + const { userHasAuthenticated } = useAppContext(); + const [isLoading, setIsLoading] = useState(false); + const [newUser, setNewUser] = useState(null); + + function validateForm() { return ( - this.state.email.length > 0 && - this.state.password.length > 0 && - this.state.password === this.state.confirmPassword + fields.email.length > 0 && + fields.password.length > 0 && + fields.password === fields.confirmPassword ); } - validateConfirmationForm() { - return this.state.confirmationCode.length > 0; + function validateConfirmationForm() { + return fields.confirmationCode.length > 0; } - handleChange = event => { - this.setState({ - [event.target.id]: event.target.value - }); - } - - handleSubmit = async event => { + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); - - this.setState({ isLoading: true }); - - this.setState({ newUser: "test" }); - - this.setState({ isLoading: false }); + setIsLoading(true); + setNewUser("test"); + setIsLoading(false); } - handleConfirmationSubmit = async event => { + async function handleConfirmationSubmit( + event: React.FormEvent + ) { event.preventDefault(); - - this.setState({ isLoading: true }); + setIsLoading(true); } - renderConfirmationForm() { + function renderConfirmationForm() { return ( -
- - Confirmation Code - - Please check your email for the code. - - - +
+ + + Confirmation Code + + Please check your email for the code. + + + Verify + + +
); } - renderForm() { + function renderForm() { return ( -
- - Email - - - - Password - - - - Confirm Password - - - - +
+ + + Email + + + + Password + + + + Confirm Password + + + + Signup + + +
); } - render() { - return ( -
- {this.state.newUser === null - ? this.renderForm() - : this.renderConfirmationForm()} -
- ); - } + return ( +
+ {newUser === null ? renderForm() : renderConfirmationForm()} +
+ ); } ``` @@ -157,17 +149,36 @@ Most of the things we are doing here are fairly straightforward but let's go ove 1. Since we need to show the user a form to enter the confirmation code, we are conditionally rendering two forms based on if we have a user object or not. + ```jsx + { + newUser === null ? renderForm() : renderConfirmationForm(); + } + ``` + 2. We are using the `LoaderButton` component that we created earlier for our submit buttons. -3. Since we have two forms we have two validation methods called `validateForm` and `validateConfirmationForm`. +3. Since we have two forms we have two validation functions called `validateForm` and `validateConfirmationForm`. 4. We are setting the `autoFocus` flags on the email and the confirmation code fields. + `` + 5. For now our `handleSubmit` and `handleConfirmationSubmit` don't do a whole lot besides setting the `isLoading` state and a dummy value for the `newUser` state. -Also, let's add a couple of styles in `src/containers/Signup.css`. +6. And you'll notice we are using the `useFormFields` custom React Hook that we [previously created]({% link _chapters/create-a-custom-react-hook-to-handle-form-fields.md %}) to handle our form fields. + + ```typescript + const [fields, handleFieldChange] = useFormFields({ + email: "", + password: "", + confirmPassword: "", + confirmationCode: "", + }); + ``` + +{%change%} Also, let's add a couple of styles in `src/containers/Signup.css`. -``` css +```css @media all and (min-width: 480px) { .Signup { padding: 60px 0; @@ -178,30 +189,28 @@ Most of the things we are doing here are fairly straightforward but let's go ove max-width: 320px; } } - -.Signup form span.help-block { - font-size: 14px; - padding-bottom: 10px; - color: #999; -} ``` ### Add the Route -Finally, add our container as a route in `src/Routes.js` below our login route. We are using the `AppliedRoute` component that we created in the [Add the session to the state]({% link _chapters/add-the-session-to-the-state.md %}) chapter. +{%change%} Finally, add our container as a route in `src/Routes.tsx` below our login route. -``` coffee - +```tsx +} /> ``` -And include our component in the header. +{%change%} And include our component in the header. -``` javascript -import Signup from "./containers/Signup"; +```typescript +import Signup from "./containers/Signup.tsx"; ``` -Now if we switch to our browser and navigate to the signup page we should see our newly created form. Our form doesn't do anything when we enter in our info but you can still try to fill in an email address, password, and the confirmation code. It'll give you an idea of how the form will behave once we connect it to Cognito. +Now if we switch to our browser and navigate to the signup page we should see our newly created form. Our form doesn't do anything when we enter in our info but you can still try to fill in an email address, password, and confirmation password. + +![Signup page added screenshot](/assets/part2/signup-page-added.png) + +Then, after hitting submit, you'll get the confirmation code form. This'll give you an idea of how the form will behave once we connect it to Cognito. -![Signup page added screenshot](/assets/signup-page-added.png) +![Signup page added screenshot](/assets/part2/signup-page-confirmation-code.png) Next, let's connect our signup form to Amazon Cognito. diff --git a/_chapters/creating-a-ci-cd-pipeline-for-serverless.md b/_chapters/creating-a-ci-cd-pipeline-for-serverless.md new file mode 100644 index 0000000000..3215fae5a4 --- /dev/null +++ b/_chapters/creating-a-ci-cd-pipeline-for-serverless.md @@ -0,0 +1,51 @@ +--- +layout: post +title: Creating a CI/CD Pipeline for serverless +date: 2020-11-04 00:00:00 +lang: en +description: In this chapter we'll be using the SST Console to set up a CI/CD pipeline for our full-stack serverless app. +ref: creating-a-ci-cd-pipeline-for-serverless +redirect_from: /chapters/automating-serverless-deployments.html +comments_id: creating-a-ci-cd-pipeline-for-serverless/174 +--- + +So to recap, here's what we've created so far. + +- A full-stack serverless app that includes: + - [Storage with DynamoDB and S3]({% link _chapters/create-a-dynamodb-table-in-sst.md %}) + - [API]({% link _chapters/add-an-api-to-create-a-note.md %}) + - [Auth with Cognito]({% link _chapters/auth-in-serverless-apps.md %}) + - [Frontend in React]({% link _chapters/create-a-new-reactjs-app.md %}) +- [A way to handle secrets locally]({% link _chapters/handling-secrets-in-sst.md %}) +- [A way to run unit tests]({% link _chapters/unit-tests-in-serverless.md %}) +- [Deployed to a prod environment with a custom domain]({% link _chapters/custom-domains-in-serverless-apis.md %}) + +All of this is neatly [committed in a Git repo]({{ site.sst_demo_repo }}){:target="_blank"}. + +So far we've been deploying our app locally through our command line — `npx sst deploy`. But if we had multiple people on our team, or if we were working on different features at the same time, we won't be able to work on our app because the changes would overwrite each other. + +To fix this we are going to implement a CI/CD pipeline for our full-stack serverless app. + +### What is a CI/CD Pipeline + +CI/CD or Continuous Integration/Continuous Delivery is the process of automating deployments by tying it to our source control system. So that when new code changes are pushed, our app is automatically deployed. + +A CI/CD pipeline usually includes multiple environments. An environment is one where there are multiple instances of our deployed app. So we can have an environment called _production_ that our users will be using. And _development_ environments that we can use while developing our app. + +Here is what our workflow is going to look like: + +- Our repo will be connected to our CI/CD service. +- Any commits that are pushed to the `production` branch will be automatically deployed to the `production` stage. +- Any PRs will be automatically deployed as preview environments. + +Our workflow is fairly simple. But as your team grows, you'll need to add additional dev, staging, or preview environments. + +### CI/CD for Serverless + +There are many common CI/CD services, like [GitHub Actions](https://github.com/features/actions){:target="_blank"}, [Travis CI](https://travis-ci.org){:target="_blank"} or [CircleCI](https://circleci.com){:target="_blank"}. These usually require you to manually configure the above pipeline. It involves a fair bit of scripts and configuration. + +SST makes this easier with the [SST Console]({{ site.sst_url }}/docs/console/){:target="_blank"}'s [Autodeploy](/docs/console/#autodeploy){:target="_blank"} feature. It also shows you all the resources in your app and allows you to monitor and debug them. + +We should mention that you don't have to use the SST Console. And this section is completely optional. + +Let's get started with setting up the SST Console. diff --git a/_chapters/cross-stack-references-in-serverless.md b/_chapters/cross-stack-references-in-serverless.md deleted file mode 100644 index 22465252ef..0000000000 --- a/_chapters/cross-stack-references-in-serverless.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -layout: post -title: Cross-Stack References in Serverless -description: AWS CloudFormation allows us to link multiple Serverless services using cross-stack references. A cross-stack reference consists of an "Export" and "Fn::ImportValue". Cross-stack references are useful for tracking the dependencies between Serverless services. -date: 2018-04-02 13:00:00 -context: true -code: mono-repo -comments_id: cross-stack-references-in-serverless/405 ---- - -In the previous chapter we looked at the [most common patterns for organizing your Serverless applications]({% link _chapters/organizing-serverless-projects.md %}). Now let's look at how to work with multiple services in your Serverless application. - -You might recall that a Serverless service is where a single `serverless.yml` is used to define the project. And the `serverless.yml` file is converted into a [CloudFormation template](https://aws.amazon.com/cloudformation/aws-cloudformation-templates/) using Serverless Framework. This means that in the case of multiple services you might need to reference a resource that is available in a different service. For example, you might have your DynamoDB tables created in one service and your APIs (which are in another service) need to refer to them. Of course you don't want to hard code this. And so over the next few chapters we will be breaking down the [note taking application]({{ site.backend_github_repo }}) into multiple resources to illustrate how to do this. - -However before we do, we need to cover the concept of cross-stack references. A cross-stack reference is a way for one CloudFormation template to refer to the resource in another CloudFormation template. - -### CloudFormation Cross-Stack References - -To create a cross-stack reference, you need to: - -1. Use the `Export:` flag in the `Outputs:` section in the `serverless.yml` of the service you would like to reference. - -2. Then in the service where you want to use the reference; use the `Fn::ImportValue` CloudFormation function. - -So as a quick example (we will go over this in detail shortly), say you wanted to refer to the DynamoDB table across services. - -1. First export the table name in your DynamoDB service using the `Export:` flag: - - ```yml - resources: - Resources: - NotesTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: notes - - # ... - - Outputs: - Value: - Ref: NotesTable - Export: - Name: NotesTableName - ``` - -2. And in your API service, import it using the `Fn::ImportValue` function: - - ```yml - 'Fn::ImportValue': NotesTableName - ``` - -The `Fn::ImportValue` function takes the export name and returns the exported value. In this case the imported value is the DynamoDB table name. - -Now before we dig into the details of cross-stack references in Serverless, let's quickly look at some of its details. - -- Cross-stack references only apply within a single region. Meaning that an exported value can be referenced by any service in that region. - -- Consequently, the `Export:` function needs to be unique within that region. - -- If a service's export is being referenced in another stack, the service cannot be removed. So for the above example, you won't be able to remove the DynamoDB service if it is still being referenced in the API service. - -- The services need to be deployed in a specific order. The service that is exporting a value needs to be deployed before the one doing the importing. Using the above example again, the DynamoDB service needs to be deployed before the API service. - -### Advantages of Cross-Stack References - -As your application grows, it can become hard to track the dependencies between the services in the application. And cross-stack references can help with that. It creates a strong link between the services. As a comparison, if you were to refer to the linked resource by hard coding the value, it'll be difficult to keep track of it as your application grows. - -The other advantage is that you can easily recreate the entire application (say for testing) with ease. This is because none of the services of your application are statically linked to each other. - -### Example Setup - -Cross-stack references can be very useful but some aspects of it can be a little confusing and the documentation can make it hard to follow. To illustrate the various ways to use cross-stack references in serverless we are going to split up our [note taking app]({{ site.backend_github_repo }}) into a [mono-repo app with multiple services that are connected through cross-stack references]({{ site.backend_mono_github_repo }}). - -We are going to do the following: - -1. Make DynamoDB a separate service. - -2. Make the S3 file uploads bucket a separate service. - -3. Split our API into two services. - -4. In the first API service, refer to the DynamoDB service using a cross-stack reference. - -5. In the second API service, do the same as the first. And additionally, link to the first API service so that we can use the same API Gateway domain as the first. - -6. Secure all our resources with a Cognito User Pool. And with an Identity Pool create an IAM role that gives authenticated users permissions to the resources we created. - -We are splitting up our app this way mainly to illustrate how to use cross-stack references. But you can split it up in a way that makes more sense for you. For example, you might choose to have all your infrastructure resources (DynamoDB and S3) in one service, your APIs in another, and your auth in a separate service. - -We've also created a [separate GitHub repo with a working example]({{ site.backend_mono_github_repo }}) of the above setup that you can use for reference. We'll be linking to it at the bottom of each of the following chapters. - -In the next chapter let's look at setting up DynamoDB as a separate service. diff --git a/_chapters/custom-domains-for-react-apps-on-aws.md b/_chapters/custom-domains-for-react-apps-on-aws.md new file mode 100644 index 0000000000..1afc4e2166 --- /dev/null +++ b/_chapters/custom-domains-for-react-apps-on-aws.md @@ -0,0 +1,70 @@ +--- +layout: post +title: Custom Domains for React Apps on AWS +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are setting a custom domain for our React.js app on AWS. We are using the SST StaticSite component to configure the custom domain. +ref: custom-domains-for-react-apps-on-aws +comments_id: custom-domains-for-react-apps-on-aws/2463 +--- + +In the [previous chapter we configured a custom domain for our serverless API]({% link _chapters/custom-domains-in-serverless-apis.md %}). Now let's do the same for our frontend React app. + +{%change%} In the `infra/web.ts` add the following above the `environment: {` line. + +```ts +domain: + $app.stage === "production" + ? { + name: "", + redirects: ["www."], + } + : undefined, +``` + +Just like the API case, we want to use our custom domain **if** we are deploying to the `production` stage. This means that when we are using our app locally or deploying to any other stage, it won't be using the custom domain. + +Of course, you can change this if you'd like to use a custom domain for the other stages. You can use something like `${app.stage}.my-serverless-app.com`. So for `dev` it'll be `dev.my-serverless-app.com`. But we'll leave this as an exercise for you. + +The `redirects` prop is necessary because we want visitors of `www.my-serverless-app.com` to be redirected to the URL we want to use. It's a good idea to support both the `www.` and root versions of our domain. You can switch these around so that the root domain redirects to the `www.` version as well. + +You won't need to set the `redirects` for the non-prod versions because we don't need `www.` versions for those. + +### Deploy the App + +Just like the previous chapter, we need to update these changes in prod. + +{%change%} Run the following from your project root. + +```bash +$ npx sst deploy --stage production +``` + +{%note%} +Deploying changes to custom domains can take a few minutes. +{%endnote%} + +At the end of the deploy process you should see something like this. + +```bash ++ Complete + Api: https://api.my-serverless-app.com + Frontend: https://my-serverless-app.com + ... +``` + +And that's it! Our React.js app is now deployed to prod under our own domain! + +![App update live screenshot](/assets/part2/app-update-live.png) + +### Commit the Changes + +{%change%} Let's commit our code so far and push it to GitHub. + +```bash +$ git add . +$ git commit -m "Setting up custom domains" +$ git push +``` + +At this stage our full-stack serverless app is pretty much complete. In the next couple of optional sections we are going at how we can automate our deployments. We want to set it up so that when we `git push` our changes, our app should deploy automatically. diff --git a/_chapters/custom-domains-in-serverless-apis.md b/_chapters/custom-domains-in-serverless-apis.md new file mode 100644 index 0000000000..eab142b663 --- /dev/null +++ b/_chapters/custom-domains-in-serverless-apis.md @@ -0,0 +1,52 @@ +--- +layout: post +title: Custom Domains in serverless APIs +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are setting a custom domain for our serverless API on AWS. We are using the SST ApiGatewayV2 component to configure the custom domain. +ref: custom-domains-in-serverless-apis +comments_id: custom-domains-in-serverless-apis/2464 +--- + +In the [previous chapter]({% link _chapters/purchase-a-domain-with-route-53.md %}) we purchased a new domain on [Route 53](https://aws.amazon.com/route53/){:target="_blank"}. Now let's use it for our serverless API. + +{%change%} In your `infra/api.ts` add this above the `transform: {` line. + +```ts +domain: $app.stage === "production" ? "" : undefined, +``` +{%note%} +Without specifying the API subdomain, the deployment will attempt to create duplicate A (IPv4) and AAAA (IPv6) DNS records and error. +{%endnote%} + +This tells SST that we want to use a custom domain **if** we are deploying to the `production` stage. We are not setting one for our `dev` stage, or any other stage. + +We could for example, base it on the stage name, `api-${app.stage}.my-serverless-app.com`. So for `dev` it might be `api-dev.my-serverless-app.com`. But we'll leave that as an exercise for you. + +The `$app` is a global variable that's available in our config. You can [learn more about it here]({{ site.sst_url }}/docs/reference/global/#app){:target="_blank"}. + +### Deploy the App + +Let's deploy these changes to prod. + +{%change%} Run the following from **your project root**. + +```bash +$ npx sst deploy --stage production +``` + +{%note%} +Deploying changes to custom domains can take a few minutes. +{%endnote%} + +At the end of the deploy process you should see something like this. + +```bash ++ Complete + Api: https://api.my-serverless-app.com + ... +``` + +This is great! We now have our app deployed to prod and our API has a custom domain. + +Next, let's use our custom domain for our React app as well. diff --git a/_chapters/debugging-full-stack-serverless-apps.md b/_chapters/debugging-full-stack-serverless-apps.md new file mode 100644 index 0000000000..d21d7c2738 --- /dev/null +++ b/_chapters/debugging-full-stack-serverless-apps.md @@ -0,0 +1,57 @@ +--- +layout: post +title: Debugging Full-Stack Serverless Apps +date: 2020-04-03 00:00:00 +lang: en +description: In this chapter we look at the debugging setup and workflow for full-stack serverless apps. We'll cover some of the most common errors, including errors inside and outside Lambda functions, timeouts and out-of-memory errors. +comments_id: debugging-full-stack-serverless-apps/1727 +ref: debugging-full-stack-serverless-apps +--- + +Now that we are ready to go live with our app, we need to make sure we are setup to monitor and debug errors. This is important because unlike our local environment where we can look at the console (browser or terminal), make changes and fix errors, we cannot do that when our app is live. + +### Debugging Workflow + +We need to make sure we have a couple of things setup before we can confidently ask others to use our app: + +- Frontend + - Be alerted when a user runs into an error. + - Get all the error details, including the stack trace. + - In the case of a backend error, get the API that failed. + +- Backend + - Look up the logs for an API endpoint. + - Get detailed debug logs for all the AWS services. + - Catch any unexpected errors (out-of-memory, timeouts, etc.). + +It's important that we have a good view of our production environments. It allows us to keep track of what our users are experiencing. + +Note that, for the frontend the setup is pretty much what you would do for any React application. But we are covering it here because we want to go over the entire debugging workflow. Right from when you are alerted that a user has gotten an error while using your app, all the way till figuring out which Lambda function caused it. + +### Debugging Setup + +Here is what we'll be doing in the next few chapters to help accomplish the above workflow. + +- Frontend + + On the frontend, we'll be setting up [Sentry](https://sentry.io); a service for monitoring and debugging errors. Sentry has a great free tier that we can use. We'll be integrating it into our React app by reporting any expected errors and unexpected errors. We'll do this by using the [React Error Boundary](https://reactjs.org/docs/error-boundaries.html). + +- Backend + + On the backend, AWS has some great logging and monitoring tools thanks to [CloudWatch](https://aws.amazon.com/cloudwatch/). We'll be using CloudWatch through the [Seed](https://seed.run) console. Note that, you can use CloudWatch directly and don't have to rely on Seed for it. We'll also be configuring some debugging helper functions for our backend code. + +### Looking Ahead + +Here's what we'll be going over in the next few chapters: + +1. [Setting up error reporting in React]({% link _chapters/setup-error-reporting-in-react.md %}) + - [Reporting API errors in React]({% link _chapters/report-api-errors-in-react.md %}) + - [Reporting unexpected React errors with an Error Boundary]({% link _chapters/setup-an-error-boundary-in-react.md %}) +2. [Setting up detailed error reporting in Lambda]({% link _chapters/setup-error-logging-in-serverless.md %}) +3. The debugging workflow for the following serverless errors: + - [Logic errors in our Lambda functions]({% link _chapters/logic-errors-in-lambda-functions.md %}) + - [Unexpected errors in our Lambda functions]({% link _chapters/unexpected-errors-in-lambda-functions.md %}) + - [Errors outside our Lambda functions]({% link _chapters/errors-outside-lambda-functions.md %}) + - [Errors in API Gateway]({% link _chapters/errors-in-api-gateway.md %}) + +This should give you a good foundation to be able to monitor your app as it goes into production. There are plenty of other great tools out there that can improve on this setup. We want to make sure we cover the basics here. Let's get started! diff --git a/_chapters/delete-a-note.md b/_chapters/delete-a-note.md index 57307a4650..d65d998c3e 100644 --- a/_chapters/delete-a-note.md +++ b/_chapters/delete-a-note.md @@ -4,21 +4,20 @@ title: Delete a Note date: 2017-01-31 00:00:00 lang: en description: We want users to be able to delete their note in our React.js app. To do this we are going to make a DELETE request to our serverless API backend using AWS Amplify. -context: true comments_id: comments-for-delete-a-note/137 ref: delete-a-note --- The last thing we need to do on the note page is allowing users to delete their note. We have the button all set up already. All that needs to be done is to hook it up with the API. -Replace our `handleDelete` method in `src/containers/Notes.js`. +{%change%} Replace our `handleDelete` function in `src/containers/Notes.tsx`. -``` coffee -deleteNote() { - return API.del("notes", `/notes/${this.props.match.params.id}`); +```typescript +function deleteNote() { + return API.del("notes", `/notes/${id}`, {}); } -handleDelete = async event => { +async function handleDelete(event: React.FormEvent) { event.preventDefault(); const confirmed = window.confirm( @@ -29,28 +28,24 @@ handleDelete = async event => { return; } - this.setState({ isDeleting: true }); + setIsDeleting(true); try { - await this.deleteNote(); - this.props.history.push("/"); + await deleteNote(); + nav("/"); } catch (e) { - alert(e); - this.setState({ isDeleting: false }); + onError(e); + setIsDeleting(false); } } ``` -We are simply making a `DELETE` request to `/notes/:id` where we get the `id` from `this.props.match.params.id`. We use the `API.del` method from AWS Amplify to do so. This calls our delete API and we redirect to the homepage on success. +We are simply making a `DELETE` request to `/notes/:id` where we get the `id` from `useParams` hook provided by React Router. We use the `API.del` method from AWS Amplify to do so. This calls our delete API and we redirect to the homepage on success. Now if you switch over to your browser and try deleting a note you should see it confirm your action and then delete the note. -![Note page deleting screenshot](/assets/note-page-deleting.png) +![Note page deleting screenshot](/assets/part2/note-page-deleting.png) Again, you might have noticed that we are not deleting the attachment when we are deleting a note. We are leaving that up to you to keep things simple. Check the [AWS Amplify API Docs](https://aws.github.io/aws-amplify/api/classes/storageclass.html#remove) on how to a delete file from S3. -Now with our app nearly complete, we'll look at securing some the pages of our app that require a login. Currently if you visit a note page while you are logged out, it throws an ugly error. - -![Note page logged out error screenshot](/assets/note-page-logged-out-error.png) - -Instead, we would like it to redirect us to the login page and then redirect us back after we login. Let's look at how to do that next. +Next, let’s add a settings page to our app. This is where a user will be able to pay for our service! diff --git a/_chapters/deploy-updates.md b/_chapters/deploy-updates.md deleted file mode 100644 index 56661eb537..0000000000 --- a/_chapters/deploy-updates.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: post -title: Deploy Updates -date: 2017-02-12 00:00:00 -lang: en -description: Tutorial on how to deploy updates to your React.js single page application hosted on AWS S3 and CloudFront. -comments_id: deploy-updates/16 -ref: deploy-updates ---- - -Now let's look at how we make changes and update our app. The process is very similar to how we deployed our code to S3 but with a few changes. Here is what it looks like. - -1. Build our app with the changes -2. Deploy to the main S3 Bucket -3. Invalidate the cache in both our CloudFront Distributions - -We need to do the last step since CloudFront caches our objects in its edge locations. So to make sure that our users see the latest version, we need to tell CloudFront to invalidate it's cache in the edge locations. - -Let's start by making a couple of changes to our app and go through the process of deploying them. diff --git a/_chapters/deploy-your-serverless-infrastructure.md b/_chapters/deploy-your-serverless-infrastructure.md deleted file mode 100644 index 08699194da..0000000000 --- a/_chapters/deploy-your-serverless-infrastructure.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -layout: post -title: Deploy Your Serverless Infrastructure -date: 2018-03-04 00:00:00 -lang: en -description: To deploy your Serverless Framework project along with your infrastructure to AWS, use the "serverless deploy -v" command. This will display the Stack Outputs as a part of the deployment. -code: backend -ref: deploy-your-serverless-infrastructure -comments_id: deploy-your-serverless-infrastructure/167 ---- - -Now that we have all our resources configured, let's go ahead and deploy our entire infrastructure. - -We should mention though that our current project has all of our resources and the Lambda functions that we had created in the first part of our tutorial. This is a common trend in serverless projects. Your *code* and *infrastructure* are not treated differently. Of course, as your projects get larger, you end up splitting them up. So you might have a separate Serverless Framework project that deploys your infrastructure while a different project just deploys your Lambda functions. - -### Deploy Your Project - -Deploying our project is fairly straightforward thanks to our `serverless deploy` command. So go ahead and run this from the root of your project. - -``` bash -$ serverless deploy -v -``` - -Your output should look something like this: - -``` bash -Serverless: Stack update finished... -Service Information -service: notes-app-2-api -stage: dev -region: us-east-1 -stack: notes-app-2-api-dev -api keys: - None -endpoints: - POST - https://mqqmkwnpbc.execute-api.us-east-1.amazonaws.com/dev/notes - GET - https://mqqmkwnpbc.execute-api.us-east-1.amazonaws.com/dev/notes/{id} - GET - https://mqqmkwnpbc.execute-api.us-east-1.amazonaws.com/dev/notes - PUT - https://mqqmkwnpbc.execute-api.us-east-1.amazonaws.com/dev/notes/{id} - DELETE - https://mqqmkwnpbc.execute-api.us-east-1.amazonaws.com/dev/notes/{id} -functions: - create: notes-app-2-api-dev-create - get: notes-app-2-api-dev-get - list: notes-app-2-api-dev-list - update: notes-app-2-api-dev-update - delete: notes-app-2-api-dev-delete - -Stack Outputs -AttachmentsBucketName: notes-app-2-api-dev-attachmentsbucket-oj4rfiumzqf5 -UserPoolClientId: ft93dvu3cv8p42bjdiip7sjqr -UserPoolId: us-east-1_yxO5ed0tq -DeleteLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:232771856781:function:notes-app-2-api-dev-delete:2 -CreateLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:232771856781:function:notes-app-2-api-dev-create:2 -GetLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:232771856781:function:notes-app-2-api-dev-get:2 -UpdateLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:232771856781:function:notes-app-2-api-dev-update:2 -IdentityPoolId: us-east-1:64495ad1-617e-490e-a6cf-fd85e7c8327e -BillingLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:232771856781:function:notes-app-2-api-dev-billing:1 -ListLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:232771856781:function:notes-app-2-api-dev-list:2 -ServiceEndpoint: https://mqqmkwnpbc.execute-api.us-east-1.amazonaws.com/dev -ServerlessDeploymentBucketName: notes-app-2-api-dev-serverlessdeploymentbucket-1p2o0dshaz2qc -``` - -A couple of things to note here: - -- We are deploying to a stage called `dev`. This has been set in our `serverless.yml` under the `provider:` block. We can override this by explicitly passing it in by running the `serverless deploy --stage $STAGE_NAME` command instead. - -- Our deploy command (with the `-v` option) prints out the output we had requested in our resources. For example, `AttachmentsBucketName` is the S3 file uploads bucket that was created and the `UserPoolId` is the Id of our User Pool. - -- Finally, you can run the deploy command and CloudFormation will only update the parts that have changed. So you can confidently run this command without worrying about it re-creating your entire infrastructure from scratch. - -And that's it! Our entire infrastructure is completely configured and deployed automatically. - -Next, we will add a new API (and Lambda function) to work with 3rd party APIs. In our case we are going to add an API that will use Stripe to bill the users of our notes app! diff --git a/_chapters/deploying-multiple-services-in-serverless.md b/_chapters/deploying-multiple-services-in-serverless.md deleted file mode 100644 index 33a262405d..0000000000 --- a/_chapters/deploying-multiple-services-in-serverless.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -layout: post -title: Deploying Multiple Services in Serverless -description: To deploy multiple Serverless services that are using CloudFormation cross-stack references, we need to ensure that we deploy them in the order of their dependencies. -date: 2018-04-02 18:00:00 -context: true -code: mono-repo -comments_id: deploying-multiple-services-in-serverless/410 ---- - -Over the last few chapters we have looked at how to: - -- [Link multiple Serverless services using CloudFormation cross-stack references]({% link _chapters/cross-stack-references-in-serverless.md %}) -- [Create our DynamoDB table as a Serverless service]({% link _chapters/dynamodb-as-a-serverless-service.md %}) -- [Create an S3 bucket as a Serverless service]({% link _chapters/s3-as-a-serverless-service.md %}) -- [Use the same API Gateway domain and resources across multiple Serverless services]({% link _chapters/api-gateway-domains-across-services.md %}) -- [Create a Serverless service for Cognito to authenticate and authorize our users]({% link _chapters/cognito-as-a-serverless-service.md %}) - -All this is available in a [sample repo that you can deploy and test]({{ site.backend_mono_github_repo }}). - -Now we can finally look at how to deploy our services. The addition of cross-stack references to our services means that we have some built-in dependencies. This means that we need to deploy some services before we deploy certain others. - -### Service Dependencies - -Following is a list of the services we created: - -``` -database -uploads -notes -users -auth -``` - -And based on our cross-stack references the dependencies look roughly like: - -``` -database > notes > users - -uploads > auth -notes -``` - -Where the `a > b` symbolizes that service `a` needs to be deployed before service `b`. To break it down in detail: - -- The `users` API service relies on the `notes` API service for the API Gateway cross-stack reference. - -- The `users` and `notes` API services rely on the `database` service for the DynamoDB cross-stack reference. - -- And the `auth` service relies on the `uploads` and `notes` service for the S3 bucket and API Gateway cross-stack references respectively. - -Hence to deploy all of our services we need to follow this order: - -1. `database` -2. `uploads` -3. `notes` -4. `users` -5. `auth` - -Now there are some intricacies here but that is the general idea. - -### Multi-Service Deployments - -Given the rough dependency graph above, you can script your CI/CD pipeline to ensure that your automatic deployments follow these rules. There are a few ways to simplify this process. - -It is very likely that your `auth`, `database`, and `uploads` service don't change very often. You might also need to follow some strict policies across your team to make sure no haphazard changes are made to it. So by separating out these resources into their own services (like we have done in the past few chapters) you can carry out updates to these services by using a manual approval step as a part of the deployment process. This leaves the API services. These need to be deployed manually once and can later be automated. - -### Service Dependencies in Seed - -[Seed](https://seed.run) has a concept of [Deploy Phases](https://seed.run/docs/configuring-deploy-phases) to handle service dependencies. - -You can configure this by heading to the app settings and hitting **Manage Deploy Phases**. - -![Hit Manage Deploy Phases screenshot](/assets/mono-repo/hit-manage-deploy-phases.png) - -Here you'll notice that by default all the services are deployed concurrently. - -![Default Deploy Phase screenshot](/assets/mono-repo/default-deploy-phase.png) - -Note that, you'll need to add your services first. To do this, head over to the app **Settings** and hit **Add a Service**. - -![Click Add Service screenshot](/assets/mono-repo/click-add-service.png) - -We can configure our service dependencies by adding the necessary deploy phases and moving the services around. - -![Edit Deploy Phase screenshot](/assets/mono-repo/edit-deploy-phase.png) - -And when you deploy your app, the deployments are carried out according to the deploy phases specified. - -![Deploying with Deploy Phase screenshot](/assets/mono-repo/deploying-with-deploy-phase.png) - -### Environments - -A quick word of handling environments across these services. The services that we have created can be easily re-created for multiple [environments or stages]({% link _chapters/stages-in-serverless-framework.md %}). A good standard practice is to have a _dev_, _staging_, and _prod_ environment. And it makes sense to replicate all your services across these three environments. - -However, when you are working on a new feature or you want to give a developer on your team their own environment, it might not make sense to replicate all of your services across them. It is more common to only replicate the API services as you create multiple _dev_ environments. - -### Mono-Repo vs Multi-Repo - -Finally, when considering how to house these services in your repository, it is worth looking at how much code is shared across them. Typically, your _infrastructure_ services (`database`, `uploads` and `auth`) don't share any code between them. In fact they probably don't have any code in them to begin with. These services can be put in their own repos. Whereas the API services that might share some code (request and response handling) can be placed in the same repo and follow the mono-repo approach outlined in the [Organizing Serverless Projects chapter]({% link _chapters/organizing-serverless-projects.md %}). - -This combined way of using the multi-repo and mono-repo strategy also makes sense when you think about how we deploy them. As we stated above, the _infrastructure_ services are probably going to be deployed manually and with caution. While the API services can be automated (using [Seed](https://seed.run) or your own CI) for the mono-repo services and handle the others ones as a special case. - -### Conclusion - -Hopefully these series of chapters have given you a sense of how to structure large Serverless applications using CloudFormation cross-stack references. And the [example repo]({{ site.backend_mono_github_repo }}) gives you a clear working demonstration of the concepts we've covered. Give the above setup a try and leave us your feedback in the comments. - diff --git a/_chapters/deploying-through-seed.md b/_chapters/deploying-through-seed.md deleted file mode 100644 index bea8eeb943..0000000000 --- a/_chapters/deploying-through-seed.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -layout: post -title: Deploying Through Seed -lang: en -date: 2018-03-14 00:00:00 -description: We are going to trigger a deployment in Seed by pushing a commit to our Serverless project in Git. In the Seed console you can view the build logs and look at the CloudFormation output. -context: true -ref: deploying-through-seed -comments_id: deploying-through-seed/177 ---- - -Now, we are ready to make our first deployment. You can either Git push a new change to master to trigger it. Or we can just go into the **dev** stage and hit the **Trigger Deploy** button. - -Let's do it through Git. - -Go back to your project root and run the following. - -``` bash -$ npm version patch -``` - -This is simply updating the NPM version for your project. It is a good way to keep track of the changes you are making to your project. And it also creates a quick Git commit for us. - -Push the change using. - -``` bash -$ git push -``` - -Now if you head into the **dev** stage in Seed, you should see a build in progress. Now to see the build logs, you can hit **Build v1**. - -![Seed dev build in progress screenshot](/assets/part2/seed-dev-build-in-progress.png) - -Here you'll see the build taking place live. Click on the service that is being deployed. In this case, we only have one service. - -![Dev build page in progress screenshot](/assets/part2/dev-build-page-in-progress.png) - -You'll see the build logs for the in progress build here. - -![Dev build logs in progress screenshot](/assets/part2/dev-build-logs-in-progress.png) - -Notice the tests are being run as a part of the build. - -![Dev build run tests screenshot](/assets/part2/dev-build-run-tests.png) - -Something cool to note here is that, the build process is split into a few parts. First the code is checked out through Git and the tests are run. But we don't directly deploy. Instead, we create a package for the `dev` stage and the `prod` stage. And finally we deploy to `dev` with that package. The reason this is split up is because we want avoid the build process while promoting to `prod`. This ensures that if we have a tested working build, it should just work when we promote to production. - -You might also notice a couple of warnings that look like the following. - -``` bash -Serverless Warning -------------------------------------- - -A valid file to satisfy the declaration 'file(env.yml):dev,file(env.yml):default' could not be found. - - -Serverless Warning -------------------------------------- - -A valid file to satisfy the declaration 'file(env.yml):dev,file(env.yml):default' could not be found. - - -Serverless Warning -------------------------------------- - -A valid service attribute to satisfy the declaration 'self:custom.environment.stripeSecretKey' could not be found. -``` - -These are expected since the `env.yml` is not a part of our Git repo and is not available in the build process. The Stripe key is instead set directly in the Seed console. - -Once the build is complete, take a look at the build log and make a note of the following: - -- Region: `region` -- Cognito User Pool Id: `UserPoolId` -- Cognito App Client Id: `UserPoolClientId` -- Cognito Identity Pool Id: `IdentityPoolId` -- S3 File Uploads Bucket: `AttachmentsBucketName` -- API Gateway URL: `ServiceEndpoint` - -We'll be needing these later in our frontend and when we test our APIs. - -![Dev build stack output screenshot](/assets/part2/dev-build-stack-output.png) - -Now head over to the app home page. You'll notice that we are ready to promote to production. - -We have a manual promotion step so that you get a chance to review the changes and ensure that you are ready to push to production. - -Hit the **Promote** button. - -![Dev build ready to promote screenshot](/assets/part2/dev-build-ready-to-promote.png) - -This brings up a dialog that will generate a Change Set. It compares the resources that are being updated with respect to what you have in production. It's a great way to compare the infrastructure changes that are being promoted. - -![Review promote change set screenshot](/assets/part2/review-promote-change-set.png) - -Scroll down and hit **Promote to Production**. - -![Confirm promote dev build screenshot](/assets/part2/confirm-promote-dev-build.png) - -You'll notice that the build is being promoted to the **prod** stage. - -![prod build in progress screenshot](/assets/part2/prod-build-in-progress.png) - -And if you head over to the **prod** stage, you should see your prod deployment in action. It should take a second to deploy to production. And just like before, make a note of the following. - -- Region: `region` -- Cognito User Pool Id: `UserPoolId` -- Cognito App Client Id: `UserPoolClientId` -- Cognito Identity Pool Id: `IdentityPoolId` -- S3 File Uploads Bucket: `AttachmentsBucketName` -- API Gateway URL: `ServiceEndpoint` - -![Prod build stack output screenshot](/assets/part2/prod-build-stack-output.png) - -Next let's configure our serverless API with a custom domain. diff --git a/_chapters/deploying-through-the-sst-console.md b/_chapters/deploying-through-the-sst-console.md new file mode 100644 index 0000000000..6dd903ac0f --- /dev/null +++ b/_chapters/deploying-through-the-sst-console.md @@ -0,0 +1,47 @@ +--- +layout: post +title: Deploying Through the SST Console +lang: en +date: 2024-07-24 00:00:00 +redirect_from: /chapters/deploying-through-seed.html +description: We are going to git push to deploy our app to production with the SST Console. +ref: deploying-through-the-sst-console +comments_id: deploying-through-the-sst-console/2958 +--- + +Now, we are ready to _git push_ to deploy our app to production with the SST Console. If you recall from the [previous chapter]({% link _chapters/setting-up-the-sst-console.md %}), we configured it to auto-deploy the `production` branch. + +Let's do that by first creating a production branch. + +{%change%} Run the following in the **project root**. + +```bash +$ git checkout -b production +``` + +{%change%} Now let's push this to GitHub. + +```bash +$ git push --set-upstream origin production +$ git push +``` + +Now if you head into the **Autodeploy** tab for your app in the SST Console, you'll notice a new deployment in progress. + +![SST Console production deploy in progress](/assets/part2/sst-console-production-deploy-in-progress.png) + +Once the deploy is complete, you'll notice the outputs at the bottom. + +![Prod build stack outputs](/assets/part2/prod-build-stack-outputs.png) + +### Test Our App in Production + +Let's check out our app in production. + +![App update live screenshot](/assets/part2/app-update-live.png) + +To give it a quick test, sign up for a new account and create a note. You can also test updating and removing a note. And also test out the billing page. + +**Congrats! Your app is now live!** + +Let's wrap things up next. diff --git a/_chapters/display-a-note.md b/_chapters/display-a-note.md index d7d2d4f968..e5177afb35 100644 --- a/_chapters/display-a-note.md +++ b/_chapters/display-a-note.md @@ -3,13 +3,12 @@ layout: post title: Display a Note date: 2017-01-28 00:00:00 lang: en -description: We want to create a page in our React.js app that will display a user’s note based on the id in the URL. We are going to use the React Router v4 Route component’s URL parameters to get the id. Using this id we are going to request our note from the serverless backend API. And use AWS Amplify's Storage.vault.get() method to get a secure link to download our attachment. -context: true +description: We want to create a page in our React.js app that will display a user’s note based on the id in the URL. We are going to use the React Router v6 Route component’s URL parameters to get the id. Using this id we are going to request our note from the serverless backend API. And use AWS Amplify's Storage.vault.get() method to get a secure link to download our attachment. comments_id: display-a-note/112 ref: display-a-note --- -Now that we have a listing of all the notes, let's create a page that displays a note and let's the user edit it. +Now that we have a listing of all the notes, let's create a page that displays a note and lets the user edit it. The first thing we are going to need to do is load the note when our container loads. Just like what we did in the `Home` container. So let's get started. @@ -17,85 +16,79 @@ The first thing we are going to need to do is load the note when our container l Let's add a route for the note page that we are going to create. -Add the following line to `src/Routes.js` below our `/notes/new` route. We are using the `AppliedRoute` component that we created in the [Add the session to the state]({% link _chapters/add-the-session-to-the-state.md %}) chapter. +{%change%} Add the following line to `src/Routes.tsx` **below** our `/notes/new` route. -``` coffee - +```tsx +} /> ``` This is important because we are going to be pattern matching to extract our note id from the URL. By using the route path `/notes/:id` we are telling the router to send all matching routes to our component `Notes`. This will also end up matching the route `/notes/new` with an `id` of `new`. To ensure that doesn't happen, we put our `/notes/new` route before the pattern matching one. -And include our component in the header. +{%change%} And include our component in the header. -``` javascript -import Notes from "./containers/Notes"; +```tsx +import Notes from "./containers/Notes.tsx"; ``` Of course this component doesn't exist yet and we are going to create it now. ### Add the Container -Create a new file `src/containers/Notes.js` and add the following. +{%change%} Create a new file `src/containers/Notes.tsx` and add the following. -``` coffee -import React, { Component } from "react"; +```tsx +import React, { useRef, useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; import { API, Storage } from "aws-amplify"; +import { onError } from "../lib/errorLib"; + +export default function Notes() { + const file = useRef(null) + const { id } = useParams(); + const nav = useNavigate(); + const [note, setNote] = useState(null); + const [content, setContent] = useState(""); + + useEffect(() => { + function loadNote() { + return API.get("notes", `/notes/${id}`, {}); + } -export default class Notes extends Component { - constructor(props) { - super(props); - - this.file = null; - - this.state = { - note: null, - content: "", - attachmentURL: null - }; - } + async function onLoad() { + try { + const note = await loadNote(); + const { content, attachment } = note; - async componentDidMount() { - try { - let attachmentURL; - const note = await this.getNote(); - const { content, attachment } = note; + if (attachment) { + note.attachmentURL = await Storage.vault.get(attachment); + } - if (attachment) { - attachmentURL = await Storage.vault.get(attachment); + setContent(content); + setNote(note); + } catch (e) { + onError(e); } - - this.setState({ - note, - content, - attachmentURL - }); - } catch (e) { - alert(e); } - } - getNote() { - return API.get("notes", `/notes/${this.props.match.params.id}`); - } + onLoad(); + }, [id]); - render() { - return
; - } + return
; } ``` We are doing a couple of things here. -1. Load the note on `componentDidMount` and save it to the state. We get the `id` of our note from the URL using the props automatically passed to us by React-Router in `this.props.match.params.id`. The keyword `id` is a part of the pattern matching in our route (`/notes/:id`). +1. We are using the `useEffect` Hook to load the note when our component first loads. We then save it to the state. We get the `id` of our note from the URL using `useParams` hook that comes with React Router. The `id` is a part of the pattern matching in our route (`/notes/:id`). -2. If there is an attachment, we use the key to get a secure link to the file we uploaded to S3. We then store this to the component's state as `attachmentURL`. +2. If there is an attachment, we use the key to get a secure link to the file we uploaded to S3. We then store this in the new note object as `note.attachmentURL`. 3. The reason why we have the `note` object in the state along with the `content` and the `attachmentURL` is because we will be using this later when the user edits the note. Now if you switch over to your browser and navigate to a note that we previously created, you'll notice that the page renders an empty container. -![Empty notes page loaded screenshot](/assets/empty-notes-page-loaded.png) +![Empty notes page loaded screenshot](/assets/part2/empty-notes-page-loaded.png) Next up, we are going to render the note we just loaded. diff --git a/_chapters/dynamodb-as-a-serverless-service.md b/_chapters/dynamodb-as-a-serverless-service.md deleted file mode 100644 index aad225037f..0000000000 --- a/_chapters/dynamodb-as-a-serverless-service.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -layout: post -title: DynamoDB as a Serverless Service -description: To use CloudFormation cross-stack references for DynamoDB in Serverless we need to "Export" the table name using the "Ref" and the ARN of the table using "Fn::GetAtt". -date: 2018-04-02 14:00:00 -context: true -code: mono-repo -comments_id: dynamodb-as-a-serverless-service/406 ---- - -While creating a Serverless application with multiple services, you might want to split the DynamoDB portion out separately. This can be useful because you are probably not going to be making changes to this very frequently. Also, if you have multiple development environments, it is not likely that you are going to connect them to different database environments. For example, you might give the developers on your team their own environment but they might all connect to the same DynamoDB environment. So it would make sense to configure DynamoDB separately from the application API services. - -In the [example repo]({{ site.backend_mono_github_repo }}), you'll notice that we have a `database` service in the `services/` directory. And the `serverless.yml` in this service helps us manage our DynamoDB table. - -``` yml -service: notes-app-mono-database - -custom: - # Our stage is based on what is passed in when running serverless - # commands. Or fallsback to what we have set in the provider section. - stage: ${opt:stage, self:provider.stage} - # Set the table name here so we can use it while testing locally - tableName: ${self:custom.stage}-mono-notes - # Set our DynamoDB throughput for prod and all other non-prod stages. - tableThroughputs: - prod: 5 - default: 1 - tableThroughput: ${self:custom.tableThroughputs.${self:custom.stage}, self:custom.tableThroughputs.default} - -provider: - name: aws - runtime: nodejs8.10 - stage: dev - region: us-east-1 - -resources: - Resources: - NotesTable: - Type: AWS::DynamoDB::Table - Properties: - # Generate a name based on the stage - TableName: ${self:custom.tableName} - AttributeDefinitions: - - AttributeName: userId - AttributeType: S - - AttributeName: noteId - AttributeType: S - KeySchema: - - AttributeName: userId - KeyType: HASH - - AttributeName: noteId - KeyType: RANGE - # Set the capacity based on the stage - ProvisionedThroughput: - ReadCapacityUnits: ${self:custom.tableThroughput} - WriteCapacityUnits: ${self:custom.tableThroughput} - - Outputs: - NotesTableArn: - Value: - Fn::GetAtt: - - NotesTable - - Arn - Export: - Name: ${self:custom.stage}-NotesTableArn -``` - -If you have followed along with [Part II of our guide]({% link _chapters/configure-dynamodb-in-serverless.md %}), the `Resources:` section should seem familiar. It is creating the Notes table that we use in our [note taking application]({{ site.backend_github_repo }}). The key addition here in regards to the cross-stack references is in the `Outputs:` section. Let's go over them quickly. - -1. We are exporting one value here. The `NotesTableArn` is the [ARN]({% link _chapters/what-is-an-arn.md %}) of the DynamoDB table that we are creating. And the `NotesTableName` which is the name of the table being created. The ARN is necessary for any IAM roles that are going to reference the DynamoDB table. - -2. The export name is based on the stage we are using to deploy this service - `${self:custom.stage}`. This is important because we want our entire application to be easily replicable across multiple stages. If we don't include the stage name the exports will thrash when we deploy to multiple stages. - -3. The names of the exported values is `${self:custom.stage}-NotesTableArn`. - -4. We get the table ARN by using the `Fn::GetAtt` CloudFormation function. This function takes a reference from the current service and the attribute we need. The reference in this case is `NotesTable`. You'll notice that the table we created in the `Resources:` section is created using `NotesTable` as the name. - -When we deploy this service we'll notice the exported values in the output and we can reference these cross-stack in our other services. - -Next we'll do something similar for our S3 bucket. diff --git a/_chapters/errors-in-api-gateway.md b/_chapters/errors-in-api-gateway.md new file mode 100644 index 0000000000..5ac105f3f5 --- /dev/null +++ b/_chapters/errors-in-api-gateway.md @@ -0,0 +1,150 @@ +--- +layout: post +title: Errors in API Gateway +date: 2020-04-03 00:00:00 +lang: en +description: In this chapter we'll look at how to debug errors that happen only in API Gateway in your serverless app. These errors are only logged to your API Gateway access logs, and not your Lambda logs in CloudWatch. +comments_id: errors-in-api-gateway/1728 +ref: errors-in-api-gateway +--- + +In the past few chapters we looked at how to debug errors in our Lambda functions. However, our APIs can fail before our Lambda function has been invoked. In these cases, we won't be able to debug using the Lambda logs. Since there won't be any requests made to our Lambda functions. + +The two common causes for these errors are: + +1. Invalid API path +2. Invalid API method + +Let's look at how to debug these. + +### Invalid API Path + +Head over to the `frontend/` directory in your project. + +{%change%} Open `src/containers/Home.tsx`, and replace the `loadNotes()` function with: + +```tsx + function loadNotes() { + return API.get("notes", "/invalid_path", {}); +} +``` + +{%change%} Let's commit this and push it. + +```bash +$ git add . +$ git commit -m "Adding faulty paths" +$ git push +``` + +Head over to your Seed dashboard and deploy it. + +Then in your notes app, load the home page. You'll notice the page fails with an error alert saying `Network Alert`. + +![Invalid path error in notes app](/assets/monitor-debug-errors/invalid-path-error-in-notes-app.png) + +On Sentry, the error will show that a `GET` request failed with status code `0`. + +![Invalid path error in Sentry](/assets/monitor-debug-errors/invalid-path-error-in-sentry.png) + +What happens here is that: + +- The browser first makes an `OPTIONS` request to `/invalid_path`. +- API Gateway returns a `403` response. +- The browser throws an error and does not continue to make the `GET` request. + +This means that our Lambda function was not invoked. And in the browser it fails as a CORS error. + + + +### Invalid API method + +Now let's look at what happens when we use an invalid HTTP method for our API requests. Instead of a `GET` request we are going to make a `PUT` request. + +{%change%} In `src/containers/Home.tsx` replace the `loadNotes()` function with: + +```tsx +function loadNotes() { + return API.put("notes", "/notes", {}); +} +``` + +{%change%} Let's push our code. + +```bash +$ git add . +$ git commit -m "Adding invalid method" +$ git push +``` + +Head over to your Seed dashboard and deploy it. + +Our notes app should fail to load the home page. + +![Invalid method error in notes app](/assets/monitor-debug-errors/invalid-method-error-in-notes-app.png) + +You should see a similar Network Error as the one above in Sentry. Select the error and you will see that the `PUT` request failed with `0` status code. + +![Invalid method error in Sentry](/assets/monitor-debug-errors/invalid-method-error-in-sentry.png) + +Here's what's going on behind the scenes: + +- The browser first makes an `OPTIONS` request to `/notes`. +- API Gateway returns a successful `200` response with the HTTP methods allowed for the path. +- The allowed HTTP methods are `GET` and `POST`. This is because we defined: + - `GET` request on `/notes` to list all the notes + - `POST` request on `/notes` to create a new note +- The browser reports the error because the request method `PUT` is not allowed. + +Similar as to the case above, our Lambda function was not invoked. And in the browser it fails as a CORS error. + + + +With that we've covered all the major types of serverless errors and how to debug them. + +### Rollback the Changes + +{%change%} Let's revert all the faulty code that we created. + +```bash +$ git checkout main +$ git branch -D debug +``` + +And rollback the prod build in Seed. + +Head to the **Activity** tab in the Seed dashboard. Then click on **prod** over on the right. This shows us all the deployments made to our prod stage. + +![Click on prod activity in Seed](/assets/monitor-debug-errors/click-on-prod-activity-in-seed.png) + +Scroll down to the last deployment from the `main` branch, past all the ones made from the `debug` branch. Hit **Rollback**. + +![Rollback on prod build in Seed](/assets/monitor-debug-errors/rollback-on-prod-build-in-seed.png) + +This will rollback our app to the state it was in before we deployed all of our faulty code. + +Now you are all set to go live with your brand new full-stack serverless app! + +Let's wrap things up next. diff --git a/_chapters/errors-outside-lambda-functions.md b/_chapters/errors-outside-lambda-functions.md new file mode 100644 index 0000000000..e541dfa8b4 --- /dev/null +++ b/_chapters/errors-outside-lambda-functions.md @@ -0,0 +1,183 @@ +--- +layout: post +title: Errors Outside Lambda Functions +date: 2020-04-06 00:00:00 +lang: en +description: In this chapter we look at how to debug errors that happen outside your Lambda function handler code. We use the CloudWatch logs through Seed to help us debug it. +comments_id: errors-outside-lambda-functions/1729 +ref: errors-outside-lambda-functions +--- + +We've covered debugging [errors in our code]({% link _chapters/logic-errors-in-lambda-functions.md %}) and [unexpected errors]({% link _chapters/unexpected-errors-in-lambda-functions.md %}) in Lambda functions. Now let's look at how to debug errors that happen outside our Lambda functions. + +### Initialization Errors + +Lambda functions could fail not because of an error inside your handler code, but because of an error outside it. In this case, your Lambda function won't be invoked. Let's add some faulty code outside our handler function. + +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +// Some faulty code +dynamoDb.notExist(); + +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of + // the item to be retrieved + Key: { + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; +}); + +``` + +{%change%} Commit this code. + +```bash +$ git add . +$ git commit -m "Adding an init error" +$ git push +``` + +Head over to your Seed dashboard, and deploy it. + +Now if you select a note in your notes app, you'll notice that it fails with an error. + +![Init error in notes app note page](/assets/monitor-debug-errors/init-error-in-notes-app-note-page.png) + +You should see an error in Sentry. And if you head over to the Issues in Seed and click on the new error. + +![Init error details in Seed](/assets/monitor-debug-errors/init-error-details-in-seed.png) + +You'll notice the error message `dynamodb_lib.notExist is not a function`. + +Note that, you might see there are 3 events for this error. This is because the Lambda runtime prints out the error message multiple times. + +### Handler Function Errors + +Another error that can happen outside a Lambda function is when the handler has been misnamed. + +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +// Wrong handler function name +export const main2 = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of + // the item to be retrieved + Key: { + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; +}); + +``` + +{%change%} Let's commit this. + +```bash +$ git add . +$ git commit -m "Adding a handler error" +$ git push +``` + +Head over to your Seed dashboard and deploy it. Then, in your notes app, try and load a note. It should fail with an error alert. + +Just as before, you'll see the error in Sentry. Head over to the new error in Seed. + +![Handler error details in Seed](/assets/monitor-debug-errors/handler-error-details-in-seed.png) + +You should see the error `Runtime.HandlerNotFound`, along with message `get.main is undefined or not exported`. + +And that about covers the main Lambda function errors. So the next time you see one of the above error messages, you'll know what's going on. + +### Remove the Faulty Code + +Let's cleanup all the faulty code. + +{%change%} Replace `packages/functions/src/get.ts` with the following. + +```typescript +import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Table } from "sst/node/table"; +import dynamoDb from "@notes/core/dynamodb"; + +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of + // the item to be retrieved + Key: { + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; +}); + +``` + +Commit and push the code. + +```bash +$ git add . +$ git commit -m "Reverting faulty code" +$ git push +``` + +Head over to your Seed dashboard and deploy it. + +Now let's move on to debugging API Gateway errors. diff --git a/_chapters/es/add-support-for-es6-and-typescript.md b/_chapters/es/add-support-for-es6-and-typescript.md new file mode 100644 index 0000000000..4c03666d1e --- /dev/null +++ b/_chapters/es/add-support-for-es6-and-typescript.md @@ -0,0 +1,108 @@ +--- +layout: post +title: Agregar soporte para ES6 y TypeScript +date: 2016-12-29 12:00:00 +lang: es +ref: add-support-for-es6-and-typescript +description: AWS Lambda soporta Node.js v10.x y v12.x. Sin embargo, para usar características ES6 o de TypeScript en nuestro proyecto Serverless Framework necesitamos usar Babel, Webpack 5 y una gran variedad de otros paquetes. Podemos lograr esto usando el plugin serverless-bundle en nuestro proyecto. +comments_id: add-support-for-es6-es7-javascript/128 +--- + +AWS Lambda soporta Node.js v10.x y v12.x. Sin embargo, la sintaxis admitida es un poco diferente en comparación con el sabor más avanzado de JavaScript ECMAScript que admite nuestra aplicación frontend en React. Tiene sentido utilizar funciones ES similares en ambas partes del proyecto – específicamente, confiaremos en las funciones ES de importar/exportar de nuestro controlador (`handler`). + +Adicionalmente, nuestra aplicación frontend React soporta automáticamente TypeScript vía [Create React App](https://create-react-app.dev). No vamos a usar TypeScript en esta guía, tiene sentido tener una configuración similar para nuestras funciones backend en Lambda. Así podrás usarlas en tus proyectos futuros. + +Para lograr esto normalmente necesitamos instalar [Babel](https://babeljs.io), [TypeScript](https://www.typescriptlang.org), [Webpack](https://webpack.js.org) y una larga lista de otros paquetes. Esto puede significar extra configuracion y complejidad a tu proyecto. + +Para ayudar en este proceso hemos creado, [`serverless-bundle`](https://github.com/AnomalyInnovations/serverless-bundle). Es un plugin de Serverless Framework que tiene algunas ventajas clave: + +- Una sola dependencia +- Soporta ES6 y TypeScript +- Genera paquetes optimizados +- Analiza (`Linting`) funciones Lambda usando [ESLint](https://eslint.org) +- Soporta conversiones (`transpiling`) de pruebas unitarias con [babel-jest](https://github.com/facebook/jest/tree/master/packages/babel-jest) +- Soporta mapeos de código fuente (`Source map`) para mensajes de error apropiados + +Este se incluye automáticamente en el proyecto inicial que usamos en el capítulo anterior — [`serverless-nodejs-starter`]({% link _archives/serverless-nodejs-starter.md %}). Para TypeScript, tenemos un iniciador tambien — [`serverless-typescript-starter`](https://github.com/AnomalyInnovations/serverless-typescript-starter). + +Sin embargo, si estas buscando agregar soporte ES6 y TypeScript a tus proyectos Serverless Framework existentes, puedes hacerlo instalando [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle): + +``` bash +$ npm install --save-dev serverless-bundle +``` + +E incluyéndolo en tu archivo `serverless.yml` usando: + +``` yml +plugins: + - serverless-bundle +``` + +Para ejecutar tus pruebas, agrega esto a tu archivo `package.json`. + +``` json +"scripts": { + "test": "serverless-bundle test" +} +``` + +### Funciones ES6 Lambda + +Revisemos las funciones Lambda que vienen con nuestro proyecto inicial. + +Tu archivo `handler.js` debería verse así. + +``` js +export const hello = async (event, context) => { + return { + statusCode: 200, + body: JSON.stringify({ + message: `Vamos Serverless v2.0! ${(await message({ time: 1, copy: 'Tu función fue ejecutada exitosamente!'}))}`, + }), + }; +}; + +const message = ({ time, ...rest }) => new Promise((resolve, reject) => + setTimeout(() => { + resolve(`${rest.copy} (con demora)`); + }, time * 1000) +); +``` + +Ejecutemos esto. En tu proyecto raíz ejecuta: + +``` bash +$ serverless invoke local --function hello +``` + +Deberías ver algo como esto en tu terminal. + +``` bash +{ + "statusCode": 200, + "body": "{\"message\":\"Vamos Serverless v2.0! Tu función fue ejecutada exitosamente! (con demora)\"}" +} +``` + +En el comando anterior estamos pidiendo al Serverless Framework invocar (localmente) una función Lambda llamada `hello`. Esto a su vez ejecutará el método `hello` que estamos exportando en nuestro archivo `handler.js`. + +Aquí estamos invocando directamente a la función Lambda . Aunque una vez desplegado, invocaremos a esta función por medio del API endpoint `/hello` (como hemos [hablado en el último capítulo]({% link _archives/setup-the-serverless-framework.md %})). + +En este momento estamos casi listos para desplegar nuestra función Lambda y nuestro API . Pero antes vamos a revisar rápidamente una de las otras cosas que se han configurado por nosotros en este proyecto inicial. + +### Paquetes optimizados + +Por default el Serverless Framework crea un simple paquete para todas tus funciones Lambda. Esto significa que cuando una función Lambda es invocada, será cargado todo el código en tu aplicación. Incluyendo todas las otras funciones Lambda. Esto afecta negativamente el rendimiento conforme tu aplicación crece en tamaño. Entre más grandes sean tus paquetes de funciones Lambda, más tiempo tardarán en ejecutarse [arranque en frío]({% link _chapters/what-is-serverless.md %}#cold-starts). + +Para evitar esto y asegurarse que el Serverless Framework esta empaquetando nuestras funciones individualmente, agreguemos a nuestro archivo `serverless.yml`. + +``` yml +package: + individually: true +``` + +Esto debería estar por defecto en nuestro proyecto inicial. + +Ten en cuenta que con la opción anterior habilitada, serverless-bundle puede usar Webpack para generar paquetes optimizados usando un [algoritmo tree shaking](https://webpack.js.org/guides/tree-shaking/). Solo incluirá el código necesario para ejecutar su función Lambda y nada más! + +Ahora estamos listos para desplegar nuestro API backend. diff --git a/_chapters/es/configure-the-aws-cli.md b/_chapters/es/configure-the-aws-cli.md index 38fdee4809..2b6a8f8fac 100644 --- a/_chapters/es/configure-the-aws-cli.md +++ b/_chapters/es/configure-the-aws-cli.md @@ -18,7 +18,7 @@ AWS CLI necesita Python 2 versión 2.6.5+ o Python 3 versión 3.3+ y [Pip](https - [Instalar Python](https://www.python.org/downloads/) - [Instalar Pip](https://pip.pypa.io/en/stable/installing/) -Ahora utilizando Pip puedes instalar la CLI de AWS (en Linux, macOS o Unix) ejecutando: +{%change%} Ahora utilizando Pip puedes instalar la CLI de AWS (en Linux, macOS o Unix) ejecutando: ``` bash $ sudo pip install awscli @@ -32,7 +32,7 @@ $ brew install awscli Si tienes problemas para instalar la CLI de AWS o necesitas instrucciones de instalación en Windows, consulta las [instrucciones de instalación completas](http://docs.aws.amazon.com/cli/latest/userguide/installing.html). -### Agregua tu clave de acceso a AWS CLI +### Agrega tu clave de acceso a AWS CLI Ahora debemos decirle a AWS CLI que use las claves de acceso del capítulo anterior. @@ -41,7 +41,7 @@ Debería verse algo como esto: - ID de clave de acceso **AKIAIOSFODNN7EXAMPLE** - Clave de acceso secreta **wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY** -Simplemente ejecuta lo siguiente con tu ID de clave secreta y tu clave de acceso. +{%change%} Simplemente ejecuta lo siguiente con tu ID de clave secreta y tu clave de acceso. ``` bash $ aws configure diff --git a/_chapters/es/create-a-dynamodb-table.md b/_chapters/es/create-a-dynamodb-table.md index e63f2abb6f..24c300a5ff 100644 --- a/_chapters/es/create-a-dynamodb-table.md +++ b/_chapters/es/create-a-dynamodb-table.md @@ -54,7 +54,7 @@ Se ha creado la tabla `notes`. Si te encuentras atascado con el mensaje **La tab ![Captura de pantalla de tabla de DynamoDB creada](/assets/es/dynamodb/dynamodb-table-created.png) -También es una buena idea configurar copias de seguridad para tu tabla de DynamoDB, especialmente si planeas usarla en producción. Cubriremos esto en un capítulo adicional, [Copias de seguridad en DynamoDB]({% link _chapters/backups-in-dynamodb.md %}). +También es una buena idea configurar copias de seguridad para tu tabla de DynamoDB, especialmente si planeas usarla en producción. Cubriremos esto en un capítulo adicional, [Copias de seguridad en DynamoDB]({% link _archives/backups-in-dynamodb.md %}). A continuación, configuraremos un bucket S3 para manejar las carga de archivos. diff --git a/_chapters/es/create-an-iam-user.md b/_chapters/es/create-an-iam-user.md index 80df3fb1bf..8ede9451c4 100644 --- a/_chapters/es/create-an-iam-user.md +++ b/_chapters/es/create-an-iam-user.md @@ -39,11 +39,11 @@ Selecciona **Añadir directamente las políticas existentes**. Busca **AdministratorAccess** y selecciona la política, luego selecciona **Siguiente: Etiquetas**. -Podemos proporcionar una política más detallada aquí y lo cubriremos más adelante en el capitulo [Personalizar la política de Serverless IAM]({% link _chapters/customize-the-serverless-iam-policy.md %}). Pero por ahora, continuemos con esto. +Podemos proporcionar una política más detallada aquí y lo cubriremos más adelante en el capitulo [Personalizar la política de Serverless IAM]({% link _archives/customize-the-serverless-iam-policy.md %}). Pero por ahora, continuemos con esto. ![Captura: política de administración añadida](/assets/es/iam-user/added-admin-policy.png) -Este paso es opcional, aquí puedes agregar las etiquetas que creas necesarias al nuevo usuario de IAM, para organizar, seguir o controlar el acceso de los usuarios, por ejemplo, la etiqueta _Application_ tiene el valor _Serverless Stack Guide_ para saber que este usuario fue creado para esta aplicación específica. Luego selecciona **Siguiente: Revisar**. +Este paso es opcional, aquí puedes agregar las etiquetas que creas necesarias al nuevo usuario de IAM, para organizar, seguir o controlar el acceso de los usuarios, por ejemplo, la etiqueta _Application_ tiene el valor _SST Guide_ para saber que este usuario fue creado para esta aplicación específica. Luego selecciona **Siguiente: Revisar**. ![Captura: agregar etiquetas a usuario de IAM](/assets/es/iam-user/add-iam-user-tags.png) diff --git a/_chapters/es/create-an-s3-bucket-for-file-uploads.md b/_chapters/es/create-an-s3-bucket-for-file-uploads.md new file mode 100644 index 0000000000..aae580bf61 --- /dev/null +++ b/_chapters/es/create-an-s3-bucket-for-file-uploads.md @@ -0,0 +1,43 @@ +--- +layout: post +title: Crear un repositorio S3 Bucket para almacenar archivos +date: 2016-12-27 00:00:00 +lang: es +ref: create-an-s3-bucket-for-file-uploads +description: Para permitir a los usuarios que puedan subir sus archivos a nuestra aplicación serverless vamos a usar Amazon S3 (Simple Storage Service). S3 te permite guardar archivos y organizarlos en repositorios ó buckets. +redirect_from: /chapters/create-a-s3-bucket-for-file-uploads.html +comments_id: create-an-s3-bucket-for-file-uploads/150 +--- + +Ahora que tenemos nuestra tabla en la base de datos lista vamos a configurar lo necesario para manejar la carga de los archivos. Esto es necesario porque cada nota puede tener un archivo cargado como adjunto. + +[Amazon S3](https://aws.amazon.com/s3/) (Simple Storage Service) ofrece un servicio de almacenamiento a través de interfaces de servicios web como REST. Puedes almacenar cualquier objeto en S3 incluyendo imágenes, videos, archivos, etc. Los objectos son organizados en buckets y pueden ser identificados dentro de cada bucket con una única llave asignada de usuario. + +En este capítulo vamos a crear un bucket S3 el cual va a ser usado para guardar archivos cargados desde nuestra aplicación de notas. + +### Crear Bucket + +Primero, ingresa a tu [Consola AWS](https://console.aws.amazon.com) y selecciona **S3** de la lista de servicios. + +![Selecciona el servicio S3 - Captura de pantalla](/assets/s3/select-s3-service.png) + +Selecciona **Crear bucket**. + +![Selecciona crear Bucket - Captura de pantalla](/assets/s3/select-create-bucket.png) + +Escribe el nombre del bucket y selecciona una región. Después selecciona **Crear**. + +- **Nombres de Bucket** son globalmente únicos, lo cual significa que no puedes elegir el mismo nombre que en este tutorial. +- **Región** es la región geográfica fisica donde los archivos son almacenados. Vamos a usar **US East (N. Virginia)** para esta guía. + +Anota el nombre y la región ya que lo usaremos más tarde en esta guía. + +![Ingresa la información del bucket S3 - Captura de pantalla](/assets/s3/enter-s3-bucket-info.png) + +Luego ve hacia la parte más abajo y dá click en **Crear bucket**. + +![Click en crear bucket S3 - Captura de pantalla](/assets/s3/click-create-s3-bucket.png) + +Esto deberá crear tu nuevo bucket S3. + +Ahora, antes de comenzar a trabajar en nuestro Serverless API backend, tengamos una idea rápida de como encajan todos nuestros recursos. diff --git a/_chapters/es/deploy-your-hello-world-api.md b/_chapters/es/deploy-your-hello-world-api.md new file mode 100644 index 0000000000..adbf8c48ab --- /dev/null +++ b/_chapters/es/deploy-your-hello-world-api.md @@ -0,0 +1,54 @@ +--- +layout: post +title: Desplegando tu API Hola Mundo +date: 2020-10-16 00:00:00 +lang: es +ref: deploy-your-first-serverless-api +description: En este capítulo vamos a estar desplegando nuestro primer API Serverless Hola Mundo. Vamos a usar el comando `serverless deploy` para desplegarlo a AWS. +comments_id: deploy-your-hello-world-api/2173 +--- + +Hasta ahora hemos configurado nuestra cuenta AWS y nuestro cliente AWS CLI. Tambien hemos creado nuestra aplicación Serverless Framework. Una gran ventaja de trabajar con Serverless es que no existe ninguna infraestructura o servidores que instalar o configurar. Solo debes desplegar tu aplicación directamente y estará lista para servir a millones de usuarios inmediatamente. + +Hagamos un pequeño despliegue para ver como funciona. + +{%change%} En la raíz de tu proyecto executa lo siguiente. + +``` bash +$ serverless deploy +``` + +La primera vez que tu aplicación Serverless es desplegada se crea un repositorio (`bucket`) S3 (para guardar tu código de la funcion Lambda), Lambda, API Gateway, y algunos otros recursos. Esto puede tomar un minuto o dos. + +Una vez completado, deberías ver algo como esto: + +``` bash +Service Information +service: notes-api +stage: prod +region: us-east-1 +stack: notes-api-prod +resources: 11 +api keys: + None +endpoints: + GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/hello +functions: + hello: notes-api-prod-hello +layers: + None +``` + +Ten en cuenta que hemos creado un nuevo endpoint GET. En nuestro caso, este apunta hacia — [https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/hello](https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/hello) + +Si te dirijes a esa URL, deberías ver algo como esto: + +``` bash +{"message":"Vamos Serverless v2.0! Tu función fue ejecutada exitosamente! (con demora)"} +``` + +Recordarás que es la misma salida que hemos recibido cuando invocamos a nuestra función Lambda localmente en el último capítulo. En este caso estamos invocando a la función mediante el API endpoint `/hello`. + +Ahora tenemos un API endpoint en Serverless. Solo pagarás por petición a este endpoint y este escalará automáticamente. Este es un gran primer paso! + +Ahora estamos listos para escribir nuestro código backend. Pero antes de eso, vamos a crear un repositorio GitHub para almacenar nuestro codigo. diff --git a/_chapters/es/how-to-get-help.md b/_chapters/es/how-to-get-help.md index f6e9aad7bf..70e5372d5d 100644 --- a/_chapters/es/how-to-get-help.md +++ b/_chapters/es/how-to-get-help.md @@ -12,7 +12,7 @@ En caso de que tengas problemas con un determinado paso, queremos asegurarnos de - Usamos los foros de [Discourse]({{site.forum_url}}) como sistema de comentarios y hemos ayudado a resolver algunos problemas antes. Así que asegúrate de revisar los comentarios debajo de cada capítulo para ver si alguien más se ha encontrado con el mismo problema que tú. - Publica en los comentarios del capítulo específico, detallando tu problema y uno de nosotros responderá. -![Captura del foro de Discourse de Serverless Stack](/assets/serverless-stack-discourse-forums.png) +![Captura del foro de Discourse de SST](/assets/serverless-stack-discourse-forums.png) Esta guía completa está alojada en [GitHub]({{site.github_repo}}). Así que si encuentras un error siempre puedes: diff --git a/_chapters/es/initialize-the-backend-repo.md b/_chapters/es/initialize-the-backend-repo.md new file mode 100644 index 0000000000..f1ad0dbd0e --- /dev/null +++ b/_chapters/es/initialize-the-backend-repo.md @@ -0,0 +1,72 @@ +--- +layout: post +title: Inicializando el repositorio backend +date: 2016-12-29 18:00:00 +lang: es +description: Para esta guia vamos a enviar nuestra aplicación Serverless Framework a un repositorio Git. Hacemos esto para, más tarde, poder automatizar nuestros despliegues con tan solo enviarlo (`pushing`) a Git. +code: backend +ref: initialize-the-backend-repo +comments_id: initialize-the-backend-repo/159 +--- + +Antes de comenzar a trabajar en nuestra app, vamos a crear un repositorio Git para este proyecto. Es una gran forma de guardar nuestro código y usaremos este repositorio más tarde para automatizar el proceso de despliegue de nuestra aplicación. + +### Creando un nuevo repositorio GitHub + +Vayamos a [GitHub](https://github.com). Asegurate que has ingresado y da click en **Nuevo repositorio** (`**New repository**`). + +![Captura de pantalla - Creando un nuevo repositorio GitHub](/assets/part2/create-new-github-repository.png) + +Nombra tu repositorio, en nuestro caso lo hemos llamado `serverless-stack-api`. Luego da click en **Crear repositorio** (`**Create repository**`). + +![Captura de pantalla - Nombrando el nuevo repositorio GitHub](/assets/part2/name-new-github-repository.png) + +Una vez que tu repositorio se ha creado, copia la URL del repositorio. Lo necesitaremos a continuación. + +![Captura de pantalla - Copiando la url del repositorio GitHub](/assets/part2/copy-new-github-repo-url.png) + +En nuestro caso la URL es: + +``` txt +https://github.com/jayair/serverless-stack-api.git +``` + +### Inicializando tu nuevo repositorio + +{%change%} Ahora regresa a tu proyecto y usa el siguiente comando para inicializar tu nuevo repositorio. + +``` bash +$ git init +``` + +{%change%} Agrega los archivos existentes. + +``` bash +$ git add . +``` + +{%change%} Crea tu primer commit. + +``` bash +$ git commit -m "Mi primer commit" +``` + +{%change%} Únelo al repositorio que acabas de crear en GitHub. + +``` bash +$ git remote add origin REPO_URL +``` + +Aquí `REPO_URL` es la URL que copiamos de GitHub en los pasos anteriores. Puedes verificar que ha sido configurado correctamente haciendo lo siguiente. + +``` bash +$ git remote -v +``` + +{%change%} Finalmente, vamos a enviar (`push`) nuestro primer commit a GitHub usando: + +``` bash +$ git push -u origin master +``` + +Ahora estamos listos para construir nuestro backend! diff --git a/_chapters/es/review-our-app-architecture.md b/_chapters/es/review-our-app-architecture.md new file mode 100644 index 0000000000..b84a30cbfc --- /dev/null +++ b/_chapters/es/review-our-app-architecture.md @@ -0,0 +1,38 @@ +--- +layout: post +title: Revisar la arquitectura de nuestra aplicación +date: 2020-10-28 00:00:00 +lang: es +ref: review-our-app-architecture +description: En este capítulo vamos a hacer un pequeño repaso de nuestra API Serverless que estamos a punto de construir. Vamos a estar usando la tabla en DynamoDB y el bucket S3 que creamos anteriormente. +comments_id: review-our-app-architecture/2178 +--- + +Hasta ahora hemos [desplegado nuestro API Hola Mundo]({% link _archives/deploy-your-hello-world-api.md %}), [creado una base de datos (DynamoDB)]({% link _archives/create-a-dynamodb-table.md %}), y [creado un bucket S3 para la carga de nuestros archivos]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}). Estamos listos para comenzar a trabajar en nuestra API backend pero tengamos una rápida idea de como encaja todo lo antes mencionado. + +### Arquitectura del API Hola Mundo + +Aqui lo que hemos construido hasta ahora en nuestra API Hola Mundo. + +![Arquitectura del API Serverless Hola mundo](/assets/diagrams/serverless-hello-world-api-architecture.png) + +API Gateway controla el `https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod` endpoint por nosotros. Y cualquier petición GET hecha a `/hello`, será enviada a la función Lambda `hello.js`. + +### Notas de la arquitectura de la aplicación API + +Ahora vamos a agregar DynamoDB y S3 a la mezcla. También vamos a estar agregando algunas funciones Lambda. + +Ahora nuestra nuevo diseño de la aplicación backend de notas se verá como esto. + +![Arquitectura pública API Serverless](/assets/diagrams/serverless-public-api-architecture.png) + +Hay un par de cosas que tomar en cuenta aquí: + +1. Nuestra base de datos no está expuesta públicamente y sólo es invocada por nuestras funciones Lambda. +2. Pero nuestros usuarios estarán cargando archivos directamente a nuestro bucket S3 que creamos. + +El segundo punto es algo que es diferente de muchas arquitecturas tradicionales basadas en servidor. Estamos acostumbrados a cargar archivos a nuestro servidor y luego moverlos al servidor de archivos. Pero vamos a cargarlos directamente a nuestro bucket S3. Veremos esto con más detalle cuando estemos en la carga de archivos. + +En las secciones siguientes estaremos viendo como asegurar el acceso a esos recursos. Vamos a configurarlos para que solo nuestros usuarios autenticados tengan permitido acceder a esos recursos. + +Ahora que tenemos una buena idea de como nuestra aplicación será diseñada, volvamos al trabajo! diff --git a/_chapters/es/setup-the-serverless-framework.md b/_chapters/es/setup-the-serverless-framework.md new file mode 100644 index 0000000000..322f1e469d --- /dev/null +++ b/_chapters/es/setup-the-serverless-framework.md @@ -0,0 +1,115 @@ +--- +layout: post +title: Configurar el Serverless Framework +date: 2016-12-29 00:00:00 +lang: es +ref: setup-the-serverless-framework +description: Para crear nuestro serverless api backend usando AWS Lambda y API Gateway vamos a usar el Serverless Framework (https://serverless.com). El Serverless Framework ayuda a los desarrolladores a construir y manejar aplicaciones serverless en AWS y otros proveedores en la nube. Podemos instalar el Serverless Framework CLI desde su paquete NPM y usarlo para crear un nuevo proyecto con Serverless Framework. +comments_id: set-up-the-serverless-framework/145 +--- + +Vamos a estar usando [AWS Lambda](https://aws.amazon.com/lambda/) y [Amazon API Gateway](https://aws.amazon.com/api-gateway/) para crear nuestro backend. AWS Lambda es un servicio computable que permite ejecutar tu código sin proveer o administrar servidores. Tú sólo pagas por el tiempo computable que consumes - no hay cargo alguno cuando tu código no se está ejecutando. Y el servicio API Gateway facilita a los desarrolladores crear, publicar, mantener, monitorear, y volver seguras las APIs. Trabajar directamente con AWS Lambda y configurar el servicio de API Gateway puede ser un poco complicado; por eso vamos a usar el [Serverless Framework](https://serverless.com) para ayudarnos. + +El Serverless Framework permite a los desarrolladores desplegar aplicaciones backend como funciones independientes que serán desplegadas hacia AWS Lambda. También configura AWS Lambda para ejecutar tu código en respuesta a peticiones HTTP usando Amazon API Gateway. + +En este capítulo vamos a configurar el Serverless Framework en nuestro ambiente local de desarrollo. + +### Instalando Serverless + +{%change%} Instalar Serverless globalmente. + +``` bash +$ npm install serverless -g +``` + +El comando anterior necesita [NPM](https://www.npmjs.com), un administrador de paquetes para JavaScript. Click [aquí](https://docs.npmjs.com/getting-started/installing-node) si necesitas ayuda para instalar NPM. + +{%change%} En tu directorio de trabajo; crea un proyecto usando Node.js. Vamos a repasar algunos detalles de este proyecto inicial en el siguiente capítulo. + +``` bash +$ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name notes-api +``` + +{%change%} Ve al directorio de nuestro proyecto api backend. + +``` bash +$ cd notes-api +``` + +Ahora, el directorio deberá contener algunos archivos incluyendo **handler.js** y **serverless.yml**. + +- El archivo **handler.js** contiene el código real para los servicios/funciones que van a ser desplegados hacía AWS Lambda. +- El archivo **serverless.yml** contiene la configuración sobre que servicios AWS Serverless proporcionará y como deben ser configurados. + +También tenemos el directorio `tests/` donde podemos agregar nuestras pruebas unitarias. + +### Instalando paquetes Node.js + +El proyecto inicial se basa en algunas dependencias listadas en el archivo `package.json`. + +{%change%} En la raíz del proyecto ejecuta. + +``` bash +$ npm install +``` + +{%change%} A continuación, instalaremos un par de paquetes especificos para nuestro backend. + +``` bash +$ npm install aws-sdk --save-dev +$ npm install uuid@7.0.3 --save +``` + +- **aws-sdk** nos permite comunicarnos con diferentes servicios AWS. +- **uuid** genera ids únicos. Los necesitamos para guardar información en DynamoDB. + +### Actualizando el nombre del servicio + +Cambiemos el nombre de nuestro servicio del que inicialmente teniamos. + +{%change%} Abre el archivo `serverless.yml` y reemplaza el contenido con lo siguiente. + +``` yaml +service: notes-api + +# Creando un paquete optimizado para nuestras funciones +package: + individually: true + +plugins: + - serverless-bundle # Empaquetar nuestras funciones con Webpack + - serverless-offline + - serverless-dotenv-plugin # Cargar el archivo .env como variables de ambiente + +provider: + name: aws + runtime: nodejs12.x + stage: prod + region: us-east-1 + +functions: + hello: + handler: handler.hello + events: + - http: + path: hello + method: get +``` + +El nombre del servicio (`service`) es muy importante. Vamos a llamar a nuestro servicio `notes-api`. Serverless Framework crea tu ambiente de trabajo en AWS usando este nombre. Esto significa que si cambias el nombre y despliegas tu proyecto, se va a crear un **proyecto completamente nuevo**! + +Vamos a definir también una función Lambda llamada `hello`. Esta tiene un controlador llamado `handler.hello` y sigue el siguiente formato: + +``` text +handler: {filename}-{export} +``` + +En este caso el controlador para nuestra función Lambda llamada `hello` es la función `hello` que se exporta en el archivo `handler.js`. + +Nuestra función Lambda también responde a un evento HTTP GET con la ruta `/hello`. Esto tendrá más sentido una vez que despleguemos nuestro API. + +Notarás que hemos incluido los plugins — `serverless-bundle`, `serverless-offline`, y `serverless-dotenv-plugin`. El plugin [serverless-offline](https://github.com/dherault/serverless-offline) es útil para nuestro desarrollo local. Mientras que el plugin [serverless-dotenv-plugin](https://github.com/colynb/serverless-dotenv-plugin) será usado después para cargar archivos `.env` como variables de ambiente Lambda. + +Por otro lado, usaremos el plugin [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle) para permitirnos escribir nuestras funciones Lambda usando un sabor de JavaScript que es similar a aquel que vamos a usar en nuestra aplicación frontend en React. + +Veamos esto en detalle. diff --git a/_chapters/es/what-does-this-guide-cover.md b/_chapters/es/what-does-this-guide-cover.md index f903019926..43d420b448 100644 --- a/_chapters/es/what-does-this-guide-cover.md +++ b/_chapters/es/what-does-this-guide-cover.md @@ -9,7 +9,7 @@ context: true comments_id: what-does-this-guide-cover/83 --- -Para repasar los principales conceptos involucrados en la creación de aplicaciones web, vamos a crear una aplicación sencilla para tomar notas llamada [**Scratch**](https://demo2.serverless-stack.com). +Para repasar los principales conceptos involucrados en la creación de aplicaciones web, vamos a crear una aplicación sencilla para tomar notas llamada [**Scratch**](https://demo2.sst.dev). ![Captura de aplicación completada en escritorio](/assets/completed-app-desktop.png) @@ -58,7 +58,7 @@ Necesitas [Node v8.10 + y NPM v5.5 +](https://nodejs.org/en/). También debes te ### Cómo está estructurada esta guía -La guía se divide en dos partes. Ambas son relativamente independientes. La primera parte cubre lo básico, mientras que la segunda cubre un par de temas avanzados junto con una forma de automatizar la configuración. Lanzamos esta guía a principios de 2017 con solo la primera parte. La comunidad de Serverless Stack ha crecido y muchos de nuestros lectores han usado la configuración descrita en esta guía para crear aplicaciones que impulsan sus negocios. +La guía se divide en dos partes. Ambas son relativamente independientes. La primera parte cubre lo básico, mientras que la segunda cubre un par de temas avanzados junto con una forma de automatizar la configuración. Lanzamos esta guía a principios de 2017 con solo la primera parte. La comunidad de SST ha crecido y muchos de nuestros lectores han usado la configuración descrita en esta guía para crear aplicaciones que impulsan sus negocios. Así que decidimos ampliar la guía y añadirle una segunda parte. Esta está dirigida a las personas que tienen la intención de utilizar esta configuración para sus proyectos. Automatiza todos los pasos manuales de la parte 1 y lo ayuda a crear un flujo de trabajo listo para producción que puede utilizar para todos sus proyectos sin servidor. Esto es lo que cubrimos en las dos partes. @@ -91,7 +91,7 @@ Para el frontend: #### Parte II -Dirigido a personas que buscan utilizar Serverless Stack para sus proyectos del día a día. Automatizamos todos los pasos de la primera parte. Aquí está lo que se cubre en orden. +Dirigido a personas que buscan utilizar SST para sus proyectos del día a día. Automatizamos todos los pasos de la primera parte. Aquí está lo que se cubre en orden. Para el backend: diff --git a/_chapters/es/what-is-aws-lambda.md b/_chapters/es/what-is-aws-lambda.md index 3279f920b3..307eb3d4d9 100644 --- a/_chapters/es/what-is-aws-lambda.md +++ b/_chapters/es/what-is-aws-lambda.md @@ -14,13 +14,13 @@ comments_id: what-is-aws-lambda/308 Comencemos rápidamente por las especificaciones técnicas de AWS Lambda. Lambda soporta los siguientes entornos de ejecución: -- Node.js: v8.10 y v6.10 -- Java 8 -- Python: 3.6 y 2.7 -- .NET Core: 1.0.1 y 2.0 +- Node.js 18.x, 16.x, y 14.x +- Java 17, 11 y 8 +- Python 3.11, 3.10, 3.9, 3.8, y 3.7 +- .NET 7 y 6 - Go 1.x -- Ruby 2.5 -- Rust +- Ruby 3.2 y 2.7 +- [Rust](https://docs.aws.amazon.com/es_es/lambda/latest/dg/lambda-rust.html) Cada función corre en un contenedor con Amazon Linux AMI a 64-bit. Y el entorno de ejecución cuenta con: @@ -64,10 +64,10 @@ Sin embargo, debido a la optimización mencionada anteriormente, la función Lam Por ejemplo, el método `createNewDbConnection` a continuación se llama una vez por instanciación de contenedor y no cada vez que se invoca la función Lambda. La función `myHandler` por otra parte se llama en cada invocación. -``` javascript +```js var dbConnection = createNewDbConnection(); -exports.myHandler = function(event, context, callback) { +exports.myHandler = function (event, context, callback) { var result = dbConnection.makeQuery(); callback(null, result); }; diff --git a/_chapters/es/who-is-this-guide-for.md b/_chapters/es/who-is-this-guide-for.md index 2983e67b47..82a2134658 100644 --- a/_chapters/es/who-is-this-guide-for.md +++ b/_chapters/es/who-is-this-guide-for.md @@ -14,6 +14,6 @@ Por lo tanto, podrías ser un desarrollador de backend que le gustaría aprender También estamos, por ahora, enfocando esto únicamente a los desarrolladores de JavaScript. Podríamos apuntar a otros idiomas y entornos en el futuro. Pero creemos que este es un buen punto de partida porque puede ser realmente beneficioso para un desarrollador fullstack usar un solo lenguaje (JavaScript) y un entorno (Node.js) para crear toda la aplicación. -Como nota personal, el enfoque sin servidor ha sido una revelación gigante para nosotros y queríamos crear un recurso donde pudiéramos compartir lo que hemos aprendido. Puedes leer más sobre nosotros [**aquí**]({% link about.md%}). Y [echar un vistazo a una muestra de lo que la gente ha creado con Serverless Stack]({% link showcase.md%}). +Como nota personal, el enfoque sin servidor ha sido una revelación gigante para nosotros y queríamos crear un recurso donde pudiéramos compartir lo que hemos aprendido. Puedes leer más sobre nosotros [**aquí**]({{ site.sst_url }}). Y [echar un vistazo a una muestra de lo que la gente ha creado con SST]({% link showcase.md %}). Comencemos mirando lo que estaremos cubriendo. diff --git a/_chapters/es/why-create-serverless-apps.md b/_chapters/es/why-create-serverless-apps.md index 5c48d98360..d37f5d2b25 100644 --- a/_chapters/es/why-create-serverless-apps.md +++ b/_chapters/es/why-create-serverless-apps.md @@ -40,4 +40,4 @@ Así que cuesta $6.10 por mes. Además, un dominio .com nos costaría $12 por a Finalmente, la facilidad de escalamiento se debe en parte a DynamoDB, que nos brinda una escala casi infinita y Lambda que simplemente se amplía para satisfacer la demanda. Y, por supuesto, nuestra interfaz es una SPA (single page application) estática simple que, casi siempre, responde de manera instantánea gracias a CloudFront. -!Muy bien! Ahora que estás convencido del por qué debes crear aplicaciones serverless, empecemos. +¡Muy bien! Ahora que estás convencido del por qué debes crear aplicaciones serverless, empecemos. diff --git a/_chapters/fr/how-to-get-help.md b/_chapters/fr/how-to-get-help.md index 20f8f612ae..59b63ab532 100644 --- a/_chapters/fr/how-to-get-help.md +++ b/_chapters/fr/how-to-get-help.md @@ -12,9 +12,9 @@ Si vous rencontrez des problèmes avec une étape en particulier, nous voulons n - Nous utilisons [les sujets d'un forum de discussion Discourse]({{ site.forum_url }}) pour recevoir des commentaires et nous avons réussi à résoudre un certain nombre de problèmes dans le passé. Vérifiez donc les commentaires sous chaque chapitre pour voir si quelqu'un a déjà rencontré le même problème que vous (en anglais). - Postez votre problème dans les commentaires pour le chapitre spécifique et l'un d'entre nous vous répondra (en anglais). -![Serverless Stack Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) +![SST Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) Le guide complet est hébergé sur[GitHub]({{ site.github_repo }}). Si vous trouvez une erreur, vous pouvez toujours: - Ouvrir un [nouveau ticket]({{ site.github_repo }}/issues/new) -- Ou si vous avez trouvé une coquille, éditer la page et soumettre une pull request! \ No newline at end of file +- Ou si vous avez trouvé une coquille, éditer la page et soumettre une pull request! diff --git a/_chapters/fr/what-does-this-guide-cover.md b/_chapters/fr/what-does-this-guide-cover.md index 4007843aab..667f7fcdff 100644 --- a/_chapters/fr/what-does-this-guide-cover.md +++ b/_chapters/fr/what-does-this-guide-cover.md @@ -9,7 +9,7 @@ context: true comments_id: what-does-this-guide-cover/83 --- -Pour parcourir les concepts principaux impliqués dans la construction d'une application web, nous allons construire une application basique de prise de notes appelée [**Scratch**](https://demo2.serverless-stack.com). +Pour parcourir les concepts principaux impliqués dans la construction d'une application web, nous allons construire une application basique de prise de notes appelée [**Scratch**](https://demo2.sst.dev). ![Completed app desktop screenshot](/assets/completed-app-desktop.png) @@ -58,7 +58,7 @@ Vous avez besoin de [Node v8.10+ and NPM v5.5+](https://nodejs.org/en/). Vous au ### Comment ce guide est structuré -Le guide est séparé en deux parties distinctes. Elles sont toutes deux relativement autoportantes. La première partie couvre les fondamentaux, la deuxième couvre des sujets plus avancés ainsi qu'un moyen d'automatiser l'infrastructure. Nous avons lancé ce guide début 2017 avec uniquement la première partie. La communauté Serverless Stack a grandi et beaucoup de nos lecteurs ont utilisé ce setup pour leur business. +Le guide est séparé en deux parties distinctes. Elles sont toutes deux relativement autoportantes. La première partie couvre les fondamentaux, la deuxième couvre des sujets plus avancés ainsi qu'un moyen d'automatiser l'infrastructure. Nous avons lancé ce guide début 2017 avec uniquement la première partie. La communauté SST a grandi et beaucoup de nos lecteurs ont utilisé ce setup pour leur business. Nous avons donc décider d'étendre ce guide et d'y ajouter une sconde partie. Cela cible les personnes qui pensent utiliser ce setup pour leurs projets. Elle automatise les étapes manuelle de la partie 1 et aide à la création d'un workfow prêt pour la production que vous pouvez utiliser pour tous vos projets serverless. Voici ce que nous aobrdons dans les deux parties. diff --git a/_chapters/fr/what-is-aws-lambda.md b/_chapters/fr/what-is-aws-lambda.md index 6247626ca4..885b58bd2b 100644 --- a/_chapters/fr/what-is-aws-lambda.md +++ b/_chapters/fr/what-is-aws-lambda.md @@ -14,13 +14,13 @@ comments_id: what-is-aws-lambda/308 Voici les spécifications techniques d'AWS Lambda. Lambda supporte les langages suivants : -- Node.js: v8.10 et v6.10 -- Java 8 -- Python: 3.6 et 2.7 -- .NET Core: 1.0.1 et 2.0 +- Node.js 18.x, 16.x, et 14.x +- Java 17, 11 et 8 +- Python 3.11, 3.10, 3.9, 3.8, et 3.7 +- .NET 7 et 6 - Go 1.x -- Ruby 2.5 -- Rust +- Ruby 3.2 et 2.7 +- [Rust](https://docs.aws.amazon.com/fr_fr/lambda/latest/dg/lambda-rust.html) Chaque fonction s'exécute dans un conteneur 64-bit Amazon Linux AMI. Et l'environnement d'exécution a : @@ -38,7 +38,7 @@ Le temps maximum d'exécution signifie que les fonctions Lambda ne peuvent pas t La taille du package correspond à tout le code nécessaire pour exécuter la fonction. Cela inclut toutes les dépendances (le dossier `node_modules/` dans le cas de Node.js) dont votre fonction a besoin, Il y a une limite à 250MB non-compressé et 50MB après compression. On va s'intéresser au processus de packaging un peu plus tard. -### Fonction Lambda +### Fonction Lambda Voici enfin ce à quoi ressemble une fonction Lambda (en Node.js). @@ -64,10 +64,10 @@ Cependant, en raison de l'optimisation précédemment décrite, la fonction Lamb Par exemple, la méthode `createNewDbConnection` ci-dessous est appelée une fois par instanciation de conteneur et non à chaque fois que la fonction Lambda est appelée. En revanche, la fonction `myHandler` est appelée à chaque appel. -``` javascript +```js var dbConnection = createNewDbConnection(); -exports.myHandler = function(event, context, callback) { +exports.myHandler = function (event, context, callback) { var result = dbConnection.makeQuery(); callback(null, result); }; diff --git a/_chapters/fr/what-is-serverless.md b/_chapters/fr/what-is-serverless.md index 11494f5e4c..e783b78a58 100644 --- a/_chapters/fr/what-is-serverless.md +++ b/_chapters/fr/what-is-serverless.md @@ -50,4 +50,4 @@ La durée des démarrages à froid dépend de la mise en œuvre du fournisseur d Outre l'optimisation de vos fonctions, vous pouvez utiliser des astuces simples, comme une fonction planifiée, pour appeler votre fonction toutes les minutes afin de la maintenir au chaud. [Serverless Framework](https://serverless.com) que nous allons utiliser dans ce tutoriel contient quelques plugins pour vous [aider à garder vos fonctions au chaud](https://github.com/FidelLimited/serverless-plugin-warmup). -Maintenant qu'on a une bonne idée de l'architecture serverless, regardons de plus près ce qu'est une fonction Lambda et comment notre code va être exécuté. \ No newline at end of file +Maintenant qu'on a une bonne idée de l'architecture serverless, regardons de plus près ce qu'est une fonction Lambda et comment notre code va être exécuté. diff --git a/_chapters/fr/who-is-this-guide-for.md b/_chapters/fr/who-is-this-guide-for.md index 37b36c7bf1..190cbe21c9 100644 --- a/_chapters/fr/who-is-this-guide-for.md +++ b/_chapters/fr/who-is-this-guide-for.md @@ -1,6 +1,6 @@ --- layout: post -title: Pour qui est ce guide? +title: Pour qui est ce guide? date: 2016-12-21 00:00:00 lang: fr ref: who-is-this-guide-for @@ -8,12 +8,12 @@ context: true comments_id: who-is-this-guide-for/96 --- -Ce guide est à destination des développeurs full-stack, ou des développeurs qui souhaitent construire une application full-stack. En fournissant un guide étape par étape pour le frontend et le backend, nous espérons couvrir tous les aspects du développement d'une application serverless. Il y a un certain nombre de tutoriels existants sur le we mais nous pensons qu'il serait utile d'avoir un point d'entrée unique pour le processus entier. Ce guide est construit pour servir de ressource pour apprendre comment construire et déployer des applications serverless, pas nécessairement le seul ni le meilleur moyen possible. +Ce guide est à destination des développeurs full-stack, ou des développeurs qui souhaitent construire une application full-stack. En fournissant un guide étape par étape pour le frontend et le backend, nous espérons couvrir tous les aspects du développement d'une application serverless. Il y a un certain nombre de tutoriels existants sur le web mais nous pensons qu'il serait utile d'avoir un point d'entrée unique pour le processus entier. Ce guide est construit pour servir de ressource pour apprendre comment construire et déployer des applications serverless, pas nécessairement le seul ni le meilleur moyen possible. En tant que développeur backend qui souhaite en apprendre plus sur la partie frontend du développement d'une application serverless ou en tant que développeur backend qui souhaite en apprendre plus à propos du backend; ce guide couvre vos besoins. Nous couvrons uniquement les besoins des développeurs JavaScript pour le moment. Nous ciblerons peut-être d'autres langages et environnements dans le future. Mais nous pensons que c'est un bon point de départ, car cela peut être réellement bénéfique d'utiiser un seul langage (JavaScript) et environnement (Node.js) pour construire votre application complète. -D'un point de vue personnel, l'approche serverless a été un énorme révélation pour nous et nous souhaitions créer une ressource pour partager ce que nous avons appris. Vous pouvez en lire plus sur nous [**ici**]({% link about.md %}). Et [voir un échantillon de ce qui nous avons fait avec Serverless Stack]({% link showcase.md %}). +D'un point de vue personnel, l'approche serverless a été un énorme révélation pour nous et nous souhaitions créer une ressource pour partager ce que nous avons appris. Vous pouvez en lire plus sur nous [**ici**]({{ site.sst_url }}). Et [voir un échantillon de ce qui nous avons fait avec SST]({% link showcase.md %}). Commençons par regarder les aspects que nous aborderons. diff --git a/_chapters/further-reading.md b/_chapters/further-reading.md index d12e424e92..5e8e222d46 100644 --- a/_chapters/further-reading.md +++ b/_chapters/further-reading.md @@ -8,34 +8,24 @@ ref: further-reading comments_id: further-reading/209 --- -Once you've completed the guide, you are probably going to use the Serverless Stack for your next project. To help you along the way we try to compile a list of docs that you can use as a reference. The following can be used to drill down in detail for some of the technologies and services used in this guide. +Once you've completed the guide, you are probably going to use SST for your next project. To help you along the way we try to compile a list of docs that you can use as a reference. The following can be used to drill down in detail for some of the technologies and services used in this guide. -- [Serverless Framework Documentation](https://serverless.com/framework/docs/): Documentation for the Serverless Framework +- [SST Documentation]({{ site.sst_url }}/docs/): Documentation for SST - [DynamoDB, explained](https://www.dynamodbguide.com): A Primer on the DynamoDB NoSQL database -- [React JS Docs](https://reactjs.org/docs/hello-world.html): The official React docs +- [Learn React](https://react.dev/learn): The official React docs -- [JSX In Depth](https://reactjs.org/docs/jsx-in-depth.html): Learn JSX in a bit more detail +- [Vite docs](https://vitejs.dev/guide/): The official Vite docs -- [Create React App User Guide](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md): The really comprehensive Create React App user guide +- [React-Bootstrap Docs](https://react-bootstrap.github.io/docs/getting-started/introduction): The official React-Bootstrap docs -- [React-Bootstrap Docs](https://react-bootstrap.github.io/getting-started/introduction): The official React-Bootstrap docs - -- [Bootstrap v3 Docs](http://getbootstrap.com/docs/3.3/getting-started/): The Bootstrap v3 docs that React-Bootstrap is based on - -- [React Router Docs](https://reacttraining.com/react-router/web/guides/philosophy): The official React Router v4 docs - -- [AWS Amplify Developer Guide](https://aws.github.io/aws-amplify/media/developer_guide): The AWS Amplify developer guide +- [Learn React Router](https://reactrouter.com/en/6.15.0/start/tutorial): The official React Router docs - [AWS Amplify API Reference](https://aws.github.io/aws-amplify/api/): The AWS Amplify API reference -- [AWS CloudFormation Docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/GettingStarted.Walkthrough.html): The AWS user guide for CloudFormation - -- [Jest Unit Test Docs](https://facebook.github.io/jest/docs/en/getting-started.html): The official Jest docs - -- [Seed Docs](https://seed.run/docs/): The official Seed docs +- [Vitest Docs](https://vitest.dev/guide/): The official Vitest docs -- [Netlify Docs](https://www.netlify.com/docs/): The official Netlify docs +- [SST Console Docs]({{ site.sst_url }}/docs/console/): The SST Console docs If you have found any other guides or tutorials helpful in building your serverless app, feel free to edit this page and submit a PR. Or you can let us know via the comments. diff --git a/_chapters/getting-production-ready.md b/_chapters/getting-production-ready.md index 7ded1b023b..6be3f53226 100644 --- a/_chapters/getting-production-ready.md +++ b/_chapters/getting-production-ready.md @@ -1,41 +1,72 @@ --- layout: post title: Getting Production Ready -date: 2018-02-23 00:00:00 +date: 2021-08-24 00:00:00 lang: en -description: To get our serverless app production ready, we are going to have to configure it using Infrastructure as Code. We are also going to need to configure separate environments for dev/production and automate our deployments. +description: To get our full-stack serverless app ready for production, we want to use a custom domain and to automate our deployments. ref: getting-production-ready comments_id: getting-production-ready/158 --- -Now that we've gone through the basics of creating a Serverless Stack, you are probably wondering if you need to do all these manual steps everytime you create a new project. Plenty of our readers have used this stack for their personal and professional projects. So in Part II we are going to address some of the common issues that they run into. Specifically, we will go over the following: +Now that we've gone through the basics of creating a full-stack serverless app, you are ready to deploy it to production. -- **Infrastructure as code** +### Deploy to Prod - Currently, you go through a bunch of manual steps with a lot of clicking around to configure your backend. This makes it pretty tricky to re-create this stack for a new project. Or to configure a new environment for the same project. Serverless Framework is really good for converting this entire stack into code. This means that it can automatically re-create the entire project from scratch without ever touching the AWS Console. +We are now going to deploy our app to prod. You can go ahead and stop the local development environments for SST and React. -- **Working with 3rd party APIs** +{%change%} Run the following **in your project root**. - A lot of our readers are curious about how to use serverless with 3rd party APIs. We will go over how to connect to the Stripe API and accept credit card payments. +```bash +$ npx sst deploy --stage production +``` -- **Unit tests** +This command will take a few minutes as it'll deploy your app to a completely new environment. Recall that we are deploying to a separate prod environment because we don't want to affect our users while we are actively developing our app. This ensures that we have a separate local dev environment and a separate prod environment. - We will also look at how to configure unit tests for our backend using [Jest](https://facebook.github.io/jest/). +{%info%} +The `production` name that we are using is arbitrary. We can call it `prod` or `live`. SST just uses the string internally to create a new version of your app. +{%endinfo%} -- **Automating deployments** +At the end of the deploy process you should see something like this. - In the current tutorial you need to deploy through your command line using the `serverless deploy` command. This can be a bit tricky when you have a team working on your project. To start with, we'll add our frontend and backend projects to GitHub. We'll then go over how to automate your deployments using [Seed](https://seed.run) (for the backend) and [Netlify](https://netlify.com) (for the frontend). +```bash ++ Complete + Api: https://7qdwu0iuga.execute-api.us-east-1.amazonaws.com + Frontend: https://d1wyq16hczgtjw.cloudfront.net + ... +``` -- **Configuring environments** +### Set Secrets in Prod - Typically while working on projects you end up creating multiple environments. For example, you'd want to make sure not to make changes directly to your app while it is in use. Thanks to the Serverless Framework and Seed we'll be able to do this with ease for the backend. And we'll do something similar for our frontend using React and Netlify. We'll also configure custom domains for our backend API environments. +We also need to configure out secrets for production. You'll recall we had previously [configured secrets for our local stage]({% link _chapters/handling-secrets-in-sst.md %}). -- **Working with secrets** +We'll do the same here but for `production`. - We will look at how to work with secret environment variables in our local environment and in production. +{%change%} Run the following **in your project root**. -The goal of Part II is to ensure that you have a setup that you can easily replicate and use for your future projects. This is almost exactly what we and a few of our readers have been using. +```bash +$ npx sst secret set --stage production StripeSecretKey +``` -This part of the guide is fairly standalone but it does rely on the original setup. If you haven't completed Part I; you can quickly browse through some of the chapters but you don't necessarily need to redo them all. We'll start by forking the code from the original setup and then building on it. +{%note%} +For this guide we are using the same Stripe secret key in production but you'll likely be using different values in production. +{%endnote%} -Let's get started by first converting our backend infrastructure into code. +You can run `npx sst secret list --stage production` to see the secrets for prod. + +Our full-stack serverless app is almost ready to go. You can play around with the prod version. + +### Custom Domains + +However the API is currently on an endpoint that's auto-generated by [API Gateway](https://aws.amazon.com/api-gateway/). + +``` txt +https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +And the frontend React app is hosted on an auto-generated [CloudFront](https://aws.amazon.com/cloudfront/) domain. + +``` txt +https://d3j4c16hczgtjw.cloudfront.net +``` + +We want to host these on our own domain. Let's look at that next. diff --git a/_chapters/give-feedback-while-logging-in.md b/_chapters/give-feedback-while-logging-in.md index efc3166767..e9f6924581 100644 --- a/_chapters/give-feedback-while-logging-in.md +++ b/_chapters/give-feedback-while-logging-in.md @@ -4,8 +4,7 @@ title: Give Feedback While Logging In date: 2017-01-18 00:00:00 lang: en ref: give-feedback-while-logging-in -description: We should give users some feedback while we are logging them in to our React.js app. To do so we are going to create a component that animates a Glyphicon refresh icon inside a React-Bootstrap Button component. We’ll do the animation while the log in call is in progress. -context: true +description: We should give users some feedback while we are logging them in to our React.js app. To do so we are going to create a component that animates a Glyphicon refresh icon inside a React-Bootstrap Button component. We’ll do the animation while the log in call is in progress. We'll also add some basic error handling to our app. comments_id: give-feedback-while-logging-in/46 --- @@ -13,31 +12,31 @@ It's important that we give the user some feedback while we are logging them in. ### Use an isLoading Flag -To do this we are going to add an `isLoading` flag to the state of our `src/containers/Login.js`. So the initial state in the `constructor` looks like the following. +{%change%} To do this we are going to add an `isLoading` flag to the state of our `src/containers/Login.tsx`. Add the following to the top of our `Login` function component. -``` javascript -this.state = { - isLoading: false, - email: "", - password: "" -}; +```tsx +const [isLoading, setIsLoading] = useState(false); ``` -And we'll update it while we are logging in. So our `handleSubmit` method now looks like so: +{%change%} And we'll update it while we are logging in. So our `handleSubmit` function now looks like so: -``` javascript -handleSubmit = async event => { +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); - this.setState({ isLoading: true }); + setIsLoading(true); try { - await Auth.signIn(this.state.email, this.state.password); - this.props.userHasAuthenticated(true); - this.props.history.push("/"); - } catch (e) { - alert(e.message); - this.setState({ isLoading: false }); + await Auth.signIn(email, password); + userHasAuthenticated(true); + nav("/"); + } catch (error) { + if (error instanceof Error) { + alert(error.message); + } else { + alert(String(error)); + } + setIsLoading(false); } } ``` @@ -46,93 +45,174 @@ handleSubmit = async event => { Now to reflect the state change in our button we are going to render it differently based on the `isLoading` flag. But we are going to need this piece of code in a lot of different places. So it makes sense that we create a reusable component out of it. -Create a new file and add the following in `src/components/LoaderButton.js`. +{%change%} Create a `src/components/` directory by running this command in the `frontend/` directory. -``` coffee -import React from "react"; -import { Button, Glyphicon } from "react-bootstrap"; +```bash +$ mkdir src/components/ +``` + +Here we'll be storing all our React components that are not dealing directly with our API or responding to routes. + +{%change%} Create a new file and add the following in `src/components/LoaderButton.tsx`. + +```tsx +import Button from "react-bootstrap/Button"; +import { BsArrowRepeat } from "react-icons/bs"; import "./LoaderButton.css"; -export default ({ - isLoading, - text, - loadingText, +export default function LoaderButton({ className = "", disabled = false, + isLoading = false, ...props -}) => - ; +}) { + return ( + + ); +} ``` -This is a really simple component that takes an `isLoading` flag and the text that the button displays in the two states (the default state and the loading state). The `disabled` prop is a result of what we have currently in our `Login` button. And we ensure that the button is disabled when `isLoading` is `true`. This makes it so that the user can't click it while we are in the process of logging them in. +This is a really simple component that takes an `isLoading` prop and `disabled` prop. The latter is a result of what we have currently in our `Login` button. And we ensure that the button is disabled when `isLoading` is `true`. This makes it so that the user can't click it while we are in the process of logging them in. + +The `className` prop that we have is to ensure that a CSS class that's set for this component, doesn't override the `LoaderButton` CSS class that we are using internally. + +When the `isLoading` flag is on, we show an icon. The icon we include is from the Bootstrap icon set of [React Icons](https://react-icons.github.io/icons?name=bs){:target="_blank"}. And let's add a couple of styles to animate our loading icon. -Add the following to `src/components/LoaderButton.css`. +{%change%} Add the following to `src/components/LoaderButton.css`. -``` css -.LoaderButton .spinning.glyphicon { +```css +.LoaderButton { + margin-top: 12px; +} + +.LoaderButton .spinning { margin-right: 7px; - top: 2px; + margin-bottom: 1px; animation: spin 1s infinite linear; } + @keyframes spin { - from { transform: scale(1) rotate(0deg); } - to { transform: scale(1) rotate(360deg); } + from { + transform: scale(1) rotate(0deg); + } + to { + transform: scale(1) rotate(360deg); + } } ``` -This spins the refresh Glyphicon infinitely with each spin taking a second. And by adding these styles as a part of the `LoaderButton` we keep them self contained within the component. +This spins the icon infinitely with each spin taking a second. And by adding these styles as a part of the `LoaderButton` we keep them self contained within the component. ### Render Using the isLoading Flag Now we can use our new component in our `Login` container. -In `src/containers/Login.js` find the ` ``` -And replace it with this. +{%change%} And replace it with this. -``` html +```html + isLoading={isLoading} + disabled={!validateForm()} +> + Login + +``` + +{%change%} Also, let's replace `Button` import in the header. Remove this. + +```tsx +import Button from "react-bootstrap/Button"; ``` -Also, import the `LoaderButton` in the header. And remove the reference to the `Button` component. +{%change%} And add the following. -``` javascript -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; -import LoaderButton from "../components/LoaderButton"; +```tsx +import LoaderButton from "../components/LoaderButton.tsx"; ``` And now when we switch over to the browser and try logging in, you should see the intermediate state before the login completes. ![Login loading state screenshot](/assets/login-loading-state.png) -If you would like to add _Forgot Password_ functionality for your users, you can refer to our [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}). +### Handling Errors + +You might have noticed in our Login and App components that we simply `alert` when there is an error. We are going to keep our error handling simple. But it'll help us further down the line if we handle all of our errors in one place. + +{%change%} To do that, create `src/lib/errorLib.ts` and add the following. + +```typescript +export function onError(error: any) { + let message = String(error); + + if (!(error instanceof Error) && error.message) { + message = String(error.message); + } + + alert(message); +} +``` + +The `Auth` package throws errors in a different format, so all this code does is `alert` the error message we need. And in all other cases simply `alert` the error object itself. + +Let's use this in our Login container (containers/Login.tsx). + +{%change%} Replace the `catch` statement in the `handleSubmit` function with: + +```tsx +catch (error) { + onError(error); + setIsLoading(false); +} +``` + +{%change%} And import the new error lib in the header of `src/containers/Login.tsx`. + +```tsx +import { onError } from "../lib/errorLib"; +``` + +We'll do something similar in the App component. + +{%change%} Replace the `catch` statement in the `onLoad` function with: + +```tsx +catch (error) { + if (error !== "No current user") { + onError(error); + } +} +``` + +{%change%} And import the error lib in the header of `src/App.tsx`. + +```tsx +import { onError } from "./lib/errorLib"; +``` + + +We'll improve our error handling a little later on in the guide. + +{%info%} +If you would like to add a _Forgot Password_ feature for your users, you can refer to our [Extra Credit series of chapters on user management]({% link _archives/manage-user-accounts-in-aws-amplify.md %}){:target="_blank"}. +{%endinfo%} -Next let's implement the sign up process for our app. +For now, we are ready to move on to the sign up process for our app. diff --git a/_chapters/giving-back.md b/_chapters/giving-back.md index 83153e7d6c..69639a5917 100644 --- a/_chapters/giving-back.md +++ b/_chapters/giving-back.md @@ -2,7 +2,7 @@ layout: post title: Giving Back lang: en -description: Contributing to Serverless Stack by supporting us on GitHub and helping keep the content up to date. +description: Contributing to SST by supporting us on GitHub and helping keep the content up to date. date: 2018-03-31 00:00:00 ref: giving-back comments_id: comments-for-giving-back/193 @@ -20,23 +20,23 @@ If you've found this guide helpful please consider helping us out by doing the f - **Keep the core guide updated** - Serverless Stack is reliant on a large number of services and open source libraries and projects. The screenshots for the services and the dependencies need to be updated every once in a while. [Here is a little more details on this]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#keep-the-core-guide-updated). + SST is reliant on a large number of services and open source libraries and projects. The screenshots for the services and the dependencies need to be updated every once in a while. [Here is a little more details on this]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#keep-the-core-guide-updated). - **Help translate the guide** - Our incredible readers are helping translate Serverless Stack into multiple languages. You can check out [our progress here]({% link _chapters/translations.md %}). If you would like to help with our translation efforts, [leave us a comment here](https://discourse.serverless-stack.com/t/help-us-translate-serverless-stack/596/15). + Our incredible readers are helping translate SST into multiple languages. You can check out [our progress here]({% link _chapters/translations.md %}). If you would like to help with our translation efforts, [leave us a comment here](https://discourse.sst.dev/t/help-us-translate-serverless-stack/596/15). - **Add an extra credit chapter** - The core chapters are missing some extra details (for the sake of simplicity) that are necessary once you start customizing the Serverless Stack setup. Additionally, there are cases that we just don't handle in the core part of the guide. We are addressing these via *Extra Credit chapters*. If you have had a chance to extend Serverless Stack consider writing a chapter on it. [Here are further details on how to add an extra credit chapter]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#add-an-extra-credit-chapter). + The core chapters are missing some extra details (for the sake of simplicity) that are necessary once you start customizing SST setup. Additionally, there are cases that we just don't handle in the core part of the guide. We are addressing these via *Extra Credit chapters*. If you have had a chance to extend SST consider writing a chapter on it. [Here are further details on how to add an extra credit chapter]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#add-an-extra-credit-chapter). - **Improve tooling** Currently we do a lot of manual work to publish updates and maintain the tutorial. You can help by contributing to improve the process. [Here are some more details on what we need help with]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#improve-tooling). -- [**Give us a Star on GitHub**]({{ site.github_repo }}) +- [**Give us a Star on GitHub**]({{ site.sst_github_repo }}) - We rely on our GitHub repo for everything from hosting this site to code samples and comments. [Starring our repo]({{ site.github_repo }}) helps us get the word out. + We rely on our GitHub repo for everything from hosting this site to code samples and comments. - **Sharing this guide** diff --git a/_chapters/handle-404s.md b/_chapters/handle-404s.md index f4dc0216a6..94f4810e6f 100644 --- a/_chapters/handle-404s.md +++ b/_chapters/handle-404s.md @@ -4,37 +4,37 @@ title: Handle 404s date: 2017-01-12 00:00:00 lang: en ref: handle-404s -description: To handle 404s in a React.js app with React Router v4 we need to set up a catch all Route at the bottom of our Switch block. A catch all Route does not have a path prop and responds to all routes. -context: true +description: To handle 404s in a React.js app with React Router v6 we need to set up a catch all Route at the bottom of our Routes block. A catch all Route does not have a path prop and responds to all routes. comments_id: handle-404s/75 --- -Now that we know how to handle the basic routes; let's look at handling 404s with the React Router. +Now that we know how to handle the basic routes; let's look at handling 404s with the React Router. These are cases when a user goes to a URL that we are not explicitly handling. We want to show a helpful sign to our users when this happens. ### Create a Component Let's start by creating a component that will handle this for us. -Create a new component at `src/containers/NotFound.js` and add the following. +{%change%} Create a new component at `src/containers/NotFound.tsx` and add the following. -``` coffee -import React from "react"; +```tsx import "./NotFound.css"; -export default () => -
-

Sorry, page not found!

-
; +export default function NotFound() { + return ( +
+

Sorry, page not found!

+
+ ); +} ``` All this component does is print out a simple message for us. -Let's add a couple of styles for it in `src/containers/NotFound.css`. +{%change%} Let's add a couple of styles for it in `src/containers/NotFound.css`. -``` css +```css .NotFound { padding-top: 100px; - text-align: center; } ``` @@ -42,23 +42,23 @@ All this component does is print out a simple message for us. Now we just need to add this component to our routes to handle our 404s. -Find the `` block in `src/Routes.js` and add it as the last line in that section. +{%change%} Find the `` block in `src/Routes.tsx` and add it as the last line in that section. -``` coffee -{ /* Finally, catch all unmatched routes */ } - +```tsx +{/* Finally, catch all unmatched routes */} +} />; ``` -This needs to always be the last line in the `` block. You can think of it as the route that handles requests in case all the other routes before it have failed. +This needs to always be the last route in the `` block. You can think of it as the route that handles requests in case all the other routes before it have failed. -And include the `NotFound` component in the header by adding the following: +{%change%} And include the `NotFound` component in the header by adding the following: -``` javascript -import NotFound from "./containers/NotFound"; +```js +import NotFound from "./containers/NotFound.tsx"; ``` And that's it! Now if you were to switch over to your browser and try clicking on the Login or Signup buttons in the Nav you should see the 404 message that we have. ![Router 404 page screenshot](/assets/router-404-page.png) -Next up, we are going to configure our app with the info of our backend resources. +Next up, we are going to allow our users to login and sign up for our app! diff --git a/_chapters/handle-cors-in-s3-for-file-uploads.md b/_chapters/handle-cors-in-s3-for-file-uploads.md new file mode 100644 index 0000000000..82a6481e18 --- /dev/null +++ b/_chapters/handle-cors-in-s3-for-file-uploads.md @@ -0,0 +1,42 @@ +--- +layout: post +title: Handle CORS in S3 for File Uploads +date: 2021-08-17 00:00:00 +lang: en +ref: handle-cors-in-s3-for-file-uploads +description: In this chapter we'll look at how to configure CORS for an S3 bucket in our serverless app. We'll be adding these settings in our SST Bucket component. +comments_id: handle-cors-in-s3-for-file-uploads/2174 +--- + +In the notes app we are building, users will be uploading files to the bucket we just created. And since our app will be served through our custom domain, it'll be communicating across domains while it does the uploads. By default, S3 does not allow its resources to be accessed from a different domain. However, [cross-origin resource sharing (CORS)](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing){:target="_blank"} defines a way for client web applications that are loaded in one domain to interact with resources in a different domain. + +Similar to the [previous chapter]({% link _chapters/handle-cors-in-serverless-apis.md %}), the [`Bucket`]({{ site.sst_url }}/docs/component/aws/bucket/){:target="_blank"} component enables CORS by default. + +```ts +new sst.aws.Bucket("Uploads", { + // Enabled by default + cors: true, +}); +``` + +You can configure this further. [Read more about this here]({{ site.sst_url }}/docs/component/aws/bucket#cors){:target="_blank"}. + +```ts +new sst.aws.Bucket("Uploads", { + cors: { + allowMethods: ["GET"] + } +}); +``` + +### Commit the Changes + +{%change%} Let's commit our changes and push it to GitHub. + +```bash +$ git add . +$ git commit -m "Enabling CORS" +$ git push +``` + +Now we are ready to use our serverless backend to create our frontend React app! diff --git a/_chapters/handle-cors-in-serverless-apis.md b/_chapters/handle-cors-in-serverless-apis.md new file mode 100644 index 0000000000..d10001fbed --- /dev/null +++ b/_chapters/handle-cors-in-serverless-apis.md @@ -0,0 +1,100 @@ +--- +layout: post +title: Handle CORS in Serverless APIs +date: 2021-08-17 00:00:00 +lang: en +ref: handle-cors-in-serverless-apis +description: In this chapter we'll look at how to configure CORS in our serverless API. We'll be adding these settings in our SST ApiGatewayV2 component and in our Lambda function responses. +comments_id: handle-cors-in-serverless-apis/2175 +--- + +Let's take stock of our setup so far. We have a serverless API backend that allows users to create notes and an S3 bucket where they can upload files. We are now almost ready to work on our frontend React app. + +However, before we can do that. There is one thing that needs to be taken care of — [CORS or Cross-Origin Resource Sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing){:target="_blank"}. + +Since our React app is going to be run inside a browser (and most likely hosted on a domain separate from our serverless API and S3 bucket), we need to configure CORS to allow it to connect to our resources. + +Let's quickly review our backend app architecture. + +![Serverless Auth API architecture](/assets/diagrams/serverless-auth-api-architecture.png) + +Our client will be interacting with our API, S3 bucket, and User Pool. CORS in the User Pool part is taken care of by its internals. That leaves our API and S3 bucket. In the next couple of chapters we'll be setting that up. + +Let's get a quick background on CORS. + +### Understanding CORS + +There are two things we need to do to support CORS in our serverless API. + +1. Preflight OPTIONS requests + + For certain types of cross-domain requests (PUT, DELETE, ones with Authentication headers, etc.), your browser will first make a _preflight_ request using the request method OPTIONS. These need to respond with the domains that are allowed to access this API and the HTTP methods that are allowed. + +2. Respond with CORS headers + + For all the other types of requests we need to make sure to include the appropriate CORS headers. These headers, just like the one above, need to include the domains that are allowed. + +There's a bit more to CORS than what we have covered here. So make sure to [check out the Wikipedia article for further details](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing){:target="_blank"}. + +If we don't set the above up, then we'll see something like this in our HTTP responses. + +```text +No 'Access-Control-Allow-Origin' header is present on the requested resource +``` + +And our browser won't show us the HTTP response. This can make debugging our API extremely hard. + +### CORS in API Gateway + +The [`ApiGatewayV2`]({{ site.sst_url }}/docs/component/aws/apigatewayv2/){:target="_blank"} component that we are using enables CORS by default. + +```ts +new sst.aws.ApiGatewayV2("Api", { + // Enabled by default + cors: true +}); +``` + +You can further configure the specifics if necessary. You can [read more about this here]({{ site.sst_url }}/docs/component/aws/apigatewayv2#cors){:target="_blank"}. + +```typescript +new sst.aws.ApiGatewayV2("Api", { + cors: { + allowMethods: ["GET"] + } +}); +``` + +We'll go with the default setting for now. + +### CORS Headers in Lambda Functions + +Next, we need to add the CORS headers in our Lambda function response. + +{%change%} Replace the `return` statement in our `packages/core/src/util/index.ts`. + +```ts +return { + body, + statusCode, +}; +``` + +{%change%} With the following. + +```ts +return { + body, + statusCode, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, +}; +``` + +Again you can customize the CORS headers but we'll go with the default ones here. + +The two steps we've taken above ensure that if our Lambda functions are invoked through API Gateway, it'll respond with the proper CORS config. + +Next, let’s add these CORS settings to our S3 bucket as well. Since our frontend React app will be uploading files directly to it. diff --git a/_chapters/handle-forgot-and-reset-password.md b/_chapters/handle-forgot-and-reset-password.md deleted file mode 100644 index 94c2cba4fe..0000000000 --- a/_chapters/handle-forgot-and-reset-password.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -layout: post -title: Handle Forgot and Reset Password -description: Use the AWS Amplify Auth.forgotPassword method to support forgot password functionality in our Serverless React app. This triggers Cognito to help our users reset their password. -date: 2018-04-14 00:00:00 -context: true -code: user-management -comments_id: handle-forgot-and-reset-password/506 ---- - -In our [Serverless notes app](https://demo.serverless-stack.com) we've used [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) to sign up and login our users. In the frontend we've used [AWS Amplify](https://aws-amplify.github.io/) in our React app. However, if our users have forgotten their passwords, we need to have a way for them to reset their password. In this chapter we will look at how to do this. - -The version of the notes app used in this chapter is hosted in a: - -- Separate GitHub repository: [**{{ site.frontend_user_mgmt_github_repo }}**]({{ site.frontend_user_mgmt_github_repo }}) -- And can be accessed through: [**https://demo-user-mgmt.serverless-stack.com**](https://demo-user-mgmt.serverless-stack.com) - -Let's look at the main changes we need to make to allow users to reset their password. - -### Add a Reset Password Form - -We are going to create a `src/containers/ResetPassword.js`. - -``` coffee -import React, { Component } from "react"; -import { Auth } from "aws-amplify"; -import { Link } from "react-router-dom"; -import { - HelpBlock, - FormGroup, - Glyphicon, - FormControl, - ControlLabel -} from "react-bootstrap"; -import LoaderButton from "../components/LoaderButton"; -import "./ResetPassword.css"; - -export default class ResetPassword extends Component { - constructor(props) { - super(props); - - this.state = { - code: "", - email: "", - password: "", - codeSent: false, - confirmed: false, - confirmPassword: "", - isConfirming: false, - isSendingCode: false - }; - } - - validateCodeForm() { - return this.state.email.length > 0; - } - - validateResetForm() { - return ( - this.state.code.length > 0 && - this.state.password.length > 0 && - this.state.password === this.state.confirmPassword - ); - } - - handleChange = event => { - this.setState({ - [event.target.id]: event.target.value - }); - }; - - handleSendCodeClick = async event => { - event.preventDefault(); - - this.setState({ isSendingCode: true }); - - try { - await Auth.forgotPassword(this.state.email); - this.setState({ codeSent: true }); - } catch (e) { - alert(e.message); - this.setState({ isSendingCode: false }); - } - }; - - handleConfirmClick = async event => { - event.preventDefault(); - - this.setState({ isConfirming: true }); - - try { - await Auth.forgotPasswordSubmit( - this.state.email, - this.state.code, - this.state.password - ); - this.setState({ confirmed: true }); - } catch (e) { - alert(e.message); - this.setState({ isConfirming: false }); - } - }; - - renderRequestCodeForm() { - return ( -
- - Email - - - - - ); - } - - renderConfirmationForm() { - return ( -
- - Confirmation Code - - - Please check your email ({this.state.email}) for the confirmation - code. - - -
- - New Password - - - - Confirm Password - - - - - ); - } - - renderSuccessMessage() { - return ( -
- -

Your password has been reset.

-

- - Click here to login with your new credentials. - -

-
- ); - } - - render() { - return ( -
- {!this.state.codeSent - ? this.renderRequestCodeForm() - : !this.state.confirmed - ? this.renderConfirmationForm() - : this.renderSuccessMessage()} -
- ); - } -} -``` - -Let's quickly go over the flow here: - -- We ask the user to put in the email address for their account in the `this.renderRequestCodeForm()`. -- Once the user submits this form, we start the process by calling `Auth.forgotPassword(this.state.email)`. Where `Auth` is a part of the AWS Amplify library. -- This triggers Cognito to send a verification code to the specified email address. -- Then we present a form where the user can input the code that Cognito sends them. This form is rendered in `this.renderConfirmationForm()`. And it also allows the user to put in their new password. -- Once they submit this form with the code and their new password, we call `Auth.forgotPasswordSubmit(this.state.email, this.state.code, this.state.password)`. This resets the password for the account. -- Finally, we show the user a sign telling them that their password has been successfully reset. We also link them to the login page where they can login using their new details. - -Let's also add a couple of styles. - -Add the following to `src/containers/ResetPassword.css`. - -``` css -@media all and (min-width: 480px) { - .ResetPassword { - padding: 60px 0; - } - - .ResetPassword form { - margin: 0 auto; - max-width: 320px; - } - - .ResetPassword .success { - max-width: 400px; - } -} - -.ResetPassword .success { - margin: 0 auto; - text-align: center; -} -.ResetPassword .success .glyphicon { - color: grey; - font-size: 30px; - margin-bottom: 30px; -} -``` - -### Add the Route - -Finally, let's link this up with the rest of our app. - -Add the route to `src/Routes.js`. - -``` html - -``` - -And import it in the header. - -``` coffee -import ResetPassword from "./containers/ResetPassword"; -``` - -### Link from the Login Page - -Now we want to make sure that our users are directed to this page when they are trying to login. - -So let's add a link in our `src/containers/Login.js`. Add it above our login button. - -``` coffee -Forgot password? -``` - -And import the `Link` component in the header. - -``` coffee -import { Link } from "react-router-dom"; -``` - -That's it! We should now be able to navigate to `/login/reset` or go to it from the login page in case we need to reset our password. - -![Login page forgot password link screenshot](/assets/user-management/login-page-forgot-password-link.png) - -And from there they can put in their email to reset their password. - -![Forgot password page screenshot](/assets/user-management/forgot-password-page.png) - -Next, let's look at how our logged in users can change their password. diff --git a/_chapters/handle-routes-with-react-router.md b/_chapters/handle-routes-with-react-router.md index 9f17249443..79e7443278 100644 --- a/_chapters/handle-routes-with-react-router.md +++ b/_chapters/handle-routes-with-react-router.md @@ -4,50 +4,51 @@ title: Handle Routes with React Router date: 2017-01-10 00:00:00 lang: en ref: handle-routes-with-react-router -description: Create React App does not ship with a way to set up routes in your app. To do so, we are going to use React Router. The latest version of React Router, React Router v4 embraces the composable nature of React’s components and makes it easy to work with routes in our single page app. -context: true +description: To handle routes in our React app we are going to use React Router. comments_id: handle-routes-with-react-router/116 --- -Create React App sets a lot of things up by default but it does not come with a built-in way to handle routes. And since we are building a single page app, we are going to use [React Router](https://reacttraining.com/react-router/) to handle them for us. +Since we are building a single page app, we are going to use [React Router](https://reactrouter.com/en/main) to handle the routes on the client side for us. +React Router allows us to specify a route like: `/login`. And specify a React Component that should be loaded when a user goes to that page. -Let's start by installing React Router. We are going to be using the React Router v4, the newest version of React Router. React Router v4 can be used on the web and in native. So let's install the one for the web. +Let's start by installing React Router. -### Installing React Router v4 +### Installing React Router -Run the following command in your working directory. +{%change%} Run the following command **in the `packages/frontend/` directory**. -``` bash -$ npm install react-router-dom@4.3.1 --save +```bash +$ npm install react-router-dom ``` -This installs the NPM package and adds the dependency to your `package.json`. +This installs the package and adds the dependency to `package.json` in your React app. ### Setting up React Router -Even though we don't have any routes set up in our app, we can get the basic structure up and running. Our app currently runs from the `App` component in `src/App.js`. We are going to be using this component as the container for our entire app. To do that we'll encapsulate our `App` component within a `Router`. +Even though we don't have any routes set up in our app, we can get the basic structure up and running. Our app currently runs from the `App` component in `src/App.tsx`. We are going to be using this component as the container for our entire app. To do that we'll encapsulate our `App` component within a `Router`. -Replace the following code in `src/index.js`: +{%change%} Replace the following in `src/main.tsx`: -``` coffee -ReactDOM.render(, document.getElementById('root')); +```tsx + + + ``` -With this: +{%change%} With this: -``` coffee -ReactDOM.render( +```tsx + - , - document.getElementById("root") -); + + ``` -And import this in the header of `src/index.js`. +{%change%} And import this in the header of `src/main.tsx`. -``` coffee +```tsx import { BrowserRouter as Router } from "react-router-dom"; ``` diff --git a/_chapters/handling-secrets-in-sst.md b/_chapters/handling-secrets-in-sst.md new file mode 100644 index 0000000000..5c0d08cc4c --- /dev/null +++ b/_chapters/handling-secrets-in-sst.md @@ -0,0 +1,62 @@ +--- +layout: post +title: Handling Secrets in SST +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we'll look at how to work with secrets in an SST app. We store secrets using the sst secret CLI and link it to our API. +ref: handling-secrets-in-sst +comments_id: handling-secrets-in-sst/2465 +--- + +In the [previous chapter]({% link _chapters/setup-a-stripe-account.md %}), we created a Stripe account and got a pair of keys. Including the Stripe secret key. We need this in our app but we do not want to store this secret in our code. In this chapter, we'll look at how to add secrets in SST. + +We will be using the [`sst secret`]({{ site.sst_url }}/docs/reference/cli/#secret){:target="_blank"} CLI to store our secrets. + +{%change%} Run the following in your project root. + +```bash +$ npx sst secret set StripeSecretKey +``` + +{%note%} +You can specify the stage for a secret. By default, the stage is your personal stage. +{%endnote%} + +You can run `npx sst secret list` to see the secrets for the current stage. + +Now that the secret is stored, we can add it into our config using the [`Secret`]({{ site.sst_url }}/docs/component/secret/){:target="_blank"} component. + +{%change%} Add the following to your `infra/storage.ts`: + +```ts +// Create a secret for Stripe +export const secret = new sst.Secret("StripeSecretKey"); +``` + +{%change%} Import `secret` in `infra/api.ts`. Replace the following. + +```typescript +import { table } from "./storage"; +``` + +{%change%} With: + +```typescript +import { table, secret } from "./storage"; +``` + +{%change%} Next, link `StripeSecretKey` to the API in `infra/api.ts`. Replace this: + +```ts +link: [table], +``` + +{%change%} With: + +```ts +link: [table, secret], +``` + +This will add `StripeSecretKey` in our infrastructure. And allow our API to access the secret. + +Now we are ready to add an API to handle billing. diff --git a/_chapters/how-to-get-help.md b/_chapters/how-to-get-help.md index 4fd29f4de5..165b84398c 100644 --- a/_chapters/how-to-get-help.md +++ b/_chapters/how-to-get-help.md @@ -9,12 +9,15 @@ comments_id: how-to-get-help/95 In case you find yourself having problems with a certain step, we want to make sure that we are around to help you fix it and figure it out. There are a few ways to get help. -- We use [Discourse forum topics]({{ site.forum_url }}) as our comments and we've helped resolve quite a few issues in the past. So make sure to check the comments under each chapter to see if somebody else has run into the same issue as you have. -- Post in the comments for the specific chapter detailing your issue and one of us will respond. +- We also have a very active [Discord]({{ site.discord_invite_url }}) community. If you have a question, start a new thread in the `#help` channel. + + [![SST Discourse Forums screenshot](/assets/how-to-get-help/sst-discord.png)]({{ site.discord_invite_url }}) -![Serverless Stack Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) +- We use [Discourse forum topics]({{ site.forum_url }}) as our comments and we've helped resolve quite a few issues in the past. So make sure to check the comments under each chapter to see if somebody else has run into the same issue as you have. This entire guide is hosted on [GitHub]({{ site.github_repo }}). So if you find an error you can always: - Open a [new issue]({{ site.github_repo }}/issues/new) - Or if you've found a typo, edit the page and submit a pull request! + +We recommend you start by [joining us on Discord]({{ site.discord_invite_url }})! diff --git a/_chapters/initialize-the-backend-repo.md b/_chapters/initialize-the-backend-repo.md deleted file mode 100644 index 88d3cc7ee6..0000000000 --- a/_chapters/initialize-the-backend-repo.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -layout: post -title: Initialize the Backend Repo -date: 2018-02-24 00:00:00 -lang: en -description: By automating deployments for our Serverless Framework app, we can simply git push to deploy our app to production. To do so, start by adding your serverless app repo to Git. -ref: initialize-the-backend-repo -comments_id: initialize-the-backend-repo/159 ---- - -To start with we are going to create our new project and add it to GitHub. We are going to be working off the code we've created so far. - -### Clone the Code so Far - -In your working directory, start by cloning the [original repo]({{ site.backend_github_repo }}). - -``` bash -$ git clone --branch handle-api-gateway-cors-errors --depth 1 https://github.com/AnomalyInnovations/serverless-stack-demo-api.git serverless-stack-2-api/ -$ cd serverless-stack-2-api/ -``` - -And remove the `.git/` dir. - -``` bash -$ rm -rf .git/ -``` - -Let's install our Node modules as well. - -``` bash -$ npm install -``` - -### Create a New Github Repo - -Let's head over to [GitHub](https://github.com). Make sure you are signed in and hit **New repository**. - -![Create new GitHub repository screenshot](/assets/part2/create-new-github-repository.png) - -Give your repository a name, in our case we are calling it `serverless-stack-2-api`. Next hit **Create repository**. - -![Name new GitHub repository screenshot](/assets/part2/name-new-github-repository.png) - -Once your repository is created, copy the repository URL. We'll need this soon. - -![Copy new GitHub repo url screenshot](/assets/part2/copy-new-github-repo-url.png) - -In our case the URL is: - -``` -https://github.com/jayair/serverless-stack-2-api.git -``` - -### Initialize Your New Repo - -Now head back to your project and use the following command to initialize your new repo. - -``` bash -$ git init -``` - -Add the existing files. - -``` bash -$ git add . -``` - -Create your first commit. - -``` bash -$ git commit -m "First commit" -``` - -Link it to the repo you created on GitHub. - -``` bash -$ git remote add origin REPO_URL -``` - -Here `REPO_URL` is the URL we copied from GitHub in the steps above. You can verify that it has been set correctly by doing the following. - -``` bash -$ git remote -v -``` - -Finally, let's push our first commit to GitHub using: - -``` bash -$ git push -u origin master -``` - -Next, let's make a couple of quick changes to our project to get organized. diff --git a/_chapters/initialize-the-frontend-repo.md b/_chapters/initialize-the-frontend-repo.md deleted file mode 100644 index 8c219f3fee..0000000000 --- a/_chapters/initialize-the-frontend-repo.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -layout: post -title: Initialize the Frontend Repo -date: 2018-03-18 00:00:00 -lang: en -description: By automating deployments for our React app, we can simply git push to deploy our app to production. To do so, start by adding your React app repo to Git. -ref: initialize-the-frontend-repo -comments_id: initialize-the-frontend-repo/181 ---- - -Just as we did in the backend portion, we'll start by creating our project and adding it to GitHub. We will use what we had in Part I as a starting point. - -### Clone the Original Repo - -In your working directory, start by cloning the [original repo]({{ site.frontend_github_repo }}). Make sure this is not inside the directory for our backend. - -``` bash -$ git clone --branch part-1 --depth 1 https://github.com/AnomalyInnovations/serverless-stack-demo-client.git serverless-stack-2-client/ -$ cd serverless-stack-2-client/ -``` - -And remove the `.git/` dir. - -``` bash -$ rm -rf .git/ -``` - -Let's install our Node modules. - -``` bash -$ npm install -``` - -### Create a New GitHub Repo - -Let's head over to [GitHub](https://github.com). Make sure you are signed in and hit **New repository**. - -![Create new GitHub repository screenshot](/assets/part2/create-new-github-repository.png) - -Give your repository a name, in our case we are calling it `serverless-stack-2-client`. And hit **Create repository**. - -![Name new client GitHub repository screenshot](/assets/part2/name-new-client-github-repository.png) - -Once your repository is created, copy the repository URL. We'll need this soon. - -![Copy new client GitHub repo url screenshot](/assets/part2/copy-new-client-github-repo-url.png) - -In our case the URL is: - -``` -https://github.com/jayair/https://github.com/jayair/serverless-stack-2-client.git -``` - -### Initialize Your New Repo - -Now head back to your project and use the following command to initialize your new repo. - -``` bash -$ git init -``` - -Add the existing files. - -``` bash -$ git add . -``` - -Create your first commit. - -``` bash -$ git commit -m "First commit" -``` - -Link it to the repo you created on GitHub. - -``` bash -$ git remote add origin REPO_URL -``` - -Here `REPO_URL` is the URL we copied from GitHub in the steps above. You can verify that it has been set correctly by doing the following. - -``` bash -$ git remote -v -``` - -Finally, let's push our first commit to GitHub using: - -``` bash -$ git push -u origin master -``` - -Next let's look into configuring our frontend client with the environments that we have in our backend. diff --git a/_chapters/ko/add-a-billing-api.md b/_chapters/ko/add-a-billing-api.md index 6555bde125..6cc251a583 100644 --- a/_chapters/ko/add-a-billing-api.md +++ b/_chapters/ko/add-a-billing-api.md @@ -13,21 +13,21 @@ ref: add-a-billing-api ### 결제 람다 추가 -Stripe NPM 패키지를 설치합니다. 프로젝트의 루트에서 다음을 실행하십시오. +{%change%} Stripe NPM 패키지를 설치합니다. 프로젝트의 루트에서 다음을 실행하십시오. ``` bash $ npm install --save stripe ``` -다음으로 `billing.js`에 다음 내용을 추가합니다. +{%change%} 다음으로 `billing.js`에 다음 내용을 추가합니다. ``` js import stripePackage from "stripe"; +import handler from "./libs/handler-lib"; import { calculateCost } from "./libs/billing-lib"; -import { success, failure } from "./libs/response-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const { storage, source } = JSON.parse(event.body); const amount = calculateCost(storage); const description = "Scratch charge"; @@ -35,18 +35,14 @@ export async function main(event, context) { // Load our secret key from the environment variables const stripe = stripePackage(process.env.stripeSecretKey); - try { - await stripe.charges.create({ - source, - amount, - description, - currency: "usd" - }); - return success({ status: true }); - } catch (e) { - return failure({ message: e.message }); - } -} + await stripe.charges.create({ + source, + amount, + description, + currency: "usd" + }); + return { status: true }; +}); ``` 위 대부분은 매우 간단하지만 빨리 넘어 가겠습니다.: @@ -63,7 +59,7 @@ export async function main(event, context) { 이제 `calculateCost` 메소드를 구현해 보겠습니다. 이것은 주로 *비즈니스 로직*입니다. -`libs/billing-lib.js` 파일을 만들고 아래 내용을 추가합니다. +{%change%} `libs/billing-lib.js` 파일을 만들고 아래 내용을 추가합니다. ``` js export function calculateCost(storage) { @@ -83,7 +79,7 @@ export function calculateCost(storage) { 새로운 API 및 Lambda 함수에 대한 참조를 추가해 보겠습니다. -`serverless.yml`의 `resources :`블럭 위에 다음을 추가하십시오. +{%change%} `serverless.yml`의 `resources :`블럭 위에 다음을 추가하십시오. ``` yml billing: @@ -100,7 +96,7 @@ export function calculateCost(storage) { ### 변경 사항 커밋 -Git으로 빠르게 커밋합니다. +{%change%} Git으로 빠르게 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/add-a-create-note-api.md b/_chapters/ko/add-a-create-note-api.md index dc22414b6a..43f4d1838b 100644 --- a/_chapters/ko/add-a-create-note-api.md +++ b/_chapters/ko/add-a-create-note-api.md @@ -12,14 +12,14 @@ comments_id: add-a-create-note-api/125 먼저 노트를 만드는 API를 추가하여 백엔드를 시작해 보겠습니다. 이 API는 노트 오브젝트를 입력으로 사용하고 새 ID로 데이터베이스에 저장합니다. 노트 오브젝트는 `content` 필드(노트의 내용)와 `attachment` 필드(업로드 된 파일의 URL)를 포함합니다. -### 함수 추가하기 +### 함수 추가하기 첫 번째 함수를 추가해 보겠습니다. -프로젝트 루트에 다음과 같이`create.js`라는 새로운 파일을 만듭니다. +{%change%} 프로젝트 루트에 다음과 같이`create.js`라는 새로운 파일을 만듭니다. -``` javascript -import uuid from "uuid"; +```js +import * as uuid from "uuid"; import AWS from "aws-sdk"; const dynamoDb = new AWS.DynamoDB.DocumentClient(); @@ -29,27 +29,22 @@ export function main(event, context, callback) { const data = JSON.parse(event.body); const params = { - TableName: "notes", - // 'Item'은 생성 될 항목의 속성을 포함합니다. -     // - 'userId': 사용자 신원은 Cognito ID 풀 ID는 인증 된 사용자의 사용자 ID를 사용합니다. -     // - 'noteId': 고유한 uuid -     // - 'content': 요청 본문으로부터 파싱 됨 -     // - 'attachment': 요청 본문에서 파싱 됨 -     // - 'createdAt': 현재 유닉스 타임 스탬프 + TableName: "notes", // - 'userId': 사용자 신원은 Cognito ID 풀 ID는 인증 된 사용자의 사용자 ID를 사용합니다. // - 'noteId': 고유한 uuid // - 'content': 요청 본문으로부터 파싱 됨 // - 'attachment': 요청 본문에서 파싱 됨 // - 'createdAt': 현재 유닉스 타임 스탬프 + // 'Item'은 생성 될 항목의 속성을 포함합니다. Item: { userId: event.requestContext.identity.cognitoIdentityId, noteId: uuid.v1(), content: data.content, attachment: data.attachment, - createdAt: Date.now() - } + createdAt: Date.now(), + }, }; dynamoDb.put(params, (error, data) => { // CORS (Cross-Origin Resource Sharing)를 사용하도록 응답 헤더를 설정합니다. const headers = { "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": true + "Access-Control-Allow-Credentials": true, }; // 에러발생시 상태코드 500을 반환합니다. @@ -57,17 +52,17 @@ export function main(event, context, callback) { const response = { statusCode: 500, headers: headers, - body: JSON.stringify({ status: false }) + body: JSON.stringify({ status: false }), }; callback(null, response); return; } - // 새로운 항목이 생성되면 상태 코드 200을 반환합니다. + // 새로운 항목이 생성되면 상태 코드 200을 반환합니다. const response = { statusCode: 200, headers: headers, - body: JSON.stringify(params.Item) + body: JSON.stringify(params.Item), }; callback(null, response); }); @@ -78,7 +73,7 @@ export function main(event, context, callback) { - AWS JS SDK는 람다 함수의 현재 리전을 기반으로 작업 리전을 가정합니다. 따라서 DynamoDB 테이블이 다른 리전에있는 경우 DynamoDB 클라이언트를 초기화하기 전에 AWS.config.update ({region : "my-region"})를 호출하여 설정해야합니다. - `event.body`에서 입력을 파싱합니다. 이것은 HTTP 요청 매개변수를 나타냅니다. -- `userId`는 요청의 일부로 들어오는 연합 ID입니다. 이것은 접속자가 사용자 풀을 통해 인증 된 후에 설정됩니다. 우리는 Cognito 인증 풀을 설정할 다음 장에서 이에 대해 더 자세히 설명하겠습니다. 별도로 사용자 풀에 있는 사용자 ID를 이용하려는 경우; [Cognito ID 매핑 및 사용자 풀 ID]({% link _chapters/mapping-cognito-identity-id-and-user-pool-id.md %}) 장을 살펴보십시오. +- `userId`는 요청의 일부로 들어오는 연합 ID입니다. 이것은 접속자가 사용자 풀을 통해 인증 된 후에 설정됩니다. 우리는 Cognito 인증 풀을 설정할 다음 장에서 이에 대해 더 자세히 설명하겠습니다. 별도로 사용자 풀에 있는 사용자 ID를 이용하려는 경우; [Cognito ID 매핑 및 사용자 풀 ID]({% link _archives/mapping-cognito-identity-id-and-user-pool-id.md %}) 장을 살펴보십시오. - DynamoDB를 호출하여 생성 된 `noteId` 및 현재 날짜가 `createdAt`인 새 객체를 넣습니다. - 성공하면 HTTP 상태 코드가 `200`인 새로 생성 된 노트 객체와 응답 헤더를 반환하여 **CORS(Cross-Origin Resource Sharing)** 를 사용하도록 설정합니다. - 그리고 DynamoDB 호출이 실패하면 HTTP 상태 코드가 '500'인 오류를 반환합니다. @@ -87,17 +82,17 @@ export function main(event, context, callback) { 이제 우리 함수에 API 엔드포인트를 정의해 보겠습니다. -`serverless.yml` 파일을 열어서 아래 코드로 대치하십시오. +{%change%} `serverless.yml` 파일을 열어서 아래 코드로 대치하십시오. -``` yaml -service: notes-app-api +```yaml +service: notes-api # ES6 변환을 위해 serverless-webpack 플러그인 사용 plugins: - serverless-webpack - serverless-offline -# serverless-webpack 구성 +# serverless-webpack 구성 # 외부 모듈 패키징 자동화 활성 custom: webpack: @@ -128,8 +123,8 @@ functions: # create.js의 메인 함수를 호출하는 HTTP API 엔드포인트를 정의 # - path: url 경로는 /notes # - method: POST 요청 - # - cors: 브라우저의 크로스 도메인 API 호출을 위해 CORS (Cross-Origin Resource Sharing) 활성화 - # - authorizer: AWS IAM 역할을 통한 인증 + # - cors: 브라우저의 크로스 도메인 API 호출을 위해 CORS (Cross-Origin Resource Sharing) 활성화 + # - authorizer: AWS IAM 역할을 통한 인증 create: handler: create.main events: @@ -140,24 +135,23 @@ functions: authorizer: aws_iam ``` -여기에 새로 추가 된 작성 기능을 구성에 추가합니다. 우리는`/notes` 엔드포인트에서`post` 요청을 처리하도록 지정합니다. 단일 람다 함수를 사용하여 단일 HTTP 이벤트에 응답하는이 패턴은 [Microservices 아키텍처](https://en.wikipedia.org/wiki/Microservices)와 매우 비슷합니다. 이 부분과 [Serverless Framework 프로젝트 구성하기]({% link _chapters/organizing-serverless-projects.md %}) 장에서 몇 가지 다른 패턴을 논의합니다. CORS 지원을 true로 설정했습니다. 프론트 엔드가 다른 도메인에서 제공되기 때문입니다. 승인자로서 사용자의 IAM 자격 증명을 기반으로 API에 대한 액세스를 제한하려고합니다. 이 내용과 Cognito Identity Pool 챕터에서 사용자 풀이 어떻게 작동하는지에 대해 알아볼 것입니다. +여기에 새로 추가 된 작성 기능을 구성에 추가합니다. 우리는`/notes` 엔드포인트에서`post` 요청을 처리하도록 지정합니다. 단일 람다 함수를 사용하여 단일 HTTP 이벤트에 응답하는이 패턴은 [Microservices 아키텍처](https://en.wikipedia.org/wiki/Microservices)와 매우 비슷합니다. 이 부분과 [Serverless Framework 프로젝트 구성하기]({% link _archives/organizing-serverless-projects.md %}) 장에서 몇 가지 다른 패턴을 논의합니다. CORS 지원을 true로 설정했습니다. 프론트 엔드가 다른 도메인에서 제공되기 때문입니다. 승인자로서 사용자의 IAM 자격 증명을 기반으로 API에 대한 액세스를 제한하려고합니다. 이 내용과 Cognito Identity Pool 챕터에서 사용자 풀이 어떻게 작동하는지에 대해 알아볼 것입니다. `iamRoleStatements` 섹션은 람다 함수가 어떤 리소스에 액세스 할 수 있는지 AWS에 알려줍니다. 이 경우 람다 함수가 위에 나열된 작업을 DynamoDB에서 수행할 수 있습니다. DynamoDB는`arn:aws:dynamodb:us-east-1:*:*`를 사용하여 지정합니다. 이것은 대략`us-east-1` 리전의 모든 DynamoDB 테이블을 가리 킵니다. 여기에 테이블 이름을 지정하여보다 구체적으로 설명 할 수 있지만 여러분에게 연습 문제로 남겨 두겠습니다. 반드시 DynamoDB 테이블이 생성 된 리전을 사용하십시오. 나중에 발생하는 문제의 대부분의 원인이 될 수 있습니다. 우리에게는 이 리전이 'us-east-1'입니다. - -### 테스트 +### 테스트 자, 이제 새로운 API를 테스트할 준비가 되었습니다. 로컬에서 테스트하기 위해 입력 파라미터를 임의로 만들겠습니다. -프로젝트 루트에서 `mocks/` 디렉토리를 생성합니다. +{%change%} 프로젝트 루트에서 `mocks/` 디렉토리를 생성합니다. -``` bash +```bash $ mkdir mocks ``` -`mocks/create-event.json` 파일을 만들고 아래 코드를 추가합니다. +{%change%} `mocks/create-event.json` 파일을 만들고 아래 코드를 추가합니다. -``` json +```json { "body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}", "requestContext": { @@ -172,21 +166,21 @@ $ mkdir mocks 그리고 함수를 호출하기 위해 루트 디렉토리에서 다음을 실행합니다. -``` bash +```bash $ serverless invoke local --function create --path mocks/create-event.json ``` 만일 여러분의 AWS SDK 자격 증명을 위한 프로필이 여러개인 경우 명시적으로 선택해야합니다. 그럴경우 다음 명령을 사용하십시오. -``` bash +```bash $ AWS_PROFILE=myProfile serverless invoke local --function create --path mocks/create-event.json ``` -`myProfile`은 여러분이 사용하고자 하는 AWS 프로필 이름입니다. 만일 서버리스에서 AWS 프로필을 어떻게 작동하는지 알고 싶으시다면 이 곳에 있는 [다중 AWS 프로필 설정하기]({% link _chapters/configure-multiple-aws-profiles.md %}) 챕터를 참고하십시오. +`myProfile`은 여러분이 사용하고자 하는 AWS 프로필 이름입니다. 만일 서버리스에서 AWS 프로필을 어떻게 작동하는지 알고 싶으시다면 이 곳에 있는 [다중 AWS 프로필 설정하기]({% link _archives/configure-multiple-aws-profiles.md %}) 챕터를 참고하십시오. 응답은 다음과 같이 나와야합니다. -``` bash +```bash { statusCode: 200, headers: { @@ -199,45 +193,49 @@ $ AWS_PROFILE=myProfile serverless invoke local --function create --path mocks/c 여기서 응답으로 나온 `noteId` 값을 적어 놓으세요. 다음 장에서 여기서 작성한 새 노트를 사용하겠습니다. -### 코드 리펙토링 +### 코드 리펙토링 다음 장으로 넘어가기 전에 앞으로 모든 API에 대해 많은 것을 처리해야 하므로 코드를 빠르게 리팩토링하겠습니다. -프로젝트 루트에서 `libs/` 디렉토리를 생성합니다. +{%change%} 프로젝트 루트에서 `libs/` 디렉토리를 생성합니다. -``` bash +```bash $ mkdir libs $ cd libs ``` -그리고 아래 내용으로 `libs/response-lib.js` 파일을 만듭니다. - -``` javascript -export function success(body) { - return buildResponse(200, body); -} - -export function failure(body) { - return buildResponse(500, body); -} - -function buildResponse(statusCode, body) { - return { - statusCode: statusCode, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": true - }, - body: JSON.stringify(body) +{%change%} 그리고 아래 내용으로 `libs/handler-lib.js` 파일을 만듭니다. + +```js +export default function handler(lambda) { + return function (event, context) { + return ( + Promise.resolve() + // Run the Lambda + .then(() => lambda(event, context)) + // On success + .then((responseBody) => [200, responseBody]) + // On failure + .catch((e) => [500, { error: e.message }]) + // Return HTTP response + .then(([statusCode, body]) => ({ + statusCode, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, + body: JSON.stringify(body), + })) + ); }; } ``` 이렇게하면 적절한 HTTP 상태 코드와 헤더를 사용하여 성공 및 실패 사례에 대한 응답 오브젝트를 작성할 수 있습니다. -다시 `libs/` 디렉토리에서 아래 내용으로 `dynamodb-lib.js` 파일을 생성합니다. +{%change%} 다시 `libs/` 디렉토리에서 아래 내용으로 `dynamodb-lib.js` 파일을 생성합니다. -``` javascript +```js import AWS from "aws-sdk"; export function call(action, params) { @@ -249,33 +247,29 @@ export function call(action, params) { 여기서는 DynamoDB 메소드의 promise 형식을 사용하고 있습니다. Promise는 표준 콜백함수 구문 대신 사용할 비동기 코드를 관리하는 방법입니다. 코드를 훨씬 쉽게 읽을 수 있습니다. -이제 우리는`create.js`로 돌아가서 우리가 만든 Helper 함수를 사용할 것입니다. `create.js`를 다음으로 대체하십시오. +{%change%} 이제 우리는`create.js`로 돌아가서 우리가 만든 Helper 함수를 사용할 것입니다. `create.js`를 다음으로 대체하십시오. -``` javascript -import uuid from "uuid"; -import * as dynamoDbLib from "./libs/dynamodb-lib"; -import { success, failure } from "./libs/response-lib"; +```js +import * as uuid from "uuid"; +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const data = JSON.parse(event.body); const params = { - TableName: "notes", + TableName: process.env.tableName, Item: { userId: event.requestContext.identity.cognitoIdentityId, noteId: uuid.v1(), content: data.content, attachment: data.attachment, - createdAt: Date.now() - } + createdAt: Date.now(), + }, }; - try { - await dynamoDbLib.call("put", params); - return success(params.Item); - } catch (e) { - return failure({ status: false }); - } -} + await dynamoDb.put(params); + return params.Item; +}); ``` 또한 람다 함수를 리팩토링하기 위해 여기 `async/await` 패턴을 사용하고 있습니다. 이렇게하면 처리가 완료되면 다시 돌아올 수 있습니다. 콜백 함수를 사용하는 대신말이죠. @@ -284,15 +278,15 @@ export async function main(event, context) { --- -#### 공통 이슈 +#### 공통 이슈 - 응답 `statusCode: 500` 함수를 호출 할 때`statusCode : 500` 응답을 보게되면 디버그하는 방법이 있습니다. 에러는 우리 코드에 의해`catch` 블록에서 생성됩니다. 이렇게`console.log`를 추가하면 문제가 무엇인지에 대한 단서를 얻을 수 있습니다. - ``` javascript - catch(e) { - console.log(e); - return failure({status: false}); - } - ``` +```js +catch(e) { + console.log(e); + return failure({status: false}); +} +``` diff --git a/_chapters/ko/add-a-delete-note-api.md b/_chapters/ko/add-a-delete-note-api.md index d121aa9371..90eca3bfa0 100644 --- a/_chapters/ko/add-a-delete-note-api.md +++ b/_chapters/ko/add-a-delete-note-api.md @@ -14,62 +14,58 @@ comments_id: add-a-delete-note-api/153 ### 함수 추가하기 -`delete.js` 파일을 생성하고 아래 코드를 붙여 넣기 합니다. +{%change%} `delete.js` 파일을 생성하고 아래 코드를 붙여 넣기 합니다. -``` javascript -import * as dynamoDbLib from "./libs/dynamodb-lib"; -import { success, failure } from "./libs/response-lib"; +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const params = { TableName: "notes", // 'Key' 삭제할 아이템의 파티션 키와 정렬 키를 정의합니다. - // - 'userId': 인증 사용자의 Cognito Identity Pool 인증 ID + // - 'userId': 인증 사용자의 Cognito Identity Pool 인증 ID // - 'noteId': 경로 파라미터 Key: { userId: event.requestContext.identity.cognitoIdentityId, - noteId: event.pathParameters.id - } + noteId: event.pathParameters.id, + }, }; - try { - const result = await dynamoDbLib.call("delete", params); - return success({ status: true }); - } catch (e) { - return failure({ status: false }); - } -} + await dynamoDb.delete(params); + return { status: true }; +}); ``` -이 파일은 삭제할 노트의 `userId` 와 `noteId` 값을 이용해 DynamoDB에 `delete`를 호출합니다. +이 파일은 삭제할 노트의 `userId` 와 `noteId` 값을 이용해 DynamoDB에 `delete`를 호출합니다. -### API 엔드포인트 구성하기 +### API 엔드포인트 구성하기 -`serverless.yml` 파일을 열어서 아래 내용을 추가합니다. +{%change%} `serverless.yml` 파일을 열어서 아래 내용을 추가합니다. -``` yaml - delete: - # delete.js의 메인 함수를 호출하는 HTTP API 엔드포인트 - # - path: url 경로는 /notes/{id} 입니다. - # - method: DELETE 요청 - handler: delete.main - events: - - http: - path: notes/{id} - method: delete - cors: true - authorizer: aws_iam +```yaml +delete: + # delete.js의 메인 함수를 호출하는 HTTP API 엔드포인트 + # - path: url 경로는 /notes/{id} 입니다. + # - method: DELETE 요청 + handler: delete.main + events: + - http: + path: notes/{id} + method: delete + cors: true + authorizer: aws_iam ``` 이것은 DELETE 요청 핸들러 함수를 `/notes/{id}` 엔드포인트에 추가합니다. ### 테스트 -`mocks/delete-event.json` 파일을 만들고 아래 내용을 붙여 넣기 합니다. +{%change%} `mocks/delete-event.json` 파일을 만들고 아래 내용을 붙여 넣기 합니다. 역시 이전과 같이 `pathParameters` 블록에 `id` 값은 `noteId` 값으로 대체합니다. -``` json +```json { "pathParameters": { "id": "578eb840-f70f-11e6-9d1a-1359b3b22944" @@ -81,15 +77,16 @@ export async function main(event, context) { } } ``` + 루트 디렉토리에서 새로 추가한 함수를 실행합니다. -``` bash +```bash $ serverless invoke local --function delete --path mocks/delete-event.json ``` 반환된 응답은 아래와 유사해야합니다. -``` bash +```bash { statusCode: 200,  headers: { diff --git a/_chapters/ko/add-a-get-note-api.md b/_chapters/ko/add-a-get-note-api.md index 9ae1a0c223..6314de156b 100644 --- a/_chapters/ko/add-a-get-note-api.md +++ b/_chapters/ko/add-a-get-note-api.md @@ -2,7 +2,7 @@ layout: post title: Add a Get Note API date: 2016-12-31 00:00:00 -description: 노트 작성 앱에서 사용자가 노트를 검색 할 수 있도록 GET 노트 API를 추가 할 예정입니다. 이것을 위해 Serverless Framework 프로젝트에 새로운 Lambda 함수를 추가할 것입니다. 생성된 Lambda 함수는 DynamoDB 테이블에서 노트를 검색합니다. +description: 노트 작성 앱에서 사용자가 노트를 검색 할 수 있도록 GET 노트 API를 추가 할 예정입니다. 이것을 위해 Serverless Framework 프로젝트에 새로운 Lambda 함수를 추가할 것입니다. 생성된 Lambda 함수는 DynamoDB 테이블에서 노트를 검색합니다. lang: ko ref: add-a-get-note-api context: true @@ -12,58 +12,52 @@ comments_id: add-a-get-note-api/132 이제 데이터베이스에 노트를 생성하고 저장했습니다. 이제 생성된 노트 ID를 이용해 노트 정보를 불러오는 API를 추가해보겠습니다. -### 함수 추가하기 +### 함수 추가하기 -신규 파일인 `get.js`를 생성하고 아래 코드를 붙여넣기 합니다. +{%change%} 신규 파일인 `get.js`를 생성하고 아래 코드를 붙여넣기 합니다. -``` javascript -import * as dynamoDbLib from "./libs/dynamodb-lib"; -import { success, failure } from "./libs/response-lib"; +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const params = { - TableName: "notes", - // 'Key'는 검색 할 항목의 파티션 키와 정렬 키를 정의합니다. -     // - 'userId': 인증 된 사용자의 ID 풀에 해당하는 인증 아이디 -     // - 'noteId': 경로 매개 변수 + TableName: "notes", // - 'userId': 인증 된 사용자의 ID 풀에 해당하는 인증 아이디 // - 'noteId': 경로 매개 변수 + // 'Key'는 검색 할 항목의 파티션 키와 정렬 키를 정의합니다. Key: { userId: event.requestContext.identity.cognitoIdentityId, - noteId: event.pathParameters.id - } + noteId: event.pathParameters.id, + }, }; - try { - const result = await dynamoDbLib.call("get", params); - if (result.Item) { - // 불러온 아이템을 반환합니다. - return success(result.Item); - } else { - return failure({ status: false, error: "Item not found." }); - } - } catch (e) { - return failure({ status: false }); + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); } -} + + // 불러온 아이템을 반환합니다. + return result.Item; +}); ``` -이 파일은 이전의 `create.js` 함수와 똑같은 구조를 따릅니다. 가장 큰 차이점은 요청을 통해 전달되는`noteId` 와 `userId`가 주어진 노트 객체를 얻기 위해 `dynamoDbLib.call ( 'get', params)`을 수행한다는 것입니다. +이 파일은 이전의 `create.js` 함수와 똑같은 구조를 따릅니다. 가장 큰 차이점은 요청을 통해 전달되는`noteId` 와 `userId`가 주어진 노트 객체를 얻기 위해 `dynamoDb.get(params)`을 수행한다는 것입니다. -### API 엔드포인트 구성하기 +### API 엔드포인트 구성하기 -`serverless.yml` 파일을 열고 아래 코드를 추가합니다. +{%change%} `serverless.yml` 파일을 열고 아래 코드를 추가합니다. -``` yaml - get: - # get.js의 main 함수를 호출하는 HTTP API 엔드포인트를 정의합니다. - # - path: /notes/{id} url 경로 - # - method: GET 요청 - handler: get.main - events: - - http: - path: notes/{id} - method: get - cors: true - authorizer: aws_iam +```yaml +get: + # get.js의 main 함수를 호출하는 HTTP API 엔드포인트를 정의합니다. + # - path: /notes/{id} url 경로 + # - method: GET 요청 + handler: get.main + events: + - http: + path: notes/{id} + method: get + cors: true + authorizer: aws_iam ``` 이 코드 블록이 앞의 `create` 블록과 정확히 같은 방법으로 들여 쓰는지 확인하십시오. @@ -74,9 +68,9 @@ export async function main(event, context) { Get note API를 테스트하려면`noteId` 매개 변수를 전달해야합니다. 우리는 이전 장에서 작성한 노트의 `noteId`를 사용하고`pathParameters` 블록을 모의 객체에 추가 할 것입니다. 그러면 해당 내용은 아래와 유사하게 보일 것입니다. `id`의 값을 이전의 `create.js` 함수를 호출 할 때 받았던 ID로 대체하십시오. -`mocks/get-event.json` 파일을 만들고 아래 코드를 추가합니다. +{%change%} `mocks/get-event.json` 파일을 만들고 아래 코드를 추가합니다. -``` json +```json { "pathParameters": { "id": "578eb840-f70f-11e6-9d1a-1359b3b22944" @@ -91,13 +85,13 @@ Get note API를 테스트하려면`noteId` 매개 변수를 전달해야합니 그리고 새로 생성된 함수를 실행합니다. -``` bash +```bash $ serverless invoke local --function get --path mocks/get-event.json ``` 반환된 응답은 아래와 유사해야 합니다. -``` bash +```bash { statusCode: 200, headers: { diff --git a/_chapters/ko/add-a-list-all-the-notes-api.md b/_chapters/ko/add-a-list-all-the-notes-api.md index 36a376d590..cb1ee7be8e 100644 --- a/_chapters/ko/add-a-list-all-the-notes-api.md +++ b/_chapters/ko/add-a-list-all-the-notes-api.md @@ -12,66 +12,61 @@ comments_id: add-a-list-all-the-notes-api/147 자 이번에는 사용자가 가진 모든 노트목록을 가져오는 API를 추가하겠습니다. -### 함수 추가하기 +### 함수 추가하기 -아래 내용을 가진 `list.js` 파일을 신규로 생성합니다. +{%change%} 아래 내용을 가진 `list.js` 파일을 신규로 생성합니다. -``` javascript -import * as dynamoDbLib from "./libs/dynamodb-lib"; -import { success, failure } from "./libs/response-lib"; +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const params = { TableName: "notes", // 'KeyConditionExpression' 조건을 가진 쿼리를 정의합니다. // - 'userId = :userId': 파티션 키인 'userId' 값과 같은 데이터를 반환하도록 합니다. // 'ExpressionAttributeValues' 조건 값을 정의합니다. // - ':userId': 'userId' 값을 사용자 인증을 완료한 Cognito Identity Pool의 인증 ID - // 를 정의합니다. + // 를 정의합니다. KeyConditionExpression: "userId = :userId", ExpressionAttributeValues: { - ":userId": event.requestContext.identity.cognitoIdentityId - } + ":userId": event.requestContext.identity.cognitoIdentityId, + }, }; - try { - const result = await dynamoDbLib.call("query", params); - // 응답 본문에 일치하는 아이템의 목록을 반환합니다. - return success(result.Items); - } catch (e) { - return failure({ status: false }); - } -} + const result = await dynamoDb.query(params); + // 응답 본문에 일치하는 아이템의 목록을 반환합니다. + return result.Items; +}); ``` 이 파일은 DynamoDB의 `query` 호출 내용에 `userId` 값을 전달한다는 것을 제외하면 `get.js`와 매우 유사합니다. -### API 엔드포인트 구성하기 - -`serverless.yml` 파일을 열고 아래 내용을 추가합니다. - -``` yaml - list: - # list.js의 메인 함수를 호출하는 HTTP API 엔드포인트를 정의합니다. - # - path: url 경로는 /notes - # - method: GET 요청 - handler: list.main - events: - - http: - path: notes - method: get - cors: true - authorizer: aws_iam +### API 엔드포인트 구성하기 + +{%change%} `serverless.yml` 파일을 열고 아래 내용을 추가합니다. + +```yaml +list: + # list.js의 메인 함수를 호출하는 HTTP API 엔드포인트를 정의합니다. + # - path: url 경로는 /notes + # - method: GET 요청 + handler: list.main + events: + - http: + path: notes + method: get + cors: true + authorizer: aws_iam ``` - 이것은 GET 요청을 취하는 `/notes` 엔드포인트를 정의합니다. -### 테스트 +### 테스트 -`mocks/list-event.json` 파일을 생성하고 아래 내용을 추가합니다. +{%change%} `mocks/list-event.json` 파일을 생성하고 아래 내용을 추가합니다. -``` json +```json { "requestContext": { "identity": { @@ -83,13 +78,13 @@ export async function main(event, context) { 그리고 이 프로젝트의 루트 디렉토리에서 함수를 실행합니다. -``` bash +```bash $ serverless invoke local --function list --path mocks/list-event.json ``` 이에 대한 응답은 아래와 유사해야합니다. -``` bash +```bash { statusCode: 200, headers: { @@ -99,6 +94,7 @@ $ serverless invoke local --function list --path mocks/list-event.json body: '[{"attachment":"hello.jpg","content":"hello world","createdAt":1487800950620,"noteId":"578eb840-f70f-11e6-9d1a-1359b3b22944","userId":"USER-SUB-1234"}]' } ``` + 이 API는 단 하나의 노트 객체를 반환하는`get.js` 함수와 대조적으로 노트 객체의 배열을 반환합니다. 그럼 다음 API를 추가하여 노트를 업데이트하겠습니다. diff --git a/_chapters/ko/add-an-update-note-api.md b/_chapters/ko/add-an-update-note-api.md index 639d28164b..fb4ba830a0 100644 --- a/_chapters/ko/add-an-update-note-api.md +++ b/_chapters/ko/add-an-update-note-api.md @@ -14,74 +14,70 @@ comments_id: add-an-update-note-api/144 ### 함수 추가하기 -`update.js` 파일을 새로 만들고 아래 코드 내용을 붙여 넣으세요. +{%change%} `update.js` 파일을 새로 만들고 아래 코드 내용을 붙여 넣으세요. -``` javascript -import * as dynamoDbLib from "./libs/dynamodb-lib"; -import { success, failure } from "./libs/response-lib"; +```js +import handler from "./libs/handler-lib"; +import dynamoDb from "./libs/dynamodb-lib"; -export async function main(event, context) { +export const main = handler(async (event, context) => { const data = JSON.parse(event.body); const params = { TableName: "notes", // 'Key' 수정하고자 하는 아이템의 파티션 키와 정렬 키를 정의합니다. // - 'userId': 인증된 사용자의 Cognito Identity Pool의 인증 ID - // - 'noteId': 경로 파라미터 + // - 'noteId': 경로 파라미터 Key: { userId: event.requestContext.identity.cognitoIdentityId, - noteId: event.pathParameters.id + noteId: event.pathParameters.id, }, // 'UpdateExpression' 업데이트 될 속성을 정의합니다. // 'ExpressionAttributeValues' 업데이트 표현식의 값을 정의합니다. UpdateExpression: "SET content = :content, attachment = :attachment", ExpressionAttributeValues: { ":attachment": data.attachment || null, - ":content": data.content || null + ":content": data.content || null, }, // 'ReturnValues' 아이템 속성을 반환할지 여부와 방법을 지정합니다. // 여기서 ALL_NEW는 업데이트 후 항목의 모든 속성을 반환합니다. // 아래에서 '결과값'을 검사하여 다른 설정에서 작동하는 방식을 확인할 수 있습니다. - ReturnValues: "ALL_NEW" + ReturnValues: "ALL_NEW", }; - try { - const result = await dynamoDbLib.call("update", params); - return success({ status: true }); - } catch (e) { - return failure({ status: false }); - } -} + await dynamoDb.update(params); + return { status: true }; +}); ``` 이것은 `create.js` 함수와 비슷하게 보일 것입니다. 여기서 우리는 `매개 변수`에 새로운`content` 와 `attachment` 값으로 `update` DynamoDB를 호출합니다. -### API 엔드포인트 구서하기 - -`serverless.yml` 파일을 열어서 아래 코드를 추가합니다. - -``` yaml - update: - # update.js의 메인 함수를 호출하는 HTTP API 엔드포인트를 정의합니다. - # - path: url 경로는 /notes/{id} 입니다. - # - method: PUT 요청 - handler: update.main - events: - - http: - path: notes/{id} - method: put - cors: true - authorizer: aws_iam +### API 엔드포인트 구서하기 + +{%change%} `serverless.yml` 파일을 열어서 아래 코드를 추가합니다. + +```yaml +update: + # update.js의 메인 함수를 호출하는 HTTP API 엔드포인트를 정의합니다. + # - path: url 경로는 /notes/{id} 입니다. + # - method: PUT 요청 + handler: update.main + events: + - http: + path: notes/{id} + method: put + cors: true + authorizer: aws_iam ``` 여기에서는 PUT 요청에 대한 핸들러를 `/notes/{id}` 엔드 포인트에 추가합니다. ### 테스트 -`mocks/update-event.json` 파일을 생성하고 아래 내용을 추가합니다. +{%change%} `mocks/update-event.json` 파일을 생성하고 아래 내용을 추가합니다. 그리고 `pathParameters` 블록에 있는 `id`에 이전에 사용했던 `noteId` 값으로 대체하는 것을 잊지 마세요. -``` json +```json { "body": "{\"content\":\"new world\",\"attachment\":\"new.jpg\"}", "pathParameters": { @@ -97,13 +93,13 @@ export async function main(event, context) { 그리고 루트 디렉토리에서 새로 만든 함수를 실행합니다. -``` bash +```bash $ serverless invoke local --function update --path mocks/update-event.json ``` 반환되는 결과는 아래와 유사해야합니다. -``` bash +```bash { statusCode: 200, headers: { diff --git a/_chapters/ko/add-app-favicons.md b/_chapters/ko/add-app-favicons.md index b05fb943f5..12eb951ba8 100644 --- a/_chapters/ko/add-app-favicons.md +++ b/_chapters/ko/add-app-favicons.md @@ -30,11 +30,11 @@ Create React App은 앱에 대한 간단한 favicon을 생성하고`public/favic 이렇게하면 favicon 패키지와 함께 제공되는 코드가 생성됩니다. -**Favicon 패키지**를 클릭하여 생성 된 favicon을 다운로드하십시오. 그리고 모든 파일을 `public/` 디렉토리에 복사하십시오. +{%change%} **Favicon 패키지**를 클릭하여 생성 된 favicon을 다운로드하십시오. 그리고 모든 파일을 `public/` 디렉토리에 복사하십시오. ![Realfavicongenerator.net 완성 화면](/assets/realfavicongenerator-completed.png) -그런 다음 `public/manifest.json`의 내용을 다음으로 대체하십시오: +{%change%} 그런 다음 `public/manifest.json`의 내용을 다음으로 대체하십시오: ``` json { @@ -61,7 +61,7 @@ Create React App은 앱에 대한 간단한 favicon을 생성하고`public/favic `public/` 디렉토리에있는 파일을 HTML에 포함 시키려면 React Create App에 `%PUBLIC_URL%` 접두사가 있어야합니다. -아래 내용을 `public/index.html`에 추가하십시오. +{%change%} 아래 내용을 `public/index.html`에 추가하십시오. ``` html @@ -71,7 +71,7 @@ Create React App은 앱에 대한 간단한 favicon을 생성하고`public/favic ``` -원래의 favicon 및 테마 색상을 참조하는 다음 줄을 **제거합니다**. +{%change%} 원래의 favicon 및 테마 색상을 참조하는 다음 줄을 **제거합니다**. ``` html diff --git a/_chapters/ko/add-stripe-keys-to-config.md b/_chapters/ko/add-stripe-keys-to-config.md index 36c85d8107..d75c2a0bf2 100644 --- a/_chapters/ko/add-stripe-keys-to-config.md +++ b/_chapters/ko/add-stripe-keys-to-config.md @@ -12,13 +12,13 @@ ref: add-stripe-keys-to-config 그때 Stripe 계정 설정은 완료되지 않았으므로 이 키의 운영 버전은 아직 없습니다. 지금은 동일한 키의 두 가지 버전이 있다고 가정합니다. -`src/config.js`의 `dev` 블럭에 다음 내용을 추가합니다. +{%change%} `src/config.js`의 `dev` 블럭에 다음 내용을 추가합니다. ``` STRIPE_KEY: "YOUR_STRIPE_DEV_PUBLIC_KEY", ``` -`src/config.js`의 `prod` 블럭에 다음 내용을 추가합니다. +{%change%} `src/config.js`의 `prod` 블럭에 다음 내용을 추가합니다. ``` STRIPE_KEY: "YOUR_STRIPE_PROD_PUBLIC_KEY", @@ -28,7 +28,7 @@ STRIPE_KEY: "YOUR_STRIPE_PROD_PUBLIC_KEY", ### 변경 사항 커밋 -Git에 빠르게 커밋합니다. +{%change%} Git에 빠르게 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/add-support-for-es6-es7-javascript.md b/_chapters/ko/add-support-for-es6-es7-javascript.md index ed5a60541d..73dc8d3b80 100644 --- a/_chapters/ko/add-support-for-es6-es7-javascript.md +++ b/_chapters/ko/add-support-for-es6-es7-javascript.md @@ -5,46 +5,44 @@ date: 2016-12-29 12:00:00 redirect_from: /chapters/add-support-for-es6-javascript.html description: AWS Lambda는 Node.js v8.10을 지원하므로 Serverless Framework 프로젝트에서 ES 가져 오기/내보내기를 사용하기 위해 Babel과 Webpack 4를 사용하여 코드를 추출해야합니다. 프로젝트에 serverless-webpack 플러그인을 사용하면됩니다. 이를 위해 serverless-nodejs-startter를 사용합니다. lang: ko -ref: add-support-for-es6-es7-javascript +ref: add-support-for-es6-and-typescript context: true code: backend comments_id: add-support-for-es6-es7-javascript/128 --- -AWS Lambda는 최근 Node.js v8.10에 대한 지원을 추가했습니다. 뒷 부분에서 다룰 프론트 엔드 React 앱과 비교할 때 지원되는 구문은 약간 다릅니다. 프로젝트의 두 부분에서 유사한 ES 기능을 사용하는 것이 합리적입니다. 특히 우리는 핸들러 기능에서 ES 가져 오기/내보내기를 사용합니다. 이를 위해 [Babel](https://babeljs.io) 및 [Webpack 4](https://webpack.github.io)를 사용하여 코드를 번역합니다. Serverless Framework는 이를 자동으로 수행하는 플러그인을 지원합니다. 우리는 [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) 플러그인을 사용할 것입니다. +AWS Lambda는 최근 Node.js v8.10와 v10.x에 대한 지원을 추가했습니다. 뒷 부분에서 다룰 프론트 엔드 React 앱과 비교할 때 지원되는 구문은 약간 다릅니다. 프로젝트의 두 부분에서 유사한 ES 기능을 사용하는 것이 합리적입니다. 특히 우리는 핸들러 함수에서 ES import/export를 사용합니다. 이를 위해 [Babel](https://babeljs.io) 및 [Webpack 4](https://webpack.github.io)를 사용하여 코드를 트랜스파일링합니다. 또한, Webpack을 사용하면 Lambda 함수에서 사용된 코드만을 포함하여 Lambda 함수 패키지의 생성을 최적화할 수 있습니다. 이를 통하여 패키지의 크기가 줄고 콜드 스타트 시간이 감소할 수 있습니다. Serverless Framework는 이를 자동으로 수행하는 플러그인을 지원합니다. 우리는 유명한 플러그인 [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack)의 확장인 [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle)을 사용할 것입니다. -이 모든 것들은 이전 장에서 [`serverless-nodejs-starter`]({% link _chapters/serverless-nodejs-starter.md %})를 사용하여 설치되었습니다. 다음 몇 가지 이유로 이 스타터를 만들었습니다. +이 모든 것들은 이전 장에서 [`serverless-nodejs-starter`]({% link _archives/serverless-nodejs-starter.md %})를 사용하여 설치되었습니다. 스타터 프로젝트를 만든 이유는 아래와 같습니다. +- Lambda 함수 패키지 생성의 최적화 - 프론트 엔드 및 백엔드에서 비슷한 버전의 JavaScript 사용 -- 번역 된 코드에 오류 메시지에 대한 올바른 줄 번호가 있는지 확인하기. -- 백엔드 API를 로컬에서 실행할 수 있습니다. -- 단위 테스트 지원 추가 +- 트랜스파일링된 코드에 오류 메시지에 대한 올바른 줄 번호가 있는지 확인하기 +- 코드 린트(Lint) 단위 테스트 지원 추가 +- 로컬에서 백엔드 API 실행이 가능하도록 하기 +- Webpack 및 Babel 설정을 신경쓰지 않도록 하기 -우리가 `serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name my-project` 명령을 사용하여이 스타터를 설치했다는 것을 상기 해주십시오. 이것은 Serverless Framework에게 [starter](https://github.com/AnomalyInnovations/serverless-nodejs-starter)를 템플릿으로 사용하여 프로젝트를 생성하도록 지시합니다. +우리가 `serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name my-project` 명령을 사용하여 이 스타터를 설치했다는 것을 상기해주십시오. 이렇게 하면 Serverless Framework가 [starter](https://github.com/AnomalyInnovations/serverless-nodejs-starter)를 템플릿으로 사용하여 프로젝트를 생성해줍니다. 이 장에서는 이 작업을 수행하는 방법에 대해 빠르게 살펴보고 필요에 따라 나중에 변경하도록 하겠습니다. ### Serverless Webpack -ES 코드를 노드 v8.10 JavaScript로 변환하는 과정은 serverless-webpack 플러그인에 의해 수행됩니다. 이 플러그인은`serverless.yml`에 추가되었습니다. 좀 더 자세히 살펴 보겠습니다. +ES 코드를 Node.js v8.10 JavaScript로 변환하는 과정은 serverless-bundle 플러그인에 의해 수행됩니다. 이 플러그인은`serverless.yml`에 추가되었습니다. 좀 더 자세히 살펴 보겠습니다. -`serverless.yml`을 열고 아래와 같이 기본값으로 대체하십시오. +{%change%} `serverless.yml`을 열고 아래와 같이 기본값으로 대체하십시오. ``` yaml -service: notes-app-api +service: notes-api + +# 우리가 사용할 함수에 최적화된 패키지를 생성합니다 +package: + indivitually: true -# serverless-webpack 플러그인을 사용하여 ES6을 가로 채기 plugins: - - serverless-webpack + - serverless-bundle # 우리의 함수를 Webpack으로 패키징합니다 - serverless-offline -# serverless-webpack 구성 -# 외부 모듈에 대한 패키징 자동화 활성화 -custom: - webpack: - webpackConfig: ./webpack.config.js - includeModules: true - provider: name: aws runtime: nodejs8.10 @@ -52,62 +50,18 @@ provider: region: us-east-1 ``` -`service` 옵션은 매우 중요합니다. 우리의 서비스를 `notes-app-api`라고 부릅니다. Serverless Framework는이 이름을 사용하여 AWS에 스택을 만듭니다. 즉, 이름을 변경하고 프로젝트를 배포하면 완전히 새로운 프로젝트가 만들어집니다. - -포함되어있는`serverless-webpack` 플러그인에 주목하십시오. 또한 플러그인을 설정하는`webpack.config.js`도 있습니다. - -`webpack.config.js`는 다음과 같습니다. 따로 수정하지 않아도됩니다. 그냥 훓어만 보십시오. - -``` js -const slsw = require("serverless-webpack"); -const nodeExternals = require("webpack-node-externals"); - -module.exports = { - entry: slsw.lib.entries, - target: "node", -      - // 적절한 오류 메시지를위한 소스 맵 생성 - devtool: 'source-map', -  // 'aws-sdk'는 webpack과 호환되지 않으므로 -  // 모든 노드 종속성을 제외합니다. - externals: [nodeExternals()], - mode: slsw.lib.webpack.isLocal ? "development" : "production", - optimization: { -    // 코드를 최소화하고 싶지는 않습니다. - minimize: false - }, - performance: { -    // 진입 점에 대한 크기 경고 끄기 - hints: false - }, - //모든 .js 파일에서 babel을 실행하고 node_modules을 건너 뜁니다. - module: { - rules: [ - { - test: /\.js$/, - loader: "babel-loader", - include: __dirname, - exclude: /node_modules/ - } - ] - } -}; -``` +`service` 옵션은 매우 중요합니다. 우리의 서비스는 `notes-api`라고 부르겠습니다. Serverless Framework는 이 이름을 사용하여 AWS에 스택을 만듭니다. 즉, 이름을 변경하고 프로젝트를 배포하면 완전히 새로운 프로젝트가 만들어집니다. -이 설정의 주요 부분은`serverless-webpack` 플러그인의 일부인`slsw.lib.entries`를 사용하여 자동으로 생성하는`entry` 속성입니다. 그러면 자동으로 모든 핸드러 함수가 선택되어 패키지화됩니다. 우리는 또한 각각의 코드에 "babel-loader"를 사용하여 코드를 변형시킵니다. 여기서 주목해야 할 또 하나의 점은 Webpack이 우리의 aws-sdk 모듈을 번들 화하기를 원치 않기 때문에`nodeExternals`를 사용한다는 것입니다. aws-sdk는 Webpack과 호환되지 않습니다. +우리가 포함한 `serverless-bundle`과 `serverless-offline` 플러그인을 주목하시기 바랍니다. `serverless-bundle`은 위에서 설명한 바와 같습니다. [`serverless-offline`](https://github.com/dherault/serverless-offline)은 로컬 개발 환경을 구성하는 데에 유용한 플러그인입니다. -마지막으로 바벨 구성을 간단히 살펴 보겠습니다. 다시 말씀드리지만 아래 코드를 변경하지 않아도됩니다. 프로젝트 루트에서`.babelrc` 파일을 열면 다음과 같이 보입니다. +또한, 아래와 같은 옵션값들을 사용하겠습니다. -``` json -{ - "plugins": ["source-map-support", "transform-runtime"], - "presets": [ - ["env", { "node": "8.10" }], - "stage-3" - ] -} +```yml +# 우리가 사용할 함수에 최적화된 패키지를 생성합니다 +package: + indivitually: true ``` -여기서 우리는 바벨에게 우리의 코드를 노드 v8.10로 변환하도록 지시하고있습니다. -이제 우리는 백엔드를 구축 할 준비가 되었습니다. +Serverless 프레임워크는 기본값으로 당신의 어플리케이션에 포함되어있는 Lambda 함수들을 모두 포함하는 커다란 패키지를 생성합니다. 큰 크기의 Lambda 함수 패키지는 더 긴 콜드 스타트를 유발할 수 있습니다. `individually: true`로 설정하면, Serverless 프레임워크가 Lambda 함수 하나당 하나의 패키지를 각각 생성하게 됩니다. 이러한 설정은 Serverless-bundle(과 Webpack)과 함께 최적화된 패키지를 생성하는 데에 도움이 됩니다. 물론 빌드가 느려지겠지만, 성능 상의 이득이 훨씬 큰 의미가 있을 것입니다. +이제 우리는 백엔드를 구축 할 준비가 되었습니다. diff --git a/_chapters/ko/add-the-create-note-page.md b/_chapters/ko/add-the-create-note-page.md index 227221e7d6..24d811927b 100644 --- a/_chapters/ko/add-the-create-note-page.md +++ b/_chapters/ko/add-the-create-note-page.md @@ -4,22 +4,22 @@ title: Add the Create Note Page date: 2017-01-22 00:00:00 lang: ko ref: add-the-create-note-page -description: 사용자가 React.js 앱에서 노트를 만들고 파일을 첨부 파일로 업로드 할 수 있도록 합니다. 그렇게하기 위해 FormGroup 및 FormControl React-Bootstrap 구성 요소를 사용하여 양식을 작성합니다. +description: 사용자가 React.js 앱에서 노트를 만들고 파일을 첨부 파일로 업로드 할 수 있도록 합니다. 그렇게하기 위해 FormGroup 및 FormControl React-Bootstrap 구성 요소를 사용하여 양식을 작성합니다. context: true comments_id: add-the-create-note-page/107 --- -이제는 사용자를 등록하고 로그인할 수 있게 되었습니다. 노트 작성 앱의 가장 중요한 부분인 노트 작성 부터 시작해 보겠습니다. +이제는 사용자를 등록하고 로그인할 수 있게 되었습니다. 노트 작성 앱의 가장 중요한 부분인 노트 작성 부터 시작해 보겠습니다. 먼저 노트용 양식을 만듭니다. 일부 콘텐츠와 첨부로 사용할 파일이 필요합니다. ### 컨테이너 추가하기 -`src/containers/NewNote.js` 파일을 생성하고 아내 내용을 작성합니다. +{%change%} `src/containers/NewNote.js` 파일을 생성하고 아내 내용을 작성합니다. -``` coffee +```coffee import React, { Component } from "react"; -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; +import { FormGroup, FormControl, FormLabel } from "react-bootstrap"; import LoaderButton from "../components/LoaderButton"; import config from "../config"; import "./NewNote.css"; @@ -73,7 +73,7 @@ export default class NewNote extends Component { /> - Attachment + Attachment 자 그럼, `src/config.js` 파일의 `export default {` 라인 바로 아래에 다음 내용을 추가합니다. +{%change%} 자 그럼, `src/config.js` 파일의 `export default {` 라인 바로 아래에 다음 내용을 추가합니다. ``` MAX_ATTACHMENT_SIZE: 5000000, ``` -이제 `src/containers/NewNote.css` 파일을 추가해서 입력 양식에 스타일을 추가합니다. +{%change%} 이제 `src/containers/NewNote.css` 파일을 추가해서 입력 양식에 스타일을 추가합니다. -``` css +```css .NewNote form { padding-bottom: 15px; } @@ -118,15 +118,15 @@ MAX_ATTACHMENT_SIZE: 5000000, ### 경로 추가하기 -마지막으로 작성한 컨테이너를 `src/Routes.js` 파일의 가입("/signup") 경로 아래에 추가하십시오. [상태에 세션 추가하기]({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한 `AppliedRoute` 컴포넌트를 사용합니다. +{%change%} 마지막으로 작성한 컨테이너를 `src/Routes.js` 파일의 가입("/signup") 경로 아래에 추가하십시오. [상태에 세션 추가하기]({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한 `AppliedRoute` 컴포넌트를 사용합니다. -``` coffee +```coffee ``` -그리고 컴포넌트 헤더에 아래 내용을 추가합니다. +{%change%} 그리고 컴포넌트 헤더에 아래 내용을 추가합니다. -``` javascript +```js import NewNote from "./containers/NewNote"; ``` diff --git a/_chapters/ko/add-the-session-to-the-state.md b/_chapters/ko/add-the-session-to-the-state.md index 73de3979b1..81fc230f8b 100644 --- a/_chapters/ko/add-the-session-to-the-state.md +++ b/_chapters/ko/add-the-session-to-the-state.md @@ -5,20 +5,20 @@ date: 2017-01-15 00:00:00 lang: ko ref: add-the-session-to-the-state redirect_from: /chapters/add-the-user-token-to-the-state.html -description: React.js 앱에서 App 세션 상태에 사용자 세션을 추가해야합니다. 상태를 추가하게 되면 해당 사용자 세션을 모든 하위 컨테이너에 전달할 수 있습니다. +description: React.js 앱에서 App 세션 상태에 사용자 세션을 추가해야합니다. 상태를 추가하게 되면 해당 사용자 세션을 모든 하위 컨테이너에 전달할 수 있습니다. context: true comments_id: add-the-session-to-the-state/136 --- 로그인 프로세스를 완료하려면 사용자가 로그인했음을 알리기 위해 세션과 함께 App state를 업데이트해야합니다. -### App State 업데이트 +### App State 업데이트 먼저 사용자 로그인을 한 상태에서 App state를 업데이트하는 것으로 시작합니다. 이 항목을 `Login` 컨테이너에 저장하고 싶지만 다른 곳에서도 이 항목을 사용하므로 가장 적합한 곳은 `App` 컨테이너입니다. -`src/App.js`를 열어서 `class App extends Component {` 줄 바로 아래에 다음 내용을 추가합니다. +{%change%} `src/App.js`를 열어서 `class App extends Component {` 줄 바로 아래에 다음 내용을 추가합니다. -``` javascript +```js constructor(props) { super(props); @@ -38,24 +38,24 @@ userHasAuthenticated = authenticated => { 우리는 `App` 컴포넌트에서 생성된 경로의 자식 컴포넌트에 두 개의 속성값을 전달함으로써 이를 수행 할 수 있습니다. -`src/App.js`의 `render() {` 줄 바로 아래에 다음 내용을 추가합니다 . +{%change%} `src/App.js`의 `render() {` 줄 바로 아래에 다음 내용을 추가합니다 . -``` javascript +```js const childProps = { isAuthenticated: this.state.isAuthenticated, - userHasAuthenticated: this.userHasAuthenticated + userHasAuthenticated: this.userHasAuthenticated, }; ``` -`src/App.js`의 `render` 메쏘드에서 다음 라인을 대체하여 `Routes` 컴포넌트로 전달하십시오. +{%change%} `src/App.js`의 `render` 메쏘드에서 다음 라인을 대체하여 `Routes` 컴포넌트로 전달하십시오. -``` coffee +```coffee ``` -위 내용을 다음 내용으로 변경 +{%change%} 위 내용을 다음 내용으로 변경 -``` coffee +```coffee ``` @@ -63,17 +63,17 @@ const childProps = { 이를 위해 새로운 컴포넌트를 생성합니다. -작업 디렉토리에서 다음 명령을 실행하여 `src/components/` 디렉토리를 만듭니다. +{%change%} 작업 디렉토리에서 다음 명령을 실행하여 `src/components/` 디렉토리를 만듭니다. -``` bash +```bash $ mkdir src/components/ ``` 이곳에 우리는 API를 직접 다루지 않거나 경로에 응답하는 모든 React 구성 요소들을 저장하겠습니다. -`src/components/AppliedRoute.js`라는 새로운 컴포넌트를 만들고 다음을 추가하십시오. +{%change%} `src/components/AppliedRoute.js`라는 새로운 컴포넌트를 만들고 다음을 추가하십시오. -``` coffee +```coffee import React from "react"; import { Route } from "react-router-dom"; @@ -93,9 +93,9 @@ export default ({ component: C, props: cProps, ...rest }) => 이제 이 컴포넌트를 사용하기 위해 우리는 `childProps`를 전달해야 할 경로에 이 컴포넌트를 포함시킵니다. -`src/Routes.js` 파일의 `export default () => (` 메소드를 다음으로 대체합니다. +{%change%} `src/Routes.js` 파일의 `export default () => (` 메소드를 다음으로 대체합니다. -``` coffee +```coffee export default ({ childProps }) => @@ -105,17 +105,17 @@ export default ({ childProps }) => ; ``` -`src/Routes.js` 파일의 헤더에 새로운 컴포넌트를 추가합니다. +{%change%} `src/Routes.js` 파일의 헤더에 새로운 컴포넌트를 추가합니다. -``` coffee +```coffee import AppliedRoute from "./components/AppliedRoute"; ``` 이제 `Login` 컨테이너에서 `userHasAuthenticated` 메소드를 호출 할 것입니다. -`src/containers/Login.js`에 `alert ( 'Logged in');` 행을 다음 행으로 대체하십시오. +{%change%} `src/containers/Login.js`에 `alert ( 'Logged in');` 행을 다음 행으로 대체하십시오. -``` javascript +```js this.props.userHasAuthenticated(true); ``` @@ -123,7 +123,7 @@ this.props.userHasAuthenticated(true); 이제 사용자가 로그인하면 `로그아웃`으로 버튼을 표시해야합니다. `src/App.js`에서 다음을 찾아보세요. -``` coffee +```coffee Signup @@ -132,9 +132,9 @@ this.props.userHasAuthenticated(true); ``` -그리고 다음 내용으로 대체합니다: +{%change%} 그리고 다음 내용으로 대체합니다: -``` coffee +```coffee {this.state.isAuthenticated ? Logout : @@ -148,25 +148,25 @@ this.props.userHasAuthenticated(true); } ``` -그리고 헤더에 `Fragment`를 import 합니다. +그리고 헤더에 `Fragment`를 import 합니다. -`src/App.js` 파일의 헤더에 `import React` 행을 다음으로 대체합니다. +{%change%} `src/App.js` 파일의 헤더에 `import React` 행을 다음으로 대체합니다. -``` coffee +```coffee import React, { Component, Fragment } from "react"; ``` `Fragment` 컴포넌트는 placeholder 컴포넌트로 생각할 수 있습니다. 사용자가 로그인하지 않은 경우 두 개의 링크를 렌더링해야하기 때문에 이 정보가 필요합니다. 이렇게 하려면 'div'와 같은 단일 컴포넌트 안에 감쌀 필요가 있습니다. 그러나 `Fragment` 컴포넌트를 사용하여 React에 두 개의 링크가 이 컴포넌트 안에 있음을 알려주지만 추가 HTML은 렌더링하지 않습니다. -`src/App.js` 파일의 `handleLogout` 메소드를 추가하고 `render() {` 위에 다음 내용을 추가합니다. +{%change%} `src/App.js` 파일의 `handleLogout` 메소드를 추가하고 `render() {` 위에 다음 내용을 추가합니다. -``` coffee +```coffee handleLogout = event => { this.userHasAuthenticated(false); } ``` -이제 브라우저로 가서 [Cognito 테스트 사용자 만들기]({% link _chapters/create-a-cognito-test-user.md %}) 챕터에서 만든 관리자 자격 증명으로 로그인 해보십시오. 로그아웃 버튼이 바로 나타납니다. +이제 브라우저로 가서 [Cognito 테스트 사용자 만들기]({% link _archives/create-a-cognito-test-user.md %}) 챕터에서 만든 관리자 자격 증명으로 로그인 해보십시오. 로그아웃 버튼이 바로 나타납니다. ![로그인 상태 업데이트 스크린 샷](/assets/login-state-updated.png) diff --git a/_chapters/ko/adding-links-in-the-navbar.md b/_chapters/ko/adding-links-in-the-navbar.md index 77a2f9a08b..39b7ea9ba1 100644 --- a/_chapters/ko/adding-links-in-the-navbar.md +++ b/_chapters/ko/adding-links-in-the-navbar.md @@ -4,16 +4,16 @@ title: Adding Links in the Navbar date: 2017-01-11 12:00:00 lang: ko ref: adding-links-in-the-navbar -description: React.js 앱의 Navbar에 링크를 추가하려면 NavItem React-Bootstrap 구성 요소를 사용합니다. 사용자가 링크를 사용하여 탐색 할 수있게하려면 React-Router의 Route 구성 요소를 사용하고 history.push 메서드를 호출해야합니다. +description: React.js 앱의 Navbar에 링크를 추가하려면 NavItem React-Bootstrap 구성 요소를 사용합니다. 사용자가 링크를 사용하여 탐색 할 수있게하려면 React-Router의 Route 구성 요소를 사용하고 nav.push 메서드를 호출해야합니다. context: true comments_id: adding-links-in-the-navbar/141 --- 이제 첫 번째 경로를 설정 했으므로 앱의 navbar에 몇 가지 링크를 더 추가해 보겠습니다. 사용자가 처음 방문했을 때 로그인 또는 가입하도록 안내합니다. -`src/App.js`에 있는`render` 메쏘드를 다음으로 대체하십시오. +{%change%} `src/App.js`에 있는`render` 메쏘드를 다음으로 대체하십시오. -``` coffee +```coffee render() { return (
@@ -41,9 +41,9 @@ render() { 헤더에 필요한 구성 요소를 포함시켜 봅시다. -import 항목인 `react-router-dom`와 `react-bootstrap`를 `src/App.js`에서 제거하고 아래 내용으로 대체합니다. +{%change%} import 항목인 `react-router-dom`와 `react-bootstrap`를 `src/App.js`에서 제거하고 아래 내용으로 대체합니다. -``` coffee +```coffee import { Link } from "react-router-dom"; import { Nav, Navbar, NavItem } from "react-bootstrap"; ``` @@ -55,21 +55,21 @@ import { Nav, Navbar, NavItem } from "react-bootstrap"; 이 문제를 해결하려면 [React Router Bootstrap](https://github.com/react-bootstrap/react-router-bootstrap)이라는 React Router 및 React Bootstrap에서 작동하는 컴포넌트가 필요합니다. 이 컴포넌트는 `Navbar` 링크를 감쌀뿐만 아니라 React Router를 사용하여 브라우저를 새로 고치지 않고도 앱을 필요한 링크에 연결할 수 있습니다. -작업 디렉토리에서 다음 명령을 실행하십시오. +{%change%} 작업 디렉토리에서 다음 명령을 실행하십시오. -``` bash +```bash $ npm install react-router-bootstrap --save ``` -그리고 `src/App.js`의 최상단에 아래 내용을 추가합니다. +{%change%} 그리고 `src/App.js`의 최상단에 아래 내용을 추가합니다. -``` coffee +```coffee import { LinkContainer } from "react-router-bootstrap"; ``` -이제 링크를`LinkContainer`로 감쌉니다. `src/App.js`에 있는 `render` 메쏘드를 아래 내용으로 바꾸십시오. +{%change%} 이제 링크를`LinkContainer`로 감쌉니다. `src/App.js`에 있는 `render` 메쏘드를 아래 내용으로 바꾸십시오. -``` coffee +```coffee render() { return (
diff --git a/_chapters/ko/automating-react-deployments.md b/_chapters/ko/automating-react-deployments.md index ca5e310889..f5103e4cc8 100644 --- a/_chapters/ko/automating-react-deployments.md +++ b/_chapters/ko/automating-react-deployments.md @@ -6,7 +6,7 @@ lang: ko description: Git 저장소에 변경 사항을 적용할 때 Create React App을 자동으로 배포하려고 합니다. 이를 위해 Netlify에서 프로젝트를 설정해야합니다. context: true comments_id: automating-react-deployments/188 -ref: automating-react-deployments +ref: creating-a-ci-cd-pipeline-for-react --- 이 가이드의 첫 번째 부분을 따라해 본다면 S3에 Create React App을 배포하고 CloudFront를 CDN으로 사용한다는 사실을 알 수 있습니다. 그런 다음 Route 53을 사용하여 도메인을 구성했습니다. 우리는 또한 도메인의 www 버전을 구성했으며 다른 S3 및 CloudFront 배포가 필요했습니다. 이 과정은 다소 번거로울 수 있습니다. diff --git a/_chapters/ko/automating-serverless-deployments.md b/_chapters/ko/automating-serverless-deployments.md index 85388731f2..5d468fdfcf 100644 --- a/_chapters/ko/automating-serverless-deployments.md +++ b/_chapters/ko/automating-serverless-deployments.md @@ -6,7 +6,7 @@ lang: ko description: Git 저장소에 변경 사항을 적용할 때 Serverless Framework 프로젝트를 자동으로 배포하려고합니다. 이를 위해 Seed (https://seed.run)라는 서비스를 사용하여 serverless 배포를 자동화합니다. CI/CD 파이프 라인을 구성하고 환경을 설정합니다. context: true comments_id: automating-serverless-deployments/174 -ref: automating-serverless-deployments +ref: creating-a-ci-cd-pipeline-for-serverless --- 다음은 우리가 지금까지 구성한 것들을 요약한 내용입니다.: diff --git a/_chapters/ko/call-the-create-api.md b/_chapters/ko/call-the-create-api.md index d2b611dc0b..1c183dd76c 100644 --- a/_chapters/ko/call-the-create-api.md +++ b/_chapters/ko/call-the-create-api.md @@ -13,15 +13,15 @@ comments_id: call-the-create-api/124 AWS Amplify가 가지고있는`API` 모듈을 사용할 필요가 있습니다. -`src/containers/NewNote.js` 헤더에 다음을 추가하여 API 모듈을 포함 시키십시오. +{%change%} `src/containers/NewNote.js` 헤더에 다음을 추가하여 API 모듈을 포함 시키십시오. -``` javascript +```js import { API } from "aws-amplify"; ``` -그리고 `handleSubmit` 함수를 아래와 같이 바꾸십시오. +{%change%} 그리고 `handleSubmit` 함수를 아래와 같이 바꾸십시오. -``` javascript +```js handleSubmit = async event => { event.preventDefault(); @@ -36,7 +36,7 @@ handleSubmit = async event => { await this.createNote({ content: this.state.content }); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e); this.setState({ isLoading: false }); @@ -52,7 +52,7 @@ createNote(note) { 위 내용은 몇 가지 간단한 일을 처리합니다. -1. `/notes`에 POST 요청을 하면서 노트 객체를 전달함으로써 `createNote`를 호출을 합니다. `API.post()` 메쏘드의 처음 두 인자는`notes` 와 `/notes`입니다. 이것은 [AWS Amplify 설정하기]({% link _chapters/configure-aws-amplify.md %}) 챕터에서 우리가 `notes`라는 이름으로 API 세트를 호출했기 때문입니다. +1. `/notes`에 POST 요청을 하면서 노트 객체를 전달함으로써 `createNote`를 호출을 합니다. `API.post()` 메쏘드의 처음 두 인자는`notes` 와 `/notes`입니다. 이것은 [AWS Amplify 설정하기]({% link _chapters/configure-aws-amplify.md %}) 챕터에서 우리가 `notes`라는 이름으로 API 세트를 호출했기 때문입니다. 2. 현재 노트 오브젝트는 단순한 노트의 내용뿐입니다. 일단 첨부 파일없이 노트를 작성하겠습니다. @@ -63,4 +63,3 @@ createNote(note) { ![새 노트 작성 스크린 샷](/assets/new-note-created.png) 다음으로 파일을 S3에 업로드하고 첨부 파일을 노트에 추가해 봅시다. - diff --git a/_chapters/ko/call-the-list-api.md b/_chapters/ko/call-the-list-api.md index 879b57fe2d..af5fe22edb 100644 --- a/_chapters/ko/call-the-list-api.md +++ b/_chapters/ko/call-the-list-api.md @@ -13,9 +13,9 @@ comments_id: call-the-list-api/127 ### 요청 만들기 -`src/containers/Home.js` 파일의 `constructor` 블럭 아래에 다음 내용을 추가합니다. +{%change%} `src/containers/Home.js` 파일의 `constructor` 블럭 아래에 다음 내용을 추가합니다. -``` javascript +```js async componentDidMount() { if (!this.props.isAuthenticated) { return; @@ -36,9 +36,9 @@ notes() { } ``` -그리고 헤더에 Amplify API을 추가합니다. +{%change%} 그리고 헤더에 Amplify API을 추가합니다. -``` javascript +```js import { API } from "aws-amplify"; ``` @@ -48,9 +48,9 @@ import { API } from "aws-amplify"; ### 목록을 렌더링하기 -`renderNotesList` 메소드를 다음 내용으로 바꿉니다. +{%change%} `renderNotesList` 메소드를 다음 내용으로 바꿉니다. -``` coffee +```coffee renderNotesList(notes) { return [{}].concat(notes).map( (note, i) => @@ -77,15 +77,15 @@ renderNotesList(notes) { } ``` -`react-bootstrap` 파일의 헤더에`ListGroupItem`을 포함 시키십시오. +{%change%} `react-bootstrap` 파일의 헤더에`ListGroupItem`을 포함 시키십시오. -``` javascript +```js import { PageHeader, ListGroup, ListGroupItem } from "react-bootstrap"; ``` -또한 `react-router-bootstrap`에서 `LinkContainer`를 포함시킵니다. +{%change%} 또한 `react-router-bootstrap`에서 `LinkContainer`를 포함시킵니다. -``` javascript +```js import { LinkContainer } from "react-router-bootstrap"; ``` @@ -97,9 +97,9 @@ import { LinkContainer } from "react-router-bootstrap"; 3. 그리고 `LinkContainer` 컴포넌트는 앱에서 각각의 노트 아이템으로 이동합니다. -`src/containers/Home.css`에 몇 가지 스타일을 추가합니다. +{%change%} `src/containers/Home.css`에 몇 가지 스타일을 추가합니다. -``` css +```css .Home .notes h4 { font-family: "Open Sans", sans-serif; font-weight: 600; diff --git a/_chapters/ko/changelog.md b/_chapters/ko/changelog.md index 1f5ac12dd0..12abb98061 100644 --- a/_chapters/ko/changelog.md +++ b/_chapters/ko/changelog.md @@ -4,20 +4,20 @@ title: 변경로그 redirect_from: /chapters/older-versions.html lang: ko date: 2018-04-01 00:00:00 -description: A list of all the updates made to Serverless Stack +description: A list of all the updates made to SST comments_id: comments-for-changelog/17 ref: changelog --- -Serverless Stack을 계속해서 업데이트하면서 모든 변경 사항을 명확하게 파악하고 싶습니다. 이는 업데이트 내용을 보완하기 위해 전체 자습서를 다시 살펴 보지 않아도 되도록 하기 위한 것입니다. 또한 참조가 필요할 경우에 대비하여 이전 버전을 그대로 두길 원합니다. 이는 업데이트 된 동안 자습서를 진행하는 독자에게도 유용합니다. +SST을 계속해서 업데이트하면서 모든 변경 사항을 명확하게 파악하고 싶습니다. 이는 업데이트 내용을 보완하기 위해 전체 자습서를 다시 살펴 보지 않아도 되도록 하기 위한 것입니다. 또한 참조가 필요할 경우에 대비하여 이전 버전을 그대로 두길 원합니다. 이는 업데이트 된 동안 자습서를 진행하는 독자에게도 유용합니다. -다음은 Serverless Stack에 대한 업데이트입니다. +다음은 SST에 대한 업데이트입니다. - 각 업데이트에는 **보관 된 버전의 자습서**에 대한 링크가 있습니다. - 자습서에 **대한 최신 버전** 업데이트 - **API 및 클라이언트 저장소** 업데이트 -튜토리얼의 호스트된 버전과 코드 스니펫은 정확하지만 각 챕터의 맨 아래에 링크된 샘플 프로젝트 저장소는 불행히도 아닙니다. 그러나 우리는 완성된 이전 버전의 샘플 프로젝트 저장소를 유지합니다. 그래서 그것들을 알아내기 위해 그것들을 사용할 수 있어야합니다. 이 모든 정보는 [GitHub repo]의 [releases page]({{ site.github_repo }}/release)를 이용할 수 있습니다. +튜토리얼의 호스트된 버전과 코드 스니펫은 정확하지만 각 챕터의 맨 아래에 링크된 샘플 프로젝트 저장소는 불행히도 아닙니다. 그러나 우리는 완성된 이전 버전의 샘플 프로젝트 저장소를 유지합니다. 그래서 그것들을 알아내기 위해 그것들을 사용할 수 있어야합니다. 이 모든 정보는 [GitHub repo]의 [releases page]({{ site.github_repo }}/releases)를 이용할 수 있습니다. 이 업데이트는 [뉴스 레터]({{ site.newsletter_signup_form }})를 통해 이메일로받을 수 있습니다. diff --git a/_chapters/ko/clear-the-session-on-logout.md b/_chapters/ko/clear-the-session-on-logout.md index 9092d8962c..781411e98d 100644 --- a/_chapters/ko/clear-the-session-on-logout.md +++ b/_chapters/ko/clear-the-session-on-logout.md @@ -4,7 +4,7 @@ title: Clear the Session on Logout date: 2017-01-16 00:00:00 lang: ko ref: clear-the-session-on-logout -description: 사용자가 로그아웃 할 때 React.js 앱에서 로그인 한 사용자의 Amazon Cognito 세션을 삭제해야합니다. AWS Amplify의 Auth.signOut() 메소드를 사용하여 이를 수행 할 수 있습니다. +description: 사용자가 로그아웃 할 때 React.js 앱에서 로그인 한 사용자의 Amazon Cognito 세션을 삭제해야합니다. AWS Amplify의 Auth.signOut() 메소드를 사용하여 이를 수행 할 수 있습니다. context: true comments_id: clear-the-session-on-logout/70 --- @@ -13,17 +13,16 @@ comments_id: clear-the-session-on-logout/70 AWS Amplify에는 Auth.signOut() 메소드가 있습니다. -`src/App.js`의 `handleLogout` 메쏘드를 다음과 같이 바꿉니다: +`src/App.js`의 `handleLogout` 메쏘드를 다음과 같이 바꿉니다: -``` javascript -handleLogout = async event => { +```js +handleLogout = async (event) => { await Auth.signOut(); this.userHasAuthenticated(false); -} +}; ``` 이제 브라우저로 가서 로그아웃 한 다음, 페이지를 새로 고침하십시오. 완전히 로그아웃되어야합니다. 처음부터 전체 로그인 흐름을 시험해 보면 알 수 있겠지만 우리는 전체 프로세스를 통해 로그인 페이지에만 계속 머물러 있습니다. 다음으로, 우리는 페이지 리디렉션을 통해 로그인하고 로그아웃하는 흐름을 보다 명확하게 만들겠습니다. - diff --git a/_chapters/ko/configure-aws-amplify.md b/_chapters/ko/configure-aws-amplify.md index 11519afd51..5abdd11b38 100644 --- a/_chapters/ko/configure-aws-amplify.md +++ b/_chapters/ko/configure-aws-amplify.md @@ -15,7 +15,7 @@ AWS Amplify는 백엔드에 쉽게 연결할 수 있도록 몇 가지 간단한 ### AWS Amplify 설치하기 -작업 디렉토리에서 다음 명령을 실행합니다. +{%change%} 작업 디렉토리에서 다음 명령을 실행합니다. ``` bash $ npm install aws-amplify --save @@ -27,7 +27,7 @@ NPM 패키지를 설치하면 `package.json`에 의존성이 추가됩니다. 먼저 우리가 만든 모든 리소스를 참조 할 수 있도록 앱의 구성 파일을 만들어 보겠습니다. -`src/config.js` 파일을 만들고 다음 내용을 추가합니다. +{%change%} `src/config.js` 파일을 만들고 다음 내용을 추가합니다. ``` coffee export default { @@ -49,19 +49,19 @@ export default { ``` 여기에서 다음을 대체해야합니다. -1. [파일 업로드를위한 S3 버킷 생성]({% link _chapters/create-an-s3-bucket-for-file-uploads.md %}) 챕터에서 S3 버킷 이름 및 리전을 나타내는 `YOUR_S3_UPLOADS_BUCKET_NAME` 및 `YOUR_S3_UPLOADS_BUCKET_REGION` 값을 대체합니다. 여기서는`notes-app-uploads`와`us-east-1`입니다. +1. [파일 업로드를위한 S3 버킷 생성]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}) 챕터에서 S3 버킷 이름 및 리전을 나타내는 `YOUR_S3_UPLOADS_BUCKET_NAME` 및 `YOUR_S3_UPLOADS_BUCKET_REGION` 값을 대체합니다. 여기서는`notes-app-uploads`와`us-east-1`입니다. -2. [API 배포] ({% link _chapters/deploy-the-apis.md %}) 챕터에서 설명한 `YOUR_API_GATEWAY_URL` 및 `YOUR_API_GATEWAY_REGION` 값을 대체합니다. 여기에서 URL은`https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod`이고 리전은 `us-east-1`입니다. +2. [API 배포] ({% link _archives/deploy-the-apis.md %}) 챕터에서 설명한 `YOUR_API_GATEWAY_URL` 및 `YOUR_API_GATEWAY_REGION` 값을 대체합니다. 여기에서 URL은`https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod`이고 리전은 `us-east-1`입니다. -3. Cognito **Pool Id**, **App Client id** 및 [Cognito 사용자 풀 만들기]({% link _chapters/create-a-cognito-user-pool.md %}) 챕터를 참조하여 `YOUR_COGNITO_USER_POOL_ID`, `YOUR_COGNITO_APP_CLIENT_ID` 및 `YOUR_COGNITO_REGION` 값을 대체합니다. +3. Cognito **Pool Id**, **App Client id** 및 [Cognito 사용자 풀 만들기]({% link _archives/create-a-cognito-user-pool.md %}) 챕터를 참조하여 `YOUR_COGNITO_USER_POOL_ID`, `YOUR_COGNITO_APP_CLIENT_ID` 및 `YOUR_COGNITO_REGION` 값을 대체합니다. -4. [Cognito ID 풀 만들기] ({% link _chapters/create-a-cognito-identity-pool.md %}) 챕터의 **자격 증명 풀 ID**를 참조하여 `YOUR_IDENTITY_POOL_ID` 값을 대체합니다. +4. [Cognito ID 풀 만들기] ({% link _archives/create-a-cognito-identity-pool.md %}) 챕터의 **자격 증명 풀 ID**를 참조하여 `YOUR_IDENTITY_POOL_ID` 값을 대체합니다. ### AWS Amplify 추가하기 다음으로 AWS Amplify를 설정합니다. -`src/index.js`의 헤더에 다음 내용을 추가하여 import합니다. +{%change%} `src/index.js`의 헤더에 다음 내용을 추가하여 import합니다. ``` coffee import Amplify from "aws-amplify"; @@ -69,13 +69,13 @@ import Amplify from "aws-amplify"; 그리고 위에서 만든 config를 불러옵니다. -역시 `src/index.js` 헤더에 다음 내용을 추가합니다. +{%change%} 역시 `src/index.js` 헤더에 다음 내용을 추가합니다. ``` coffee import config from "./config"; ``` -그리고 AWS Amplify를 초기화합니다. `src/index.js`의`ReactDOM.render` 행 위에 다음을 추가하십시오. +{%change%} 그리고 AWS Amplify를 초기화합니다. `src/index.js`의`ReactDOM.render` 행 위에 다음을 추가하십시오. ``` coffee Amplify.configure({ diff --git a/_chapters/ko/configure-cognito-identity-pool-in-serverless.md b/_chapters/ko/configure-cognito-identity-pool-in-serverless.md index b424935427..d1c6a0c84c 100644 --- a/_chapters/ko/configure-cognito-identity-pool-in-serverless.md +++ b/_chapters/ko/configure-cognito-identity-pool-in-serverless.md @@ -13,7 +13,7 @@ ref: configure-cognito-identity-pool-in-serverless ### 리소스 만들기 -`resources/cognito-identity-pool.yml`에 다음 내용을 추가합니다. +{%change%} `resources/cognito-identity-pool.yml`에 다음 내용을 추가합니다. ``` yml Resources: @@ -110,7 +110,7 @@ Outputs: Ref: CognitoIdentityPool ``` -여기서 많은 작업이 일어나고 있는 것처럼 보입니다. 그러나 우리가 [Cognito ID 풀 만들기]({% link _chapters/create-a-cognito-identity-pool.md %}) 챕터에서했던 것과 거의 같습니다. CloudFormation이 보다 자세한 정보일 수 있으며 약간 어렵게 보일 수도 있습니다. +여기서 많은 작업이 일어나고 있는 것처럼 보입니다. 그러나 우리가 [Cognito ID 풀 만들기]({% link _archives/create-a-cognito-identity-pool.md %}) 챕터에서했던 것과 거의 같습니다. CloudFormation이 보다 자세한 정보일 수 있으며 약간 어렵게 보일 수도 있습니다. 이 구성의 여러 부분을 빠르게 살펴 보겠습니다. @@ -118,11 +118,11 @@ Outputs: 2. `AllowUnauthenticatedIdentities:false`를 추가하여 로그인한 사용자만 원한다고 설정했습니다. -3. 다음으로 사용자 풀을 ID 공급자로 사용하겠다고 명시합니다. 우리는 특별히 `Ref:CognitoUserPoolClient` 라인을 사용하여 이를 수행합니다. 다시 [Serverless에서 Cognito 사용자 풀 만들기]({% link _chapters/configure-cognito-user-pool-in-serverless.md %}) 챕터를 참조하면 `CognitoUserPoolClient` 블럭이 있음을 알 수 있습니다. 그리고 여기에서 참조하고있다. +3. 다음으로 사용자 풀을 ID 공급자로 사용하겠다고 명시합니다. 우리는 특별히 `Ref:CognitoUserPoolClient` 라인을 사용하여 이를 수행합니다. 다시 [Serverless에서 Cognito 사용자 풀 만들기]({% link _archives/configure-cognito-user-pool-in-serverless.md %}) 챕터를 참조하면 `CognitoUserPoolClient` 블럭이 있음을 알 수 있습니다. 그리고 여기에서 참조하고있다. 4. 그런 다음 인증된 사용자에게 IAM 역할을 부여합니다. -5. 이 역할에 다양한 요소를 추가합니다. 이것은 우리가 [Cognito ID 풀 만들기]({% link _chapters/create-a-cognito-identity-pool.md %}) 챕터에서 사용하는 방법과 같습니다. CloudFormation을 사용하려면 이 방법으로 포맷해야합니다. +5. 이 역할에 다양한 요소를 추가합니다. 이것은 우리가 [Cognito ID 풀 만들기]({% link _archives/create-a-cognito-identity-pool.md %}) 챕터에서 사용하는 방법과 같습니다. CloudFormation을 사용하려면 이 방법으로 포맷해야합니다. 6. `apiGatewayRestApi` ref는 serverless 프레임 워크가 `serverless.yml`에서 API 엔드포인트를 정의할 때 생성됩니다. 따라서이 경우에는 생성중인 API 리소스를 참조하고 있습니다. @@ -132,7 +132,7 @@ Outputs: ### 리소스 추가 -`serverless.yml`에서 `resources:` 블럭을 다음으로 대체하십시오. +{%change%} `serverless.yml`에서 `resources:` 블럭을 다음으로 대체하십시오. ``` yml # Create our resources with separate CloudFormation templates @@ -150,7 +150,7 @@ resources: ### 코드 커밋 -지금까지 변경 사항을 커밋합니다. +{%change%} 지금까지 변경 사항을 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/configure-cognito-user-pool-in-serverless.md b/_chapters/ko/configure-cognito-user-pool-in-serverless.md index 6311805cc0..56f7268a32 100644 --- a/_chapters/ko/configure-cognito-user-pool-in-serverless.md +++ b/_chapters/ko/configure-cognito-user-pool-in-serverless.md @@ -9,11 +9,11 @@ comments_id: configure-cognito-user-pool-in-serverless/164 ref: configure-cognito-user-pool-in-serverless --- -이제 'serverless.yml'을 통해 Cognito 사용자 풀을 설정하는 방법을 살펴 보겠습니다. 이것은 [Cognito 사용자 풀 만들기]({% link _chapters/create-a-cognito-user-pool.md %}) 챕터에서 직접 작성한 것과 매우 유사해야합니다. +이제 'serverless.yml'을 통해 Cognito 사용자 풀을 설정하는 방법을 살펴 보겠습니다. 이것은 [Cognito 사용자 풀 만들기]({% link _archives/create-a-cognito-user-pool.md %}) 챕터에서 직접 작성한 것과 매우 유사해야합니다. ### 리소스 만들기 -`resources/cognito-user-pool.yml`에 아래 내용을 추가합니다. +{%change%} `resources/cognito-user-pool.yml`에 아래 내용을 추가합니다. ``` yml Resources: @@ -60,7 +60,7 @@ Outputs: ### 리소스 추가 -`serverless.yml`에서 자원을 참조합니다. `resources:` 블럭을 다음으로 대체하십시오. +{%change%} `serverless.yml`에서 자원을 참조합니다. `resources:` 블럭을 다음으로 대체하십시오. ``` yml # Create our resources with separate CloudFormation templates @@ -77,7 +77,7 @@ resources: ### 코드 커밋 -지금까지 변경 내용을 커밋합니다. +{%change%} 지금까지 변경 내용을 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/configure-dynamodb-in-serverless.md b/_chapters/ko/configure-dynamodb-in-serverless.md index 9d534377be..b343c3f8fc 100644 --- a/_chapters/ko/configure-dynamodb-in-serverless.md +++ b/_chapters/ko/configure-dynamodb-in-serverless.md @@ -13,7 +13,7 @@ ref: configure-dynamodb-in-serverless ### 리소스 만들기 -`resources/dynamodb-table.yml`에 아래 내용을 추가합니다. +{%change%} `resources/dynamodb-table.yml`에 아래 내용을 추가합니다. ``` yml Resources: @@ -51,7 +51,7 @@ Resources: 이제 프로젝트에서 이 리소스에 대한 참조를 추가해 보겠습니다. -`serverless.yml` 파일의 아래쪽에 있는 `resources:` 블럭 내용을 다음으로 대체합니다.: +{%change%} `serverless.yml` 파일의 아래쪽에 있는 `resources:` 블럭 내용을 다음으로 대체합니다.: ``` yml # Create our resources with separate CloudFormation templates @@ -62,12 +62,12 @@ resources: - ${file(resources/dynamodb-table.yml)} ``` -`serverless.yml` 위쪽에 `custom:` 블럭을 다음 내용으로 대체합니다.: +{%change%} `serverless.yml` 위쪽에 `custom:` 블럭을 다음 내용으로 대체합니다.: ``` yml custom: # Our stage is based on what is passed in when running serverless - # commands. Or fallsback to what we have set in the provider section. + # commands. Or falls back to what we have set in the provider section. stage: ${opt:stage, self:provider.stage} # Set the table name here so we can use it while testing locally tableName: ${self:custom.stage}-notes @@ -96,7 +96,7 @@ custom: 또한 생성하려는 DynamoDB 리소스를 참조할 수 있도록 변경을 빠르게 처리하겠습니다. -`serverless.yml`의 `iamRoleStatements:` 블럭을 다음으로 대체하십시오. +{%change%} `serverless.yml`의 `iamRoleStatements:` 블럭을 다음으로 대체하십시오. ``` yml # These environment variables are made available to our functions @@ -133,7 +133,7 @@ custom: ### 코드 커밋 -지금까지 수정한 내용을 커밋합니다. +{%change%} 지금까지 수정한 내용을 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/configure-s3-in-serverless.md b/_chapters/ko/configure-s3-in-serverless.md index 48c5851558..2e07608af6 100644 --- a/_chapters/ko/configure-s3-in-serverless.md +++ b/_chapters/ko/configure-s3-in-serverless.md @@ -14,7 +14,7 @@ ref: configure-s3-in-serverless ### 리소스 만들기 -`resources/s3-bucket.yml` 파일을 만들고 아래 내용을 추가합니다. +{%change%} `resources/s3-bucket.yml` 파일을 만들고 아래 내용을 추가합니다. ``` yml Resources: @@ -44,13 +44,13 @@ Outputs: Ref: AttachmentsBucket ``` -[파일 업로드를 위한 S3 버킷 생성]({% link _chapters/create-an-s3-bucket-for-file-uploads.md %}) 챕터를 떠올려 보면 버킷을 만들고 CORS 정책을 구성했습니다. 프론트엔드 클라이언트에서 직접 업로드를 해야만 했기 때문에 이 작업을 수행해야 했습니다. 여기서도 동일한 정책을 구성합니다. +[파일 업로드를 위한 S3 버킷 생성]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}) 챕터를 떠올려 보면 버킷을 만들고 CORS 정책을 구성했습니다. 프론트엔드 클라이언트에서 직접 업로드를 해야만 했기 때문에 이 작업을 수행해야 했습니다. 여기서도 동일한 정책을 구성합니다. S3 버킷(DynamoDB 테이블과 달리)은 전역적으로 이름이 지정됩니다. 그래서 사전에 적합한 이름이 무엇인지를 알 수가 없습니다. 따라서 CloudFormation에서 버킷 이름을 생성하게하고 나중에 출력할 수 있도록 `Outputs:` 블럭을 추가합니다. ### 리소스 추가 -`serverless.yml`에서 자원을 참조합니다. `resources :` 블럭을 다음으로 대체하십시오. +{%change%} `serverless.yml`에서 자원을 참조합니다. `resources :` 블럭을 다음으로 대체하십시오. ``` yml # Create our resources with separate CloudFormation templates @@ -65,7 +65,7 @@ resources: ### 코드 커밋 -지금까지 변경한 내용을 커밋합니다. +{%change%} 지금까지 변경한 내용을 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/configure-secrets-in-seed.md b/_chapters/ko/configure-secrets-in-seed.md index c7949638d9..a9a0003573 100644 --- a/_chapters/ko/configure-secrets-in-seed.md +++ b/_chapters/ko/configure-secrets-in-seed.md @@ -19,7 +19,7 @@ ref: configure-secrets-in-seed ![dev env 변수 설정 화면](/assets/part2/show-dev-env-variables-settings.png) -그리고 **Key**로 `stripeSecretKey`를 입력하고 그 값은 [env.yml에서 비밀 키 불러오기]({% link _chapters/load-secrets-from-env-yml.md %}) 챕터의 `STRIPE_TEST_SECRET_KEY` 값을 입력합니다. 비밀 키를 저장하려면 **추가**를 누르십시오. +그리고 **Key**로 `stripeSecretKey`를 입력하고 그 값은 [env.yml에서 비밀 키 불러오기]({% link _archives/load-secrets-from-env.md %}) 챕터의 `STRIPE_TEST_SECRET_KEY` 값을 입력합니다. 비밀 키를 저장하려면 **추가**를 누르십시오. ![dev 환경 변수로 비밀 키 추가 화면](/assets/part2/add-secret-dev-environment-variable.png) @@ -31,7 +31,7 @@ ref: configure-secrets-in-seed ![Show prod env 변수 설정 화면](/assets/part2/show-prod-env-variables-settings.png) -그리고 **Key**로 `stripeSecretKey`를 입력하고 그 값은 [env.yml에서 비밀 키 불러오기]({% link _chapters/load-secrets-from-env-yml.md %}) 챕터의 `STRIPE_PROD_SECRET_KEY` 값을 입력합니다. 비밀 키를 저장하려면 **추가**를 누르십시오. +그리고 **Key**로 `stripeSecretKey`를 입력하고 그 값은 [env.yml에서 비밀 키 불러오기]({% link _archives/load-secrets-from-env.md %}) 챕터의 `STRIPE_PROD_SECRET_KEY` 값을 입력합니다. 비밀 키를 저장하려면 **추가**를 누르십시오. ![prod 환경 변수로 비밀 키 추가 화면](/assets/part2/add-secret-prod-environment-variable.png) diff --git a/_chapters/ko/configure-the-aws-cli.md b/_chapters/ko/configure-the-aws-cli.md index 675b7af0f9..2e5ac193cd 100644 --- a/_chapters/ko/configure-the-aws-cli.md +++ b/_chapters/ko/configure-the-aws-cli.md @@ -18,7 +18,7 @@ AWS CLI는 Python 2 버전 2.6.5+ 또는 Python 3 버전 3.3+ 와 [Pip](https:// - [Python 인스톨하기](https://www.python.org/downloads/) - [Pip 인스톨하기](https://pip.pypa.io/en/stable/installing/) -이제 Pip를 실행서 AWS CLI (Linux, macOS, 또는 Unix)를 설치할 수 있습니다: +{%change%} 이제 Pip를 실행서 AWS CLI (Linux, macOS, 또는 Unix)를 설치할 수 있습니다: ``` bash $ sudo pip install awscli @@ -41,7 +41,7 @@ $ brew install awscli - 액세스 키 ID **AKIAIOSFODNN7EXAMPLE** - 보안 액세스 키 **wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY** -비밀 키 ID와 액세스 키를 설정하기 위해 다음을 실행하면됩니다. +{%change%} 비밀 키 ID와 액세스 키를 설정하기 위해 다음을 실행하면됩니다. ``` bash $ aws configure diff --git a/_chapters/ko/connect-the-billing-form.md b/_chapters/ko/connect-the-billing-form.md index 72e8966452..120e6847db 100644 --- a/_chapters/ko/connect-the-billing-form.md +++ b/_chapters/ko/connect-the-billing-form.md @@ -13,15 +13,15 @@ ref: connect-the-billing-form Stripe.js를 HTML에 포함시켜 보겠습니다. -`public/index.html` 파일의 `` 아래에 다음을 추가합니다. +{%change%} `public/index.html` 파일의 `` 아래에 다음을 추가합니다. -``` html +```html ``` -`src/containers/Settings.js`에 `render` 메소드를 다음으로 대체합니다. +{%change%} `src/containers/Settings.js`에 `render` 메소드를 다음으로 대체합니다. -``` coffee +```coffee handleFormSubmit = async (storage, { token, error }) => { if (error) { alert(error); @@ -37,7 +37,7 @@ handleFormSubmit = async (storage, { token, error }) => { }); alert("Your card has been charged successfully!"); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e); this.setState({ isLoading: false }); @@ -60,9 +60,9 @@ render() { } ``` -그리고 헤더에 다음 내용을 추가합니다. +{%change%} 그리고 헤더에 다음 내용을 추가합니다. -``` js +```js import { Elements, StripeProvider } from "react-stripe-elements"; import BillingForm from "../components/BillingForm"; import config from "../config"; @@ -75,9 +75,9 @@ import "./Settings.css"; 마지막으로 Setting 페이지의 스타일을 전체적으로 처리해 보겠습니다. -`src/containers/Settings.css`에 다음을 추가합니다. +{%change%} `src/containers/Settings.css`에 다음을 추가합니다. -``` css +```css @media all and (min-width: 480px) { .Settings { padding: 60px 0; @@ -108,9 +108,9 @@ Stripe 테스트 카드에 대한 자세한 내용은 [Stripe API Docs](https:// ### 변경 사항 적용 -Git에 빠르게 커밋합니다. +{%change%} Git에 빠르게 커밋합니다. -``` bash +```bash $ git add . $ git commit -m "Connecting the billing form" ``` diff --git a/_chapters/ko/create-a-billing-form.md b/_chapters/ko/create-a-billing-form.md index db3bd2cefe..4ee5498bb4 100644 --- a/_chapters/ko/create-a-billing-form.md +++ b/_chapters/ko/create-a-billing-form.md @@ -11,7 +11,7 @@ ref: create-a-billing-form 이제 설정 페이지에는 사용자의 신용 카드 정보를 가져 와서 Stripe 토큰을 받고 결제 API를 호출하는 양식을 추가합니다. Stripe React SDK를 프로젝트에 추가해 보겠습니다. -프로젝트 루트에서 다음을 실행합니다. +{%change%} 프로젝트 루트에서 다음을 실행합니다. ``` bash $ npm install --save react-stripe-elements @@ -19,12 +19,12 @@ $ npm install --save react-stripe-elements 다음으로 청구서 양식 컴포넌트를 생성합니다. -아래 내용을 추가한 `src/components/BillingForm.js`파일을 생성합니다. +{%change%} 아래 내용을 추가한 `src/components/BillingForm.js`파일을 생성합니다. {% raw %} ``` coffee import React, { Component } from "react"; -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; +import { FormGroup, FormControl, FormLabel } from "react-bootstrap"; import { CardElement, injectStripe } from "react-stripe-elements"; import LoaderButton from "./LoaderButton"; import "./BillingForm.css"; @@ -81,7 +81,7 @@ class BillingForm extends Component { return (
- Storage + Storage
- Cardholder's name + Cardholder's name - Credit Card Info + Credit Card Info 아래 내용으로 `src/components/BillingForm.css` 파일을 생성합니다. +{%change%} 아래 내용으로 `src/components/BillingForm.css` 파일을 생성합니다. ``` css .BillingForm .card-field { @@ -165,7 +165,7 @@ export default injectStripe(BillingForm); ### 변경 사항 커밋 -Git에 변경 사항을 빠르게 커밋합니다. +{%change%} Git에 변경 사항을 빠르게 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/create-a-build-script.md b/_chapters/ko/create-a-build-script.md index 85d67d8ad1..c0b7842bb0 100644 --- a/_chapters/ko/create-a-build-script.md +++ b/_chapters/ko/create-a-build-script.md @@ -1,19 +1,19 @@ --- layout: post -title: Create a Build Script +title: Create a Netlify Build Script date: 2018-03-26 00:00:00 lang: ko code: frontend_full description: Netlify로 Create React App을 구성하려면 프로젝트 루트에 빌드 스크립트를 추가해야합니다. 우리가 React Router 라우트에 대해 HTTP 상태 코드 200을 반환하도록하려면 리다이렉트 규칙을 추가해야합니다. comments_id: create-a-build-script/189 -ref: create-a-build-script +ref: create-a-netlify-build-script --- 프로젝트를 [Netlify](https://www.netlify.com)에 추가하기 전에 빌드 스크립트를 설정합니다. 이전의 상황을 떠올려보면, 우리는 `REACT_APP_STAGE` 빌드 환경 변수를 사용하도록 애플리케이션을 구성했었습니다. 우리는 Netlify가 다른 배포 사례에 대해 이 변수를 설정하도록 빌드 스크립트를 작성하려고합니다. ### Netlify 빌드 스크립트 추가 -프로젝트 루트에 `netlify.toml` 파일을 생성 후 다음 내용을 추가합니다. +{%change%} 프로젝트 루트에 `netlify.toml` 파일을 생성 후 다음 내용을 추가합니다. ``` toml # Global settings applied to the whole site. @@ -48,7 +48,7 @@ ref: create-a-build-script 2. `publish` 옵션은 빌드가 생성되는 곳을 가리 킵니다. React Create App의 경우 프로젝트 루트의 `build` 디렉토리입니다. -3. `command` 옵션은 Netlify가 사용할 빌드 명령입니다. [Create React App의 환경 관리]({% link _chapters/manage-environments-in-create-react-app.md %}) 챕터를 떠올려보십시오. 기본 컨텍스트에서 명령은 `REACT_APP_STAGE=dev npm run build`입니다. +3. `command` 옵션은 Netlify가 사용할 빌드 명령입니다. [Create React App의 환경 관리]({% link _archives/manage-environments-in-create-react-app.md %}) 챕터를 떠올려보십시오. 기본 컨텍스트에서 명령은 `REACT_APP_STAGE=dev npm run build`입니다. `context.production`이라는 운영 컨텍스트는 `REACT_APP_STAGE` 변수를 `prod`로 설정한 유일한 컨텍스트입니다. 이것은 우리가 `마스터`에게 푸시할 때 실행됩니다. `branch-deploy`는 다른 비 운영 브랜치로 푸시할 때 사용할 것입니다. 그리고 `deploy-preview`는 PR 요청을위한 것입니다. @@ -56,7 +56,7 @@ ref: create-a-build-script 튜토리얼의 첫 번째 파트와 마찬가지로 앱의 경로가 루트가 아닌 경우, 이에 대한 요청을 처리해야합니다. 프론트엔드는 단일 페이지 앱이며 라우팅은 클라이언트측에서 처리됩니다. 우리는 Netlify에게 요청을 항상 우리의`index.html`에 리다이렉트시키고 200 상태 코드를 리턴 할 필요가 있습니다. -이를 위해, `netlify.toml` 아래에 리디렉션 규칙을 추가합니다.: +{%change%} 이를 위해, `netlify.toml` 아래에 리디렉션 규칙을 추가합니다.: ``` toml # Always redirect any request to our index.html @@ -72,7 +72,7 @@ ref: create-a-build-script 애플리케이션을 Netlify에 배포하기 위해 우리는`package.json`의 빌드 명령어를 수정해야 합니다. -`package.json`에 있는`scripts` 블록을 이것으로 바꾸십시오. +{%change%} `package.json`에 있는`scripts` 블록을 이것으로 바꾸십시오. ``` coffee "scripts": { @@ -87,7 +87,7 @@ ref: create-a-build-script ### 변경 사항 커밋 -Git에 빠르게 커밋합니다. +{%change%} Git에 빠르게 커밋합니다. ``` bash $ git add . @@ -96,7 +96,7 @@ $ git commit -m "Adding a Netlify build script" ### 변경 사항 푸시 -우리는 프로젝트에 많은 변경들을 반영했습니다. 이제 GitHub으로 이동해 봅시다. +{%change%} 우리는 프로젝트에 많은 변경들을 반영했습니다. 이제 GitHub으로 이동해 봅시다. ``` bash $ git push diff --git a/_chapters/ko/create-a-cognito-identity-pool.md b/_chapters/ko/create-a-cognito-identity-pool.md index 54d213745c..e673a767af 100644 --- a/_chapters/ko/create-a-cognito-identity-pool.md +++ b/_chapters/ko/create-a-cognito-identity-pool.md @@ -31,7 +31,7 @@ Amazon Cognito 연동 자격증명은 개발자가 사용자에 대해 고유 ![Cognito 자격 증명 풀 정보 입력 화면](/assets/cognito-identity-pool/fill-identity-pool-info.png) -**인증 공급자**를 선택하고 **Cognito** 탭 아래에 [Cognito 사용자 풀 만들기]({% link _chapters/create-a-cognito-user-pool.md %}) 챕터에서 만들었던 사용자 풀의 **사용자 풀 ID**와 **App Client ID**를 입력합니다. **풀 생성**을 클릭합니다. +**인증 공급자**를 선택하고 **Cognito** 탭 아래에 [Cognito 사용자 풀 만들기]({% link _archives/create-a-cognito-user-pool.md %}) 챕터에서 만들었던 사용자 풀의 **사용자 풀 ID**와 **App Client ID**를 입력합니다. **풀 생성**을 클릭합니다. ![인증 공급자 정보 입력 화면](/assets/cognito-identity-pool/fill-authentication-provider-info.png) @@ -47,7 +47,7 @@ Amazon Cognito 연동 자격증명은 개발자가 사용자에 대해 고유 ![정책 편집 확인 버튼 선택 화면](/assets/cognito-identity-pool/select-confirm-edit-policy.png) -아래 정책을 편집화면에 추가합니다. 그리고 `YOUR_S3_UPLOADS_BUCKET_NAME`을 [S3 파일 업로드 버킷 만들기]({% link _chapters/create-an-s3-bucket-for-file-uploads.md %}) 챕터에서 만든 **버킷 이름**으로 대체합니다. 그리고 지난 장에서 여러분이 만든 API 배포시 확인한 `YOUR_API_GATEWAY_REGION` 와 `YOUR_API_GATEWAY_ID`을 입력합니다. +{%change%} 아래 정책을 편집화면에 추가합니다. 그리고 `YOUR_S3_UPLOADS_BUCKET_NAME`을 [S3 파일 업로드 버킷 만들기]({% link _archives/create-an-s3-bucket-for-file-uploads.md %}) 챕터에서 만든 **버킷 이름**으로 대체합니다. 그리고 지난 장에서 여러분이 만든 API 배포시 확인한 `YOUR_API_GATEWAY_REGION` 와 `YOUR_API_GATEWAY_ID`을 입력합니다. 여기에서는 `YOUR_S3_UPLOADS_BUCKET_NAME` 는 `notes-app-uploads`로, `YOUR_API_GATEWAY_ID` 는 `ly55wbovq4`, 그리고 `YOUR_API_GATEWAY_REGION` 는 `us-east-1`로 입력합니다. diff --git a/_chapters/ko/create-a-cognito-test-user.md b/_chapters/ko/create-a-cognito-test-user.md index 4782da4367..75e03eba07 100644 --- a/_chapters/ko/create-a-cognito-test-user.md +++ b/_chapters/ko/create-a-cognito-test-user.md @@ -15,7 +15,7 @@ comments_id: create-a-cognito-test-user/126 먼저 AWS CLI를 사용하여 이메일과 비밀번호로 사용자를 등록합니다. -여러분의 터미널에서 실행합니다. +{%change%} 여러분의 터미널에서 실행합니다. ``` bash $ aws cognito-idp sign-up \ @@ -27,7 +27,7 @@ $ aws cognito-idp sign-up \ 이제 사용자는 Cognito 사용자 풀에서 생성됩니다. 그러나 사용자가 사용자 풀을 사용하여 인증을 받기 전에 계정을 확인해야합니다. 관리자 명령을 사용하여 사용자를 신속하게 확인해 봅시다. -여러분의 터미널에서 실행합니다. +{%change%} 여러분의 터미널에서 실행합니다. ``` bash $ aws cognito-idp admin-confirm-sign-up \ diff --git a/_chapters/ko/create-a-dynamodb-table.md b/_chapters/ko/create-a-dynamodb-table.md index 56880b357f..9dd304c081 100644 --- a/_chapters/ko/create-a-dynamodb-table.md +++ b/_chapters/ko/create-a-dynamodb-table.md @@ -40,13 +40,17 @@ DynamoDB에서 인덱스가 작동하는 방식에 대한 이해를 돕기 위 다음 메시지가 표시되는 화면에서 **기본 설정 사용**을 선택 취소하십시오. -![자동 스케일링 IAM 역할 경고 스크린샷](/assets/dynamodb/auto-scaling-iam-role-warning.png) +![기본 설정 사용 선택 취소 스크린샷](/assets/dynamodb/deselect-use-default-settings.png) -맨 아래로 스크롤하여 **DynamoDB AutoScaling Service Linked Role**이 선택되었는지 확인하고 **생성**을 선택하십시오. +맨 아래로 스크롤하여 Provisioned 대신 **On-demand**가 선택되었는지 확인하십시오. -![프로비저닝 된 용량 테이블 설정 스크린샷](/assets/dynamodb/set-table-provisioned-capacity.png) +![프로비저닝대신 온디맨드 선택 스크린샷](/assets/dynamodb/select-on-demand-capacity.png) -그렇지 않으면 **기본 설정 사용**이 선택되어 있는지 확인한 다음 **생성**을 선택합니다. +기본적으로 우리는 온디맨드를 사용하겠습니다. 프로비저닝에 관한 [더 많은 정보는 여기](https://aws.amazon.com/dynamodb/pricing/provisioned/)에 있습니다. + +그 다음 **기본 설정 사용**이 선택되어 있는지 확인한 다음 **생성**을 선택합니다. + +![디폴트 선택하고 생성하는 스크린샷](/assets/dynamodb/create-dynamodb-table.png) 기본 설정은 5개의 읽기와 5개의 쓰기를 제공합니다. 테이블을 작성할 때 읽기와 쓰기에 예약할 처리 용량을 지정합니다. DynamoDB는 처리 요구량을 충족시키는 데 필요한 리소스를 예약하는 동시에 일관성 있고 짧은 지연 시간에 대한 성능을 보장합니다. 하나의 읽기 용량 단위는 초당 최대 8KB를 읽을 수 있으며 하나의 쓰기 용량 단위는 초당 최대 1KB를 쓸 수 있습니다. 프로비저닝된 처리량 설정을 변경하여 필요에 따라 용량을 늘리거나 줄일 수 있습니다. @@ -54,7 +58,7 @@ DynamoDB에서 인덱스가 작동하는 방식에 대한 이해를 돕기 위 ![DynamoDB 서비스 선택 스크린샷](/assets/dynamodb/dynamodb-table-created.png) -DynamoDB 테이블에 대한 백업을 설정하는 것이 좋습니다. 특히 운영 환경에서 사용하는 경우에는 말이죠. 백업에 관해서는 별도 크래딧 챕터에서 다룹니다. [DynamoDB에서의 백업]({% link _chapters/backups-in-dynamodb.md %}). +DynamoDB 테이블에 대한 백업을 설정하는 것이 좋습니다. 특히 운영 환경에서 사용하는 경우에는 말이죠. 백업에 관해서는 별도 크래딧 챕터에서 다룹니다. [DynamoDB에서의 백업]({% link _archives/backups-in-dynamodb.md %}). 다음으로 파일 업로드를 처리할 수 있는 S3 버킷을 설정합니다. diff --git a/_chapters/ko/create-a-login-page.md b/_chapters/ko/create-a-login-page.md index 2bbf7e584d..d4d2dd82b6 100644 --- a/_chapters/ko/create-a-login-page.md +++ b/_chapters/ko/create-a-login-page.md @@ -6,19 +6,20 @@ ref: create-a-login-page description: React.js 앱에 로그인 페이지를 추가합니다. 로그인 양식을 만들기 위해서 FormGroup과 FormControl React-Bootstrap 컴포넌트들을 사용합니다. context: true comments_id: create-a-login-page/71 + --- 사용자가 자격 증명으로 로그인할 수 있는 페이지를 만들어 보겠습니다. 사용자가 로그인하거나 회원 가입시에 사용자 이름으로 이메일을 등록 할 수 있도록 이미 앞선 챕터에서 사용자 풀을 만들어 처리해 두었습니다. 나중에 가입 양식을 만들 때에도 다시 언급하겠습니다. 먼저 사용자의 이메일(사용자 이름)과 비밀번호를 입력받기 위한 기본 양식을 만들어 보겠습니다. -### 컨테이너 추가하기 +### 컨테이너 추가하기 -`src/containers/Login.js` 파일을 만들고 다음 내용을 추가합니다. +{%change%} `src/containers/Login.js` 파일을 만들고 다음 내용을 추가합니다. -``` coffee +```coffee import React, { Component } from "react"; -import { Button, FormGroup, FormControl, ControlLabel } from "react-bootstrap"; +import { Button, FormGroup, FormControl, FormLabel } from "react-bootstrap"; import "./Login.css"; export default class Login extends Component { @@ -50,7 +51,7 @@ export default class Login extends Component {
- Email + Email - Password + Password `src/containers/Login.css` 파일 안에 몇 가지 스타일을 추가해 보겠습니다. +{%change%} `src/containers/Login.css` 파일 안에 몇 가지 스타일을 추가해 보겠습니다. -``` css +```css @media all and (min-width: 480px) { .Login { padding: 60px 0; @@ -114,15 +115,15 @@ export default class Login extends Component { ### Add the Route -이제 `src/Routes.js`의 `` 바로 아래에 다음 줄(`반드시 ` 보다는 위에 줄)을 추가하여 이 컨테이너를 나머지 응용 프로그램과 연결합니다. +{%change%} 이제 `src/Routes.js`의 `` 바로 아래에 다음 줄(`반드시 ` 보다는 위에 줄)을 추가하여 이 컨테이너를 나머지 응용 프로그램과 연결합니다. -``` coffee +```coffee ``` -그리고 헤더 부분에 컴포넌트를 추가합니다. +{%change%} 그리고 헤더 부분에 컴포넌트를 추가합니다. -``` javascript +```js import Login from "./containers/Login"; ``` diff --git a/_chapters/ko/create-a-new-reactjs-app.md b/_chapters/ko/create-a-new-reactjs-app.md index 325c0b683c..bbeb84e716 100644 --- a/_chapters/ko/create-a-new-reactjs-app.md +++ b/_chapters/ko/create-a-new-reactjs-app.md @@ -11,7 +11,7 @@ comments_id: create-a-new-react-js-app/68 이번에는 프론트엔드를 시작하겠습니다. 우리는 [React.js](https://facebook.github.io/react/)를 사용하여 단일 페이지 응용 프로그램을 만들 계획입니다. [Create React App](https://github.com/facebookincubator/create-react-app) 프로젝트를 사용하여 모든 것을 설정합니다. 이것은 React팀이 공식적으로 지원하며, React.js 프로젝트의 모든 의존성을 편리하게 패키징해줍니다. -백앤드를 위해 작업했던 디렉토리에서 나옵니다. +{%change%} 백앤드를 위해 작업했던 디렉토리에서 나옵니다. ``` bash $ cd ../ @@ -19,7 +19,7 @@ $ cd ../ ### 신규 React App 만들기 -노트 앱을 위한 클라이언트를 만들기 위해 아래 명령어를 실행합니다. +{%change%} 노트 앱을 위한 클라이언트를 만들기 위해 아래 명령어를 실행합니다. ``` bash $ npx create-react-app notes-app-client @@ -27,7 +27,7 @@ $ npx create-react-app notes-app-client 이 작업을 실행하는 데는 1 초가 걸리고 신규 프로젝트와 신규 작업 디렉토리가 만들어집니다. -생성된 디렉토리로 이동해서 프론트앤드 프로젝트를 바로 실행해 보겠습니다. +{%change%} 생성된 디렉토리로 이동해서 프론트앤드 프로젝트를 바로 실행해 보겠습니다. ``` bash $ cd notes-app-client @@ -40,7 +40,7 @@ $ npm start ### 제목 바꾸기 -노트 작성 앱의 제목을 빠르게 변경해 보겠습니다. `public/index.html`을 열고`title` 태그를 다음과 같이 편집하십시오: +{%change%} 노트 작성 앱의 제목을 빠르게 변경해 보겠습니다. `public/index.html`을 열고`title` 태그를 다음과 같이 편집하십시오: ``` html Codestin Search App diff --git a/_chapters/ko/create-a-route-that-redirects.md b/_chapters/ko/create-a-route-that-redirects.md index d0bf2d69ca..e1725b08d7 100644 --- a/_chapters/ko/create-a-route-that-redirects.md +++ b/_chapters/ko/create-a-route-that-redirects.md @@ -2,9 +2,9 @@ layout: post title: Create a Route That Redirects date: 2017-02-02 00:00:00 -lang: ko +lang: ko redirect_from: /chapters/create-a-hoc-that-checks-auth.html -description: React.js 앱에서는 로그인하지 않은 사용자를 로그인 페이지로 리디렉션하고 로그인한 사용자는 로그인 페이지가 아닌 곳으로 리디렉션하려고합니다. 이렇게하려면 React Router v4의 Redirect 컴포넌트를 사용합니다. +description: React.js 앱에서는 로그인하지 않은 사용자를 로그인 페이지로 리디렉션하고 로그인한 사용자는 로그인 페이지가 아닌 곳으로 리디렉션하려고합니다. 이렇게하려면 React Router v6의 Redirect 컴포넌트를 사용합니다. context: true comments_id: create-a-route-that-redirects/47 ref: create-a-route-that-redirects @@ -12,11 +12,11 @@ ref: create-a-route-that-redirects 먼저 사용자가 라우팅하기 전에 로그인했는지 확인하는 경로를 만들어 보겠습니다. -`src/components/AuthenticatedRoute.js`에 다음을 추가합니다. +{%change%} `src/components/AuthenticatedRoute.js`에 다음을 추가합니다. -``` coffee +```coffee import React from "react"; -import { Route, Redirect } from "react-router-dom"; +import { Route, Navigate } from "react-router-dom"; export default ({ component: C, props: cProps, ...rest }) => render={props => cProps.isAuthenticated ? - : } />; ``` -이 컴포넌트는 [세션을 상태에 추가하기] ({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한`AppliedRoute` 컴포넌트와 유사합니다. 가장 큰 차이점은 사용자가 인증되었는지 확인하기 위해 전달된 속성을 살펴 보는 것입니다. 사용자가 인증되면 전달된 컴포넌트를 단순히 렌더링합니다. 그리고 사용자가 인증되지 않았으면 `Redirect` React Router v4 컴포넌트를 사용하여 사용자를 로그인 페이지로 리디렉션합니다. 또한 현재 로그인 경로를 전달합니다(쿼리 문자열의 `redirect`). 이것을 이용해 사용자가 로그인한 후에 바로 다시 리디렉션 할 것입니다. +이 컴포넌트는 [세션을 상태에 추가하기] ({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한`AppliedRoute` 컴포넌트와 유사합니다. 가장 큰 차이점은 사용자가 인증되었는지 확인하기 위해 전달된 속성을 살펴 보는 것입니다. 사용자가 인증되면 전달된 컴포넌트를 단순히 렌더링합니다. 그리고 사용자가 인증되지 않았으면 `Redirect` React Router v6 컴포넌트를 사용하여 사용자를 로그인 페이지로 리디렉션합니다. 또한 현재 로그인 경로를 전달합니다(쿼리 문자열의 `redirect`). 이것을 이용해 사용자가 로그인한 후에 바로 다시 리디렉션 할 것입니다. 사용자가 인증되지 않았음을 보장하기 위한 검증에서도 비슷한 방법을 사용할 것입니다. -`src/components/UnauthenticatedRoute.js`에 다음 내용을 추가합니다. +{%change%} `src/components/UnauthenticatedRoute.js`에 다음 내용을 추가합니다. -``` coffee +```coffee import React from "react"; -import { Route, Redirect } from "react-router-dom"; +import { Route, Navigate } from "react-router-dom"; export default ({ component: C, props: cProps, ...rest }) => render={props => !cProps.isAuthenticated ? - : } + : } />; ``` 여기에서는 전달된 컴포넌트를 렌더링하기 전에 사용자가 인증되지 않았는지 확인합니다. 그리고 사용자가 인증된 경우에는 `Redirect` 컴포넌트를 사용하여 사용자를 홈페이지로 보냅니다. -다음으로, 응용 프로그램에서 이 컴포넌트들을 사용해 보겠습니다. +다음으로, 응용 프로그램에서 이 컴포넌트들을 사용해 보겠습니다. diff --git a/_chapters/ko/create-a-settings-page.md b/_chapters/ko/create-a-settings-page.md index 916115982b..05d0a532d9 100644 --- a/_chapters/ko/create-a-settings-page.md +++ b/_chapters/ko/create-a-settings-page.md @@ -18,7 +18,7 @@ ref: create-a-settings-page 시작하려면 설정 페이지를 추가하십시오. -`src/containers/Settings.js` 파일을 새로 만들어 다음 내용을 추가합니다. +{%change%} `src/containers/Settings.js` 파일을 새로 만들어 다음 내용을 추가합니다. ``` coffee import React, { Component } from "react"; @@ -48,13 +48,13 @@ export default class Settings extends Component { } ``` -다음으로 `src/Routes.js` 파일의 헤더에 다음 내용을 추가합니다. +{%change%} 다음으로 `src/Routes.js` 파일의 헤더에 다음 내용을 추가합니다. ``` js import Settings from "./containers/Settings"; ``` -그리고 `src/Routes.js` 파일의 `` 블럭을 다음 내용으로 바꿉니다. +{%change%} 그리고 `src/Routes.js` 파일의 `` 블럭을 다음 내용으로 바꿉니다. ``` coffee @@ -71,7 +71,7 @@ import Settings from "./containers/Settings"; 새로 만든 설정 페이지에 대한 경로를 추가한 것을 주목하십시오. -다음으로 `src/App.js`의 `render` 메쏘드를 이용하여 Navbar의 설정 페이지에 링크를 추가하십시오. +{%change%} 다음으로 `src/App.js`의 `render` 메쏘드를 이용하여 Navbar의 설정 페이지에 링크를 추가하십시오. ``` coffee render() { @@ -125,7 +125,7 @@ render() { ### 변경 사항 커밋 -Git에 빠르게 커밋합니다. +{%change%} Git에 빠르게 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/create-an-iam-user.md b/_chapters/ko/create-an-iam-user.md index 4de784a265..5d8476d3ea 100644 --- a/_chapters/ko/create-an-iam-user.md +++ b/_chapters/ko/create-an-iam-user.md @@ -17,45 +17,45 @@ Amazon IAM (Identity and Access Management)을 사용하면 AWS에서 사용자 먼저, [AWS 콘솔](https://console.aws.amazon.com)에 로그인해서 서비스 목록 중에 IAM을 선택합니다. -![IAM Service 선택 스크린샷](/assets/iam-user/select-iam-service.png) +![IAM Service 선택 스크린샷](/assets/ko/iam-user/select-iam-service.png) **사용자**를 선택합니다. -![IAM 사용자 스크린샷](/assets/iam-user/select-iam-users.png) +![IAM 사용자 스크린샷](/assets/ko/iam-user/select-iam-users.png) **사용자 추가**를 선택합니다. -![IAM 사용자 추가 스크린샷](/assets/iam-user/add-iam-user.png) +![IAM 사용자 추가 스크린샷](/assets/ko/iam-user/add-iam-user.png) **사용자 이름**을 입력하고 **프로그래밍 방식 액세스**를 선택, 그리고 나서 **다음:권한**을 클릭합니다. 이 계정은 [AWS CLI](https://aws.amazon.com/cli/) 와 [Serverless Framework](https://serverless.com)에서 사용할 예정입니다. 모두 AWS API와 직접 연결하기 위해 사용되며 관리 콘솔을 이용하기 위해 사용되지는 않을 것입니다. -![IAM 사용자 정보 입력하기 스크린샷](/assets/iam-user/fill-in-iam-user-info.png) +![IAM 사용자 정보 입력하기 스크린샷](/assets/ko/iam-user/fill-in-iam-user-info.png) **기존 정책 직접 연결**을 선택합니다. -![IAM 사용자 정책 추가하기 스크린샷](/assets/iam-user/add-iam-user-policy.png) +![IAM 사용자 정책 추가하기 스크린샷](/assets/ko/iam-user/add-iam-user-policy.png) **AdministratorAccess**를 검색해서 정책을 선택한 후, **다음: 태그**를 누르고 **다음: 검토**를 클릭합니다. -나중에 [Serverless IAM 정책 사용자화]({% link _chapters/customize-the-serverless-iam-policy.md %})에서 보다 세부적인 정책을 적용하는 법을 알아보기로 하고 지금은 이 정책설정으로 계속하겠습니다. +나중에 [Serverless IAM 정책 사용자화]({% link _archives/customize-the-serverless-iam-policy.md %})에서 보다 세부적인 정책을 적용하는 법을 알아보기로 하고 지금은 이 정책설정으로 계속하겠습니다. -![Admin 정책이 추가된 스크린샷](/assets/iam-user/added-admin-policy.png) +![Admin 정책이 추가된 스크린샷](/assets/ko/iam-user/added-admin-policy.png) **사용자 만들기**를 클릭합니다. -![IAM 사용자 검토 스크린샷](/assets/iam-user/review-iam-user.png) +![IAM 사용자 검토 스크린샷](/assets/ko/iam-user/review-iam-user.png) **표시**를 선택하면 **비밀 액세스 키**를 볼 수 있습니다. -![IAM 사용자 추가된 스크린샷](/assets/iam-user/added-iam-user.png) +![IAM 사용자 추가된 스크린샷](/assets/ko/iam-user/added-iam-user.png) **액세스 키 ID** 와 **비밀 액세스 키**는 나중에 사용해야 하므로 따로 적어둡니다. -![IAM 사용자 자격증명 스크린샷](/assets/iam-user/iam-user-credentials.png) +![IAM 사용자 자격증명 스크린샷](/assets/ko/iam-user/iam-user-credentials.png) IAM에 대한 개념은 AWS 서비스로 작업 할 때 자주 접할 수 있습니다. 따라서 IAM이 무엇이고 서버리스 보안에 어떻게 도움이 되는지 보다 자세히 살펴볼 필요가 있습니다. diff --git a/_chapters/ko/create-an-s3-bucket-for-file-uploads.md b/_chapters/ko/create-an-s3-bucket-for-file-uploads.md index a6fab467b0..9a69e27053 100644 --- a/_chapters/ko/create-an-s3-bucket-for-file-uploads.md +++ b/_chapters/ko/create-an-s3-bucket-for-file-uploads.md @@ -56,19 +56,25 @@ comments_id: create-an-s3-bucket-for-file-uploads/150 CORS 구성 편집기에 아래 내용을 추가하고 **저장** 버튼을 클릭합니다. -``` xml - - - * - GET - PUT - POST - HEAD - DELETE - 3000 - * - - +``` json +[ + { + "AllowedMethods": [ + "GET", + "PUT", + "POST", + "HEAD", + "DELETE" + ], + "AllowedOrigins": [ + "*" + ], + "AllowedHeaders": [ + "*" + ], + "MaxAgeSeconds": 3000 + } +] ``` 운영 환경에서 사용할 때에는 여러분의 도메인이나 도메인 목록을 사용해서 구성할 수 있습니다. diff --git a/_chapters/ko/create-an-s3-bucket.md b/_chapters/ko/create-an-s3-bucket.md index 886af12fb2..5afa4af8e5 100644 --- a/_chapters/ko/create-an-s3-bucket.md +++ b/_chapters/ko/create-an-s3-bucket.md @@ -46,7 +46,7 @@ ref: create-an-s3-bucket ![AWS S3 버킷 권한 추가 화면](/assets/add-bucket-policy.png) -편집기에 다음 버킷 정책을 추가하십시오. 여기서 `notes-app-client`는 S3 버킷의 이름입니다. 여기에 여러분의 버킷 이름을 사용하십시오. +{%change%} 편집기에 다음 버킷 정책을 추가하십시오. 여기서 `notes-app-client`는 S3 버킷의 이름입니다. 여기에 여러분의 버킷 이름을 사용하십시오. ``` json { diff --git a/_chapters/ko/create-containers.md b/_chapters/ko/create-containers.md index 4f5768a11e..61f8dfe2b9 100644 --- a/_chapters/ko/create-containers.md +++ b/_chapters/ko/create-containers.md @@ -4,7 +4,7 @@ title: Create Containers date: 2017-01-11 00:00:00 lang: ko ref: create-containers -description: React.js 앱을 여러 경로로 나누기 위해 React Router v4의 컨테이너를 사용하여 구조화 할 것이다. 또한 Navbar React-Bootstrap 구성 요소를 App 컨테이너에 추가 할 예정입니다. +description: React.js 앱을 여러 경로로 나누기 위해 React Router v6의 컨테이너를 사용하여 구조화 할 것이다. 또한 Navbar React-Bootstrap 구성 요소를 App 컨테이너에 추가 할 예정입니다. context: true comments_id: create-containers/62 --- @@ -17,13 +17,13 @@ comments_id: create-containers/62 시작하려면, Create React App에 원래 있던 `src/logo.svg`를 제거합니다. -``` bash +```bash $ rm src/logo.svg ``` -그리고 `src/App.js`에서도 코드를 제거합니다. 그리고 아래 내용으로 대체합니다. +{%change%} 그리고 `src/App.js`에서도 코드를 제거합니다. 그리고 아래 내용으로 대체합니다. -``` coffee +```coffee import React, { Component } from "react"; import { Link } from "react-router-dom"; import { Navbar } from "react-bootstrap"; @@ -57,9 +57,9 @@ export default App; 또한 몇 줄의 스타일을 추가하여 좀 더 공간을 넓힙니다. -`src/App.css` 안에 모든 코드를 제거하고 아래 내용으로 바꿉니다: +{%change%} `src/App.css` 안에 모든 코드를 제거하고 아래 내용으로 바꿉니다: -``` css +```css .App { margin-top: 15px; } @@ -69,21 +69,21 @@ export default App; } ``` -### Home 컨테이너 추가하기 +### Home 컨테이너 추가하기 이제 외부 컨텐트를 넣기 위한 준비가 되었으므로 홈페이지에 컨테이너를 추가해 봅시다. 홈페이지 컨테이너를 추가하면 `/` 경로에 응답합니다. -작업 디렉토리에 아래 명령을 실행해서 `src/containers/` 디렉터리를 만듭니다. +{%change%} 작업 디렉토리에 아래 명령을 실행해서 `src/containers/` 디렉터리를 만듭니다. -``` bash +```bash $ mkdir src/containers/ ``` 최상위 레벨의 모든 구성 요소를 여기에 저장합니다. 이는 단일 페이지 앱의 경로에 응답하고 API 요청을 처리할 구성 요소들입니다. 이 자습서의 나머지 부분에서는 이들을 *컨테이너*라고 부르겠습니다. -새로운 컨테이너를 생성하기 위해 아래 코드를 새로 만든 파일인 `src/containers/Home.js`에 추가합니다. +{%change%} 새로운 컨테이너를 생성하기 위해 아래 코드를 새로 만든 파일인 `src/containers/Home.js`에 추가합니다. -``` coffee +```coffee import React, { Component } from "react"; import "./Home.css"; @@ -105,9 +105,9 @@ export default class Home extends Component { 이제 스타일을 지정하기 위해 몇 줄을 추가해 보겠습니다. -`src/containers/Home.css`에 아래 내용을 추가합니다. +{%change%} `src/containers/Home.css`에 아래 내용을 추가합니다. -``` css +```css .Home .lander { padding: 80px 0; text-align: center; @@ -127,40 +127,40 @@ export default class Home extends Component { 이제 `/`에 응답하는 컨테이너를 위해 경로를 설정하겠습니다. -`src/Routes.js`를 만들고 아래 내용을 작성합니다. +{%change%} `src/Routes.js`를 만들고 아래 내용을 작성합니다. -``` coffee +```coffee import React from "react"; -import { Route, Switch } from "react-router-dom"; +import { Route, Routes } from "react-router-dom"; import Home from "./containers/Home"; export default () => - - - ; + + + ; ``` 이 구성 요소는 React-Router의 `Switch` 컴포넌트를 사용하여 그 안에 정의 된 첫 번째로 일치하는 경로를 렌더링합니다. 지금은 단 하나의 경로만을 가지고 있습니다. `/`를 찾아서 일치 할 때 `Home` 컴포넌트를 렌더링합니다. 그리고 `exact` 속성을 사용하여 `/` 경로와 정확히 일치하는지 확인합니다. `/` 경로는 `/`로 시작하는 다른 경로와도 일치하기 때문입니다. -### 경로 렌더링하기 +### 경로 렌더링하기 이제 경로를 App 구성 요소로 렌더링 해 봅시다. -`src/App.js`의 헤더 부분에 아래 내용을 추가합니다. +{%change%} `src/App.js`의 헤더 부분에 아래 내용을 추가합니다. -``` coffee +```coffee import Routes from "./Routes"; ``` -그리고 `src/App.js`의 `render` 내부에 있는 `Navbar` 컴포넌트 아래에 다음 내용을 추가합니다. +{%change%} 그리고 `src/App.js`의 `render` 내부에 있는 `Navbar` 컴포넌트 아래에 다음 내용을 추가합니다. -``` coffee +```coffee ``` 그래서 `src/App.js`의 `render` 메소드는 다음과 같이 보일 것입니다. -``` coffee +```coffee render() { return (
diff --git a/_chapters/ko/create-the-signup-form.md b/_chapters/ko/create-the-signup-form.md index 5785b9e287..30ef00172e 100644 --- a/_chapters/ko/create-the-signup-form.md +++ b/_chapters/ko/create-the-signup-form.md @@ -4,7 +4,7 @@ title: Create the Signup Form date: 2017-01-20 00:00:00 lang: ko ref: create-the-signup-form -description: 우리는 React.js 앱을 위한 가입 페이지를 만들 예정입니다. Amazon Cognito를 사용하여 사용자를 등록하려면 사용자가 이메일로 전송된 인증 코드를 입력할 수있는 양식을 만들어야합니다. +description: 우리는 React.js 앱을 위한 가입 페이지를 만들 예정입니다. Amazon Cognito를 사용하여 사용자를 등록하려면 사용자가 이메일로 전송된 인증 코드를 입력할 수있는 양식을 만들어야합니다. context: true comments_id: create-the-signup-form/52 --- @@ -13,15 +13,15 @@ comments_id: create-the-signup-form/52 ### 컨테이너 추가 -다음과 같이 `src/containers/Signup.js`에 새 컨테이너를 만듭니다. +{%change%} 다음과 같이 `src/containers/Signup.js`에 새 컨테이너를 만듭니다. -``` coffee +```coffee import React, { Component } from "react"; import { - HelpBlock, + FormText, FormGroup, FormControl, - ControlLabel + FormLabel } from "react-bootstrap"; import LoaderButton from "../components/LoaderButton"; import "./Signup.css"; @@ -78,14 +78,14 @@ export default class Signup extends Component { return ( - Confirmation Code + Confirmation Code - Please check your email for the code. + Please check your email for the code. - Email + Email - Password + Password - Confirm Password + Confirm Password 그리고 `src/containers/Signup.css`에 몇 가지 스타일을 추가해 보겠습니다. - +{%change%} 그리고 `src/containers/Signup.css`에 몇 가지 스타일을 추가해 보겠습니다. -``` css +```css @media all and (min-width: 480px) { .Signup { padding: 60px 0; @@ -190,15 +189,15 @@ export default class Signup extends Component { ### 경로 추가 -마지막으로 컨테이너를 로그인 경로 아래의 `src/Routes.js`에있는 경로로 추가하십시오. 참고로 ["세션을 state에 추가하기"]({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한 `AppliedRoute` 컴포넌트를 사용하고 있습니다. +{%change%} 마지막으로 컨테이너를 로그인 경로 아래의 `src/Routes.js`에있는 경로로 추가하십시오. 참고로 ["세션을 state에 추가하기"]({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한 `AppliedRoute` 컴포넌트를 사용하고 있습니다. -``` coffee +```coffee ``` 그리고 헤더에 컴포넌트를 import합니다. -``` javascript +```js import Signup from "./containers/Signup"; ``` diff --git a/_chapters/ko/custom-domain-in-netlify.md b/_chapters/ko/custom-domain-in-netlify.md index 2d1a0e24fb..309e10d0b1 100644 --- a/_chapters/ko/custom-domain-in-netlify.md +++ b/_chapters/ko/custom-domain-in-netlify.md @@ -15,15 +15,15 @@ ref: custom-domains-in-netlify **Part I의 독자에게 보내는 메모** -다음 섹션에서는 [Part I](/#part-1)을 독자적으로 작성하고 아래 사용자 정의 도메인을 처음부터 설정한다고 가정합니다. 그러나 Part I을 방금 완성한 경우 다음 두 가지 옵션을 사용할 수 있습니다. +다음 섹션에서는 [Part I](/#the-basics)을 독자적으로 작성하고 아래 사용자 정의 도메인을 처음부터 설정한다고 가정합니다. 그러나 Part I을 방금 완성한 경우 다음 두 가지 옵션을 사용할 수 있습니다. 1. 새 사용자 정의 도메인 구성 -   Part I의 도메인이 `https://notes-app.my-domain.com`과 같다고 가정 해 보겠습니다. 다음 섹션에서는 `https://notes-app-2.my-domain.com`과 같은 것을 설정할 수 있습니다. 이것은 이전에 구성된 내용을 변경하지 않기 때문에 선호되는 옵션입니다. 여기까지 튜토리얼에서 데모 응용 프로그램을 위해 우리가 작업한 내용들입니다. [Part I 버전](https://demo.serverless-stack.com)과 [Part II 버전](https://demo2.serverless-stack.com)을 볼 수 있습니다. 단점은 frontend React 앱의 두 가지 버전이 있다는 것입니다. +   Part I의 도메인이 `https://notes-app.my-domain.com`과 같다고 가정 해 보겠습니다. 다음 섹션에서는 `https://notes-app-2.my-domain.com`과 같은 것을 설정할 수 있습니다. 이것은 이전에 구성된 내용을 변경하지 않기 때문에 선호되는 옵션입니다. 여기까지 튜토리얼에서 데모 응용 프로그램을 위해 우리가 작업한 내용들입니다. [Part I 버전](https://demo.sst.dev)과 [Part II 버전](https://demo2.sst.dev)을 볼 수 있습니다. 단점은 frontend React 앱의 두 가지 버전이 있다는 것입니다. 2. 이전 도메인 바꾸기 -이 가이드를 통해 앱을 만드는 방법을 배우는 대신 바로 앱을 만들 수 있습니다. 그렇다면 프런트엔드의 두 가지 버전이 주위에 운영되는 것은 이해가 되지 않습니다. Part 1에서 생성된 도메인 연결을 해제 해야합니다. 그렇게하려면 [apex 도메인]({% link _chapters/setup-your-domain-with-cloudfront.md %}#point-domain-to-cloudfront-distribution)과 [www 도메인]({% link _chapters/setup-www-domain-redirect.md %})에서 만든 Route53 레코드 세트를 제거하십시오. +이 가이드를 통해 앱을 만드는 방법을 배우는 대신 바로 앱을 만들 수 있습니다. 그렇다면 프런트엔드의 두 가지 버전이 주위에 운영되는 것은 이해가 되지 않습니다. Part 1에서 생성된 도메인 연결을 해제 해야합니다. 그렇게하려면 [apex 도메인]({% link _archives/setup-your-domain-with-cloudfront.md %}#point-domain-to-cloudfront-distribution)과 [www 도메인]({% link _archives/setup-www-domain-redirect.md %})에서 만든 Route53 레코드 세트를 제거하십시오. 위의 두 가지 옵션에 대해 잘 모르거나 질문이있는 경우, 이 챕터의 맨 아래에 있는 토론 스레드에 의견을 게시하십시오. @@ -57,7 +57,7 @@ Netlify의 프로젝트 페이지에서 **Site setting**을 누릅니다. ![ Add custom domain 클릭화면](/assets/part2/click-add-custom-domain.png) -우리 도메인의 이름을 입력하십시오. 예를 들어, `demo-serverless-stack.com` 일 수 있습니다. **Save**를 누르십시오. +우리 도메인의 이름을 입력하십시오. 예를 들어, `demo-sst.dev` 일 수 있습니다. **Save**를 누르십시오. ![사용자 정의 도메인 입력 화면](/assets/part2/enter-custom-domain.png) diff --git a/_chapters/ko/delete-a-note.md b/_chapters/ko/delete-a-note.md index c8aed8135f..f994c150e3 100644 --- a/_chapters/ko/delete-a-note.md +++ b/_chapters/ko/delete-a-note.md @@ -2,8 +2,8 @@ layout: post title: Delete a Note date: 2017-01-31 00:00:00 -lang: ko -description: 사용자가 React.js 앱에서 노트를 삭제할 수 있습니다. 이를 위해 AWS Amplify를 사용하여 serverless 백앤드 API에 DELETE 요청을 할 것입니다. +lang: ko +description: 사용자가 React.js 앱에서 노트를 삭제할 수 있습니다. 이를 위해 AWS Amplify를 사용하여 serverless 백앤드 API에 DELETE 요청을 할 것입니다. context: true comments_id: comments-for-delete-a-note/137 ref: delete-a-note @@ -11,9 +11,9 @@ ref: delete-a-note 노트 페이지에서 마지막으로 해야할 일은 사용자가 노트를 삭제할 수 있게하는 것입니다. 버튼은 이미 설정되어 있습니다. API에 연결해서 마무리할 일만 남았습니다. - `src/containers/Notes.js`에서 `handleDelete` 메소드를 대체합니다. +{%change%} `src/containers/Notes.js`에서 `handleDelete` 메소드를 대체합니다. -``` coffee +```coffee deleteNote() { return API.del("notes", `/notes/${this.props.match.params.id}`); } @@ -33,7 +33,7 @@ handleDelete = async event => { try { await this.deleteNote(); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e); this.setState({ isDeleting: false }); @@ -53,4 +53,4 @@ handleDelete = async event => { ![로그 아웃 상태에서 노트 페이지 에러 화면](/assets/note-page-logged-out-error.png) -대신에, 로그인 페이지로 리디렉션 한 다음 로그인 한 후에 다시 요청한 페이지로 리디렉션하고 싶습니다. 다음에 그 방법을 살펴 보겠습니다. +대신에, 로그인 페이지로 리디렉션 한 다음 로그인 한 후에 다시 요청한 페이지로 리디렉션하고 싶습니다. 다음에 그 방법을 살펴 보겠습니다. diff --git a/_chapters/ko/deploy-again.md b/_chapters/ko/deploy-again.md index fd1830f430..014dd10d71 100644 --- a/_chapters/ko/deploy-again.md +++ b/_chapters/ko/deploy-again.md @@ -2,21 +2,20 @@ layout: post title: Deploy Again date: 2017-02-14 00:00:00 -lang: ko -description: S3 및 CloudFront에서 호스팅되는 React.js 앱에 업데이트를 배포하려면 S3에 앱을 업로드하고 CloudFront 캐시를 무효화해야합니다. AWS CLI에서 “aws cloudfront create-invalidation” 명령을 사용하여 이 작업을 수행 할 수 있습니다. “npm run deploy”를 실행하여 이러한 단계를 자동화하려면 이 명령을 추가하여 package.json에 사전 배포, 배포 및 사후 배포를 수행합니다. +lang: ko +description: S3 및 CloudFront에서 호스팅되는 React.js 앱에 업데이트를 배포하려면 S3에 앱을 업로드하고 CloudFront 캐시를 무효화해야합니다. AWS CLI에서 “aws cloudfront create-invalidation” 명령을 사용하여 이 작업을 수행 할 수 있습니다. “npx sst deploy”를 실행하여 이러한 단계를 자동화하려면 이 명령을 추가하여 package.json에 사전 배포, 배포 및 사후 배포를 수행합니다. context: true -code: frontend_part1 comments_id: deploy-again/138 ref: deploy-again --- -이제 앱을 약간 변경 했으므로 업데이트를 배포해 보겠습니다. 참고로 업데이트를 배포 할 때마다 반복해야하는 프로세스입니다. +이제 앱을 약간 변경 했으므로 업데이트를 배포해 보겠습니다. 참고로 업데이트를 배포 할 때마다 반복해야하는 프로세스입니다. -### 앱 빌드 +### 앱 빌드 먼저 앱을 제작하여 앱을 준비해 봅시다. 작업 디렉토리에서 다음을 실행하십시오. -``` bash +```bash $ npm run build ``` @@ -24,9 +23,9 @@ $ npm run build ### S3에 업로드 -작업 디렉토리에서 다음을 실행하여 S3 버킷에 앱을 업로드하십시오. `YOUR_S3_DEPLOY_BUCKET_NAME`을 [S3 버킷 생성하기]({% link _chapters/create-an-s3-bucket.md %}) 챕터에서 생성한 S3 버킷으로 바꿔야합니다. +작업 디렉토리에서 다음을 실행하여 S3 버킷에 앱을 업로드하십시오. `YOUR_S3_DEPLOY_BUCKET_NAME`을 [S3 버킷 생성하기]({% link _archives/create-an-s3-bucket.md %}) 챕터에서 생성한 S3 버킷으로 바꿔야합니다. -``` bash +```bash $ aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME --delete ``` @@ -48,7 +47,7 @@ CloudFront를 사용하면 객체 경로를 전달하여 배포본의 객체를 이제 AWS CLI를 사용하여 두 배포본의 캐시를 무효화 할 수 있습니다. `YOUR_CF_DISTRIBUTION_ID`와 `YOUR_WWW_CF_DISTRIBUTION_ID`을 위에있는 것과 바꾸십시오. -``` bash +```bash $ aws cloudfront create-invalidation --distribution-id YOUR_CF_DISTRIBUTION_ID --paths "/*" $ aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ID --paths "/*" ``` @@ -67,9 +66,9 @@ $ aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ NPM을 사용하면 `package.json`에`deploy` 명령을 추가할 수 있습니다. -`package.json`에서 `eject` 위에 `scripts` 블럭을 다음 내용으로 추가합니다. +{%change%} `package.json`에서 `eject` 위에 `scripts` 블럭을 다음 내용으로 추가합니다. -``` coffee +```coffee "predeploy": "npm run build", "deploy": "aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME --delete", "postdeploy": "aws cloudfront create-invalidation --distribution-id YOUR_CF_DISTRIBUTION_ID --paths '/*' && aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ID --paths '/*'", @@ -85,14 +84,14 @@ An error occurred (InvalidArgument) when calling the CreateInvalidation operatio `/*`에 따옴표가 없는지 확인하십시오. -``` coffee +```coffee "postdeploy": "aws cloudfront create-invalidation --distribution-id YOUR_CF_DISTRIBUTION_ID --paths /* && aws cloudfront create-invalidation --distribution-id YOUR_WWW_CF_DISTRIBUTION_ID --paths /*", ``` 이제 업데이트를 배포할 때 프로젝트 루트에서 다음 명령을 실행하기만하면됩니다. 앱을 빌드하고 S3에 업로드하고 CloudFront 캐시를 무효화합니다. -``` bash -$ npm run deploy +```bash +$ npx sst deploy ``` -이제 앱이 완성되었습니다. 그리고 여기까지가 Part I의 끝입니다. 다음 챕터에서는 이 스택을 자동화하여 향후 프로젝트에 사용할 수있는 방법을 살펴 보겠습니다. [AWS Amplify를 사용하는 Cognito의 Facebook 로그인]({% link _chapters/facebook-login-with-cognito-using-aws-amplify.md %}) 챕터에서 Facebook 로그인을 추가하는 방법을 살펴볼 수도 있는데, 모두 지금까지 Part I에서 다루었던 것을 토대로 진행됩니다. +이제 앱이 완성되었습니다. 그리고 여기까지가 Part I의 끝입니다. 다음 챕터에서는 이 스택을 자동화하여 향후 프로젝트에 사용할 수있는 방법을 살펴 보겠습니다. [AWS Amplify를 사용하는 Cognito의 Facebook 로그인]({% link _archives/facebook-login-with-cognito-using-aws-amplify.md %}) 챕터에서 Facebook 로그인을 추가하는 방법을 살펴볼 수도 있는데, 모두 지금까지 Part I에서 다루었던 것을 토대로 진행됩니다. diff --git a/_chapters/ko/deploy-the-apis.md b/_chapters/ko/deploy-the-apis.md index c676e7aa5f..0e632ea475 100644 --- a/_chapters/ko/deploy-the-apis.md +++ b/_chapters/ko/deploy-the-apis.md @@ -11,7 +11,7 @@ comments_id: deploy-the-apis/121 이제 API가 완성되었으니 배포를 진행합니다. -현재 작업 디렉토리에서 아래 명령어를 실행합니다. +{%change%} 현재 작업 디렉토리에서 아래 명령어를 실행합니다. ``` bash $ serverless deploy @@ -23,13 +23,13 @@ $ serverless deploy $ serverless deploy --aws-profile myProfile ``` -여기서`myProfile`은 사용할 AWS 프로파일의 이름입니다. Serverless에서 AWS 프로파일을 사용하는 방법에 대한 자세한 정보가 필요하면 [AWS 다중 프로파일 구성하기]({% link _chapters/configure-multiple-aws-profiles.md %}) 챕터를 참조하십시오. +여기서`myProfile`은 사용할 AWS 프로파일의 이름입니다. Serverless에서 AWS 프로파일을 사용하는 방법에 대한 자세한 정보가 필요하면 [AWS 다중 프로파일 구성하기]({% link _archives/configure-multiple-aws-profiles.md %}) 챕터를 참조하십시오. 이 명령의 출력 하단에 **서비스 정보**가 있습니다. ``` bash Service Information -service: notes-app-api +service: notes-api stage: prod region: us-east-1 api keys: @@ -41,11 +41,11 @@ endpoints: PUT - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes/{id} DELETE - https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/notes/{id} functions: - notes-app-api-prod-create - notes-app-api-prod-get - notes-app-api-prod-list - notes-app-api-prod-update - notes-app-api-prod-delete + notes-api-prod-create + notes-api-prod-get + notes-api-prod-list + notes-api-prod-update + notes-api-prod-delete ``` 여기에는 작성된 API 엔드포인트 목록이 있습니다. 프런트엔드를 생성하고 나서 나중에 사용할 예정이기 때문에 이러한 엔드포인트 주소를 기록해 두십시오. 또한 이 엔드포인트에서 리전과 ID를 기록해두면 다음 장에서 사용할 수 있습니다. 여기서 `us-east-1`은 API 게이트웨이 리전이고`ly55wbovq4`는 API 게이트웨이 ID입니다. diff --git a/_chapters/ko/deploy-to-s3.md b/_chapters/ko/deploy-to-s3.md index 6575a1cb12..7580c74ef7 100644 --- a/_chapters/ko/deploy-to-s3.md +++ b/_chapters/ko/deploy-to-s3.md @@ -23,7 +23,7 @@ $ npm run build ### S3에 업로드 -이제 배포하려면 다음 명령을 실행하십시오. 여기서 `YOUR_S3_DEPLOY_BUCKET_NAME`은 [S3 버킷 생성하기]({% link _chapters/create-an-s3-bucket.md %}) 챕터에서 생성한 S3 버킷 이름입니다. +이제 배포하려면 다음 명령을 실행하십시오. 여기서 `YOUR_S3_DEPLOY_BUCKET_NAME`은 [S3 버킷 생성하기]({% link _archives/create-an-s3-bucket.md %}) 챕터에서 생성한 S3 버킷 이름입니다. ``` bash $ aws s3 sync build/ s3://YOUR_S3_DEPLOY_BUCKET_NAME diff --git a/_chapters/ko/deploying-through-seed.md b/_chapters/ko/deploying-through-seed.md index 5244de99e7..fe2eb0e005 100644 --- a/_chapters/ko/deploying-through-seed.md +++ b/_chapters/ko/deploying-through-seed.md @@ -13,7 +13,7 @@ ref: deploying-through-seed Git을 통해 해보겠습니다. -프로젝트 루트로 돌아가서 다음을 실행합니다. +{%change%} 프로젝트 루트로 돌아가서 다음을 실행합니다. ``` bash $ npm version patch @@ -21,7 +21,7 @@ $ npm version patch 이것은 단순히 프로젝트의 NPM 버전을 업데이트하는 것입니다. 프로젝트의 변경 사항을 추적하는 좋은 방법입니다. 그리고 그것은 Seed를 통한 자동 배포를 위한 Git 커밋을 빠르게 만들어줍니다. -변경 사항을 Push합니다. +{%change%} 변경 사항을 Push합니다. ``` bash $ git push diff --git a/_chapters/ko/display-a-note.md b/_chapters/ko/display-a-note.md index 3e21684d14..a0f01cb9e2 100644 --- a/_chapters/ko/display-a-note.md +++ b/_chapters/ko/display-a-note.md @@ -4,7 +4,7 @@ title: Display a Note date: 2017-01-28 00:00:00 lang: ko ref: display-a-note -description: React.js 앱에 URL의 ID를 기반으로 사용자 노트를 표시하는 페이지를 만들고 싶습니다. React Router v4 Route 구성 요소의 URL 매개 변수를 사용하여 ID를 가져옵니다. 이 ID를 사용하여 serverless 백엔드 API에서 노트를 요청할 것입니다. AWS Amplify의 Storage.vault.get() 메소드를 사용하여 첨부 파일을 다운로드하는 보안 링크를 얻을 수 있습니다. +description: React.js 앱에 URL의 ID를 기반으로 사용자 노트를 표시하는 페이지를 만들고 싶습니다. React Router v6 Route 구성 요소의 URL 매개 변수를 사용하여 ID를 가져옵니다. 이 ID를 사용하여 serverless 백엔드 API에서 노트를 요청할 것입니다. AWS Amplify의 Storage.vault.get() 메소드를 사용하여 첨부 파일을 다운로드하는 보안 링크를 얻을 수 있습니다. context: true comments_id: display-a-note/112 ref: display-a-note @@ -14,13 +14,13 @@ ref: display-a-note 우리가 해야할 첫 번째 일은 컨테이너가 로드 될 때 노트를 불러오는 것입니다. 우리가 'Home' 컨테이너에서 했던 것과 마찬가지 방법으로 시작해 보겠습니다. -### 경로 추가하기 +### 경로 추가하기 노트 불러오기 화면의 경로를 추가 합니다. -`src/Routes.js` 파일의 `/notes/new` 경로 아래에 다음 행을 추가하십시오. 우리는 [세션을 상태에 추가하기]({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한 `AppliedRoute` 컴포넌트를 사용하고 있습니다. +{%change%} `src/Routes.js` 파일의 `/notes/new` 경로 아래에 다음 행을 추가하십시오. 우리는 [세션을 상태에 추가하기]({% link _chapters/add-the-session-to-the-state.md %}) 챕터에서 작성한 `AppliedRoute` 컴포넌트를 사용하고 있습니다. -``` coffee +```coffee ``` @@ -28,9 +28,9 @@ URL에서 노트 ID를 추출하기 위해 패턴 매칭을 이용하는 부분 루트 경로 `/notes/:id`를 사용함으로써 우리는 라우터에게 해당되는 경로에 컴포넌트인 `Notes`를 설정합니다. 하지만 이것은 `/notes/new` 경로의 `new`의 `id`와 매칭 시킨 결과도 가져올 수 있습니다. 따라서 이를 방지하기 위해 `/notes/new` 경로의 뒤에 놓입니다. -그리고 헤더에 컴포넌트를 추가합니다. +{%change%} 그리고 헤더에 컴포넌트를 추가합니다. -``` javascript +```js import Notes from "./containers/Notes"; ``` @@ -38,9 +38,9 @@ import Notes from "./containers/Notes"; ### 컨테이너 추가하기 -`src/containers/Notes.js` 파일을 만들고 아래 내용을 추가합니다. +{%change%} `src/containers/Notes.js` 파일을 만들고 아래 내용을 추가합니다. -``` coffee +```coffee import React, { Component } from "react"; import { API, Storage } from "aws-amplify"; diff --git a/_chapters/ko/frontend-workflow.md b/_chapters/ko/frontend-workflow.md index ac0e8cb880..fe42b03e7a 100644 --- a/_chapters/ko/frontend-workflow.md +++ b/_chapters/ko/frontend-workflow.md @@ -15,7 +15,7 @@ code: frontend_full 좋은 연습이 되려면 새로운 기능을 개발할 때 새로운 브랜치를 만드는 것입니다. -프로젝트 루트에서 다음 명령을 실행하세요. +{%change%} 프로젝트 루트에서 다음 명령을 실행하세요. ``` bash $ git checkout -b "new-feature" @@ -25,7 +25,7 @@ $ git checkout -b "new-feature" 오류가있는 커밋을 만들어서 롤백하는 과정을 살펴 보겠습니다. -`src/containers/Home.js`의 `renderLander` 메소드를 다음으로 대체합니다. +{%change%} `src/containers/Home.js`의 `renderLander` 메소드를 다음으로 대체합니다. ``` coffee renderLander() { @@ -46,7 +46,7 @@ renderLander() { } ``` -그리고 이 변경사항을 Git에 커밋합니다. +{%change%} 그리고 이 변경사항을 Git에 커밋합니다. ``` bash $ git add . @@ -127,7 +127,7 @@ Netlify에서 배포중인 화면. 변경 사항을 신속하게 정리합시다. -`src/containers/Home.js`의 `renderLander` 메소드를 원래 코드로 대체합니다. +{%change%} `src/containers/Home.js`의 `renderLander` 메소드를 원래 코드로 대체합니다. ``` coffee renderLander() { @@ -148,7 +148,7 @@ renderLander() { } ``` -위 변경 사항을 적용하고 푸시해서 변경 내용을 적용하십시오. +{%change%} 위 변경 사항을 적용하고 푸시해서 변경 내용을 적용하십시오. ``` bash $ git add . diff --git a/_chapters/ko/further-reading.md b/_chapters/ko/further-reading.md index cbd5e192bc..67f02f4370 100644 --- a/_chapters/ko/further-reading.md +++ b/_chapters/ko/further-reading.md @@ -2,40 +2,40 @@ layout: post title: 더 읽어볼 것들 lang: ko -description: 이 가이드를 좀 더 확장하고자 한다면 유용한 기사 및 문서 목록입니다. +description: 이 가이드를 좀 더 확장하고자 한다면 유용한 기사 및 문서 목록입니다. date: 2018-03-30 12:00:00 comments_id: further-reading/209 ref: further-reading --- -이 가이드를 마치면 다음 프로젝트에 Serverless Stack을 사용할 것입니다. 그 과정에서 도움을 드리기 위해 참조용으로 사용할 수있는 문서 목록을 작성하려고합니다. 다음은이 가이드에서 사용된 일부 기술 및 서비스에 대해 자세하게 설명하는데 사용할 수 있습니다. +이 가이드를 마치면 다음 프로젝트에 SST을 사용할 것입니다. 그 과정에서 도움을 드리기 위해 참조용으로 사용할 수있는 문서 목록을 작성하려고합니다. 다음은이 가이드에서 사용된 일부 기술 및 서비스에 대해 자세하게 설명하는데 사용할 수 있습니다. -- [Serverless Framework 문서](https://serverless.com/framework/docs/): Serverless 프레임워크를 위한 문서 +- [Serverless Framework 문서](https://serverless.com/framework/docs/): Serverless 프레임워크를 위한 문서 - [DynamoDB, explained](https://www.dynamodbguide.com): DynamoDB NoSQL 데이터베이스의 입문서 -- [React JS Docs](https://reactjs.org/docs/hello-world.html): React 공식 문서 +- [React JS Docs](https://reactjs.org/docs/hello-world.html): React 공식 문서 - [JSX In Depth](https://reactjs.org/docs/jsx-in-depth.html): JSX에 대해 좀 더 자세히 배우기 -- [Create React App User Guide](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md): 정말 포괄적 인 Create React App 사용자 가이드 +- [Create React App User Guide](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md): 정말 포괄적 인 Create React App 사용자 가이드 -- [React-Bootstrap Docs](https://react-bootstrap.github.io/getting-started/introduction): React-Bootstrap 공식 문서 +- [React-Bootstrap Docs](https://react-bootstrap.github.io/getting-started/introduction): React-Bootstrap 공식 문서 -- [Bootstrap v3 Docs](http://getbootstrap.com/docs/3.3/getting-started/): React-Bootstrap이 기반으로하는 Bootstrap v3 문서 +- [Bootstrap v5 Docs](http://getbootstrap.com/docs/3.3/getting-started/): React-Bootstrap이 기반으로하는 Bootstrap v5 문서 -- [React Router Docs](https://reacttraining.com/react-router/web/guides/philosophy): React Router v4 공식 문서 +- [React Router Docs](https://reacttraining.com/react-router/web/guides/philosophy): React Router v6 공식 문서 - [AWS Amplify Developer Guide](https://aws.github.io/aws-amplify/media/developer_guide): AWS Amplify 개발자 가이드 -- [AWS Amplify API Reference](https://aws.github.io/aws-amplify/api/): AWS Amplify API 레퍼런스 +- [AWS Amplify API Reference](https://aws.github.io/aws-amplify/api/): AWS Amplify API 레퍼런스 -- [AWS CloudFormation Docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/GettingStarted.Walkthrough.html): AWS CloudFormation 사용자 가이드 +- [AWS CloudFormation Docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/GettingStarted.Walkthrough.html): AWS CloudFormation 사용자 가이드 -- [Jest Unit Test Docs](https://facebook.github.io/jest/docs/en/getting-started.html): Jest 공식 문서 +- [Jest Unit Test Docs](https://facebook.github.io/jest/docs/en/getting-started.html): Jest 공식 문서 -- [Seed Docs](https://seed.run/docs/): Seed 공식 문서 +- [Seed Docs](https://seed.run/docs/): Seed 공식 문서 -- [Netlify Docs](https://www.netlify.com/docs/): Netlify 공식 문서 +- [Netlify Docs](https://www.netlify.com/docs/): Netlify 공식 문서 서버리스 앱을 제작하는데 도움이되는 다른 가이드 또는 자습서를 찾은 경우이 페이지를 편집하고 PR을 제출하십시오. 또는 의견을 통해 알려주십시오. diff --git a/_chapters/ko/getting-production-ready.md b/_chapters/ko/getting-production-ready.md index 2f4c11d385..9bd95174c3 100644 --- a/_chapters/ko/getting-production-ready.md +++ b/_chapters/ko/getting-production-ready.md @@ -8,7 +8,7 @@ comments_id: getting-production-ready/158 ref: getting-production-ready --- -이제 Serverless Stack을 만드는 기본적인 사항들을 살펴 보았으므로 새 프로젝트를 만들 때마다 이러한 모든 수동 단계를 수행해야 하는지 궁금할 것입니다. 많은 독자들이 개인 및 전문 프로젝트에 이러한 스택을 사용했습니다. 그래서 Part II에서 우리는 공통적인 문제들을 다루게 될 것입니다. 구체적으로 다음과 같은 내용을 살펴 보겠습니다. +이제 SST을 만드는 기본적인 사항들을 살펴 보았으므로 새 프로젝트를 만들 때마다 이러한 모든 수동 단계를 수행해야 하는지 궁금할 것입니다. 많은 독자들이 개인 및 전문 프로젝트에 이러한 스택을 사용했습니다. 그래서 Part II에서 우리는 공통적인 문제들을 다루게 될 것입니다. 구체적으로 다음과 같은 내용을 살펴 보겠습니다. - **Infrastructure as Code** diff --git a/_chapters/ko/give-feedback-while-logging-in.md b/_chapters/ko/give-feedback-while-logging-in.md index 738b695536..420dc1be63 100644 --- a/_chapters/ko/give-feedback-while-logging-in.md +++ b/_chapters/ko/give-feedback-while-logging-in.md @@ -4,7 +4,7 @@ title: Give Feedback While Logging In date: 2017-01-18 00:00:00 lang: ko ref: give-feedback-while-logging-in -description: React.js 앱에 로그인하는 동안 사용자에게 몇 가지 피드백을 제공해야합니다. 이렇게하려면 React-Bootstrap Button 구성 요소 내에서 Glyphicon 새로 고침 아이콘을 움직이는 컴포넌트를 만듭니다. 로그인 호출이 진행되는 동안 애니메이션을 수행합니다. +description: React.js 앱에 로그인하는 동안 사용자에게 몇 가지 피드백을 제공해야합니다. 이렇게하려면 React-Bootstrap Button 구성 요소 내에서 Glyphicon 새로 고침 아이콘을 움직이는 컴포넌트를 만듭니다. 로그인 호출이 진행되는 동안 애니메이션을 수행합니다. context: true comments_id: give-feedback-while-logging-in/46 --- @@ -13,20 +13,20 @@ comments_id: give-feedback-while-logging-in/46 ### isLoading 플래그 사용하기 -`src/containers/Login.js`의 state에 `isLoading` 플래그를 추가합니다. 그러면 `constructor`의 초기 state는 다음과 같습니다. +{%change%} `src/containers/Login.js`의 state에 `isLoading` 플래그를 추가합니다. 그러면 `constructor`의 초기 state는 다음과 같습니다. -``` javascript +```js this.state = { isLoading: false, email: "", - password: "" + password: "", }; ``` -그리고 로그인하는 동안 업데이트합니다. 그러면 `handleSubmit` 메쏘드는 다음과 같습니다: +{%change%} 그리고 로그인하는 동안 업데이트합니다. 그러면 `handleSubmit` 메쏘드는 다음과 같습니다: -``` javascript -handleSubmit = async event => { +```js +handleSubmit = async (event) => { event.preventDefault(); this.setState({ isLoading: true }); @@ -34,21 +34,21 @@ handleSubmit = async event => { try { await Auth.signIn(this.state.email, this.state.password); this.props.userHasAuthenticated(true); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e.message); this.setState({ isLoading: false }); } -} +}; ``` ### Loader Button 만들기 이제 버튼의 상태 변화를 반영하기 위해 `isLoading` 플래그에 따라 다르게 렌더링 합니다. 그리고 우리는 다른 곳들에서 이 코드를 사용할 예정입니다. 따라서 재사용 가능한 컴포넌트를 만드는 것이 보다 합리적입니다. -`src/components/LoaderButton.js` 파일을 만들고 아래 내용을 추가합니다. +{%change%} `src/components/LoaderButton.js` 파일을 만들고 아래 내용을 추가합니다. -``` coffee +```coffee import React from "react"; import { Button, Glyphicon } from "react-bootstrap"; import "./LoaderButton.css"; @@ -75,17 +75,21 @@ export default ({ 로딩 아이콘에 애니메이션을 적용하는 몇 가지 스타일을 추가해 보겠습니다. -`src/components/LoaderButton.css` 파일에 아래 내용을 추가합니다. +{%change%} `src/components/LoaderButton.css` 파일에 아래 내용을 추가합니다. -``` css +```css .LoaderButton .spinning.glyphicon { margin-right: 7px; top: 2px; animation: spin 1s infinite linear; } @keyframes spin { - from { transform: scale(1) rotate(0deg); } - to { transform: scale(1) rotate(360deg); } + from { + transform: scale(1) rotate(0deg); + } + to { + transform: scale(1) rotate(360deg); + } } ``` @@ -95,37 +99,32 @@ export default ({ 이제 우리의 새로운 컴포넌트를 `Login` 컨테이너에서 사용할 수 있습니다. -`src/containers/Login.js` 파일의 `render` 함수에서 ` + ``` -그리고 위 내용을 아래와 같이 바꿉니다. +{%change%} 그리고 위 내용을 아래와 같이 바꿉니다. -``` html +```html ``` -또한 헤더에서 `LoaderButton`을 import 합니다. 그리고 `Button`에 대한 참조를 제거합니다. +{%change%} 또한 헤더에서 `LoaderButton`을 import 합니다. 그리고 `Button`에 대한 참조를 제거합니다. -``` javascript -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; +```js +import { FormGroup, FormControl, FormLabel } from "react-bootstrap"; import LoaderButton from "../components/LoaderButton"; ``` @@ -133,6 +132,6 @@ import LoaderButton from "../components/LoaderButton"; ![로그인 로딩 상태 화면](/assets/login-loading-state.png) -사용자를 위한 _비밀번호 찾기_ 기능을 추가하려면 [사용자 관리에 관한 추가 크레딧 시리즈]({% link _chapters/manage-user-accounts-in-aws-amplify.md %})를 참조하십시오. +사용자를 위한 _비밀번호 찾기_ 기능을 추가하려면 [사용자 관리에 관한 추가 크레딧 시리즈]({% link _archives/manage-user-accounts-in-aws-amplify.md %})를 참조하십시오. 이제 계속해서 회원 가입 프로세스를 구현해보도록 하겠습니다. diff --git a/_chapters/ko/giving-back.md b/_chapters/ko/giving-back.md index 64403d4351..e8f8c860e1 100644 --- a/_chapters/ko/giving-back.md +++ b/_chapters/ko/giving-back.md @@ -20,15 +20,15 @@ ref: giving-back - **핵심 가이드 업데이트 유지** -  Serverless Stack은 많은 수의 서비스와 오픈 소스 라이브러리 및 프로젝트에 의존합니다. 서비스 및 종속성에 대한 스크린 샷은 매번 업데이트됩니다. [여기에 대한 자세한 내용은 이 곳에 있습니다]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#keep-the-core-guide-updated#keep-the-core-guide-updated). +  SST은 많은 수의 서비스와 오픈 소스 라이브러리 및 프로젝트에 의존합니다. 서비스 및 종속성에 대한 스크린 샷은 매번 업데이트됩니다. [여기에 대한 자세한 내용은 이 곳에 있습니다]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#keep-the-core-guide-updated#keep-the-core-guide-updated). - **가이드 번역에 도움주기** -  독자들의 헌신 덕분에 Serverless Stack을 여러 언어로 번역하는데 도움을 받고 있습니다. 여기서 [저희의 진행 상황]({% link _chapters/translations.md %})을 확인할 수 있습니다. 번역 작업을 돕고 싶다면 [여기에 의견을 남겨주세요.](https://discourse.serverless-stack.com/t/help-us-translate-serverless-stack/596/15) +  독자들의 헌신 덕분에 SST을 여러 언어로 번역하는데 도움을 받고 있습니다. 여기서 [저희의 진행 상황]({% link _chapters/translations.md %})을 확인할 수 있습니다. 번역 작업을 돕고 싶다면 [여기에 의견을 남겨주세요.](https://discourse.sst.dev/t/help-us-translate-serverless-stack/596/15) - **보너스 챕터 추가** -  핵심 챕터에서는 Severless Stack 설정을 사용자 지정하기 위한 필요한 몇 가지 추가 정보가 빠져 있습니다(단순성을 위해). 또한 가이드의 핵심 부분에서 다루지 않는 경우도 있습니다. 우리는 *보너스 챕터*를 통해 이러한 문제를 해결합니다. Serverless Stack을 확장 할 기회가 있다면 그에 대한 챕터를 작성하는 것을 고려하십시오. [여기에 보너스 챕터를 추가하는 방법에 대한 자세한 내용이 있습니다]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#add-an-extra-credit-chapter). +  핵심 챕터에서는 Severless Stack 설정을 사용자 지정하기 위한 필요한 몇 가지 추가 정보가 빠져 있습니다(단순성을 위해). 또한 가이드의 핵심 부분에서 다루지 않는 경우도 있습니다. 우리는 *보너스 챕터*를 통해 이러한 문제를 해결합니다. SST을 확장 할 기회가 있다면 그에 대한 챕터를 작성하는 것을 고려하십시오. [여기에 보너스 챕터를 추가하는 방법에 대한 자세한 내용이 있습니다]({{ site.github_repo }}/blob/master/CONTRIBUTING.md#add-an-extra-credit-chapter). - **Tooling 개선** diff --git a/_chapters/ko/handle-404s.md b/_chapters/ko/handle-404s.md index 75939bda96..9609bf1e36 100644 --- a/_chapters/ko/handle-404s.md +++ b/_chapters/ko/handle-404s.md @@ -4,7 +4,7 @@ title: Handle 404s date: 2017-01-12 00:00:00 lang: ko ref: handle-404s -description: React.js 앱에서 React Router v4를 사용하여 404를 처리하려면 Switch 블록 하단에 모든 경로에 대한 감지 설정을 해야합니다. 모든 경로 감지에는 경로 속성 없이 모든 경로에 응답합니다. +description: React.js 앱에서 React Router v6를 사용하여 404를 처리하려면 Switch 블록 하단에 모든 경로에 대한 감지 설정을 해야합니다. 모든 경로 감지에는 경로 속성 없이 모든 경로에 응답합니다. context: true comments_id: handle-404s/75 --- @@ -15,9 +15,9 @@ comments_id: handle-404s/75 404를 처리하는 컴포넌트를 만드는 것으로 시작하겠습니다. -`src/containers/NotFound.js` 이름으로 새로운 컴포넌트를 만듭니다. 그리고 다음 내용을 추가합니다. +{%change%} `src/containers/NotFound.js` 이름으로 새로운 컴포넌트를 만듭니다. 그리고 다음 내용을 추가합니다. -``` coffee +```coffee import React from "react"; import "./NotFound.css"; @@ -29,31 +29,31 @@ export default () => 이 컴포넌트는 간단한 메시지를 출력합니다. -`src/containers/NotFound.css`에 몇 가지 스타일을 추가합니다. +{%change%} `src/containers/NotFound.css`에 몇 가지 스타일을 추가합니다. -``` css +```css .NotFound { padding-top: 100px; text-align: center; } ``` -### 모든 경로에 대한 감지 추가하기 +### 모든 경로에 대한 감지 추가하기 이제 모든 경로에서 404 처리를 위해 위 컴포넌트를 추가합니다. -`src/Routes.js`에서 `` 블록을 찾아 해당 블록 마지막 줄에 다음 내용을 추가합니다. +{%change%} `src/Routes.js`에서 `` 블록을 찾아 해당 블록 마지막 줄에 다음 내용을 추가합니다. -``` coffee +```coffee { /* 최종적으로 일치하지 않는 모든 경로를 감지합니다. */ } ``` 항상 `` 블록의 마지막 줄에 위치해야합니다. 그래야 다른 경로들에 대한 요청 실패 전에 이를 처리하는 경로라고 생각할 수 있습니다. -그리고 `NotFound` 컴포넌트를 import하기 위해 헤더 부분에 다음 내용을 추가합니다: +{%change%} 그리고 `NotFound` 컴포넌트를 import하기 위해 헤더 부분에 다음 내용을 추가합니다: -``` javascript +```js import NotFound from "./containers/NotFound"; ``` diff --git a/_chapters/ko/handle-api-gateway-cors-errors.md b/_chapters/ko/handle-api-gateway-cors-errors.md index bc7ce1de60..9abea69e80 100644 --- a/_chapters/ko/handle-api-gateway-cors-errors.md +++ b/_chapters/ko/handle-api-gateway-cors-errors.md @@ -10,7 +10,7 @@ code: backend comments_id: handle-api-gateway-cors-errors/780 --- -API를 배포하기 전에 마지막으로 설정할 내용이 있습니다. 바로 CORS 헤더를 API Gateway 오류에 추가하는 일입니다. [노트 추가 API 생성하기]({% link _chapters/add-a-create-note-api.md %}) 장에서 우리는 람다 함수에 CORS 헤더를 추가했었습니다. 사실 API 요청을 할 때, API Gateway는 Lambda 함수 전에 호출됩니다. 즉, API Gateway 수준에서 오류가 발생하면 CORS 헤더가 설정되지 않습니다. +API를 배포하기 전에 마지막으로 설정할 내용이 있습니다. 바로 CORS 헤더를 API Gateway 오류에 추가하는 일입니다. [노트 추가 API 생성하기]({% link _archives/add-a-create-note-api.md %}) 장에서 우리는 람다 함수에 CORS 헤더를 추가했었습니다. 사실 API 요청을 할 때, API Gateway는 Lambda 함수 전에 호출됩니다. 즉, API Gateway 수준에서 오류가 발생하면 CORS 헤더가 설정되지 않습니다. 이것 때문에 고객이 오류 메시지를 볼 수 없어 디버깅하기가 어렵습니다. 그냥 다음과 같이 표시됩니다. @@ -24,13 +24,13 @@ CORS 관련 오류는 가장 일반적인 Serverless API 오류 중 하나입니 API Gateway 오류를 구성하기 위해 우리는`serverless.yml`에 몇 가지를 추가 할 것입니다. 기본적으로 [Serverless Framework](https://serverless.com)는 [CloudFormation] (https://aws.amazon.com/cloudformation/)을 지원하므로 코드를 통해 API Gateway를 구성 할 수 있습니다. -리소스를 추가 할 디렉토리를 만들어 보겠습니다. 나중에이 가이드의 뒷부분에 추가하겠습니다. +{%change%} 리소스를 추가 할 디렉토리를 만들어 보겠습니다. 나중에이 가이드의 뒷부분에 추가하겠습니다. ``` bash $ mkdir resources/ ``` -그리고 `resources/api-gateway-errors.yml`에 다음 내용을 추가합니다. +{%change%} 그리고 `resources/api-gateway-errors.yml`에 다음 내용을 추가합니다. ``` yml Resources: @@ -60,7 +60,7 @@ Resources: 이제 위의 CloudFormation 리소스를`serverless.yml`에 포함시켜 보겠습니다. -`serverless.yml` 파일에 아래 내용을 추가합니다. +{%change%} `serverless.yml` 파일에 아래 내용을 추가합니다. ``` yml # 분리된 CloudFormation 템플릿을 생성합니다. diff --git a/_chapters/ko/handle-routes-with-react-router.md b/_chapters/ko/handle-routes-with-react-router.md index 30fce4e324..a45e5f348b 100644 --- a/_chapters/ko/handle-routes-with-react-router.md +++ b/_chapters/ko/handle-routes-with-react-router.md @@ -2,41 +2,40 @@ layout: post title: Handle Routes with React Router date: 2017-01-10 00:00:00 -lang: ko +lang: ko ref: handle-routes-with-react-router -description: Create React App은 경로가 설정된 상태로 제공되지 않습니다. 이를 위해 React Router를 사용할 것입니다. React Router의 최신 버전인 React Router v4는 React 구성 요소의 조합 가능한 특성을 포함하며 단일 페이지 응용 프로그램에서 경로 작업을 하기가 쉽습니다. +description: Create React App은 경로가 설정된 상태로 제공되지 않습니다. 이를 위해 React Router를 사용할 것입니다. React Router의 최신 버전인 React Router v6는 React 구성 요소의 조합 가능한 특성을 포함하며 단일 페이지 응용 프로그램에서 경로 작업을 하기가 쉽습니다. context: true comments_id: handle-routes-with-react-router/116 --- Create React App은 기본적으로 많은 것이 이미 설정되어 있지만, 경로에 대한 처리는 기본으로 제공되지 않습니다. 따라서 단일 페이지 앱을 개발하기 위해 [React Router](https://reacttraining.com/react-router/)를 사용하여 처리 할 것입니다. +먼저 React Router를 설치해 보겠습니다. React Router의 최신 버전인 React Router v6를 사용할 것입니다. React Router v6는 웹과 네이티브에서 사용할 수 있습니다. 여기서는 웹 용으로 설치하십시오. -먼저 React Router를 설치해 보겠습니다. React Router의 최신 버전인 React Router v4를 사용할 것입니다. React Router v4는 웹과 네이티브에서 사용할 수 있습니다. 여기서는 웹 용으로 설치하십시오. +### React Router v6 설치하기 -### React Router v4 설치하기 +{%change%} 작업 디렉토리에서 아래 명령어를 실행하세요. -작업 디렉토리에서 아래 명령어를 실행하세요. - -``` bash +```bash $ npm install react-router-dom@4.3.1 --save ``` 이렇게하면 NPM 패키지가 설치되고 `package.json`에 의존성이 추가됩니다. -### React Router 설정하기 +### React Router 설정하기 앱에 경로가 설정되어 있지는 않지만 기본 구조를 구성하고 실행할 수 있습니다. 우리의 앱은 현재 `src/App.js`의 `App` 컴포넌트에서 실행됩니다. 우리는 이 구성 요소를 전체 앱의 컨테이너로 사용하려고합니다. 이를 위해 우리는 `Router` 내에 `App` 컴포넌트를 캡슐화 할 것입니다. -`src/index.js` 에서 아래 코드를 대체합니다: +{%change%} `src/index.js` 에서 아래 코드를 대체합니다: -``` coffee +```coffee ReactDOM.render(, document.getElementById('root')); ``` -바꿀 내용: +{%change%} 바꿀 내용: -``` coffee +```coffee ReactDOM.render( @@ -45,9 +44,9 @@ ReactDOM.render( ); ``` -그리고 `src/index.js`에 아래 내용을 맨 윗 부분에 붙여 넣습니다. +{%change%} 그리고 `src/index.js`에 아래 내용을 맨 윗 부분에 붙여 넣습니다. -``` coffee +```coffee import { BrowserRouter as Router } from "react-router-dom"; ``` @@ -59,4 +58,3 @@ import { BrowserRouter as Router } from "react-router-dom"; 이제 브라우저로 이동하면 앱이 이전과 마찬가지로 로드됩니다. 유일한 차이점은 React Router를 사용하여 페이지를 제공한다는 것입니다. 다음으로 앱의 다른 페이지를 추가하는 방법을 살펴 보겠습니다. - diff --git a/_chapters/ko/how-to-get-help.md b/_chapters/ko/how-to-get-help.md index 81f1862dd5..67546aeba4 100644 --- a/_chapters/ko/how-to-get-help.md +++ b/_chapters/ko/how-to-get-help.md @@ -1,7 +1,7 @@ --- layout: post title: How to Get Help? -lang: ko +lang: ko ref: how-to-get-help date: 2016-12-23 00:00:00 comments_id: how-to-get-help/95 @@ -12,7 +12,7 @@ comments_id: how-to-get-help/95 - 우리는 [담론 포럼 주제]({{site.forum_url}})를 우리의 의견으로 사용하고 과거에 많은 문제를 해결하는 데 도움을주었습니다. 따라서 각 장의 주석을 확인하여 다른 사람이 가졌던 문제와 동일한 문제가 발생했는지 확인하십시오. - 여러분의 쟁점을 상세히 설명하는 특정 챕터에 대한 의견을 게시하십시오. 저희들 중 한 명이 답변 드릴 것입니다. -! [Serverless Stack Discourse Forums 스크린 샷](/assets /serverless-stack-discourse-forums.png) +! [SST Discourse Forums 스크린 샷](/assets/serverless-stack-discourse-forums.png) 이 전체 가이드는 [GitHub]({{site.github_repo}})에서 호스팅됩니다. 따라서 오류를 발견하면 항상 다음 작업을 수행할 수 있습니다. diff --git a/_chapters/ko/initialize-the-backend-repo.md b/_chapters/ko/initialize-the-backend-repo.md index 1a0b873995..4ee502e64d 100644 --- a/_chapters/ko/initialize-the-backend-repo.md +++ b/_chapters/ko/initialize-the-backend-repo.md @@ -12,20 +12,20 @@ ref: initialize-the-backend-repo ### 코드 복제하기 -작업 디렉토리에서 [original 저장소]({{ site.backend_github_repo }})를 복제합니다. +{%change%} 작업 디렉토리에서 [original 저장소]({{ site.backend_github_repo }})를 복제합니다. ``` bash $ git clone --branch handle-api-gateway-cors-errors --depth 1 https://github.com/AnomalyInnovations/serverless-stack-demo-api.git serverless-stack-2-api/ $ cd serverless-stack-2-api/ ``` -그리고 `.git/` 디렉토리를 삭제합니다. +{%change%} 그리고 `.git/` 디렉토리를 삭제합니다. ``` bash $ rm -rf .git/ ``` -Node 모듈을 설치합니다. +{%change%} Node 모듈을 설치합니다. ``` bash $ npm install @@ -53,25 +53,25 @@ https://github.com/jayair/serverless-stack-2-api.git ### 여러분의 신규 저장소 초기화하기 -이제 프로젝트로 돌아가서 다음 명령을 사용하여 새 repo를 초기화하십시오. +{%change%} 이제 프로젝트로 돌아가서 다음 명령을 사용하여 새 repo를 초기화하십시오. ``` bash $ git init ``` -기존 파일을 추가합니다. +{%change%} 기존 파일을 추가합니다. ``` bash $ git add . ``` -여러분의 첫 커밋을 생성합니다. +{%change%} 여러분의 첫 커밋을 생성합니다. ``` bash $ git commit -m "First commit" ``` -이제 여러분이 Github에 생성한 저장소로 링크합니다. +{%change%} 이제 여러분이 Github에 생성한 저장소로 링크합니다. ``` bash $ git remote add origin REPO_URL @@ -83,7 +83,7 @@ $ git remote add origin REPO_URL $ git remote -v ``` -마지막으로 첫 커밋을 GitHub에 푸시(Push)합니다: +{%change%} 마지막으로 첫 커밋을 GitHub에 푸시(Push)합니다: ``` bash $ git push -u origin master diff --git a/_chapters/ko/initialize-the-frontend-repo.md b/_chapters/ko/initialize-the-frontend-repo.md index 207b36e8a4..709a2602dd 100644 --- a/_chapters/ko/initialize-the-frontend-repo.md +++ b/_chapters/ko/initialize-the-frontend-repo.md @@ -12,20 +12,20 @@ ref: initialize-the-frontend-repo ### 원본 저장소 복제 -작업 디렉토리에서 [원본 저장소]({{ site.frontend_github_repo }})를 복제하는 것으로 시작합니다. 주의할 점은 현재 위치가 백앤드 디렉토리 내부가 아니어야 한다는 점입니다. +{%change%} 작업 디렉토리에서 [원본 저장소]({{ site.frontend_github_repo }})를 복제하는 것으로 시작합니다. 주의할 점은 현재 위치가 백앤드 디렉토리 내부가 아니어야 한다는 점입니다. ``` bash $ git clone --branch part-1 --depth 1 https://github.com/AnomalyInnovations/serverless-stack-demo-client.git serverless-stack-2-client/ $ cd serverless-stack-2-client/ ``` -`.git/` 디렉토리를 삭제합니다. +{%change%} `.git/` 디렉토리를 삭제합니다. ``` bash $ rm -rf .git/ ``` -Node 모듈을 설치합니다. +{%change%} Node 모듈을 설치합니다. ``` bash $ npm install @@ -53,25 +53,25 @@ https://github.com/jayair/https://github.com/jayair/serverless-stack-2-client.gi ### 새로운 저장소 초기화하기 -이제 프로젝트로 돌아가서 다음 명령을 사용하여 새 저장소를 초기화하십시오. +{%change%} 이제 프로젝트로 돌아가서 다음 명령을 사용하여 새 저장소를 초기화하십시오. ``` bash $ git init ``` -기존에 작업 파일들을 추가합니다. +{%change%} 기존에 작업 파일들을 추가합니다. ``` bash $ git add . ``` -첫 번째 커밋을 실행합니다. +{%change%} 첫 번째 커밋을 실행합니다. ``` bash $ git commit -m "First commit" ``` -그리고 여러분이 생성한 Github에 연결합니다. +{%change%} 그리고 여러분이 생성한 Github에 연결합니다. ``` bash $ git remote add origin REPO_URL @@ -83,7 +83,7 @@ $ git remote add origin REPO_URL $ git remote -v ``` -마지막으로, 커밋한 내용을 다음과 같이 푸시합니다.: +{%change%} 마지막으로, 커밋한 내용을 다음과 같이 푸시합니다.: ``` bash $ git push -u origin master diff --git a/_chapters/ko/list-all-the-notes.md b/_chapters/ko/list-all-the-notes.md index 978b8b8b83..6a52ea8f55 100644 --- a/_chapters/ko/list-all-the-notes.md +++ b/_chapters/ko/list-all-the-notes.md @@ -13,7 +13,7 @@ comments_id: list-all-the-notes/156 현재 Home 컨테이너는 매우 간단합니다. 여기에 조건부로 렌더링을 추가합니다. -`src/containers/Home.js` 파일을 다음 내용으로 변경합니다. +{%change%} `src/containers/Home.js` 파일을 다음 내용으로 변경합니다. ``` coffee import React, { Component } from "react"; diff --git a/_chapters/ko/load-secrets-from-env-yml.md b/_chapters/ko/load-secrets-from-env-yml.md index 3d89be1ad2..10f7134242 100644 --- a/_chapters/ko/load-secrets-from-env-yml.md +++ b/_chapters/ko/load-secrets-from-env-yml.md @@ -13,7 +13,7 @@ ref: load-secrets-from-env-yml 이러한 용도로 사용하기 위해 미리 `env.example` 파일을 준비해놨습니다. -`env.example` 파일을 `env.yml`로 이름을 바꾸고 내용을 다음과 같이 변경합니다. +{%change%} `env.example` 파일을 `env.yml`로 이름을 바꾸고 내용을 다음과 같이 변경합니다. ``` yml # Add the environment variables for the various stages @@ -29,7 +29,7 @@ default: 다음으로 이들에 대한 참조를 추가해 보겠습니다. -`serverless.yml`의 `custom:` 블럭에 다음 내용을 추가합니다. +{%change%} `serverless.yml`의 `custom:` 블럭에 다음 내용을 추가합니다. ``` yml # Load our secret environment variables based on the current stage. @@ -42,7 +42,7 @@ default: ``` yml custom: # Our stage is based on what is passed in when running serverless - # commands. Or fallsback to what we have set in the provider section. + # commands. Or falls back to what we have set in the provider section. stage: ${opt:stage, self:provider.stage} # Set the table name here so we can use it while testing locally tableName: ${self:custom.stage}-notes @@ -60,7 +60,7 @@ custom: environment: ${file(env.yml):${self:custom.stage}, file(env.yml):default} ``` -그리고 `serverless.yml`의 `environment:` 블럭에 다음 내용을 추가합니다. +{%change%} 그리고 `serverless.yml`의 `environment:` 블럭에 다음 내용을 추가합니다. ``` yml stripeSecretKey: ${self:custom.environment.stripeSecretKey} @@ -93,7 +93,7 @@ env.yml 위 파일은 Git으로 하여금 커밋할 경우, `env.yml` 파일을 제외하도록 합니다. -그럼 나머지 변경사항들을 커밋하겠습니다. +{%change%} 그럼 나머지 변경사항들을 커밋하겠습니다. ``` bash $ git add . diff --git a/_chapters/ko/load-the-state-from-the-session.md b/_chapters/ko/load-the-state-from-the-session.md index c1c22f217d..e74980945f 100644 --- a/_chapters/ko/load-the-state-from-the-session.md +++ b/_chapters/ko/load-the-state-from-the-session.md @@ -17,24 +17,24 @@ Amplify는 `Auth.currentSession()` 메소드를 사용하여 현재 사용자 앱이 브라우저에 로드 될 때 사용자 세션을 불러오겠습니다. 먼저 `componentDidMount`에서 이를 처리합니다. `Auth.currentSession()`이 promise를 반환하므로 앱이 완전히 로드가 완료된 상태로 준비가 되어 있어야합니다. -이를 위해 `isAuthenticating`라고 하는 `src/App.js`의 state에 플래그를 추가합니다. 생성자의 초기 상태는 다음과 같아야합니다. +{%change%} 이를 위해 `isAuthenticating`라고 하는 `src/App.js`의 state에 플래그를 추가합니다. 생성자의 초기 상태는 다음과 같아야합니다. -``` javascript +```js this.state = { isAuthenticated: false, - isAuthenticating: true + isAuthenticating: true, }; ``` -`Auth` 모듈을 `src/App.js` 헤더에 다음과 같이 추가해 보겠습니다. +{%change%} `Auth` 모듈을 `src/App.js` 헤더에 다음과 같이 추가해 보겠습니다. -``` javascript +```js import { Auth } from "aws-amplify"; ``` -이제 사용자 세션을 불러오기 위해 우리는 `src/App.js`의 `constructor` 메소드 아래에 다음을 추가 할 것입니다. +{%change%} 이제 사용자 세션을 불러오기 위해 우리는 `src/App.js`의 `constructor` 메소드 아래에 다음을 추가 할 것입니다. -``` javascript +```js async componentDidMount() { try { await Auth.currentSession(); @@ -58,9 +58,9 @@ async componentDidMount() { 여기서는`isAuthenticating` 플래그에 기반하여 앱을 조건부로 렌더링합니다. -`src/App.js`의 `render` 메소드는 아래와 같아야 합니다. +{%change%} `src/App.js`의 `render` 메소드는 아래와 같아야 합니다. -``` coffee +```coffee render() { const childProps = { isAuthenticated: this.state.isAuthenticated, diff --git a/_chapters/ko/login-with-aws-cognito.md b/_chapters/ko/login-with-aws-cognito.md index fb33b7d553..50cf3a4867 100644 --- a/_chapters/ko/login-with-aws-cognito.md +++ b/_chapters/ko/login-with-aws-cognito.md @@ -13,9 +13,9 @@ AWS Amplify를 사용하여 Amazon Cognito 설정에 로그인합니다. 가져 ### AWS Amplify에서 Auth 가져 오기 -`src/containers/Login.js`에 있는 Login 컨테이너의 헤더에 다음을 추가하십시오. +{%change%} `src/containers/Login.js`에 있는 Login 컨테이너의 헤더에 다음을 추가하십시오. -``` coffee +```coffee import { Auth } from "aws-amplify"; ``` @@ -23,10 +23,10 @@ import { Auth } from "aws-amplify"; 로그인 코드 자체는 비교적 간단합니다. -`src/containers/Login.js` 파일의 `handleSubmit` 메소드를 다음과 같이 바꾸기만 하면 됩니다. +{%change%} `src/containers/Login.js` 파일의 `handleSubmit` 메소드를 다음과 같이 바꾸기만 하면 됩니다. -``` javascript -handleSubmit = async event => { +```js +handleSubmit = async (event) => { event.preventDefault(); try { @@ -35,7 +35,7 @@ handleSubmit = async event => { } catch (e) { alert(e.message); } -} +}; ``` 여기서는 두 가지를 처리하고 있습니다. @@ -44,7 +44,7 @@ handleSubmit = async event => { 2. `await` 키워드를 사용하여 promise를 반환하는 Auth.signIn() 메소드를 호출합니다. 그리고 `handleSubmit` 메쏘드에 `async`라는 키워드를 붙일 필요가 있습니다. -이제 `admin@example.com` 사용자([Cognito 테스트 사용자 만들기]({% link _chapters/create-a-cognito-test-user.md %}) 챕터에서 작성한 사용자)로 로그인하면, 로그인이 성공했다는 브라우저 경고가 표시됩니다. +이제 `admin@example.com` 사용자([Cognito 테스트 사용자 만들기]({% link _archives/create-a-cognito-test-user.md %}) 챕터에서 작성한 사용자)로 로그인하면, 로그인이 성공했다는 브라우저 경고가 표시됩니다. ![로그인 성공 스크린 샷](/assets/login-success.png) diff --git a/_chapters/ko/manage-environments-in-create-react-app.md b/_chapters/ko/manage-environments-in-create-react-app.md index 12a3b4fef0..9d94ee7175 100644 --- a/_chapters/ko/manage-environments-in-create-react-app.md +++ b/_chapters/ko/manage-environments-in-create-react-app.md @@ -59,7 +59,7 @@ console.log(process.env.REACT_APP_TEST_VAR); 우리의 목적을 위해 `REACT_APP_STAGE`라는 환경 변수를 사용합시다. 이 변수는 `dev` 와 `prod` 값을 취합니다. 그리고 기본적으로 `dev`로 설정되어 있습니다. 이제 우리는 config로 이것을 재 작성할 수 있습니다. -`src/config.js`를 다음 내용으로 대체합니다. +{%change%} `src/config.js`를 다음 내용으로 대체합니다. ``` js const dev = { @@ -108,7 +108,7 @@ export default { }; ``` -다른 버전의 자원을 [Seed를 통해 배포]({% link _chapters/deploying-through-seed.md %}) 챕터의 자원으로 바꿔야합니다. +다른 버전의 자원을 [Seed를 통해 배포]({% link _chapters/deploying-through-the-sst-console.md %}) 챕터의 자원으로 바꿔야합니다. `REACT_APP_STAGE`가 설정되어 있지 않으면 dev 환경을 기본값으로 설정한다는 것에주의하십시오. 이것은 현재의 빌드 프로세스(`npm start` 와 `npm run build`)가 `dev` 환경을 기본값으로한다는 것을 의미합니다. 그리고 두 환경에 공통적인 `MAX_ATTACHMENT_SIZE`와 같은 설정 값에 대해서는 다른 섹션에서 다루겠습니다. diff --git a/_chapters/ko/monitoring-deployments-in-seed.md b/_chapters/ko/monitoring-deployments-in-seed.md index 6e77574504..a3bd6c643e 100644 --- a/_chapters/ko/monitoring-deployments-in-seed.md +++ b/_chapters/ko/monitoring-deployments-in-seed.md @@ -16,7 +16,7 @@ ref: monitoring-deployments-in-seed 먼저 분명히 잘못된 코드를 push 하십시오. -`functions/create.js` 파일의 맨 위 함수에 다음을 추가합니다. +{%change%} `functions/create.js` 파일의 맨 위 함수에 다음을 추가합니다. ``` js gibberish.what; @@ -24,7 +24,7 @@ gibberish.what; `gibberish` 변수가 없기 때문에 이 코드는 실패해야합니다. -dev에 커밋하고 푸시합니다. +{%change%} dev에 커밋하고 푸시합니다. ``` bash $ git add . @@ -60,7 +60,7 @@ $ git push ### 오류 코드 테스트 -이제 코드를 테스트하기 위해 [마지막 챕터]({% link _chapters/test-the-configured-apis.md %})에서 동일한 명령을 실행하여 API를 테스트하십시오. +이제 코드를 테스트하기 위해 [마지막 챕터]({% link _chapters/secure-our-serverless-apis.md %})에서 동일한 명령을 실행하여 API를 테스트하십시오. ``` bash $ npx aws-api-gateway-cli-test \ diff --git a/_chapters/ko/organize-the-backend-repo.md b/_chapters/ko/organize-the-backend-repo.md index 67cfd755e0..dc55f370c9 100644 --- a/_chapters/ko/organize-the-backend-repo.md +++ b/_chapters/ko/organize-the-backend-repo.md @@ -12,7 +12,7 @@ ref: organize-the-backend-repo ### 사용하지 않는 파일들 삭제하기 -이제 스타터 프로젝트에서 두 개의 파일을 제거할 수 있습니다. +{%change%} 이제 스타터 프로젝트에서 두 개의 파일을 제거할 수 있습니다. ``` bash $ rm handler.js @@ -23,13 +23,13 @@ $ rm tests/handler.test.js 우리는 다른 서비스명을 사용할 것입니다. -`serverless.yml` 파일을 열어서 다음 행을 찾습니다.: +{%change%} `serverless.yml` 파일을 열어서 다음 행을 찾습니다.: ``` yml -service: notes-app-api +service: notes-api ``` -그리고 아래 내용으로 변경합니다.: +{%change%} 그리고 아래 내용으로 변경합니다.: ``` yml service: notes-app-2-api @@ -37,13 +37,13 @@ service: notes-app-2-api 이것을 하는 이유는 Serverless Framework가 프로젝트를 식별하기 위해 `service` 이름을 사용하기 때문입니다. 우리는 새로운 프로젝트를 만들었으므로 원래 이름과 다른 이름을 사용해야 합니다. 그냥 간단히 기존 프로젝트를 덮어 쓸 수 있지만 코드를 통해 생성하려고할 때 이전에 손으로 작성한 리소스가 충돌합니다. -먼저 `serverless.yml`에서 아래 행을 찾습니다: +{%change%} 먼저 `serverless.yml`에서 아래 행을 찾습니다: ``` yml stage: prod ``` -그리고 다음으로 바꿉니다.: +{%change%} 그리고 다음으로 바꿉니다.: ``` yml stage: dev @@ -51,7 +51,7 @@ stage: dev 우리는 stage를 `prod` 대신 `dev`로 기본 설정하고 있습니다. 나중에 여러 환경을 만들 때 구분할 수 있습니다. -변경 사항을 빠르게 커밋합니다. +{%change%} 변경 사항을 빠르게 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/redirect-on-login-and-logout.md b/_chapters/ko/redirect-on-login-and-logout.md index 7775197a4e..c157655732 100644 --- a/_chapters/ko/redirect-on-login-and-logout.md +++ b/_chapters/ko/redirect-on-login-and-logout.md @@ -4,7 +4,7 @@ title: Redirect on Login and Logout date: 2017-01-17 00:00:00 lang: ko ref: redirect-on-login-and-logout -description: 로그인 후 사용자가 React.js 앱에서 로그아웃 한 후 리디렉션 되도록 React Router v4의 withRouter higher-order 컴포넌트를 사용하고 history.push 메서드를 사용하여 앱을 탐색합니다. +description: 로그인 후 사용자가 React.js 앱에서 로그아웃 한 후 리디렉션 되도록 React Router v6의 withRouter higher-order 컴포넌트를 사용하고 nav.push 메서드를 사용하여 앱을 탐색합니다. context: true comments_id: redirect-on-login-and-logout/154 --- @@ -14,75 +14,75 @@ comments_id: redirect-on-login-and-logout/154 1. 로그인 한 후 사용자를 홈페이지로 리디렉션하십시오. 2. 로그아웃 한 후 다시 로그인 페이지로 리디렉션하십시오. -React Router v4와 함께 제공되는 `history.push` 메소드를 사용합니다. +React Router v6와 함께 제공되는 `nav.push` 메소드를 사용합니다. ### 로그인시 홈 화면으로 리디렉션 -`Login` 컴포넌트는 `Route`를 사용하여 렌더링되기 때문에, 라우터 속성을 추가합니다. 그래서 `this.props.history.push` 메쏘드를 사용하여 리디렉션할 수 있습니다. +`Login` 컴포넌트는 `Route`를 사용하여 렌더링되기 때문에, 라우터 속성을 추가합니다. 그래서 `this.props.nav.push` 메쏘드를 사용하여 리디렉션할 수 있습니다. -``` javascript -this.props.history.push("/"); +```js +this.props.nav("/"); ``` -`src/containers/Login.js`의 `handleSubmit` 메소드를 다음과 같이 갱신하십시오: +{%change%} `src/containers/Login.js`의 `handleSubmit` 메소드를 다음과 같이 갱신하십시오: -``` javascript -handleSubmit = async event => { +```js +handleSubmit = async (event) => { event.preventDefault(); try { await Auth.signIn(this.state.email, this.state.password); this.props.userHasAuthenticated(true); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e.message); } -} +}; ``` 이제 브라우저로 가서 로그인을 시도하면 로그인 한 후에 홈페이지로 리디렉션되어야합니다. -![React Router v4 로그인 후 홈페이지로 리디렉션 화면](/assets/redirect-home-after-login.png) +![React Router v6 로그인 후 홈페이지로 리디렉션 화면](/assets/redirect-home-after-login.png) ### 로그아웃 한 후에 로그인 화면으로 리디렉션 -이제 실제와 같은 로그아웃 프로세스와 동일한 처리를 하겠습니다. 그러나 `App` 컴포넌트는 `Route` 컴포넌트 내부에서 렌더링되지 않기 때문에 라우터 속성에 직접 접근 할 수 없습니다. `App` 컴포넌트에서 라우터 속성을 사용하기 위해서는 `withRouter` [Higher-Order 컴포넌트](https://facebook.github.io/react/docs/higher-order-components)(또는 HOC)를 사용할 필요가 있습니다. [여기서](https://reacttraining.com/react-router/web/api/withRouter)에 `withRouter` HOC에 대한 자세한 내용을 볼 수 있습니다. +이제 실제와 같은 로그아웃 프로세스와 동일한 처리를 하겠습니다. 그러나 `App` 컴포넌트는 `Route` 컴포넌트 내부에서 렌더링되지 않기 때문에 라우터 속성에 직접 접근 할 수 없습니다. `App` 컴포넌트에서 라우터 속성을 사용하기 위해서는 `withRouter` [Higher-Order 컴포넌트](https://facebook.github.io/react/docs/higher-order-components)(또는 HOC)를 사용할 필요가 있습니다. [여기서](https://reacttraining.com/react-router/web/api/withRouter)에 `withRouter` HOC에 대한 자세한 내용을 볼 수 있습니다. 이 HOC를 사용하기 위해 App 컴포넌트를 내보내는 방식을 변경하겠습니다. -`src/App.js`에서 아래 행을 변경합니다. +{%change%} `src/App.js`에서 아래 행을 변경합니다. -``` coffee +```coffee export default App; ``` -위 내용을 다음으로 바꿉니다. +{%change%} 위 내용을 다음으로 바꿉니다. -``` coffee +```coffee export default withRouter(App); ``` -그리고 `src/App.js`의 헤더에 있는 `import {Link}` 행을 다음과 같이 바꾸어 `withRouter`를 가져옵니다: +{%change%} 그리고 `src/App.js`의 헤더에 있는 `import {Link}` 행을 다음과 같이 바꾸어 `withRouter`를 가져옵니다: -``` coffee +```coffee import { Link, withRouter } from "react-router-dom"; ``` -`src/App.js`에 있는 `handleLogout` 메소드의 맨 아래에 다음을 추가하십시오. +{%change%} `src/App.js`에 있는 `handleLogout` 메소드의 맨 아래에 다음을 추가하십시오. -``` coffee -this.props.history.push("/login"); +```coffee +this.props.nav("/login"); ``` 그래서 handleLogout 메소드는 다음과 같이 보일 것입니다. -``` coffee +```coffee handleLogout = async event => { await Auth.signOut(); this.userHasAuthenticated(false); - this.props.history.push("/login"); + this.props.nav("/login"); } ``` diff --git a/_chapters/ko/redirect-on-login.md b/_chapters/ko/redirect-on-login.md index e01e8ae7a3..a35034f008 100644 --- a/_chapters/ko/redirect-on-login.md +++ b/_chapters/ko/redirect-on-login.md @@ -2,8 +2,8 @@ layout: post title: Redirect on Login date: 2017-02-04 00:00:00 -lang: ko -description: React.js가 로그인 한 후 사용자를 올바른 페이지로 리디렉션하도록하려면 React Router v4 Redirect 컴포넌트를 사용합니다. +lang: ko +description: React.js가 로그인 한 후 사용자를 올바른 페이지로 리디렉션하도록하려면 React Router v6 Redirect 컴포넌트를 사용합니다. context: true comments_id: redirect-on-login/24 ref: redirect-on-login @@ -13,9 +13,9 @@ ref: redirect-on-login URL의 쿼리 문자열에서 `redirect`를 읽는 메소드를 추가해 보겠습니다. -`src/components/UnauthenticatedRoute.js`의 import 구문 밑에 다음 메소드를 추가합니다. +{%change%} `src/components/UnauthenticatedRoute.js`의 import 구문 밑에 다음 메소드를 추가합니다. -``` coffee +```coffee function querystring(name, url = window.location.href) { name = name.replace(/[[]]/g, "\\$&"); @@ -37,9 +37,9 @@ function querystring(name, url = window.location.href) { 이제 리디렉션 할 때이 매개 변수를 사용하도록 컴포넌트를 업데이트 해 보겠습니다. -현재 `export default ({ component: C, props: cProps, ...rest }) =>` 메소드를 아래와 같이 수정합니다. +{%change%} 현재 `export default ({ component: C, props: cProps, ...rest }) =>` 메소드를 아래와 같이 수정합니다. -``` coffee +```coffee export default ({ component: C, props: cProps, ...rest }) => { const redirect = querystring("redirect"); return ( @@ -48,7 +48,7 @@ export default ({ component: C, props: cProps, ...rest }) => { render={props => !cProps.isAuthenticated ? - : } /> @@ -56,10 +56,10 @@ export default ({ component: C, props: cProps, ...rest }) => { }; ``` -`src/containers/Login.js`의 `handleSubmit` 메소드에서 다음을 삭제합니다. . +{%change%} `src/containers/Login.js`의 `handleSubmit` 메소드에서 다음을 삭제합니다. . -``` coffee -this.props.history.push("/"); +```coffee +this.props.nav("/"); ``` 이제 로그인 페이지가 리디렉션되어야합니다. diff --git a/_chapters/ko/render-the-note-form.md b/_chapters/ko/render-the-note-form.md index b6b6caa36d..b827cff943 100644 --- a/_chapters/ko/render-the-note-form.md +++ b/_chapters/ko/render-the-note-form.md @@ -2,7 +2,7 @@ layout: post title: Render the Note Form date: 2017-01-29 00:00:00 -lang: ko +lang: ko description: 우리는 React.js 앱의 형태로 사용자 노트를 렌더링하려고합니다. 양식 필드를 렌더링하기 위해 React-Bootstrap의 FormGroup 및 FormControl 구성 요소를 사용합니다. context: true comments_id: render-the-note-form/140 @@ -11,9 +11,9 @@ ref: render-the-note-form 이제 컨테이너가 `componentDidMount`에 메모를 적재 했으므로 편집을 위해 사용할 양식을 렌더링 해 보겠습니다. -`src/containers/Notes.js` 파일에 아래 내용과 같이 메소드를 추가하고 `render` 메소드를 변경합니다. +{%change%} `src/containers/Notes.js` 파일에 아래 내용과 같이 메소드를 추가하고 `render` 메소드를 변경합니다. -``` coffee +```coffee validateForm() { return this.state.content.length > 0; } @@ -71,7 +71,7 @@ render() { {this.state.note.attachment && - Attachment + Attachment } {!this.state.note.attachment && - Attachment} + Attachment} 그럼 `constructor`의 state는 다음 처럼 설정되어 있어야 합니다. +{%change%} 그럼 `constructor`의 state는 다음 처럼 설정되어 있어야 합니다. -``` javascript +```js this.state = { isLoading: null, isDeleting: null, note: null, content: "", - attachmentURL: null + attachmentURL: null, }; ``` -`src/containers/Notes.css`에 다음을 추가해서 약간의 스타일을 반영하겠습니다. +{%change%} `src/containers/Notes.css`에 다음을 추가해서 약간의 스타일을 반영하겠습니다. -``` css +```css .Notes form { padding-bottom: 15px; } @@ -153,11 +153,10 @@ this.state = { } ``` -또한 헤더에 다음을 추가하여 여기에서 사용중인 React-Bootstrap 구성 요소와 스타일인 `LoaderButton` 그리고 `config`를 포함 시키십시오. +{%change%} 또한 헤더에 다음을 추가하여 여기에서 사용중인 React-Bootstrap 구성 요소와 스타일인 `LoaderButton` 그리고 `config`를 포함 시키십시오. - -``` javascript -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; +```js +import { FormGroup, FormControl, FormLabel } from "react-bootstrap"; import LoaderButton from "../components/LoaderButton"; import config from "../config"; import "./Notes.css"; diff --git a/_chapters/ko/save-changes-to-a-note.md b/_chapters/ko/save-changes-to-a-note.md index b64b98a163..e697fa3a2f 100644 --- a/_chapters/ko/save-changes-to-a-note.md +++ b/_chapters/ko/save-changes-to-a-note.md @@ -2,8 +2,8 @@ layout: post title: Save Changes to a Note date: 2017-01-30 00:00:00 -lang: ko -description: 사용자가 React.js 앱에서 노트를 편집하려면 AWS Amplify를 사용하여 Serverless 백엔드 API에 PUT 요청을해야합니다. 또한 파일을 S3에 직접 업로드하고 노트에 첨부 파일로 추가 할 수 있어야합니다. +lang: ko +description: 사용자가 React.js 앱에서 노트를 편집하려면 AWS Amplify를 사용하여 Serverless 백엔드 API에 PUT 요청을해야합니다. 또한 파일을 S3에 직접 업로드하고 노트에 첨부 파일로 추가 할 수 있어야합니다. context: true comments_id: save-changes-to-a-note/131 ref: save-changes-to-a-note @@ -11,9 +11,9 @@ ref: save-changes-to-a-note 이제 노트가 양식에 로드되었으므로 변경 사항을 노트에 저장해 보겠습니다. -`src/containers/Notes.js`에 있는 `handleSubmit` 메소드를 다음으로 대체하십시오. +{%change%} `src/containers/Notes.js`에 있는 `handleSubmit` 메소드를 다음으로 대체하십시오. -``` coffee +```coffee saveNote(note) { return API.put("notes", `/notes/${this.props.match.params.id}`, { body: note @@ -41,7 +41,7 @@ handleSubmit = async event => { content: this.state.content, attachment: attachment || this.state.note.attachment }); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e); this.setState({ isLoading: false }); @@ -50,9 +50,9 @@ handleSubmit = async event => { ``` -그리고 헤더에 `s3Upload` helper 메소드를 추가합니다.: +{%change%} 그리고 헤더에 `s3Upload` helper 메소드를 추가합니다.: -``` javascript +```js import { s3Upload } from "../libs/awsLib"; ``` diff --git a/_chapters/ko/setting-up-your-project-on-seed.md b/_chapters/ko/setting-up-your-project-on-seed.md index 66a1472c13..db38d52091 100644 --- a/_chapters/ko/setting-up-your-project-on-seed.md +++ b/_chapters/ko/setting-up-your-project-on-seed.md @@ -31,7 +31,7 @@ ref: setting-up-your-project-on-seed 이제 Seed가 대신해서 AWS 계정에 배포합니다. 프로젝트에 필요한 정확한 권한을 가진 별도의 IAM 사용자를 만들어야합니다. 이에 대한 자세한 내용은 [여기](https://seed.run/docs/customizing-your-iam-policy)를 참조하십시오. 하지만 여기서는 간단히 이 튜토리얼에서 사용했던 것을 사용한다. -다음 명령을 실행합니다. +{%change%} 다음 명령을 실행합니다. ``` bash $ cat ~/.aws/credentials diff --git a/_chapters/ko/setup-bootstrap.md b/_chapters/ko/setup-bootstrap.md index a8de964b96..98bf6b2c52 100644 --- a/_chapters/ko/setup-bootstrap.md +++ b/_chapters/ko/setup-bootstrap.md @@ -13,7 +13,7 @@ comments_id: set-up-bootstrap/118 ### React Bootstrap 설치하기 -작업 디렉토리에서 다음 명령을 실행하십시오. +{%change%} 작업 디렉토리에서 다음 명령을 실행하십시오. ``` bash $ npm install react-bootstrap@0.32.4 --save @@ -23,7 +23,7 @@ $ npm install react-bootstrap@0.32.4 --save ### 부트스트랩 스타일 추가하기 -React Bootstrap은 표준 부트스트랩 v3 스타일을 사용합니다. 그래서 `public/index.html`에 다음 스타일을 추가하십시오. +{%change%} React Bootstrap은 표준 부트스트랩 v3 스타일을 사용합니다. 그래서 `public/index.html`에 다음 스타일을 추가하십시오. ``` html @@ -31,7 +31,7 @@ $ npm install react-bootstrap@0.32.4 --save 또한 양식 필드의 스타일을 조정하여 모바일 브라우저가 포커스를 확대하지 않도록합니다. 확대/축소를 방지하려면 글꼴 크기를 최소 `16px`로 지정하면됩니다. -그러기 위해 `src/index.css`에 아래 내용을 추가합니다. +{%change%} 그러기 위해 `src/index.css`에 아래 내용을 추가합니다. ``` css select.form-control, diff --git a/_chapters/ko/setup-custom-fonts.md b/_chapters/ko/setup-custom-fonts.md index 77ae777c80..fc84fa866a 100644 --- a/_chapters/ko/setup-custom-fonts.md +++ b/_chapters/ko/setup-custom-fonts.md @@ -19,7 +19,7 @@ comments_id: set-up-custom-fonts/81 먼저 HTML에 포함시켜 보겠습니다. 우리의 React.js 앱은 하나의 HTML 파일을 사용하고 있습니다. -`public/index.html`을 편집하고, 이 두 서체를 추가하기 위해 HTML의 `` 섹션에 다음 행을 추가하십시오. +{%change%} `public/index.html`을 편집하고, 이 두 서체를 추가하기 위해 HTML의 `` 섹션에 다음 행을 추가하십시오. ``` html @@ -31,7 +31,7 @@ comments_id: set-up-custom-fonts/81 이제 새로 추가된 글꼴을 스타일 시트에 추가할 준비가되었습니다. Create React App은 개별 컴포넌트의 스타일을 분리하는 데 도움이되며 `src/index.css`에 위치한 프로젝트의 마스터 스타일 시트를 가지고 있습니다. -`body` 태그에 대한 `src/index.css`의 현재 글꼴을 다음과 같이 변경합니다. +{%change%} `body` 태그에 대한 `src/index.css`의 현재 글꼴을 다음과 같이 변경합니다. ``` css body { @@ -45,7 +45,7 @@ body { } ``` -그리고 css 파일에 아래 블록을 추가하여 헤더 태그의 글꼴을 새로운 Serif 글꼴로 변경해 봅시다. +{%change%} 그리고 css 파일에 아래 블록을 추가하여 헤더 태그의 글꼴을 새로운 Serif 글꼴로 변경해 봅시다. ``` css h1, h2, h3, h4, h5, h6 { diff --git a/_chapters/ko/setup-ssl.md b/_chapters/ko/setup-ssl.md index e34f0b07af..8e89329f13 100644 --- a/_chapters/ko/setup-ssl.md +++ b/_chapters/ko/setup-ssl.md @@ -6,7 +6,7 @@ lang: ko description: AWS의 React.js 앱에서 SSL 또는 HTTPS를 사용하도록 설정하려고합니다. 그렇게하기 위해 우리는 AWS의 Certificate Manager 서비스를 사용하여 인증서를 요청할 것입니다. 그런 다음 CloudFront 배포판에서 새 인증서를 사용하려고합니다. context: true comments_id: comments-for-set-up-ssl/133 -ref: setup-ssl +ref: setup-a-custom-domain-with-ssl --- 이제 우리 응용 프로그램이 도메인을 통해 제공되고 있으므로 HTTPS로 전환하여 보안 기능을 추가해 보겠습니다. 인증서 관리자 덕분에 AWS를 사용하면 이 작업을 매우 쉽게 처리 할 수 있습니다. diff --git a/_chapters/ko/setup-the-serverless-framework.md b/_chapters/ko/setup-the-serverless-framework.md index 82cbfb685b..498395b1be 100644 --- a/_chapters/ko/setup-the-serverless-framework.md +++ b/_chapters/ko/setup-the-serverless-framework.md @@ -18,7 +18,7 @@ Serverless Framework를 사용하면 개발자가 AWS Lambda에 배포할 독립 ### Serverless 설치하기 -Serverless를 전역으로 설치합니다. +{%change%} Serverless를 전역으로 설치합니다. ``` bash $ npm install serverless -g @@ -29,13 +29,13 @@ $ npm install serverless -g 작업 디렉토리에서 Node.js 스타터를 사용하여 프로젝트를 생성하십시오. 다음 장에서이 스타터 프로젝트의 세부 사항을 살펴 보겠습니다. ``` bash -$ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name notes-app-api +$ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name notes-api ``` -백앤드 API 프로젝트 디렉토리로 이동합니다. +{%change%} 백앤드 API 프로젝트 디렉토리로 이동합니다. ``` bash -$ cd notes-app-api +$ cd notes-api ``` 이제 디렉토리에는 **handler.js** 및 **serverless.yml**과 같은 몇 개의 파일이 있어야합니다. @@ -49,13 +49,13 @@ $ cd notes-app-api 이 스타터 프로젝트는 `package.json` 목록에서 볼 수 있듯이 몇 가지 의존성이 있습니다. -프로젝트의 루트 경로에서 아래 명령어를 실행합니다. +{%change%} 프로젝트의 루트 경로에서 아래 명령어를 실행합니다. ``` bash $ npm install ``` -다음은 백앤드를 위해 특별한 몇 가지 패키지를 설치합니다. +{%change%} 다음은 백앤드를 위해 특별한 몇 가지 패키지를 설치합니다. ``` bash $ npm install aws-sdk --save-dev diff --git a/_chapters/ko/signup-with-aws-cognito.md b/_chapters/ko/signup-with-aws-cognito.md index 0ea9cd09fc..7e93d85bd9 100644 --- a/_chapters/ko/signup-with-aws-cognito.md +++ b/_chapters/ko/signup-with-aws-cognito.md @@ -11,10 +11,10 @@ comments_id: signup-with-aws-cognito/130 이제 `handleSubmit` 과 `handleConfirmationSubmit` 메소드를 구현해서 AWS Cognito 설정과 연결해 보겠습니다. -`src/containers/Signup.js` 파일에서 `handleSubmit` 과 `handleConfirmationSubmit` 메소드를 아래 내용으로 변경합니다. +{%change%} `src/containers/Signup.js` 파일에서 `handleSubmit` 과 `handleConfirmationSubmit` 메소드를 아래 내용으로 변경합니다. -``` javascript -handleSubmit = async event => { +```js +handleSubmit = async (event) => { event.preventDefault(); this.setState({ isLoading: true }); @@ -22,19 +22,19 @@ handleSubmit = async event => { try { const newUser = await Auth.signUp({ username: this.state.email, - password: this.state.password + password: this.state.password, }); this.setState({ - newUser + newUser, }); } catch (e) { alert(e.message); } this.setState({ isLoading: false }); -} +}; -handleConfirmationSubmit = async event => { +handleConfirmationSubmit = async (event) => { event.preventDefault(); this.setState({ isLoading: true }); @@ -44,17 +44,17 @@ handleConfirmationSubmit = async event => { await Auth.signIn(this.state.email, this.state.password); this.props.userHasAuthenticated(true); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e.message); this.setState({ isLoading: false }); } -} +}; ``` -그리고 Amplify의 Auth를 헤더에 추가합니다. +{%change%} 그리고 Amplify의 Auth를 헤더에 추가합니다. -``` javascript +```js import { Auth } from "aws-amplify"; ``` @@ -74,7 +74,6 @@ import { Auth } from "aws-amplify"; 7. 마지막으로 홈페이지로 리디렉션합니다. - 이제 브라우저로 전환하여 위 순서대로 새로운 계정을 신청할 경우, 성공적으로 완료된 후에 홈페이지로 리디렉션되어야합니다. ![가입 후 홈페이지 이동하기 화면](/assets/redirect-home-after-signup.png) @@ -100,6 +99,6 @@ aws cognito-idp admin-confirm-sign-up \ Cognito 사용자 풀 ID와 계정을 만드는 데 사용한 이메일을 사용하십시오. -사용자가 이메일 또는 암호를 변경할 수 있도록 허용하려면 [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}) 챔터를 참조하십시오. +사용자가 이메일 또는 암호를 변경할 수 있도록 허용하려면 [Extra Credit series of chapters on user management]({% link _archives/manage-user-accounts-in-aws-amplify.md %}) 챔터를 참조하십시오. 다음으로 첫 번째 노트를 작성해 보겠습니다. diff --git a/_chapters/ko/staying-up-to-date.md b/_chapters/ko/staying-up-to-date.md index 7ae02d99a7..3399104101 100644 --- a/_chapters/ko/staying-up-to-date.md +++ b/_chapters/ko/staying-up-to-date.md @@ -2,7 +2,7 @@ layout: post title: 최신 상태 유지 lang: ko -description: 우리 이메일 뉴스 레터에 가입하여 Serverless Stack을 최신으로 유지하십시오. +description: 우리 이메일 뉴스 레터에 가입하여 SST을 최신으로 유지하십시오. date: 2018-04-02 00:00:00 comments_id: staying-up-to-date/28 ref: staying-up-to-date @@ -10,10 +10,10 @@ ref: staying-up-to-date 우리는 이 가이드를 오픈 소스로 만들어 커뮤니티의 도움을 받아 내용이 최신으로 정확하게 유지되도록 했습니다. 우리는 커뮤니터의 필요와 우리가 받는 피드백을 기반으로 새로운 챕터를 추가하고 있습니다. -사람들이 변경 사항을 최신 상태로 유지할 수 있도록 Serverless Stack 뉴스 레터를 실행합니다. 뉴스 레터는 다음과 같습니다. +사람들이 변경 사항을 최신 상태로 유지할 수 있도록 SST 뉴스 레터를 실행합니다. 뉴스 레터는 다음과 같습니다. - 짧은 일반 텍스트 전자 메일 -- Serverless Stack에 대한 최근 업데이트 개요 +- SST에 대한 최근 업데이트 개요 - 일주일에 한 번 이상 발송하지 않음 - 한 번의 클릭으로 구독 취소 - 전체 가이드를 400 페이지의 PDF로 제공합니다. diff --git a/_chapters/ko/test-the-apis.md b/_chapters/ko/test-the-apis.md index f3f67b8c23..315b91fcc0 100644 --- a/_chapters/ko/test-the-apis.md +++ b/_chapters/ko/test-the-apis.md @@ -2,11 +2,10 @@ layout: post title: Test the APIs date: 2017-01-05 18:00:00 -description: IAM 및 Cognito 사용자 풀을 사용하여 인증 요청이 반영된 서버리스 백엔드 API를 테스트하려면 몇 단계를 수행해야합니다. 먼저 사용자 풀을 사용하여 인증하여 사용자 토큰을 생성합니다. 그런 다음 사용자 토큰을 사용하여 자격 증명 풀을 사용하여 임시 IAM 자격 증명을 가져옵니다. 마지막으로 IAM 자격 증명으로 API를 서명(Signature Version 4) 처리하여 요청합니다. 이 프로세스를 단순화하기 위해 "aws-api-gateway-cli-test" 도구를 사용할 것입니다. +description: IAM 및 Cognito 사용자 풀을 사용하여 인증 요청이 반영된 서버리스 백엔드 API를 테스트하려면 몇 단계를 수행해야합니다. 먼저 사용자 풀을 사용하여 인증하여 사용자 토큰을 생성합니다. 그런 다음 사용자 토큰을 사용하여 자격 증명 풀을 사용하여 임시 IAM 자격 증명을 가져옵니다. 마지막으로 IAM 자격 증명으로 API를 서명(Signature Version 4) 처리하여 요청합니다. 이 프로세스를 단순화하기 위해 "aws-api-gateway-cli-test" 도구를 사용할 것입니다. lang: ko ref: test-the-apis context: true -code: backend_part1 comments_id: comments-for-test-the-apis/122 --- @@ -17,13 +16,13 @@ API 엔드포인트를 안전하게 사용하려면 다음 단계를 따라야 1. 사용자 풀에 대해 인증하고 사용자 토큰을 획득하십시오. 2. 사용자 토큰으로 자격 증명 풀에서 임시 IAM 자격 증명을 가져옵니다. 3. IAM 자격 증명을 사용하여 Google API 요청에 [Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html으로)으로 서명합니다. -서명하십시오. + 서명하십시오. 이러한 과정들은 모두 손수 해야하는 작업으로 다소 까다로울 수 있습니다. 그래서 [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test)라고 불리는 간단한 도구를 만들었습니다. 이를 사용하려면 다음을 실행하십시오. -``` bash +```bash $ npx aws-api-gateway-cli-test ``` @@ -31,14 +30,13 @@ $ npx aws-api-gateway-cli-test 이제 위 단계를 완료하기 위해 많은 정보를 전달해야합니다. -- [Cognito 테스트 사용자 만들기]({% link _chapters/create-a-cognito-test-user.md %}) 챕터에서 만든 사용자 이름과 암호를 이용하기 -- [Cognito 사용자 풀 만들기]({% link _chapters/create-a-cognito-user-pool.md %}) 챕터에서 생성한 값으로 **YOUR_COGNITO_USER_POOL_ID**, **YOUR_COGNITO_APP_CLIENT_ID**, 그리고 **YOUR_COGNITO_REGION** 를 바꾸기. 여기서 사용하는 리전은 `us-east-1` 입니다. --[Cognito 자격 증명 풀 만들기]({% link _chapters/create-a-cognito-identity-pool.md %}) 챕터에서 생성한 값으로 **YOUR_IDENTITY_POOL_ID** 를 바꾸기. -- [API 배포하기]({% link _chapters/deploy-the-apis.md %}) 챕터에서 생성한 값으로 **YOUR_API_GATEWAY_URL** 와 **YOUR_API_GATEWAY_REGION**을 바꾸기. 여기서는 `https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod` 와 `us-east-1`를 사용합니다. +- [Cognito 테스트 사용자 만들기]({% link _archives/create-a-cognito-test-user.md %}) 챕터에서 만든 사용자 이름과 암호를 이용하기 +- [Cognito 사용자 풀 만들기]({% link _archives/create-a-cognito-user-pool.md %}) 챕터에서 생성한 값으로 **YOUR_COGNITO_USER_POOL_ID**, **YOUR_COGNITO_APP_CLIENT_ID**, 그리고 **YOUR_COGNITO_REGION** 를 바꾸기. 여기서 사용하는 리전은 `us-east-1` 입니다. -[Cognito 자격 증명 풀 만들기]({% link _archives/create-a-cognito-identity-pool.md %}) 챕터에서 생성한 값으로 **YOUR_IDENTITY_POOL_ID** 를 바꾸기. +- [API 배포하기]({% link _archives/deploy-the-apis.md %}) 챕터에서 생성한 값으로 **YOUR_API_GATEWAY_URL** 와 **YOUR_API_GATEWAY_REGION**을 바꾸기. 여기서는 `https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod` 와 `us-east-1`를 사용합니다. 그리고 다음 내용을 실행합니다. -``` bash +```bash $ npx aws-api-gateway-cli-test \ --username='admin@example.com' \ --password='Passw0rd!' \ @@ -57,19 +55,19 @@ $ npx aws-api-gateway-cli-test \ Windows 사용자인 경우 아래 명령을 사용하십시오. 각 옵션 사이의 간격은 매우 중요합니다. -``` bash +```bash $ npx aws-api-gateway-cli-test --username admin@example.com --password Passw0rd! --user-pool-id YOUR_COGNITO_USER_POOL_ID --app-client-id YOUR_COGNITO_APP_CLIENT_ID --cognito-region YOUR_COGNITO_REGION --identity-pool-id YOUR_IDENTITY_POOL_ID --invoke-url YOUR_API_GATEWAY_URL --api-gateway-region YOUR_API_GATEWAY_REGION --path-template /notes --method POST --body "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}" ``` 아래와 유사한 결과를 보인다면 명령 실행이 성공한겁니다. -``` bash +```bash Authenticating with User Pool Getting temporary credentials Making API request { status: 200, statusText: 'OK', - data: + data: { userId: 'us-east-1:9bdc031d-ee9e-4ffa-9a2d-123456789', noteId: '8f7da030-650b-11e7-a661-123456789', content: 'hello world', @@ -87,34 +85,33 @@ Making API request 이 응답은 가장 자주 접하는 공통되는 문제이며, 다소 일반적인 사항으로 디버깅하기가 어려울 수 있습니다. 다음은 디버깅을 시작하기 전에 확인해야할 몇 가지 사항입니다. -   - `apig-test` 명령의`--path-template` 옵션이`notes`가 아니라`/notes`를 가리키고 있는지 확인하십시오. 형식은 요청을 안전하게 서명하는데 중요합니다. +- `apig-test` 명령의`--path-template` 옵션이`notes`가 아니라`/notes`를 가리키고 있는지 확인하십시오. 형식은 요청을 안전하게 서명하는데 중요합니다. -   - `YOUR_API_GATEWAY_URL`에는 슬래시가 없습니다. 예제의 경우 URL은`https ://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod`입니다. `/`로 끝나지 않습니다. +- `YOUR_API_GATEWAY_URL`에는 슬래시가 없습니다. 예제의 경우 URL은`https ://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod`입니다. `/`로 끝나지 않습니다. -   - Windows에서 Git Bash를 사용하는 경우 `--path-template`에서 선행 슬래시를 제거하는 반면 `YOUR_API_GATEWAY_URL`에는 후행 슬래시를 추가하십시오. 예제의 경우 `--invoke-url https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/ --path-template notes`가 됩니다. 보다 궁금한 부분에 대해서는 [이 곳에서](https://github.com/AnomalyInnovations/serverless-stack-com/issues/112#issuecomment-345996566) 논의 하실 수 있습니다. +- Windows에서 Git Bash를 사용하는 경우 `--path-template`에서 선행 슬래시를 제거하는 반면 `YOUR_API_GATEWAY_URL`에는 후행 슬래시를 추가하십시오. 예제의 경우 `--invoke-url https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/prod/ --path-template notes`가 됩니다. 보다 궁금한 부분에 대해서는 [이 곳에서](https://github.com/AnomalyInnovations/serverless-stack-com/issues/112#issuecomment-345996566) 논의 하실 수 있습니다. - 람다 함수가 호출되기 전에도이 오류가 발생할 가능성이 큽니다. 따라서 IAM 역할이 자격 증명 풀에 대해 올바르게 구성되어 있는지 확인할 필요가 있습니다. [서버리스 API 문제 디버깅]({% link _chapters/debugging-serverless-api-issues.md %}#missing-iam-policy) 챕터에 설명 된 단계를 수행하여 IAM 역할에 올바른 권한이 정의되어 있는지 확인하십시오. +람다 함수가 호출되기 전에도이 오류가 발생할 가능성이 큽니다. 따라서 IAM 역할이 자격 증명 풀에 대해 올바르게 구성되어 있는지 확인할 필요가 있습니다. [서버리스 API 문제 디버깅]({% link _archives/debugging-serverless-api-issues.md %}#missing-iam-policy) 챕터에 설명 된 단계를 수행하여 IAM 역할에 올바른 권한이 정의되어 있는지 확인하십시오. -  다음으로 [API 게이트웨이 로그 사용]({% link _chapters/api-gateway-and-lambda-logs.md %}#enable-api-gateway-cloudwatch-logs) 및 [지침] ({% link _chapters/api-gateway-and-lambda-logs.md %}#viewing-api-gateway-cloudwatch-logs)를 사용하여 기록중인 로그를 조회합니다. 무슨 일이 일어났는지 더 잘 이해할 수 있습니다. +다음으로 [API 게이트웨이 로그 사용]({% link _archives/api-gateway-and-lambda-logs.md %}#enable-api-gateway-cloudwatch-logs) 및 [지침] ({% link _archives/api-gateway-and-lambda-logs.md %}#viewing-api-gateway-cloudwatch-logs)를 사용하여 기록중인 로그를 조회합니다. 무슨 일이 일어났는지 더 잘 이해할 수 있습니다. -  마지막으로, 아래의 주석 스레드를 확인하십시오. 비슷한 문제를 가진 상당수의 사람들의 사례가 도움이 될 수 있으며 누군가와 유사한 문제가 발생했을 가능성이 큽니다. +마지막으로, 아래의 주석 스레드를 확인하십시오. 비슷한 문제를 가진 상당수의 사람들의 사례가 도움이 될 수 있으며 누군가와 유사한 문제가 발생했을 가능성이 큽니다. - `{status : false}` 응답 -  만일 실행된 명령이`{status : false}` 응답으로 실패하면; 우리는 이것을 디버깅하기 위해 몇 가지 작업이 필요할 수 있습니다. 이 응답은 오류가있을 때 Lambda 함수에 의해 생성됩니다. 여러분의 핸들러 함수에서 `console.log`를 추가하십시오. +만일 실행된 명령이`{status : false}` 응답으로 실패하면; 우리는 이것을 디버깅하기 위해 몇 가지 작업이 필요할 수 있습니다. 이 응답은 오류가있을 때 Lambda 함수에 의해 생성됩니다. 여러분의 핸들러 함수에서 `console.log`를 추가하십시오. - ``` javascript - catch(e) { - console.log(e); - callback(null, failure({status: false})); - } - ``` +```js +catch(e) { + console.log(e); + callback(null, failure({status: false})); +} +``` - 그리고 `serverless deploy function -f create`를 사용하여 배포하십시오. 그러나 console의 로그가 HTTP 응답에서 전송되지 않기 때문에 우리가 HTTP 요청을 할 때 이 디버그에 대한 출력을 볼 수 없습니다. 이를 확인하려면이 로그를 확인해야합니다. API 게이트웨이 및 람다 로그 작업에 대한 [자세한 정보]({% link _chapters/api-gateway-and-lambda-logs.md %}#viewing-lambda-cloudwatch-logs)가 있습니다. 디버그 메시지는 [여기]({% link _chapters/api-gateway-and-lambda-logs.md %}#viewing-lambda-cloudwatch-logs)를 확인하십시오. +그리고 `serverless deploy function -f create`를 사용하여 배포하십시오. 그러나 console의 로그가 HTTP 응답에서 전송되지 않기 때문에 우리가 HTTP 요청을 할 때 이 디버그에 대한 출력을 볼 수 없습니다. 이를 확인하려면이 로그를 확인해야합니다. API 게이트웨이 및 람다 로그 작업에 대한 [자세한 정보]({% link _archives/api-gateway-and-lambda-logs.md %}#viewing-lambda-cloudwatch-logs)가 있습니다. 디버그 메시지는 [여기]({% link _archives/api-gateway-and-lambda-logs.md %}#viewing-lambda-cloudwatch-logs)를 확인하십시오. -  일반적인 에러의 대부분은 잘못 들여 쓰여진 `serverless.yml`입니다. `serverless.yml`에서 들여 쓰기를 [해당 챕터](https://github.com/AnomalyInnovations/serverless-stack-demo-api/blob/master/serverless.yml)의 것과 비교하여 거듭 확인하십시오. +일반적인 에러의 대부분은 잘못 들여 쓰여진 `serverless.yml`입니다. `serverless.yml`에서 들여 쓰기를 [해당 챕터](https://github.com/AnomalyInnovations/serverless-stack-demo-api/blob/master/serverless.yml)의 것과 비교하여 거듭 확인하십시오. - `‘User: arn:aws:... is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:...’` -  이 오류는 기본적으로 Lambda 함수에 DynamoDB 요청을 수행할 수있는 올바른 권한이 없다는 것을 나타냅니다. 람다 함수가 DynamoDB에 요청할 수있게하는 IAM 역할은 `serverless.yml`에서 설정된다는 것을 상기하십시오. 그리고 이 에러의 일반적인 원인은 `iamRoleStatements :`가 부적절하게 들여 쓰기되었을 때입니다. 이 `serverless.yml`을 [repo에있는 것과]({{ site.backend_github_repo }}) 비교하십시오. - +이 오류는 기본적으로 Lambda 함수에 DynamoDB 요청을 수행할 수있는 올바른 권한이 없다는 것을 나타냅니다. 람다 함수가 DynamoDB에 요청할 수있게하는 IAM 역할은 `serverless.yml`에서 설정된다는 것을 상기하십시오. 그리고 이 에러의 일반적인 원인은 `iamRoleStatements :`가 부적절하게 들여 쓰기되었을 때입니다. 이 `serverless.yml`을 [repo에있는 것과]({{ site.backend_github_repo }}) 비교하십시오. diff --git a/_chapters/ko/test-the-billing-api.md b/_chapters/ko/test-the-billing-api.md index a70b2da755..968f881362 100644 --- a/_chapters/ko/test-the-billing-api.md +++ b/_chapters/ko/test-the-billing-api.md @@ -10,7 +10,7 @@ ref: test-the-billing-api 이제 결제 API를 모두 설정했으므로 로컬 환경에서 신속하게 테스트 해보겠습니다. -`mocks/billing-event.json` 파일을 만들고 다음 내용을 추가합니다. +{%change%} `mocks/billing-event.json` 파일을 만들고 다음 내용을 추가합니다. ``` json { @@ -46,7 +46,7 @@ $ serverless invoke local --function billing --path mocks/billing-event.json ### 변경사항 커밋 -변경사항을 Git에 커밋합니다. +{%change%} 변경사항을 Git에 커밋합니다. ``` bash $ git add . diff --git a/_chapters/ko/test-the-configured-apis.md b/_chapters/ko/test-the-configured-apis.md index 5c577da913..cf01afc4f9 100644 --- a/_chapters/ko/test-the-configured-apis.md +++ b/_chapters/ko/test-the-configured-apis.md @@ -8,15 +8,15 @@ comments_id: test-the-configured-apis/179 ref: test-the-configured-apis --- -이제 두 세트의 API(prod 및 dev)가 있습니다. 프론트엔드를 플러그인에 연결하기 전에 신속하게 테스트하여 제대로 작동하는지 확인하십시오. [API 테스트]({% link _chapters/test-the-apis.md %}) 챕터에서 [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test)라는 간단한 유틸리티를 사용했습니다. +이제 두 세트의 API(prod 및 dev)가 있습니다. 프론트엔드를 플러그인에 연결하기 전에 신속하게 테스트하여 제대로 작동하는지 확인하십시오. [API 테스트]({% link _archives/test-the-apis.md %}) 챕터에서 [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test)라는 간단한 유틸리티를 사용했습니다. -테스트를 수행하기 전에 두 환경에 대한 테스트 사용자를 생성하십시오. [Cognito 테스트 사용자 만들기]({% link _chapters/create-a-cognito-test-user.md %}) 챕터와 동일한 과정입니다. +테스트를 수행하기 전에 두 환경에 대한 테스트 사용자를 생성하십시오. [Cognito 테스트 사용자 만들기]({% link _archives/create-a-cognito-test-user.md %}) 챕터와 동일한 과정입니다. ### 테스트 사용자 생성 이를 위해 AWS CLI를 사용할 것입니다. -터미널에서 다음을 실행합니다. +{%change%} 터미널에서 다음을 실행합니다. ``` bash $ aws cognito-idp sign-up \ @@ -26,7 +26,7 @@ $ aws cognito-idp sign-up \ --password Passw0rd! ``` -Cognito App Client ID의 **dev** 버전을 찾으려면 [Seed를 통한 배포]({% link _chapters/deploying-through-seed.md %}) 챕터를 참조하십시오. 그리고`YOUR_DEV_COGNITO_REGION`을 배포한 지역으로 대체하십시오. +Cognito App Client ID의 **dev** 버전을 찾으려면 [Seed를 통한 배포]({% link _chapters/deploying-through-the-sst-console.md %}) 챕터를 참조하십시오. 그리고`YOUR_DEV_COGNITO_REGION`을 배포한 지역으로 대체하십시오. 다음으로 Cognito Admin CLI를 통해 사용자를 확인합니다. @@ -37,11 +37,11 @@ $ aws cognito-idp admin-confirm-sign-up \ --username admin@example.com ``` -리전과 `YOUR_DEV_COGNITO_USER_POOL_ID`를 [Seed를 통한 배포]({% link _chapters/deploying-through-seed.md %}) 챕터의 Cognito User Pool ID의 **dev** 버전으로 대체하십시오. +리전과 `YOUR_DEV_COGNITO_USER_POOL_ID`를 [Seed를 통한 배포]({% link _chapters/deploying-through-the-sst-console.md %}) 챕터의 Cognito User Pool ID의 **dev** 버전으로 대체하십시오. **prod** 버전에서도 동일한 작업을 신속하게 수행합니다. -터미널에서 다음을 실행합니다. +{%change%} 터미널에서 다음을 실행합니다. ``` bash $ aws cognito-idp sign-up \ @@ -53,7 +53,7 @@ $ aws cognito-idp sign-up \ prod 버전의 Cognito 상세 정보를 이용합니다. -그리고 사용자를 확인합니다. +{%change%} 그리고 사용자를 확인합니다. ``` bash $ aws cognito-idp admin-confirm-sign-up \ @@ -85,11 +85,11 @@ $ npx aws-api-gateway-cli-test \ --body='{"content":"hello world","attachment":"hello.jpg"}' ``` -다음을 위해 [Seed를 통한 배포]({% link _chapters/deploying-through-seed.md %}) 챕터를 참조하십시오. +다음을 위해 [Seed를 통한 배포]({% link _chapters/deploying-through-the-sst-console.md %}) 챕터를 참조하십시오. - `YOUR_DEV_COGNITO_USER_POOL_ID` 및 `YOUR_DEV_COGNITO_APP_CLIENT_ID`는 모두 사용자의 Cognito 사용자 풀과 관련이 있습니다. - `YOUR_DEV_IDENTITY_POOL_ID`는 여러분의 Cognito ID 풀을 위한 것입니다. -- 그리고 `YOUR_DEV_API_GATEWAY_URL`은 API 게이트웨이 엔드포인트입니다. `https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/dev`와 유사하게 보일겁니다. 그러나 사용자 정의 도메인으로 구성한 경우 [사용자 정의 도메인을 통해 시드 설정]({% link _chapters/set-custom-domains-through-seed.md %}) 챕터를 사용하십시오. +- 그리고 `YOUR_DEV_API_GATEWAY_URL`은 API 게이트웨이 엔드포인트입니다. `https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/dev`와 유사하게 보일겁니다. 그러나 사용자 정의 도메인으로 구성한 경우 [사용자 정의 도메인을 통해 시드 설정]({% link _archives/set-custom-domains-through-seed.md %}) 챕터를 사용하십시오. - 마지막으로, `YOUR_DEV_API_GATEWAY_REGION` 과 `YOUR_DEV_COGNITO_REGION`은 배포한 지역입니다. 여기서는 `us-east-1`입니다. 명령이 성공하면 다음과 같이 보일 것입니다. diff --git a/_chapters/ko/translations.md b/_chapters/ko/translations.md index 7893690c40..04a7148f42 100644 --- a/_chapters/ko/translations.md +++ b/_chapters/ko/translations.md @@ -11,7 +11,7 @@ ref: translations ![챕터 번역 링크 화면](/assets/chapter-translation-links.png) -다음은 여러 언어로 제공되는 모든 챕터들의 목록입니다. 번역 작업에 관심이 있으시면 [여기](https://discourse.serverless-stack.com/t/help-us-translate-serverless-stack/596/15)에 의견을 남겨 주십시오. +다음은 여러 언어로 제공되는 모든 챕터들의 목록입니다. 번역 작업에 관심이 있으시면 [여기](https://discourse.sst.dev/t/help-us-translate-serverless-stack/596/15)에 의견을 남겨 주십시오. --- @@ -33,7 +33,7 @@ ref: translations --- -Serverless Stack을 보다 쉽게 사용할 수 있도록 공헌해주신 여러분께 진심으로 큰 감사를 드립니다! +SST을 보다 쉽게 사용할 수 있도록 공헌해주신 여러분께 진심으로 큰 감사를 드립니다! - [Bernardo Bugmann](https://github.com/bernardobugmann) - [Sebastian Gutierrez](https://github.com/pepas24) diff --git a/_chapters/ko/unit-tests-in-serverless.md b/_chapters/ko/unit-tests-in-serverless.md index 268d45a768..17b25ef383 100644 --- a/_chapters/ko/unit-tests-in-serverless.md +++ b/_chapters/ko/unit-tests-in-serverless.md @@ -32,7 +32,7 @@ $ npm install --save-dev jest ### 단위 테스트 추가 -새로운 `tests/billing.test.js` 파일을 생성하고 다음 내용을 추가합니다. +{%change%} 새로운 `tests/billing.test.js` 파일을 생성하고 다음 내용을 추가합니다. ``` js import { calculateCost } from "../libs/billing-lib"; @@ -96,7 +96,7 @@ Ran all test suites. ### 변경사항 커밋 -변경사항을 커밋합니다. +{%change%} 변경사항을 커밋합니다. ``` bash $ git add . @@ -105,7 +105,7 @@ $ git commit -m "Adding unit tests" ### 변경사항 푸시 -프로젝트를 변경 했으므로 GitHub에 푸시(Push)하겠습니다. +{%change%} 프로젝트를 변경 했으므로 GitHub에 푸시(Push)하겠습니다. ``` bash $ git push diff --git a/_chapters/ko/update-the-app.md b/_chapters/ko/update-the-app.md index 318f81d36a..81e956a0a4 100644 --- a/_chapters/ko/update-the-app.md +++ b/_chapters/ko/update-the-app.md @@ -2,9 +2,8 @@ layout: post title: Update the App date: 2017-02-13 00:00:00 -lang: ko +lang: ko description: React.js 단일 페이지 응용 프로그램을 변경하는 방법에 대한 자습서. -code: frontend_part1 comments_id: comments-for-update-the-app/43 ref: update-the-app --- @@ -13,9 +12,9 @@ ref: update-the-app 우리는 첫 페이지에 로그인 및 가입 버튼을 추가하여 사용자에게 명확한 가이드를 제공할 것입니다. -`src/containers/Home.js`의 `renderLander` 메소드를 다음과 같이 변경합니다. +{%change%} `src/containers/Home.js`의 `renderLander` 메소드를 다음과 같이 변경합니다. -``` coffee +```coffee renderLander() { return (
@@ -34,15 +33,15 @@ renderLander() { } ``` -그리고 React-Router 헤더에서 `Link` 컴포넌트를 import합니다. +{%change%} 그리고 React-Router 헤더에서 `Link` 컴포넌트를 import합니다. -``` javascript +```js import { Link } from "react-router-dom"; ``` -또한 `src/containers/Home.css`에 몇 가지 스타일을 추가합니다. +{%change%} 또한 `src/containers/Home.css`에 몇 가지 스타일을 추가합니다. -``` css +```css .Home .lander div { padding-top: 20px; } diff --git a/_chapters/ko/upload-a-file-to-s3.md b/_chapters/ko/upload-a-file-to-s3.md index 9824ee860f..f9cdb0cc52 100644 --- a/_chapters/ko/upload-a-file-to-s3.md +++ b/_chapters/ko/upload-a-file-to-s3.md @@ -15,21 +15,21 @@ comments_id: comments-for-upload-a-file-to-s3/123 2. 파일이 s3의 사용자 폴더에 업로드되고 키가 반환됩니다. 3. 파일 키가 첨부된 노트를 작성하십시오. -AWS Amplify의 스토리지 모듈을 사용할 예정입니다. [Cognito 자격증명 풀 만들기]({% link _chapters/create-a-cognito-identity-pool.md %}) 챕터를 보면 로그인 한 사용자는 S3 버킷에 있는 폴더에 액세스할 수 있습니다 . AWS Amplify는 파일을 *본인만 볼 수 있도록* 저장하려면 이 폴더에 직접 저장합니다. +AWS Amplify의 스토리지 모듈을 사용할 예정입니다. [Cognito 자격증명 풀 만들기]({% link _archives/create-a-cognito-identity-pool.md %}) 챕터를 보면 로그인 한 사용자는 S3 버킷에 있는 폴더에 액세스할 수 있습니다 . AWS Amplify는 파일을 _본인만 볼 수 있도록_ 저장하려면 이 폴더에 직접 저장합니다. 그리고 노트를 새로 작성해서 저장하거나 기존 노트를 편집해서 저장할 경우에만 파일이 업로드됩니다. 따라서 이를 위해 쉽고 편리한 방법을 만들어 보겠습니다. ### S3에 업로드하기 -이를 위해 `src/libs/` 디렉토리를 만듭니다. +{%change%} 이를 위해 `src/libs/` 디렉토리를 만듭니다. -``` bash +```bash $ mkdir src/libs/ ``` -`src/libs/awsLib.js` 파일을 만들고 아래 내용을 작성합니다. +{%change%} `src/libs/awsLib.js` 파일을 만들고 아래 내용을 작성합니다. -``` coffee +```coffee import { Storage } from "aws-amplify"; export async function s3Upload(file) { @@ -57,39 +57,41 @@ export async function s3Upload(file) { 업로드 메소드가 준비되었으므로 노트 작성 메소드에서 호출해 보겠습니다. -`src/containers/NewNote.js` 파일에서 `handleSubmit` 메소드를 다음 내용으로 바꿉니다. +{%change%} `src/containers/NewNote.js` 파일에서 `handleSubmit` 메소드를 다음 내용으로 바꿉니다. -``` javascript -handleSubmit = async event => { +```js +handleSubmit = async (event) => { event.preventDefault(); if (this.file && this.file.size > config.MAX_ATTACHMENT_SIZE) { - alert(`Please pick a file smaller than ${config.MAX_ATTACHMENT_SIZE/1000000} MB.`); + alert( + `Please pick a file smaller than ${ + config.MAX_ATTACHMENT_SIZE / 1000000 + } MB.` + ); return; } this.setState({ isLoading: true }); try { - const attachment = this.file - ? await s3Upload(this.file) - : null; + const attachment = this.file ? await s3Upload(this.file) : null; await this.createNote({ attachment, - content: this.state.content + content: this.state.content, }); - this.props.history.push("/"); + this.props.nav("/"); } catch (e) { alert(e); this.setState({ isLoading: false }); } -} +}; ``` -그리고 `src/containers/NewNote.js` 헤더에 다음과 같이 `s3Upload`를 추가합니다. +{%change%} 그리고 `src/containers/NewNote.js` 헤더에 다음과 같이 `s3Upload`를 추가합니다. -``` javascript +```js import { s3Upload } from "../libs/awsLib"; ``` diff --git a/_chapters/ko/use-environment-variables-in-lambda-functions.md b/_chapters/ko/use-environment-variables-in-lambda-functions.md index 20fde7160c..b6cc029112 100644 --- a/_chapters/ko/use-environment-variables-in-lambda-functions.md +++ b/_chapters/ko/use-environment-variables-in-lambda-functions.md @@ -8,7 +8,7 @@ comments_id: use-environment-variables-in-lambda-functions/166 ref: use-environment-variables-in-lambda-functions --- -[Serverless의 DynamoDB 구성]({% link _chapters/configure-dynamodb-in-serverless.md %}) 챕터 뒷부분에서 CloudFormation을 통해 테이블을 생성했습니다. 생성된 테이블은 현재 stage를 기반으로합니다. 즉, 우리 Lambda 함수에서 데이터베이스와 대화할 때 테이블 이름을 단순히 하드코딩할 수는 없습니다. `dev` stage에서는 `dev-notes`라고 불리우며 `prod`에서는 `prod-notes`라고 불릴 것입니다. +[Serverless의 DynamoDB 구성]({% link _archives/configure-dynamodb-in-serverless.md %}) 챕터 뒷부분에서 CloudFormation을 통해 테이블을 생성했습니다. 생성된 테이블은 현재 stage를 기반으로합니다. 즉, 우리 Lambda 함수에서 데이터베이스와 대화할 때 테이블 이름을 단순히 하드코딩할 수는 없습니다. `dev` stage에서는 `dev-notes`라고 불리우며 `prod`에서는 `prod-notes`라고 불릴 것입니다. 이와 같이 우리가 람다 함수에서 환경변수를 사용하여 어떤 테이블을 사용해야 하는지를 알려줍니다. 지금 `create.js`를 열어 보면 다음 내용을 보게 될 것입니다. @@ -20,12 +20,12 @@ const params = { noteId: uuid.v1(), content: data.content, attachment: data.attachment, - createdAt: new Date().getTime() + createdAt: Date.now() } }; ``` -관련 테이블 이름을 사용하려면 `TableName : "notes"` 행을 변경해야합니다. [Serverless의 DynamoDB 구성]({% link _chapters/configure-dynamodb-in-serverless.md %}) 챕터에서는 `environment:` 블록 아래의 `serverless.yml`에도 `tableName :`을 추가했습니다. +관련 테이블 이름을 사용하려면 `TableName : "notes"` 행을 변경해야합니다. [Serverless의 DynamoDB 구성]({% link _archives/configure-dynamodb-in-serverless.md %}) 챕터에서는 `environment:` 블록 아래의 `serverless.yml`에도 `tableName :`을 추가했습니다. ``` yml # These environment variables are made available to our functions @@ -38,61 +38,61 @@ environment: 그럼 해당 내용으로 변경해보겠습니다. -`create.js`의 아래 행을 +{%change%} `create.js`의 아래 행을 ``` TableName: "notes", ``` -다음 내용으로 바꿉니다.: +{%change%} 다음 내용으로 바꿉니다.: ``` TableName: process.env.tableName, ``` -같은 방법으로, `get.js` 파일도 수정합니다.: +{%change%} 같은 방법으로, `get.js` 파일도 수정합니다.: ``` TableName: "notes", ``` -다음 내용으로 바꿉니다.: +{%change%} 다음 내용으로 바꿉니다.: ``` TableName: process.env.tableName, ``` -`list.js` 파일도 마찬가지로: +{%change%} `list.js` 파일도 마찬가지로: ``` TableName: "notes", ``` -수정합니다.: +{%change%} 수정합니다.: ``` TableName: process.env.tableName, ``` -`update.js` 파일: +{%change%} `update.js` 파일: ``` TableName: "notes", ``` -마찬가지로 수정합니다.: +{%change%} 마찬가지로 수정합니다.: ``` TableName: process.env.tableName, ``` -마지막으로 `delete.js` 파일도: +{%change%} 마지막으로 `delete.js` 파일도: ``` TableName: "notes", ``` -아래 내용으로 수정합니다.: +{%change%} 아래 내용으로 수정합니다.: ``` TableName: process.env.tableName, @@ -100,7 +100,7 @@ TableName: process.env.tableName, ### 코드 커밋 -지금까지 수정한 내용을 커밋합니다.: +{%change%} 지금까지 수정한 내용을 커밋합니다.: ``` bash $ git add . diff --git a/_chapters/ko/use-the-redirect-routes.md b/_chapters/ko/use-the-redirect-routes.md index d509642f46..50fde547a0 100644 --- a/_chapters/ko/use-the-redirect-routes.md +++ b/_chapters/ko/use-the-redirect-routes.md @@ -2,9 +2,9 @@ layout: post title: Use the Redirect Routes date: 2017-02-03 00:00:00 -lang: ko +lang: ko redirect_from: /chapters/use-the-hoc-in-the-routes.html -description: React.js 앱에서 우리는 보안이 필요한 경로 대신 AuthenticatedRoute 및 UnauthenticatedRoute를 사용할 수 있습니다. React Router v4의 스위치 컴포넌트에서 이 작업을 수행 할 것입니다. +description: React.js 앱에서 우리는 보안이 필요한 경로 대신 AuthenticatedRoute 및 UnauthenticatedRoute를 사용할 수 있습니다. React Router v6의 스위치 컴포넌트에서 이 작업을 수행 할 것입니다. context: true comments_id: use-the-redirect-routes/152 ref: use-the-redirect-routes @@ -12,9 +12,9 @@ ref: use-the-redirect-routes 지난 챕터에서`AuthenticatedRoute` 와 `UnauthenticatedRoute`을 만들었으니, 우리가 원하는 컨테이너에서 사용하도록합시다. -먼저 `src/Routes.js`의 헤더에 앞서 만든 컴포넌트를 import합니다. +{%change%} 먼저 `src/Routes.js`의 헤더에 앞서 만든 컴포넌트를 import합니다. -``` javascript +```js import AuthenticatedRoute from "./components/AuthenticatedRoute"; import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; ``` @@ -23,16 +23,16 @@ import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; 그래서 `src/Routes.js`의 다음 경로들이 영향을 받습니다. -``` coffee +```coffee ``` -위 컴포넌트들 대신 다음과 같이 바뀌어야 합니다: +{%change%} 위 컴포넌트들 대신 다음과 같이 바뀌어야 합니다: -``` coffee +```coffee @@ -44,4 +44,3 @@ import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; ![로그인 스크린 샷으로 리디렉션 된 노트 페이지](/assets/note-page-redirected-to-login.png) 다음으로, 우리는 로그인 후 바로 노트 페이지로 리디렉션하는 참조를 사용해 보겠습니다. - diff --git a/_chapters/ko/what-does-this-guide-cover.md b/_chapters/ko/what-does-this-guide-cover.md index ab1bc80a4c..65b0faecc9 100644 --- a/_chapters/ko/what-does-this-guide-cover.md +++ b/_chapters/ko/what-does-this-guide-cover.md @@ -9,7 +9,7 @@ context: true comments_id: what-does-this-guide-cover/83 --- -웹 애플리케이션 구축과 관련된 주요 개념을 단계별로 수행하기 위해 [**스크래치**](https://demo2.serverless-stack.com)라는 간단한 노트 작성 응용 프로그램을 제작하려고합니다. +웹 애플리케이션 구축과 관련된 주요 개념을 단계별로 수행하기 위해 [**스크래치**](https://demo2.sst.dev)라는 간단한 노트 작성 응용 프로그램을 제작하려고합니다. ![완성된 데스크탑 앱 화면](/assets/completed-app-desktop.png) @@ -58,7 +58,7 @@ JavaScript로 작성된 서버리스 API로 구동되는 단일 페이지 응용 ### 이 안내서의 구조 -가이드는 두 부분으로 나뉩니다. 둘 다 상대적으로 독립적입니다. 첫 번째 부분은 기본 사항을 다루지 만 두 번째 항목은 설정을 자동화하는 방법과 함께 몇 가지 고급 주제를 다룹니다. 우리는 2017년 초반에이 안내서를 시작했습니다. Serverless Stack 커뮤니티가 성장했으며 많은 독자들이 이 가이드에서 설명한 설정을 사용하여 비즈니스를 강화하는 앱을 개발했습니다. +가이드는 두 부분으로 나뉩니다. 둘 다 상대적으로 독립적입니다. 첫 번째 부분은 기본 사항을 다루지 만 두 번째 항목은 설정을 자동화하는 방법과 함께 몇 가지 고급 주제를 다룹니다. 우리는 2017년 초반에이 안내서를 시작했습니다. SST 커뮤니티가 성장했으며 많은 독자들이 이 가이드에서 설명한 설정을 사용하여 비즈니스를 강화하는 앱을 개발했습니다. 그래서 우리는 가이드를 확장하고 두 번째 파트를 추가하기로 결정했습니다. 이 프로젝트는 이 설정을 사용하려는 사람들을 대상으로합니다. 파트 I의 모든 수동 단계를 자동화하고 상용 서비스를 포함해서 모든 서버리스 프로젝트에 바로 사용할 수있는 워크 플로우를 작성하는데 도움을줍니다. 다음은 두 부분에서 다루는 내용입니다. @@ -91,7 +91,7 @@ JavaScript로 작성된 서버리스 API로 구동되는 단일 페이지 응용 #### 파트 II -일상적인 프로젝트에 Serverless Stack을 사용하려는 사람들을 대상으로합니다. 우리는 첫 번째 부분부터 모든 단계를 자동화합니다. 여기에 순서대로 적용되는 내용이 있습니다. +일상적인 프로젝트에 SST을 사용하려는 사람들을 대상으로합니다. 우리는 첫 번째 부분부터 모든 단계를 자동화합니다. 여기에 순서대로 적용되는 내용이 있습니다. 백엔드의 경우 : diff --git a/_chapters/ko/what-is-an-arn.md b/_chapters/ko/what-is-an-arn.md index f5bd996bc1..15852ddf84 100644 --- a/_chapters/ko/what-is-an-arn.md +++ b/_chapters/ko/what-is-an-arn.md @@ -62,7 +62,7 @@ ARN은 여러 AWS 리소스가 포함된 시스템을 조율할 때 특정 리 "Resource": "arn:aws:s3:::Hello-bucket/*" } ``` - + ARN은 액세스 권한이 부여되는 리소스(이 경우 S3 버킷)를 정의하는 데 사용됩니다. 와일드 카드`*`문자는 여기서 *Hello-bucket* 내부의 모든 자원을 일치시키는 데 사용됩니다. 다음으로 AWS CLI를 구성 해 보겠습니다. 이전에 생성 IAM 사용자 계정의 정보를 사용합니다. diff --git a/_chapters/ko/what-is-aws-lambda.md b/_chapters/ko/what-is-aws-lambda.md index 7875a43f6c..1c61709152 100644 --- a/_chapters/ko/what-is-aws-lambda.md +++ b/_chapters/ko/what-is-aws-lambda.md @@ -10,23 +10,23 @@ comments_id: what-is-aws-lambda/308 [AWS Lambda](https://aws.amazon.com/lambda/) (또는 줄여서 Lambda)는 AWS에서 제공하는 서버리스 컴퓨팅 서비스입니다. 이번 장에서 우리는 Lambda를 사용하여 serverless 응용 프로그램을 구축할 것입니다. 그리고 Lambda가 어떻게 내부적으로 작동하는 것까지는 다룰 필요가 없지만 함수가 어떻게 실행되는 것인지에 대해서 고민해보는 것은 아주 중요합니다. -### Lambda 스펙 +### Lambda 스펙 AWS Lambda의 기술 사양을 빠르게 살펴보겠습니다. 우선 Lambda는 다음과 같은 런타임(Runtime)을 지원합니다. -- Node.js: v8.10 and v6.10 -- Java 8 -- Python: 3.6 and 2.7 -- .NET Core: 1.0.1 and 2.0 +- Node.js 18.x, 16.x, and 14.x +- Java 17, 11 and 8 +- Python 3.11, 3.10, 3.9, 3.8, and 3.7 +- .NET 7 and 6 - Go 1.x -- Ruby 2.5 -- Rust +- Ruby 3.2 and 2.7 +- [Rust](https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/lambda-rust.html) 각 기능은 64 비트의 Amazon Linux AMI가 설치된 컨테이너에서 실행됩니다. 그리고 실행 환경은 다음과 같습니다. - 메모리: 128MB - 3008MB, 64 MB 씩 증가 - 임시 저장용 디스크 공간: 512MB -- 최대 실행 시간: 900 초 +- 최대 실행 시간: 900 초 - 압축시 패키지 크기: 50MB - 압축을 풀었을 경우 패키지 크기: 250MB @@ -42,7 +42,6 @@ AWS Lambda의 기술 사양을 빠르게 살펴보겠습니다. 우선 Lambda는 Lambda 함수(Node.js 버전)의 모습입니다. - ![Lambda 함수 구성 이미지](/assets/anatomy-of-a-lambda-function.png) 여기서 `myHandler`는 Lambda 함수의 이름입니다. `event` 객체는 이 Lambda를 트리거 한 이벤트에 대한 모든 정보를 포함합니다. 이벤트가 HTTP 요청인 경우, 해당 HTTP 요청에 대한 모든 정보가 들어 있습니다. `context` 객체는 Lambda 함수가 실행되는 런타임에 대한 정보를 포함합니다. Lambda 함수 내부에서 모든 작업을 수행한 후에는 그에 대한 결과(또는 오류)와 함께 `callback` 함수를 호출하고 이를 AWS가 HTTP 요청에 대한 응답으로 처리합니다. @@ -51,25 +50,24 @@ Lambda 함수(Node.js 버전)의 모습입니다. Lambda 함수를 패키지화하여 AWS로 보내게 되는데, 이것은 함수와 모든 종속성을 압축하여 S3 버킷에 업로드하는 프로세스입니다. 특정 이벤트가 발생할 때 AWS에 이 패키지를 사용하고 싶다는 내용을 알려주게 되는데 이 과정을 돕기 위해서 [Serverless 프레임워크](https://serverless.com)를 사용합니다. 자세한 내용은 안내서에서 설명하겠습니다. -### 실행 모델 +### 실행 모델 -Lambda 함수가 실행되는 컨테이너(및 컨테이너가 사용하는 리소스)는 AWS에 의해 완벽하게 관리됩니다. 이벤트가 발생할 때 생성되고 더 이상 사용하지 않을 때는 제거됩니다. 원래 이벤트가 제공되는 동안 추가 요청이 이루어지면 새 컨테이너가 요청을 처리하기 위해 준비됩니다. 즉, 사용량이 급증할 경우 클라우드 제공 업체는 해당 요청을 처리하는 기능을 가진 컨텐이너들로 구성된 여러 인스턴스를 간단히 생성하게 됩니다. +Lambda 함수가 실행되는 컨테이너(및 컨테이너가 사용하는 리소스)는 AWS에 의해 완벽하게 관리됩니다. 이벤트가 발생할 때 생성되고 더 이상 사용하지 않을 때는 제거됩니다. 원래 이벤트가 제공되는 동안 추가 요청이 이루어지면 새 컨테이너가 요청을 처리하기 위해 준비됩니다. 즉, 사용량이 급증할 경우 클라우드 제공 업체는 해당 요청을 처리하는 기능을 가진 컨테이너들로 구성된 여러 인스턴스를 간단히 생성하게 됩니다. 여기에는 몇 가지 흥미로운 내용이 있습니다. 첫째, Lambda 함수는 상태를 저장하지 않습니다. 둘째, 각 요청(또는 이벤트)은 하나의 Lambda 함수를 지닌 단일 인스턴스에서 제공합니다. 즉, 코드에서 동시 요청을 처리하지 않습니다. AWS는 새로운 요청이 있을 때마다 컨테이너를 띄웁니다. 여기에서 최적화를 수행하게 되는데 몇 분(컨테이너의 부하에 따라 5분~15분) 동안 기다리면 이후에는 콜드 스타트 없이 요청에 바로 응답할 수 있습니다. -### Stateless 함수 +### Stateless 함수 위 실행 모델은 Lambda 함수를 효과적으로 상태를 저장하지 않도록 합니다. 이것은 Lambda 함수가 이벤트에 의해 트리거 될 때마다 완전히 새로운 환경에서 호출된다는 것을 의미합니다. 이전 이벤트의 실행 컨텍스트에 대한 액세스 권한이 없습니다. 그러나 위에서 언급한 최적화로 인해 실제 Lambda 함수는 인스턴스화된 컨테이너 당 한 번만 호출됩니다. 함수는 컨테이너에서 실행된다는 점을 상기해 보세요. 따러서 함수가 처음 호출 될 때, handler 함수의 모든 코드가 실행되고 핸들러 함수가 호출됩니다. 만일 컨테이너가 후속 요청에 계속 사용할 수 있는 경우에는 해당 함수가 호출되고 주위의 코드는 호출되지 않습니다. -예를들어, 아래의 `createNewDbConnection` 메소드는 Lambda 함수가 호출 될 때마다가 아니라 인스턴스화된 컨테이너 당 한 번 실행됩니다. 반면에 `myHandler` 함수는 호출 될 때마다 실행됩니다. - +예를들어, 아래의 `createNewDbConnection` 메소드는 Lambda 함수가 호출 될 때마다가 아니라 인스턴스화된 컨테이너 당 한 번 실행됩니다. 반면에 `myHandler` 함수는 호출 될 때마다 실행됩니다. -``` javascript +```js var dbConnection = createNewDbConnection(); -exports.myHandler = function(event, context, callback) { +exports.myHandler = function (event, context, callback) { var result = dbConnection.makeQuery(); callback(null, result); }; @@ -79,7 +77,6 @@ exports.myHandler = function(event, context, callback) { 이제 Lambda 함수를 상태 저장(Stateful)으로 만드는데 좋은 방법이 아니라는 것을 짐작할 수 있습니다. 왜냐하면 Lambda가 호출되거나 컨테이너가 캐시 처리되는 것에 관한 기본 프로세스를 제어하지 못하기 때문입니다. - ### 가격정책 마지막으로 Lambda 함수는 함수를 실행하는 데 걸리는 시간 동안만 비용을 청구하게 됩니다. 그리고 해당 시간은 실행을 시작할 때부터 리턴되거나 종료 될 때까지 계산됩니다. 또한 계산된 시간은 100ms 단위로 올림처리됩니다. @@ -93,5 +90,3 @@ Lambda 무료 티어에는 한 달에 1M 의 무료 요청과 한 달에 400,000 경험에 따르면, Lambda는 일반적으로 인프라 비용 중 가장 저렴한 서비스입니다. 다음으로, 데모 애플리케이션을 실행하는 데 드는 총 비용을 포함하여 서버리스의 장점에 대해 자세히 살펴 보겠습니다. - - diff --git a/_chapters/ko/what-is-iam.md b/_chapters/ko/what-is-iam.md index 585eaf13c4..a029c29c4f 100644 --- a/_chapters/ko/what-is-iam.md +++ b/_chapters/ko/what-is-iam.md @@ -4,7 +4,7 @@ title: What is IAM date: 2016-12-25 16:00:00 lang: ko ref: what-is-iam -description: AWS ID 및 액세스 관리 (또는 IAM)는 AWS 리소스에 대한 액세스를 안전하게 제어하는 데 도움이되는 서비스입니다. IAM 사용자를 생성하고 IAM 사용자를 적용 할 수 있습니다. IAM 정책은 리소스에 대해 수행 할 수있는 작업을 정의하는 규칙 또는 규칙 집합입니다. IAM 역할은 권한이있는 ID이지만 사용자와 달리 자격 증명이 연결되지 않은 IAM 사용자와 매우 유사합니다. 대신 일시적으로 해당 사용 권한이 필요한 사용자 나 리소스가 IAM 역할을 수행 할 수 있습니다. +description: AWS 자격 및 접근 관리(AWS Identity and Access Management 또는 IAM)는 AWS 리소스에 대한 접근을 안전하게 제어하는 데 도움이 되는 서비스입니다. IAM 사용자를 생성하고, 해당 사용자에게 IAM 정책을 적용할 수 있습니다. IAM 정책은 리소스에 대해 수행할 수 있는 작업을 정의하는 규칙 또는 규칙 집합입니다. IAM 역할은 IAM 사용자와 유사한 것으로, 권한을 갖는 자격이지만 IAM 사용자와는 달리 연결된 자격 증명(credentials)이 없습니다. 그 대신, 일시적으로 해당 권한이 필요한 사용자나 리소스가 IAM 역할을 수행할 수 있습니다. context: true comments_id: what-is-iam/23 --- @@ -13,38 +13,38 @@ comments_id: what-is-iam/23 IAM의 공식적인 정의부터 시작해보겠습니다. -AWS ID 및 액세스 관리(IAM)는 사용자를 위해 AWS 리소스에 대한 액세스를 안전하게 제어할 수 있게 해주는 웹 서비스입니다. IAM을 사용하여 AWS 리소스를 이용할 수 있는 사용자(인증:authentication)와 사용자가 접근 가능한 리소스를 어떤 방식으로(권한부여:authorization) 이용할 수 있는지를 제어합니다. +> AWS 자격 및 접근 관리(AWS Identity and Access Management, IAM)는 는 당신의 사용자들이 AWS 리소스에 대하여 가지는 접근 권한을 안전하게 제어할 수 있도록 도와주는 웹 서비스입니다. IAM을 사용하여 누가 AWS 리소스를 이용할 수 있는지(인증:Authentication), 그리고 사용자가 접근 가능한 리소스를 어떤 방식으로 이용할 수 있는지(권한 부여:Authorization)를 제어합니다. -여기서 주목해야 할 첫 번째 점은 IAM은 AWS에 있는 다른 모든 서비스와 마찬가지로 그냥 하나의 서비스입니다. 하지만 어떤면에서는 안전한 방법으로 모든 서비스들을 아우를 수 있게 도와줍니다. IAM은 몇 가지 다른 부분들로 이루어져 있으므로 처음부터 가장 기본적인 것을 먼저 살펴 보겠습니다. +여기서 첫번째로 주목해야 하는 점은 바로 IAM은 AWS에 있는 다른 모든 서비스와 마찬가지로 그냥 하나의 서비스라는 것입니다. 하지만 한편으로는, IAM은 다른 AWS 서비스들을 안전한 방법으로 통합할 수 있도록 도와줍니다. IAM은 여러 가지 부분들로 이루어져 있으므로 먼저 가장 기본적인 것부터 살펴보겠습니다. ### IAM 사용자는 무엇입니까? -AWS 계정을 처음 생성하면 루트 사용자가 됩니다. 계정을 만들때 사용한 전자 메일 주소와 암호를 루트 게정 자격 증명이라고합니다. 이를 사용해 AWS 관리 콘솔에 로그인할 수 있습니다. 그렇게하면 결제 정보에 대한 액세스 및 비밀번호 변경 기능을 포함하여 AWS 계정의 모든 리소스에 대한 무제한 액세스 권한을 갖게 됩니다. +AWS 계정을 처음 생성하면 루트 사용자가 됩니다. 계정을 만들때 사용한 전자 메일 주소와 암호를 루트 계정 자격 증명(credentials)이라고합니다. 이를 사용해 AWS 관리 콘솔에 로그인할 수 있습니다. 그렇게 하면 결제 정보에 대한 접근 및 비밀번호 변경 기능을 포함하여 AWS 계정의 모든 리소스에 대한 무제한 접근 권한을 갖게 됩니다. ![IAM Root 사용자 다이어그램](/assets/iam/iam-root-user.png) -이 수준의 액세스 권한을 가지고 정기적으로 계정에 액세스하는 것은 좋지 않지만 계정에 유일한 사람인 경우 문제가 되지 않습니다. 그러나 다른 사람이 AWS 계정에 액세스하고 관리해야할 경우, 루트 자격 증명을 제공하지 않을 것입니다. 대신 IAM 사용자를 생성합니다. +이 수준의 접근 권한을 가지고 정기적으로 계정에 액세스하는 것은 좋지 않지만, 당신이 계정을 사용하는 유일한 사람인 경우 문제가 되지 않습니다. 그러나 다른 사람이 AWS 계정에 접근하고 관리해야할 경우, 루트 자격 증명을 제공하지 말아야 합니다. 대신, 별도의 IAM 사용자를 생성하는 것이 좋습니다. IAM 사용자는 AWS 관리 콘솔에 로그인하기 위한 이름, 비밀번호 및 API 또는 CLI와 함께 사용할 수 있는 최대 2개의 액세스 키로 구성됩니다. ![IAM 사용자 다이어그램](/assets/iam/iam-user.png) -기본적으로 사용자는 계정의 모든 서비스 항목에 액세스할 수 없습니다. 여러분이 직접 정책을 작성하고 사용자에게 해당 정책을 첨부하여 권한을 부여해야합니다. 이러한 정책 중 하나 이상을 부여하여 사용자가 액세스할 수 있는 것과 액세스할 수 없는 것을 제한할 수 있습니다. +기본적으로 사용자는 계정의 그 어떤 서비스 항목에도 접근할 수 없습니다. 여러분이 직접 정책을 작성하고 사용자에게 해당 정책을 첨부하여 권한을 부여해야합니다. 하나 이상의 정책을 부여하여 해당 사용자의 접근 범위를 제한할 수 있습니다. ### IAM 정책은 무엇인가요? -IAM 정책은 AWS 리소스에서 수행할 수 있는 작업을 정의하는 규칙 또는 규칙의 집합입니다. +IAM 정책(Policy)은 AWS 리소스에서 수행할 수 있는 작업을 정의하는 규칙 또는 규칙들의 집합입니다. 정책은 여러가지 방법으로 부여될 수 있습니다: - *관리되는 정책*을 첨부하기. AWS는 *AmazonS3ReadOnlyAccess*와 같은 사전 정의된 정책 목록을 제공합니다. -- *인라인 정책*을 첨후하기. 인라인 정책은 여러분이 직접 손으로 작성한 사용자 지정 정책입니다. -- 적절한 권한 정책이 첨부된 그룹에 사용자를 추가하기. 아래에 그룹에 관련해서 보다 자세히 살펴 보겠습니다. +- *인라인 정책*을 첨부하기. 인라인 정책은 여러분이 직접 손으로 작성한 사용자 지정 정책입니다. +- 적절한 권한 정책이 첨부된 그룹에 사용자를 추가하기. 그룹이 무엇인지 아래에서 자세히 살펴 보겠습니다. - 기존 IAM 사용자의 권한을 복제하기. ![IAM 정책 다이어그램](/assets/iam/iam-policy.png) -예를들어, 모든 S3 버킷에 모든 작업을 부여하는 정책은 다음과 같습니다. +예를 들어, 모든 S3 버킷에 모든 작업을 부여하는 정책은 다음과 같습니다. ``` json { @@ -57,7 +57,7 @@ IAM 정책은 AWS 리소스에서 수행할 수 있는 작업을 정의하는 } ``` -그리고 더 세분화된 접근을 허락하는 정책은 `Hello-bucket` 이라는 버킷에서 문자열 `Bobs-`로 시작하는 파일의 검색만 허용합니다. +그리고 아래의 예시는 더 세분화된 접근을 허락하는 정책으로, `Hello-bucket`이라는 버킷에서 오직 문자열 `Bobs-`로 시작하는 파일의 반환만 허용합니다. ``` json { @@ -70,29 +70,28 @@ IAM 정책은 AWS 리소스에서 수행할 수 있는 작업을 정의하는 } ``` -우리는 위의 예에서 S3 리소스를 사용하고 있습니다. 그러나 정책은 다른 AWS 서비스들과 모두 비슷합니다. 단지 `Resource` 속성에 대한 리소스 ARN 값만 다릅니다. ARN은 AWS의 리소스에 대한 식별자이며 다음 장에서 더 자세히 살펴볼 것입니다. `Action`과 `Condition` 속성에 해당 서비스에 대한 액션과 조건 컨텍스트 키를 추가합니다. IAM 정책들[여기](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actionsconditions.html)에서 사용 가능한 모든 AWS 서비스 작업 및 조건 컨텍스트 키를 찾을 수 있습니다. 정책을 사용자에게 연결하는 것 이외에도 역할이나 그룹에 정책을 직접 첨부할 수 있습니다. +위의 예시들은 모두 S3 리소스를 사용하고 있습니다. 그러나 다른 AWS 서비스에서도 정책은 비슷한 방식으로 사용됩니다. 단지 `Resource` 속성에 대한 리소스 ARN 값만 다릅니다. ARN은 AWS의 리소스에 대한 식별자이며 다음 장에서 더 자세히 살펴볼 것입니다. 또한 서비스 액션과 조건 컨텍스트 키에 대한 값을 `Action`과 `Condition` 속성 각각에 추가했습니다. IAM 정책에서 사용할 수 있는 AWS 서비스 액션과 조건 컨텍스트 키의 목록은 [여기](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actionsconditions.html)에서 확인할 수 있습니다. 정책을 사용자에게 첨부하는 것 이외에도 역할이나 그룹에 정책을 첨부할 수 있습니다. -### IAM 역할은 무엇인가요? +### IAM 역할은 무엇인가요? -AWS 리소스가 계정의 다른 리소스에 액세스해야하는 경우가 있습니다. 예를 들어 DynamoDB를 쿼리하여 일부 테이터를 검색하고 처리한 다음 Bob에게 결과가 포함된 전자 메일을 보내는 Lambda 함수가 있습니다. 이 경우 Lambda는 실수로 데이터베이스를 변경하지 않도록 읽기 쿼리만 만들 수 있기를 원합니다. Lambda에게 이메일을 보낼 수 있도록 Lambda를 제한하여 다른 사람들에게 스팸 메일을 발송하지 못하도록 하고 싶습니다. 이러한 작업은 IAM 사용자를 만들고 사용자의 자격증명을 Lambda 함수에 넣거나 코드에 자격증명을 포함 시키면 모두 가능하지만 이것은 안전한 방법이 아닙니다. 누군가 이 자격 증명을 얻는다면, 당신을 대신하여 전화를 걸 수 있습니다. 바로 여기에서 IAM 역할이 필요합니다. +한 AWS 리소스가 같은 계정 내의 다른 리소스에 접근해야하는 경우가 있습니다. 예를 들어, DynamoDB에 쿼리하여 일부 테이터를 검색하고 처리한 다음 Bob에게 결과가 포함된 전자 메일을 보내는 Lambda 함수가 있습니다. 이 경우 Lambda가 실수로 데이터베이스를 변경하지 않도록 읽기 쿼리만 실행할 수 있으면 좋겠습니다. 또한, Lambda가 Bob에게만 전자 메일을 보낼 수 있고, 그 이외의 다른 사람에게는 전자 메일을 보낼 수 없도록 제한하고 싶습니다. 이러한 작업은 IAM 사용자를 만들고 해당 사용자의 자격 증명을 Lambda 함수에 넣거나 코드 상에 자격 증명을 포함시키면 모두 가능하겠지만, 안전한 방법은 아닙니다. 누군가 이 자격 증명을 손에 넣는다면, 당신을 사칭하여 이러한 작업을 대신 수행하게될 수도 있습니다. 바로 여기에서 IAM 역할이 필요합니다. -IAM 역할은 AWS에서 ID를 이용해 인증된 사용자가 수행할 수 있는 작업과 수행할 수 없는 작업을 결정하는 권한 정책이 *ID 인증*에 있는 것 처럼 IAM 사용자와 매우 유사합니다. 그러나 역할에는 이와 관련된 자격 증명(암호 또는 액세스 키)이 없습니다. 한 사람과 고유하게 연결되는 대신, 필요한 사람이 역할을 수행할 수 있습니다. 이 경우 Lambda 함수는 일시적으로 권한을 수행하기 위해 역할을 할당 받는 것입니다. +IAM 역할은 권한 정책이 부여된 자격(Identity)으로, 권한 정책은 어떤 자격을 가졌을 때 AWS 상에서 수행할 수 있는 작업의 범위를 정헤줍니다. 이런 점에서 IAM 역할은 사용자와 아주 유사합니다. 하지만 역할은 자격 증명(암호 또는 접근 키)을 갖지 않는다는 차이점을 갖습니다. 역할은 한 사용자에 고유하게 연결되는 것이 아니라, 해당 역할을 필요로 하는 사용자에 의하여 사용될 수 있습니다. 이 경우 Lambda 함수는 일시적으로 권한을 얻기 위해 역할을 할당 받는 것입니다. ![AWS IAM 역할 서비스에 대한 다이어그램](/assets/iam/service-as-iam-role.png) -역할은 사용자에게도 적용될 수 있습니다. 이 경우 사용자는 IAM 역할에 대한 정책 세트를 사용하고 있습니다. 이 기능은 사용자가 조직에서 여러 개의 "모자"를 쓰고 있는 경우에 유용합니다. 어떤 특정한 역할을 수행하기 위해 이러한 역할을 한 번만 만들면 되기 때문에 훨씬 수월하며 이러한 작업을 수행하려는 다른 사람들에게 다시 사용할 수 있습니다. +역할은 사용자에게도 적용될 수 있습니다. 이 경우 사용자는 특정 IAM 역할에 대한 정책 집합을 부여받게 됩니다. 이 기능은 사용자가 조직에서 여러 개의 "모자"를 쓰고 있는 경우에 유용합니다. 어떤 특정한 역할을 수행하기 위해 이러한 역할을 한 번만 만들면 되기 때문에 훨씬 수월하며 이러한 역할을 수행하려는 다른 사람들에게 다시 사용할 수 있습니다. ![IAM 역할을 가진 IAM 사용자 다이어그램](/assets/iam/iam-user-as-iam-role.png) -다른 조직의 사용자 ARN에 연결된 역할을 가질 수도 있습니다. 이렇게하면 외부 사용자가 해당 역할을 조직의 일부로 간주할 수 있습니다. 일반적으로 AWS 조직에서 작동하는 타사 서비스를 사용하는 경우에 유용합니다. 교차 계정 IAM 역할을 만들고 외부 사용자를 *신뢰 관계(Trust Relationship)*로 추가하면 됩니다. *신뢰 관계*는 지정된 외부 사용자가 이 역할을 맡을 수 있음 AWS에 알리는 것입니다. +다른 조직에 소속된 외부 사용자가 소유하는 ARN에 연결되는 역할을 사용할 수도 있습니다. 이렇게 하면 외부 사용자로 하여금 해당 역할이 당신 조직의 일부분인 것으로 여기도록 만들 수 있습니다. 이것은 당신의 AWS 조직 상에서 작동하는 타사(Third party) 서비스가 존재하는 경우에 흔히 사용됩니다. 교차 계정(Cross-Account) IAM 역할을 만들고 외부 사용자를 *신뢰 관계(Trust Relationship)*로 추가하면 됩니다. *신뢰 관계*란, 특정 외부 사용자가 해당 역할을 수행할 수 있다고 AWS에 알리는 것입니다. ![IAM 역할을 가진 외부 IAM 사용자 다이어그램](/assets/iam/external-user-with-iam-role.png) -### IAM 그룹은 무엇인가요? - -IAM 그룹은 단순히 IAM 사용사들의 모음입니다. 그룹을 사용하여 사용자들의 모음에 대한 사용 권한을 지정할 수 있습니다. 예를 들어 Admins라는 그룹을 만들어 관리자에게 일반적으로 필요한 권한 유형을 부여 할 수 있습니다. 해당 그룹의 모든 사용자는 그룹에 할당 된 사용 권한을 자동으로 갖습니다. 새 사용자가 조직에 참여하고 관리자 권한이 있어야하는 경우 해당 그룹에 사용자를 추가하여 적절한 권한을 할당 할 수 있습니다. 마찬가지로 사용자가 조직의 작업을 변경하는 경우 해당 사용자의 권한을 편집하는 대신 이전 그룹에서 해당 사용자를 제거하고 적절한 새 그룹에 추가 할 수 있습니다. +### IAM 그룹은 무엇인가요? + +IAM 그룹은 단순히 IAM 사용자들의 모음입니다. 그룹을 사용하면 여러 사용자들의 권한을 지정할 수 있습니다. 예를 들어, Admins라는 그룹을 만들고, 관리자라면 일반적으로 필요한 권한 유형을 해당 그룹에 부여 할 수 있습니다. 해당 그룹의 모든 사용자는 그룹에 할당 된 권한을 자동으로 갖습니다. 그룹에 새로운 사용자가 합류하고 관리자 권한이 부여되어야 하는 경우, 이 사용자를 해당 그룹에 추가하면 됩니다. 마찬가지로, 어떤 사용자의 조직 내 업무 내용이 변경되었다면, 해당 사용자의 권한을 직접 수정하는 것이 아니라, 해당 사용자를 기존의 그룹에서 제거한 뒤 적절한 새 그룹에 추가하면 됩니다. ![IAM 그룹, IAM 역할, IAM 사용자, IAM 정책 완성 다이어그램](/assets/iam/complete-iam-concepts.png) - -이러한 정보들은 여러분이 IAM과 그 개념들에 대한 빠른 이해를 도울 수 있을 것이라고 생각합니다. 다음 장에서 이들 중 몇 가지를 다시 언급할 예정이며 계속해서 다른 AWS 개념인 ARN에 대해서 빠르게 살펴 보겠습니다. +이 정도면 IAM과 관련 개념에 대하여 빠르게 이해할 수 있을 겁니다. 이어지는 장에서도 위의 내용들은 계속하여 언급됩니다. 다음으로, 또 다른 AWS 개념인 ARN에 대하여 빠르게 살펴 보겠습니다. diff --git a/_chapters/ko/what-is-serverless.md b/_chapters/ko/what-is-serverless.md index 0ec8a6eab7..fab98b6802 100644 --- a/_chapters/ko/what-is-serverless.md +++ b/_chapters/ko/what-is-serverless.md @@ -8,19 +8,20 @@ ref: what-is-serverless comments_id: what-is-serverless/27 --- -일반적으로 HTTP 요청을 처리하기 위해 어느정도 제어할 수 있는 서버를 통해 웹 응용프로그램을 구축하고 배포했습니다. 이렇게 배포된 응용프로그램은 해당 서버에서 실행되며 해당 서버의 자원을 구성하고 관리하는 것은 우리의 책임입니다. 여기에는 몇 가지 이슈들이 있습니다. +일반적으로 HTTP 요청을 처리하기 위해 어느 정도 제어할 수 있는 서버를 통해 웹 응용프로그램을 구축하고 배포했습니다. 이렇게 배포된 응용프로그램은 해당 서버에서 실행되며 해당 서버의 자원을 구성하고 관리하는 것은 우리의 책임입니다. 여기에는 몇 가지 이슈들이 있습니다. -1. 우리는 요청을 처리하지 않을 때에도 서버를 유지해야 합니다. + +1. 우리는 요청을 처리하지 않는 시간대에도 서버 유지 비용을 지불해야 합니다. 2. 우리는 서버 및 모든 리소스의 가동 시간 및 유지 보수를 책임져야 합니다. -3. 우리는 서버에 적절한 보안 업데이트를 적용 할 책임이 있습니다. -4. 우리는 사용 규모가 커짐에 따라 서버의 규모를 키워야하며 사용량이 많지 않을 때에는 규모를 축소해야 합니다. +3. 우리는 서버에 적절한 보안 업데이트를 적용할 책임이 있습니다. +4. 우리는 사용 규모가 커짐에 따라 서버의 규모를 키워야 하며 사용량이 많지 않을 때는 규모를 축소해야 합니다. -소규모의 기업과 개별 개발자들이 처리하기에는 적지 않은 일들이지만, 정말로 처리해야할 중요한 업무들을(실제 응용프로그램을 작성하고 관리하는 일) 소홀히하기에는 충분한 일들입니다. -대규모 조직의 경우에는 인프라 팀에서 처리하는 일이며 보통은 개별 개발자들이 담당하는 업무가 아닙니다. 그러나 이를 지원하는데 필요한 프로세스가 개발 시간을 지연시킬 수 있습니다. 따라서 애플리케이션을 구축하고 실행하기 위해서는 인프라팀과 협력해야만 합니다. 개발자들이 이런 문제들에 대한 해결책을 찾고 있었으며 이에 서버리스가 등장하게 됩니다. +소규모의 기업과 개별 개발자들이 처리하기에는 적지 않은 일들이지만, 정말로 처리해야 할 중요한 업무들을(실제 응용프로그램을 작성하고 관리하는 일) 소홀히 하기에는 충분한 일들입니다. +대규모 조직의 경우에는 인프라팀에서 처리하는 일이며 보통은 개별 개발자들이 담당하는 업무가 아닙니다. 그러나 이를 지원하는 데 필요한 프로세스가 개발 시간을 지연시킬 수 있습니다. 따라서 애플리케이션을 구축하고 실행하기 위해서는 인프라팀과 협력해야만 합니다. 개발자들이 이런 문제들에 대한 해결책을 찾고 있었으며 이에 서버리스가 등장하게 됩니다. ### 서버리스 컴퓨팅 -서버리스 컴퓨팅(줄여서 서버리스)은 클라우드 서비스 공급자들(AWS, 애저, 구글 클라우드)이 리소스를 동적으로 구성하여 코드 실행을 책임지는 실행 모델입니다. 또한 코드를 실행하는 데에 사용된 리소스의 양에 따라서 요금을 부과합니다. 이러한 코드는 일반적으로 http 요청, 데이터베이스 이벤트, 대기열 서비스, 모니터링 경고, 파일 업로드, 예약된 이벤트(cron 작업) 등 다양한 이벤트에 의해 트리거되는 스테이트리스(Stateless: 특정 상태를 저장하지 않는) 컨테이너 내부에서 실행됩니다. 실행을 위해 클라우스 서비스 공급자에게 보내지는 코드는 보통 함수 형태로 구성됩니다. 그래서 서버리스는 가끔 "서비스로 제공되는 함수(_Functions as a Service_)" 또는 "_FaaS_" 라고도 불립니다. 다음은 FaaS를 제공하는 주요 클라우드 서비스 공급업체들입니다. +서버리스 컴퓨팅(줄여서 서버리스)은 클라우드 서비스 공급자들(AWS, 애저, 구글 클라우드)이 리소스를 동적으로 구성하여 코드 실행을 책임지는 실행 모델입니다. 또한, 코드를 실행하는 데에 사용된 리소스의 양에 따라서 요금을 부과합니다. 이러한 코드는 일반적으로 http 요청, 데이터베이스 이벤트, 대기열 서비스, 모니터링 경고, 파일 업로드, 예약된 이벤트(cron 작업) 등 다양한 이벤트에 의해 트리거되는 스테이트리스(Stateless: 특정 상태를 저장하지 않는) 컨테이너 내부에서 실행됩니다. 실행을 위해 클라우스 서비스 공급자에게 보내지는 코드는 보통 함수 형태로 구성됩니다. 그래서 서버리스는 가끔 "서비스로 제공되는 함수(_Functions as a Service_)" 또는 "_FaaS_" 라고도 불립니다. 다음은 FaaS를 제공하는 주요 클라우드 서비스 공급업체들입니다. - AWS: [AWS Lambda](https://aws.amazon.com/lambda/) @@ -34,7 +35,7 @@ comments_id: what-is-serverless/27 ### 마이크로서비스(Microservices) -서버리스 세계로 전환하면서 우리가 직면 한 가장 큰 변화는 애플리케이션의 기능을 구조화해야한다는 것입니다. 하나의 Rails 또는 Express 모노리스 응용 프로그램을 기존처럼 단순히 배포하는 데 사용할 수도 있습니다. 그러나 serverless 세계에서는 일반적으로 더욱 마이크로 서비스 기반의 아키텍처를 채택해야합니다. 모노리스 처럼 하나의 함수로 전체 애플리케이션을 실행하고 라우팅을 직접 처리하면 됩니다. 그러나 함수의 크기를 줄이는 것이 좋기 때문에 권장하지 않습니다. 이것에 대해서는 아래에서 좀 더 자세히 이야기 하겠습니다. +서버리스 세계로 전환하면서 우리가 직면 한 가장 큰 변화는 애플리케이션의 기능을 구조화해야 한다는 것입니다. 하나의 Rails 또는 Express 모노리스 응용 프로그램을 기존처럼 단순히 배포하는 데 사용할 수도 있습니다. 그러나 serverless 세계에서는 일반적으로 더욱 마이크로 서비스 기반의 아키텍처를 채택해야합니다. 모노리스 처럼 하나의 함수로 전체 애플리케이션을 실행하고 라우팅을 직접 처리하면 됩니다. 그러나 함수의 크기를 줄이는 것이 좋기 때문에 권장하지 않습니다. 이것에 대해서는 아래에서 좀 더 자세히 이야기하겠습니다. ### 스테이트리스 함수(Stateless Functions) diff --git a/_chapters/ko/who-is-this-guide-for.md b/_chapters/ko/who-is-this-guide-for.md index ddee5de609..032dc27468 100644 --- a/_chapters/ko/who-is-this-guide-for.md +++ b/_chapters/ko/who-is-this-guide-for.md @@ -14,6 +14,6 @@ comments_id: who-is-this-guide-for/96 우리는 현재 자바 스크립트 개발자만을 대상으로 이 문제를 해결하고 있습니다. 앞으로 다른 언어와 환경을 목표로 삼을 수 있습니다. 그러나 풀스택 개발자가 단일 언어 (JavaScript) 및 환경 (Node.js)을 사용하여 전체 응용 프로그램을 빌드하는 것이 실제로 도움이 될 수 있기 때문에 이것이 좋은 출발점이라고 생각합니다. -개인적으로 서버리스 방식은 우리에게 커다란 일종의 계시였으며 우리가 배운 것을 공유 할 수있는 리소스를 만들고 싶었습니다. 우리에 대한 자세한 내용은 [**여기**]({% link about.md %})를 참조하십시오. 그리고 [Serverless Stack으로 구축 한 사람들의 샘플을 확인하십시오.]({% link showcase.md %}). +개인적으로 서버리스 방식은 우리에게 커다란 일종의 계시였으며 우리가 배운 것을 공유 할 수있는 리소스를 만들고 싶었습니다. 우리에 대한 자세한 내용은 [**여기**]({{ site.sst_url }})를 참조하십시오. 그리고 [SST으로 구축 한 사람들의 샘플을 확인하십시오.]({% link showcase.md %}). 이제 우리가 무엇을 다루는지 살펴 보겠습니다. diff --git a/_chapters/ko/working-with-3rd-party-apis.md b/_chapters/ko/working-with-3rd-party-apis.md index 6232016648..ecd0147da2 100644 --- a/_chapters/ko/working-with-3rd-party-apis.md +++ b/_chapters/ko/working-with-3rd-party-apis.md @@ -10,7 +10,7 @@ ref: working-with-3rd-party-apis 튜토리얼의 첫 번째 부분에서는 기본적인 CRUD API를 만들었습니다. 우리는 3rd-party API와 함께 작동하는 엔드포인트를 작성하여 기존 API에 기능을 추가할 것입니다. 이 섹션에서는 비밀 환경 변수를 사용하는 방법과 Stripe을 사용하여 신용 카드 지불을 수락하는 방법을 설명합니다. -일반적인 Serverless Stack의 확장 스택은 Stripe와 함께 작동하는 빌링 API를 추가하는 것입니다. 노트 앱의 경우 사용자가 특정 수의 노트를 저장하는데 비용을 지불할 수있게 됩니다. 순서는 다음과 같습니다. +일반적인 SST의 확장 스택은 Stripe와 함께 작동하는 빌링 API를 추가하는 것입니다. 노트 앱의 경우 사용자가 특정 수의 노트를 저장하는데 비용을 지불할 수있게 됩니다. 순서는 다음과 같습니다. 1. 사용자는 그가 저장하고자 하는 노트의 수를 선택하고 신용 카드 정보를 입력할 것입니다. diff --git a/_chapters/list-all-the-notes.md b/_chapters/list-all-the-notes.md index 38ea8d361f..76b59f3025 100644 --- a/_chapters/list-all-the-notes.md +++ b/_chapters/list-all-the-notes.md @@ -5,7 +5,6 @@ date: 2017-01-26 00:00:00 lang: en ref: list-all-the-notes description: We want to display all the notes a user has in our React.js app. To do this we are going to use our Home container and render a list if a user is logged in. -context: true comments_id: list-all-the-notes/156 --- @@ -13,64 +12,64 @@ Now that we are able to create a new note, let's create a page where we can see Currently, our Home container is very simple. Let's add the conditional rendering in there. -Replace our `src/containers/Home.js` with the following. +{%change%} Replace our `src/containers/Home.tsx` with the following. -``` coffee -import React, { Component } from "react"; -import { PageHeader, ListGroup } from "react-bootstrap"; +```tsx +import { useState } from "react"; +import ListGroup from "react-bootstrap/ListGroup"; +import { useAppContext } from "../lib/contextLib"; import "./Home.css"; -export default class Home extends Component { - constructor(props) { - super(props); +export default function Home() { + const [notes, setNotes] = useState([]); + const { isAuthenticated } = useAppContext(); + const [isLoading, setIsLoading] = useState(true); - this.state = { - isLoading: true, - notes: [] - }; - } - - renderNotesList(notes) { + function renderNotesList(notes: { [key: string | symbol]: any }) { return null; } - renderLander() { + function renderLander() { return (

Scratch

-

A simple note taking app

+

A simple note taking app

); } - renderNotes() { + function renderNotes() { return (
- Your Notes - - {!this.state.isLoading && this.renderNotesList(this.state.notes)} - +

Your Notes

+ {!isLoading && renderNotesList(notes)}
); } - render() { - return ( -
- {this.props.isAuthenticated ? this.renderNotes() : this.renderLander()} -
- ); - } + return ( +
+ {isAuthenticated ? renderNotes() : renderLander()} +
+ ); } ``` We are doing a few things of note here: -1. Rendering the lander or the list of notes based on `this.props.isAuthenticated`. +1. Rendering the lander or the list of notes based on `isAuthenticated` flag in our app context. + + ```tsx + { + isAuthenticated ? renderNotes() : renderLander(); + } + ``` 2. Store our notes in the state. Currently, it's empty but we'll be calling our API for it. -3. Once we fetch our list we'll use the `renderNotesList` method to render the items in the list. +3. Once we fetch our list we'll use the `renderNotesList` method to render the items in the list. + +4. We're using the [Bootstrap utility classes](https://getbootstrap.com/docs/4.5/utilities/spacing/){:target="_blank"} `pb-3` (padding bottom), `mt-4` (margin top), `mb-3` (margin bottom), and `border-bottom` to style the _Your Notes_ header. And that's our basic setup! Head over to the browser and the homepage of our app should render out an empty list. diff --git a/_chapters/load-secrets-from-env-yml.md b/_chapters/load-secrets-from-env-yml.md deleted file mode 100644 index 28621f68f4..0000000000 --- a/_chapters/load-secrets-from-env-yml.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -layout: post -title: Load Secrets from env.yml -date: 2018-03-08 00:00:00 -lang: en -description: We should not store secret environment variables in our serverless.yml. For this we will create a env.yml file that will not be checked into source control. We load this file in our serverless.yml. -context: true -ref: load-secrets-from-env-yml -comments_id: load-secrets-from-env-yml/171 ---- - -As we had previously mentioned, we do not want to store our secret environment variables in our code. In our case it is the Stripe secret key. In this chapter, we'll look at how to do that. - -We have a `env.example` file for this exact purpose. - -Start by renaming the `env.example` file to `env.yml` and replace its contents with the following. - -``` yml -# Add the environment variables for the various stages - -prod: - stripeSecretKey: "STRIPE_PROD_SECRET_KEY" - -default: - stripeSecretKey: "STRIPE_TEST_SECRET_KEY" -``` - -Make sure to replace the `STRIPE_PROD_SECRET_KEY` and `STRIPE_TEST_SECRET_KEY` with the **Secret key** from the [Setup a Stripe account]({% link _chapters/setup-a-stripe-account.md %}) chapter. In our case we only have the test versions of the Stripe Secret key, so both these will be the same. - -Next, let's add a reference to these. - -Add the following in the `custom:` block of `serverless.yml`. - -``` yml - # Load our secret environment variables based on the current stage. - # Fallback to default if it is not in prod. - environment: ${file(env.yml):${self:custom.stage}, file(env.yml):default} -``` - -The `custom:` block of our `serverless.yml` should look like the following: - -``` yml -custom: - # Our stage is based on what is passed in when running serverless - # commands. Or fallsback to what we have set in the provider section. - stage: ${opt:stage, self:provider.stage} - # Set the table name here so we can use it while testing locally - tableName: ${self:custom.stage}-notes - # Load our secret environment variables based on the current stage. - # Fallback to default if it is not in prod. - environment: ${file(env.yml):${self:custom.stage}, file(env.yml):default} -``` - -And add the following in the `environment:` block in your `serverless.yml`. - -``` yml - stripeSecretKey: ${self:custom.environment.stripeSecretKey} -``` - -Your `environment:` block should look like this: - -``` yml - # These environment variables are made available to our functions - # under process.env. - environment: - tableName: ${self:custom.tableName} - stripeSecretKey: ${self:custom.environment.stripeSecretKey} -``` - -A quick explanation on the above: - -- We are loading a custom variable called `environment` from the `env.yml` file. This is based on the stage (we are deploying to) using `file(env.yml):${self:custom.stage}`. But if that stage is not defined in the `env.yml` then we fallback to loading everything under the `default:` block using `file(env.yml):default`. So Serverless Framework checks if the first is available before falling back to the second. - -- We then use this to add it to our environment variables by adding `stripeSecretKey` to the `environment:` block using `${self:custom.environment.stripeSecretKey}`. This makes it available as `process.env.stripeSecretKey` in our Lambda functions. You'll recall this from the previous chapter. - -### Commit Our Changes - -Now we need to ensure that we don't commit our `env.yml` file to git. The starter project that we are using has the following in the `.gitignore`. - -``` -# Env -env.yml -``` - -This will tell Git to not commit this file. - -Next let's commit the rest of our changes. - -``` bash -$ git add . -$ git commit -m "Adding stripe environment variable" -``` - -Now we are ready to test our billing API. diff --git a/_chapters/load-the-state-from-the-session.md b/_chapters/load-the-state-from-the-session.md index 72b9a2b638..954dfc9887 100644 --- a/_chapters/load-the-state-from-the-session.md +++ b/_chapters/load-the-state-from-the-session.md @@ -4,8 +4,7 @@ title: Load the State from the Session date: 2017-01-15 00:00:00 lang: en ref: load-the-state-from-the-session -description: To keep a user logged in to Amazon Cognito in our React.js app, we are going to load the user session in the App component state. We load the session in componentDidMount using the AWS Amplify Auth.currentSession() method. -context: true +description: To keep a user logged in to Amazon Cognito in our React.js app, we are going to load the user session in the App component state using a React Context. We load the session in componentDidMount using the AWS Amplify Auth.currentSession() method. comments_id: load-the-state-from-the-session/157 --- @@ -15,42 +14,73 @@ Amplify gives us a way to get the current user session using the `Auth.currentSe ### Load User Session -Let's load this when our app loads. We are going to do this in `componentDidMount`. Since `Auth.currentSession()` returns a promise, it means that we need to ensure that the rest of our app is only ready to go after this has been loaded. +Let's load this when our app loads. To do this we are going to use another React hook, called useEffect. Since `Auth.currentSession()` returns a promise, it means that we need to ensure that the rest of our app is only ready to go after this has been loaded. -To do this, let's add a flag to our `src/App.js` state called `isAuthenticating`. The initial state in our `constructor` should look like the following. +{%change%} To do this, let's add another state variable to our `src/App.tsx` state called `isAuthenticating`. Add it to the top of our `App` function. -``` javascript -this.state = { - isAuthenticated: false, - isAuthenticating: true -}; +```tsx +const [isAuthenticating, setIsAuthenticating] = useState(true); ``` -Let's include the `Auth` module by adding the following to the header of `src/App.js`. +We start with the value set to `true` because as we first load our app, it'll start by checking the current authentication state. -``` javascript -import { Auth } from "aws-amplify"; -``` +{%change%} To load the user session we'll add the following to our `src/App.tsx` right below our variable declarations. -Now to load the user session we'll add the following to our `src/App.js` below our `constructor` method. +```tsx +useEffect(() => { + onLoad(); +}, []); -``` javascript -async componentDidMount() { +async function onLoad() { try { await Auth.currentSession(); - this.userHasAuthenticated(true); - } - catch(e) { - if (e !== 'No current user') { + userHasAuthenticated(true); + } catch (e) { + if (e !== "No current user") { alert(e); } } - this.setState({ isAuthenticating: false }); + setIsAuthenticating(false); } ``` +{%change%} Then include the `Auth` module by adding the following to the header of `src/App.tsx`. + +```tsx +import { Auth } from "aws-amplify"; +``` + +{%change%} Let's make sure to include the `useEffect` hook by replacing the React import in the header of `src/App.tsx` with: + +```tsx +import { useState, useEffect } from "react"; +``` + +Let's understand how this and the `useEffect` hook works. + +The `useEffect` hook takes a function and an array of variables. The function will be called every time the component is rendered. And the array of variables tell React to only re-run our function if the passed in array of variables have changed. This allows us to control when our function gets run. This has some neat consequences: -All this does is load the current session. If it loads, then it updates the `isAuthenticating` flag once the process is complete. The `Auth.currentSession()` method throws an error `No current user` if nobody is currently logged in. We don't want to show this error to users when they load up our app and are not signed in. +1. If we don't pass in an array of variables, our hook gets executed every time our component is rendered. +2. If we pass in some variables, on every render React will first check if those variables have changed, before running our function. +3. If we pass in an empty list of variables, then it'll only run our function on the FIRST render. + +In our case, we only want to check the user's authentication state when our app first loads. So we'll use the third option; just pass in an empty list of variables — `[]`. + +When our app first loads, it'll run the `onLoad` function. All this does is load the current session. If it loads, then it updates the `isAuthenticating` state variable once the process is complete. It does so by calling `setIsAuthenticating(false)`. The `Auth.currentSession()` method throws an error `No current user` if nobody is currently logged in. We don't want to show this error to users when they load up our app and are not signed in. Once `Auth.currentSession()` runs successfully, we call `userHasAuthenticated(true)` to set that the user is logged in. + +So the top of our `App` function should now look like this: + +```tsx +function App() { + const [isAuthenticating, setIsAuthenticating] = useState(true); + const [isAuthenticated, userHasAuthenticated] = useState(false); + + useEffect(() => { + onLoad(); + }, []); + + ... +``` ### Render When the State Is Ready @@ -58,47 +88,49 @@ Since loading the user session is an asynchronous process, we want to ensure tha We'll conditionally render our app based on the `isAuthenticating` flag. -Our `render` method in `src/App.js` should be as follows. +{%change%} Replace the `return` statement in `src/App.tsx` with the following. -``` coffee -render() { - const childProps = { - isAuthenticated: this.state.isAuthenticated, - userHasAuthenticated: this.userHasAuthenticated - }; +{% raw %} +```tsx return ( - !this.state.isAuthenticating && -
- - - - Scratch - + !isAuthenticating && ( +
+ + + Scratch + - - -
+ + )} + + +
+ + + +
+ ) ); -} + ``` +{% endraw %} + Now if you head over to your browser and refresh the page, you should see that a user is logged in. ![Login from session loaded screenshot](/assets/login-from-session-loaded.png) diff --git a/_chapters/logic-errors-in-lambda-functions.md b/_chapters/logic-errors-in-lambda-functions.md new file mode 100644 index 0000000000..58b4309ac7 --- /dev/null +++ b/_chapters/logic-errors-in-lambda-functions.md @@ -0,0 +1,110 @@ +--- +layout: post +title: Logic Errors in Lambda Functions +date: 2020-04-06 00:00:00 +lang: en +description: In this chapter we look at how to use a combination of Sentry and CloudWatch logs through Seed, to debug errors in our Lambda function code. +comments_id: logic-errors-in-lambda-functions/1730 +ref: logic-errors-in-lambda-functions +--- + +Now that we've [setup error logging for our API]({% link _chapters/setup-error-logging-in-serverless.md %}), we are ready to go over the workflow for debugging the various types of errors we'll run into. + +First up, there are errors that can happen in our Lambda function code. Now we all know that we almost never make mistakes in our code. However, it's still worth going over this very _"unlikely"_ scenario. + +### Create a New Branch + +Let's start by creating a new branch that we'll use while working through the following examples. + +{%change%} In the project root for your backend repo, run the following: + +```bash +$ git checkout -b debug +``` + +### Push Some Faulty Code + +Let's trigger an error in `get.ts` by commenting out the `noteId` field in the DynamoDB call's Key definition. This will cause the DynamoDB call to fail and in turn cause the Lambda function to fail. + +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of0 + // the item to be retrieved + Key: { + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, // The id of the author + // noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; +}); +``` + +Note the line that we've commented out. + +{%change%} Let's commit our changes. + +```bash +$ git add . +$ git commit -m "Adding some faulty code" +$ git push --set-upstream origin debug; +``` + +### Deploy the Faulty Code + +Head over to your Seed dashboard and select the **prod** stage in the pipeline and hit **Deploy**. + +![Click deploy in Seed pipeline](/assets/monitor-debug-errors/click-deploy-in-seed-pipeline.png) + +Type in the **debug** branch and hit **Deploy**. + +![Select branch and confirm deploy in Seed](/assets/monitor-debug-errors/select-branch-and-confirm-deploy-in-seed.png) + +This will deploy our faulty code to production. + +Head over on to your notes app, and select a note. You'll notice the page fails to load with an error alert. + +![Error alert in notes app note page](/assets/monitor-debug-errors/error-alert-in-notes-app-note-page.png) + +### Debug Logic Errors + +To start with, you should get an email from Sentry about this error. Go to Sentry and you should see the error showing at the top. Select the error. + +![New network error in Sentry](/assets/monitor-debug-errors/new-network-error-in-sentry.png) + +You'll see that our frontend error handler is logging the API endpoint that failed. + +![Error details in Sentry](/assets/monitor-debug-errors/error-details-in-sentry.png) + +You'll also get an email from Seed telling you that there was an error in your Lambda functions. If you click on the **Issues** tab you'll see the error at the top. + +![View Issues in Seed](/assets/monitor-debug-errors/view-issues-in-seed.png) + +And if you click on the error, you'll see the error message and stack trace. + +![Error details in Seed](/assets/monitor-debug-errors/error-details-in-seed.png) + +If you scroll down a bit further you'll notice the entire request log. Including debug messages from the AWS SDK as it tries to call DynamoDB. + +![Lambda request log in error details in Seed](/assets/monitor-debug-errors/lambda-request-log-in-error-details-in-seed.png) + +The message `The provided key element does not match the schema`, says that there is something wrong with the `Key` that we passed in. Our debug messages helped guide us to the source of the problem! + +Next let's look at how we can debug unexpected errors in our Lambda functions. diff --git a/_chapters/login-with-aws-cognito.md b/_chapters/login-with-aws-cognito.md index dcd96ee864..0cae515220 100644 --- a/_chapters/login-with-aws-cognito.md +++ b/_chapters/login-with-aws-cognito.md @@ -5,46 +5,50 @@ date: 2017-01-14 00:00:00 lang: en ref: login-with-aws-cognito description: To allow users to login using Amazon Cognito in our React.js app, we are going to use AWS Amplify. We need the Cognito User Pool Id and our App Client Id. We login the user by calling the Auth.signIn() method from AWS Amplify. -context: true comments_id: login-with-aws-cognito/129 --- -We are going to use AWS Amplify to login to our Amazon Cognito setup. Let's start by importing it. - -### Import Auth from AWS Amplify - -Add the following to the header of our Login container in `src/containers/Login.js`. - -``` coffee -import { Auth } from "aws-amplify"; -``` +We are going to use AWS Amplify to login to our Amazon Cognito setup. Let's start by importing it. ### Login to Amazon Cognito The login code itself is relatively simple. -Simply replace our placeholder `handleSubmit` method in `src/containers/Login.js` with the following. +{%change%} Simply replace our placeholder `handleSubmit` method in `src/containers/Login.tsx` with the following. + +```tsx +async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + + try { + await Auth.signIn(email, password); + alert("Logged in"); + } catch (error) { + // Prints the full error + console.error(error); + if (error instanceof Error) { + alert(error.message); + } else { + alert(String(error)); + } + } +} +``` -``` javascript -handleSubmit = async event => { - event.preventDefault(); +{%change%} And import `Auth` in the header of `src/containers/Login.tsx`. - try { - await Auth.signIn(this.state.email, this.state.password); - alert("Logged in"); - } catch (e) { - alert(e.message); - } -} +```tsx +import { Auth } from "aws-amplify"; ``` + We are doing two things of note here. -1. We grab the `email` and `password` from `this.state` and call Amplify's `Auth.signIn()` method with it. This method returns a promise since it will be logging the user asynchronously. +1. We grab the `email` and `password` and call Amplify's `Auth.signIn()` method. This method returns a promise since it will be logging in the user asynchronously. 2. We use the `await` keyword to invoke the `Auth.signIn()` method that returns a promise. And we need to label our `handleSubmit` method as `async`. -Now if you try to login using the `admin@example.com` user (that we created in the [Create a Cognito Test User]({% link _chapters/create-a-cognito-test-user.md %}) chapter), you should see the browser alert that tells you that the login was successful. +Now if you try to login using the `admin@example.com` user (that we created in the [Create a Cognito Test User]({% link _archives/create-a-cognito-test-user.md %}) chapter), you should see the browser alert that tells you that the login was successful. ![Login success screenshot](/assets/login-success.png) diff --git a/_chapters/monitoring-deployments-in-seed.md b/_chapters/monitoring-deployments-in-seed.md deleted file mode 100644 index 25c1da9c29..0000000000 --- a/_chapters/monitoring-deployments-in-seed.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -layout: post -title: Monitoring Deployments in Seed -date: 2018-03-17 00:00:00 -lang: en -description: We can monitor our Serverless deployments in Seed by viewing CloudWatch logs and metrics for our Lambda functions and our API Gateway endpoints. We can also enable access logs for API Gateway from the Seed console. -context: true -code: backend_full -ref: monitoring-deployments-in-seed -comments_id: monitoring-deployments-in-seed/180 ---- - -Despite our best intentions we might run into cases where some faulty code ends up in production. We want to make sure we have a plan for that. Let's go through what this would look like in [Seed](https://seed.run). - -### Push Some Faulty Code - -First start by pushing an obvious mistake. - -Add the following to `functions/create.js` right at the top of our function. - -``` js -gibberish.what; -``` - -Now there is no such variable as `gibberish` so this code should fail. - -Let's commit and push this to dev. - -``` bash -$ git add . -$ git commit -m "Making a mistake" -$ git push -``` - -Now you can see a build in progress. Wait for it to complete and hit **Promote**. - -![Promote changes to prod screenshot](/assets/part2/promote-changes-to-prod.png) - -Confirm the Change Set by hitting **Promote to Production**. - -![Confirm Change Set to prod screenshot](/assets/part2/confirm-changeset-to-prod.png) - -### Enable Access Logs - -Now before we test our faulty code, we'll turn on API Gateway access logs so we can see the error. Click on the **prod** stage **View Resources**. - -![Click View Deployment in prod screenshot](/assets/part2/click-view-deployment-in-prod.png) - -Hit **Settings**. - -![Click deployment settings in prod screenshot](/assets/part2/click-deployment-settings-in-prod.png) - -Hit **Enable Access Logs**. - -![Enable access logs in prod screenshot](/assets/part2/enable-access-logs-in-prod.png) - -This will take a couple of minutes but Seed will automatically configure the IAM roles necessary for this and enable API Gateway access logs for your prod environment. - -### Test the Faulty Code - -Now to test our code, run the same command from [the last chapter]({% link _chapters/test-the-configured-apis.md %}) to test our API. - -``` bash -$ npx aws-api-gateway-cli-test \ ---username='admin@example.com' \ ---password='Passw0rd!' \ ---user-pool-id='YOUR_PROD_COGNITO_USER_POOL_ID' \ ---app-client-id='YOUR_PROD_COGNITO_APP_CLIENT_ID' \ ---cognito-region='YOUR_PROD_COGNITO_REGION' \ ---identity-pool-id='YOUR_PROD_IDENTITY_POOL_ID' \ ---invoke-url='YOUR_PROD_API_GATEWAY_URL' \ ---api-gateway-region='YOUR_PROD_API_GATEWAY_REGION' \ ---path-template='/notes' \ ---method='POST' \ ---body='{"content":"hello world","attachment":"hello.jpg"}' -``` - -Make sure to use the prod version of your resources. - -You should see an error that looks something like this. - -``` bash -Authenticating with User Pool -Getting temporary credentials -Making API request -{ status: 502, - statusText: 'Bad Gateway', - data: { message: 'Internal server error' } } -``` - -### View Logs and Metrics - -Back in the Seed console, you should be able to click on **Access Logs**. - -![Click access logs in prod screenshot](/assets/part2/click-access-logs-in-prod.png) - -This should show you that there was a `502` error on a recent request. - -![View access logs in prod screenshot](/assets/part2/view-access-logs-in-prod.png) - -If you go back, you can click on **Metrics** to get a good overview of our requests. - -![Click API metrics in prod screenshot](/assets/part2/click-api-metrics-in-prod.png) - -You'll notice the number of requests that were made, 4xx errors, 5xx error, and latency for those requests. - -![View API metrics in prod screenshot](/assets/part2/view-api-metrics-in-prod.png) - -Now if we go back and click on the **Logs** for the **create** Lambda function. - -![Click lambda logs in prod screenshot](/assets/part2/click-lambda-logs-in-prod.png) - -This should show you clearly that there was an error in our code. Notice, that it is complaining that `gibberish` is not defined. - -![View lambda logs in prod screenshot](/assets/part2/view-lambda-logs-in-prod.png) - -And just like the API metrics, the Lambda metrics will show you an overview of what is going on at a function level. - -![View lambda metrics in prod screenshot](/assets/part2/view-lambda-metrics-in-prod.png) - -### Rollback in Production - -Now obviously, we have a problem. Usually you might be tempted to fix the code and push and promote the change. But since our users might be affected by faulty promotions to prod, we want to rollback our changes immediately. - -To do this, head back to the **prod** stage. And hit the **Rollback** button on the previous build we had in production. - -![Click rollback in prod screenshot](/assets/part2/click-rollback-in-prod.png) - -Seed keeps track of your past builds and simply uses the previously built package to deploy it again. - -![Rollback complete in prod screenshot](/assets/part2/rollback-complete-in-prod.png) - -And now if you run your test command from before. - -``` bash -$ npx aws-api-gateway-cli-test \ ---username='admin@example.com' \ ---password='Passw0rd!' \ ---user-pool-id='YOUR_PROD_COGNITO_USER_POOL_ID' \ ---app-client-id='YOUR_PROD_COGNITO_APP_CLIENT_ID' \ ---cognito-region='YOUR_PROD_COGNITO_REGION' \ ---identity-pool-id='YOUR_PROD_IDENTITY_POOL_ID' \ ---invoke-url='YOUR_PROD_API_GATEWAY_URL' \ ---api-gateway-region='YOUR_PROD_API_GATEWAY_REGION' \ ---path-template='/notes' \ ---method='POST' \ ---body='{"content":"hello world","attachment":"hello.jpg"}' -``` - -You should see it succeed this time. - -``` bash -Authenticating with User Pool -Getting temporary credentials -Making API request -{ status: 200, - statusText: 'OK', - data: - { userId: 'us-east-1:9bdc031d-ee9e-4ffa-9a2d-123456789', - noteId: '8f7da030-650b-11e7-a661-123456789', - content: 'hello world', - attachment: 'hello.jpg', - createdAt: 1499648598452 } } -``` - -### Revert the Code - -Finally, don't forget to revert your code in `functions/create.js`. - -``` js -gibberish.what; -``` - -And commit and push the changes. - -``` bash -$ git add . -$ git commit -m "Fixing the mistake" -$ git push -``` - -And that's it! We are now ready to plug this into our frontend. diff --git a/_chapters/organize-the-backend-repo.md b/_chapters/organize-the-backend-repo.md deleted file mode 100644 index 8c3dc63ff1..0000000000 --- a/_chapters/organize-the-backend-repo.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -layout: post -title: Organize the Backend Repo -date: 2018-02-25 00:00:00 -lang: en -description: Serverless Framework uses the service name to identify projects. Since we are creating a new project we want to ensure that we use a different name from the original. -ref: organize-the-backend-repo -comments_id: organize-the-backend-repo/160 ---- - -Let's make a couple of quick changes to our project before we get started. - -### Remove Unused Files - -We have a couple of files as a part of the starter project that we can now remove. - -``` bash -$ rm handler.js -$ rm tests/handler.test.js -``` - -### Update the serverless.yml - -We are going to use a different service name. - -Open the `serverless.yml` and find the following line: - -``` yml -service: notes-app-api -``` - -And replace it with this: - -``` yml -service: notes-app-2-api -``` - -The reason we are doing this is because Serverless Framework uses the `service` name to identify projects. Since we are creating a new project we want to ensure that we use a different name from the original. Now we could have simply overwritten the existing project but the resources were previously created by hand and will conflict when we try to create them through code. - -Also, find this line in the `serverless.yml`: - -``` yml - stage: prod -``` - -And replace it with: - -``` yml - stage: dev -``` - -We are defaulting the stage to `dev` instead of `prod`. This will become clear later when we create multiple environments. - -Let's quickly commit these changes. - -``` bash -$ git add . -$ git commit -m "Organizing project" -``` - -Next let's look into configuring our entire notes app backend via our `serverless.yml`. This is commonly known as **Infrastructure as code**. diff --git a/_chapters/organizing-serverless-projects.md b/_chapters/organizing-serverless-projects.md deleted file mode 100644 index 295d6e1935..0000000000 --- a/_chapters/organizing-serverless-projects.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -layout: post -title: Organizing Serverless Projects -description: The Microservices + Mono-Repo pattern is the most common way to organize your Serverless Framework application. In this chapter we'll also examine the Multi-Repo and Monolith approach. -date: 2018-04-02 12:00:00 -context: true -comments_id: organizing-serverless-projects/350 ---- - -Once your serverless projects start to grow, you are faced with some choices on how to organize your growing projects. In this chapter we'll examine some of the most common ways to structure your projects at a services and application (multiple services) level. - -First let's start by quickly looking at the common terms used when talking about Serverless Framework projects. - -- **Service** - - A service is what you might call a Serverless project. It has a single `serverless.yml` file driving it. - -- **Application** - - An application or app is a collection of multiple services. - -Now let's look at the most common pattern for organizing serverless projects. - -### Microservices + Mono-Repo - -Mono-repo, as the term suggests is the idea of a single repository. This means that your entire application and all its services are in a single repository. - -The microservice pattern on the other hand is a concept of keeping each of your services modular and lightweight. So for example; if your app allows users to create profiles and submit posts; you could have a service that deals with user profiles and one that deals with posts. - -The directory structure of your entire application under the microservice + mono-repo pattern would look something like this. - -``` -|- services/ -|--- posts/ -|----- get.js -|----- list.js -|----- create.js -|----- update.js -|----- delete.js -|----- serverless.yml -|--- users/ -|----- get.js -|----- list.js -|----- create.js -|----- update.js -|----- delete.js -|----- serverless.yml -|- lib/ -|- package.json -``` - -A couple of things to notice here: - -1. We are going over a Node.js project here but this pattern applies to other languages as well. -2. The `services/` dir at the root is made up of a collection of services. Where a service contains a single `serverless.yml` file. -3. Each service deals with a relatively small and self-contained function. So for example, the `posts` service deals with everything from creating to deleting posts. Of course, the degree to which you want to separate your application is entirely up to you. -4. The `package.json` (and the `node_modules/` dir) are at the root of the repo. However, it is fairly common to have a separate `package.json` inside each service directory. -5. The `lib/` dir is just to illustrate that any common code that might be used across all services can be placed in here. -6. To deploy this application you are going to need to run `serverless deploy` separately in each of the services. -7. [Environments (or stages)]({% link _chapters/stages-in-serverless-framework.md %}) need to be co-ordinated across all the different services. So if your team is using a `dev`, `staging`, and `prod` environment, then you are going to need to define the specifics of this in each of the services. - -#### Advantages of Mono-Repo - -The microservice + mono-repo pattern has grown in popularity for a couple of reasons: - -1. Lambda functions are a natural fit for a microservice based architecture. This is due to a few of reasons. Firstly, the performance of Lambda functions is related to the size of the function. Secondly, debugging a Lambda function that deals with a specific event is much easier. Finally, it is just easier to conceptually relate a Lambda function with a single event. - -2. The easiest way to share code between services is by having them all together in a single repository. Even though your services end up dealing with separate portions of your app, they still might need to share some code between them. Say for example; you have some code that formats your requests and responses in your Lambda functions. This would ideally be used across the board and it would not make sense to replicate this code in all the services. - -#### Disadvantages of Mono-Repo - -Before we go through alternative patterns, let's quickly look at the drawbacks of the microservice + mono-repo pattern. - -1. Microservices can grow out of control and each added service increases the complexity of your application. -2. This also means that you can end up with hundreds of Lambda functions. -3. Managing deployments for all these services and functions can get complicated. - -Most of the issues described above start to appear when your application begins to grow. However, there are services that help you deal with some these issues. Services like [IOpipe](https://www.iopipe.com), [Epsagon](https://epsagon.com), and [Dashbird](https://dashbird.io) help you with observability of your Lambda functions. And our own [Seed](https://seed.run) helps you with managing deployments and environments of mono-repo Serverless Framework applications. - -Now let's look at some alternative approaches. - -### Multi-Repo - -The obvious counterpart to the mono-repo pattern is the multi-repo approach. In this pattern each of your repositories has a single Serverless Framework project. - -A couple of things to watch out for with the multi-repo pattern. - -1. Code sharing across repos can be tricky since your application is spread across multiple repos. There are a couple of ways to deal with this. In the case of Node you can use private NPM modules. Or you can find ways to link the common shared library of code to each of the repos. In both of these cases your deployment process needs to accommodate for the shared code. - -2. Due to the friction involved in code sharing, we typically see each service (or repo) grow in the number of Lambda functions. This can cause you to hit the CloudFormation resource limit and get a deployment error that looks like: - - ``` - Error -------------------------------------------------- - - The CloudFormation template is invalid: Template format error: Number of resources, 201, is greater than maximum allowed, 200 - ``` - -Even with the disadvantages the multi-repo pattern does have its place. We have come across cases where some infrastructure related pieces (setting up DynamoDB, Cognito, etc) is done in a service that is placed in a separate repo. And since this typically doesn't need a lot of code or even share anything with the rest of your application, it can live on it's own. So in effect you can run a multi-repo setup where the standalone repos are for your _infrastructure_ and your _API endpoints_ live in a microservice + mono-repo setup. - -Finally, it's worth looking at the less common monolith pattern. - -### Monolith - -The monolith pattern involves taking advantage of API Gateway's `{proxy+}` and `ANY` method to route all the requests to a single Lambda function. In this Lambda function you can potentially run an application server like [Express](https://expressjs.com). So as an example, all the API requests below would be handled by the same Lambda function. - -``` -GET https://api.example.com/posts -POST https://api.example.com/posts -PUT https://api.example.com/posts -DELETE https://api.example.com/posts - -GET https://api.example.com/users -POST https://api.example.com/users -PUT https://api.example.com/users -DELETE https://api.example.com/users -``` - -And the specific section in your `serverless.yml` might look like the following: - -``` yml -handler: app.main -events: - - http: - method: any - path: /{proxy+} -``` - -Where the `main` function in your `app.js` is responsible for parsing the routes and figuring out the HTTP methods to do the specific action necessary. - -The biggest drawback here is that the size of your functions keeps growing. And this can affect the performance of your functions. It also makes it harder to debug your Lambda functions. - -And that should roughly cover the main ways to organize your Serverless Framework applications. Hopefully, this chapter has given you a good overview of the various approaches involved along with their benefits and drawbacks. - -In the next series of chapters we'll be looking at how to work with multiple services in your Serverless Framework application. diff --git a/_chapters/pl/how-to-get-help.md b/_chapters/pl/how-to-get-help.md new file mode 100644 index 0000000000..54c717b4c8 --- /dev/null +++ b/_chapters/pl/how-to-get-help.md @@ -0,0 +1,19 @@ +--- +layout: post +title: Jak uzyskać pomoc? +lang: pl +ref: how-to-get-help +date: 2020-10-17 00:00:00 +comments_id: how-to-get-help/95 +--- + +Jeśli napotkasz na problem na którymś etapie, będziemy w pobliżu, aby pomóc Ci go rozwiązać. Możesz uzyskać pomoc na kilka sposobów. + +- Używamy [tematów na forum Discourse]({{ site.forum_url }}) jako naszych komentarzy i udało nam się już rozwiązać tam sporo problemów. Dlatego sprawdzaj komentarze pod każdym rozdziałem, aby zobaczyć, czy inni nie napotkali na ten sam problem. +- Opisz szczegółowo swój problem w komentarzu pod określonym rozdziałem, aby uzyskać odpowiedź od jednego z nas. + +![SST Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) + +Cały przewodnik jest hostowany na GitHubie. W przypadku, gdy znajdziesz błąd, możesz: +- Stworzyć nowy [raport]({{ site.github_repo }}/issues/new) +- Albo w przypadku literówki, możesz edytować stronę i przesłać prośbę o dodanie poprawek (pull request)! diff --git a/_chapters/pl/what-does-this-guide-cover.md b/_chapters/pl/what-does-this-guide-cover.md new file mode 100644 index 0000000000..9472a6d2a9 --- /dev/null +++ b/_chapters/pl/what-does-this-guide-cover.md @@ -0,0 +1,146 @@ +--- +layout: post +title: Co zawiera ten przewodnik? +date: 2020-10-16 00:00:00 +lang: pl +ref: what-does-this-guide-cover +comments_id: what-does-this-guide-cover/83 +--- + +Aby poruszyć główne kwestie związane z tworzeniem aplikacji internetowych, zamierzamy zbudować prostą aplikację do robienia notatek o nazwie [**Scratch**](https://demo2.sst.dev). Jednak w przeciwieństwie do większości samouczków naszym celem jest zgłębienie się w szczegóły każdego z etapów tworzenia i wdrożenia full-stackowej aplikacji. + +![Completed app desktop screenshot](/assets/completed-app-desktop.png) + +Completed app mobile screenshot + +Jest to aplikacja jednostronicowa obsługiwana przez bezserwerowe API, napisana w całości w języku JavaScript. Tutaj znajdziesz kompletny kod [backendowy]({{ site.backend_github_repo }}) i [frontendowy]({{ site.frontend_github_repo }}). Aplikacja jest stosunkowo prosta, niemniej jednak zamierzamy spełnić następujące wymagania: + +- Aplikacja powinna umożliwiać użytkownikom rejestrację i logowanie do swoich kont +- Użytkownicy powinni mieć możliwość tworzenia notatek z treścią +- Każda notatka może zawierać również załącznik, przesłany jako plik +- Zezwala użytkownikom na edytowanie ich notatek i załączników +- Użytkownicy mogą również usuwać swoje notatki +- Aplikacja powinna mieć możliwość przetwarzania płatności kartą płatniczą +- Dostęp do aplikacji powinien być gwarantowany za pomocą HTTPS w domenie niestandardowej +- Interfejsy API muszą być zabezpieczone +- Aplikacja musi być responsywna +- Aplikacja powinna zostać wdrożona za pomocą git push +- Powinniśmy być w stanie monitorować i usuwać wszelkie błędy + +Do tworzenia aplikacji będziemy używać platformy AWS. Możliwe, że w przyszłości rozszerzymy zakres przewodnika i uwzględnimy inne platformy. Nie mniej uznaliśmy, że platforma AWS będzie dobra na początek. + +### Technologie i usługi + +Do stworzenia naszej aplikacji serverless użyjemy następujących usług i technologii: + +- [Lambda][Lambda] i [API Gateway][APIG] do bezserwerowych interfejsów API +- [DynamoDB][DynamoDB] jako bazy danych +- [Cognito][Cognito] do uwierzytelniania użytkowników i zabezpieczania interfejsów API +- [S3][S3] do hostingu aplikacji i przesyłania plików +- [CloudFront][CF] do udostępniania aplikacji +- [Route 53][R53] do obsługi domeny +- [Certificate Manager][CM] do połączenia SSL +- [CloudWatch][CloudWatch] do monitorowania Lambdy i logów dostępu do API +- [React.js][React] do stworzenia aplikacji jednostronicowej +- [React Router][RR] do routingu +- [Bootstrap][Bootstrap] do interfejsu użytkownika +- [Stripe][Stripe] do obsługi płatności kartą płatniczą +- [Seed][Seed] do automatyzacji wdrożeń serverless +- [Netlify][Netlify] do automatyzacji wdrożeń React +- [GitHub][GitHub] do hostingu repozytoriów naszego projektu +- [Sentry][Sentry] do zgłaszania błędów + +Będziemy korzystać z **darmowych pakietów** powyższych usług, zatem powinieneś móc zarejestrować się do nich bez dodatkowych kosztów. Naturalnie, nie dotyczy to zakupu nowej domeny do hostingu twojej aplikacji. Dodatkowo, aby założyć konto AWS wymagane jest podanie karty płatniczej, więc jeśli stworzysz zasoby wykraczające poza to, co omówimy w tym samouczku, możesz zostać obciążony kosztami. + +Pomimo że powyższa lista może wydawać się zniechęcająca, chcemy mieć pewność, że po ukończeniu tego samouczka będziesz przygotowany, aby tworzyć **praktyczne**, **bezpieczne** i **w pełni funkcjonalne** aplikacje internetowe. Bez obaw, będziemy tu, aby pomóc! + +### Wymagania + +Do pracy z tym przewodnikiem potrzebujesz jedynie paru rzeczy: +- [Node v8.10+ i NPM v5.5+](https://nodejs.org/en/) zainstalowane na Twoim komputerze. +- Darmowe [konto na GitHub](https://github.com/join). +- Oraz podstawową wiedzę na temat korzystania z wiersza poleceń. + +### Struktura tego przewodnika + +Przewodnik jest podzielony z grubsza na kilka części: +1. **Podstawy** + + Tutaj omawiamy jak zbudować Twoją pierwszą full-stackową aplikację serverless. Rozdziały są mniej więcej podzielone na backend (Serverless) i frontend (React). Omówimy również jak wdrożyć Twoją aplikację serverless i aplikację React w środowisku produkcyjnym. +Ta część przewodnika została starannie opracowana, aby zrealizować ją od początku do końca. Szczegółowo omawiamy wszystkie kroki i prezentujemy mnóstwo zrzutów ekranu, które pomogą Ci stworzyć Twoją pierwszą aplikację. + +2. **Najlepsze praktyki** + + Na początku 2017 roku udostępniliśmy pierwszą wersję tego przewodnika, która obejmowała jedynie podstawy. Z biegiem czasu społeczność SST rozrosła się i wielu naszych czytelników postanowiło wykorzystać konfigurację opisaną w tym przewodniku do stworzenia aplikacji, które napędzają ich biznes. W tej sekcji omówimy najlepsze praktyki związane z działaniem aplikacji w trybie produkcyjnym. Nabierają one wielkiego znaczenia, gdy baza kodu aplikacji się rozrasta lub gdy dodajesz więcej osób do swojego zespołu. Rozdziały w tej sekcji są stosunkowo niezależne i zwykle obracają się wokół określonych tematów. + +3. **Odsyłacze** + + Na koniec, mamy zbiór niezależnych rozdziałów na różne tematy. Odnosimy się do nich w przewodniku lub używamy ich do omówienia tematów, które niekoniecznie są powiązane z poprzednimi sekcjami. + +### Tworzenie Twojej pierwszej aplikacji serverless + +Pierwsza część tego przewodnika pomoże Ci zbudować aplikację do robienia notatek i wdrożyć ją w trybie produkcyjnym. Omówimy wszelkie podstawy. Każda usługa będzie tworzona ręcznie. Oto co omówimy po kolei: + +Backend: + +- Konfiguracja konta AWS +- Tworzenie bazy danych za pomocą DynamoDB +- Konfiguracja S3 do przesyłania plików +- Konfiguracja Cognito User Pools, aby zarządzać kontami użytkowników +- Konfiguracja Cognito Identity Pool, aby zabezpieczyć przesyłanie plików +- Konfiguracja Serverless Framework do pracy z Lambda i API Gateway +- Tworzenie backendowych interfejsów API +- Praca z zewnętrznymi interfejsami API (Stripe) +- Wdrożenie aplikacji za pomocą wiersza poleceń + +Frontend: + +- Konfiguracja projektu za pomocą aplikacji Create React App +- Dodanie favicon, czcionek i UI Kit za pomocą Bootstrap +- Konfiguracja ścieżek za pomocą React-Router +- Użycie AWS Cognito SDK do logowania i rejestracji użytkowników +- Wtyczka do backendowych interfejsów API, służąca do zarządzania notatkami +- Użycie AWS JS SDK do przesyłu plików +- Przyjmowanie płatności kartą płatniczą w React +- Środowiska w aplikacji Create React App +- Wdrożenie frontendu na produkcję przy użyciu Netlify +- Konfiguracja domeny niestandardowej za pomocą Netlify + +Automatyzacja backendowych wdrożeń: + +- Konfiguracja DynamoDB za pomocą kodu +- Konfiguracja S3 za pomocą kodu +- Konfiguracja Cognito User Pool za pomocą kodu +- Konfiguracja Cognito Identity Pool za pomocą kodu +- Zmienne środowiskowe w Serverless Framework +- Praca z sekretami w Serverless Framework +- Testy jednostkowe w Serverless +- Automatyzacja wdrożeń za pomocą Seed +- Konfiguracja domen niestandardowych poprzez Seed + +Monitorowanie i debugowanie aplikacji serverless: + +- Konfiguracja raportowanie błędów w React za pomocą Sentry +- Konfiguracja Error Boundary w React +- Dodanie logowania błędów do interfejsów Serverless API +- Uwzględnienie praktyk debugowania dla typowych błędów Serverless + +Uważamy, że zapewni Ci to dobre podstawy do budowania gotowych do pracy w środowisku produkcyjnym full-stackowych aplikacji serverless. Jeśli chciałbyś, abyśmy uwzględnili inne koncepcje lub technologie, daj nam znać na naszych [forach]({{ site.forum_url }}). + +[Cognito]: https://aws.amazon.com/cognito/ +[CM]: https://aws.amazon.com/certificate-manager +[R53]: https://aws.amazon.com/route53/ +[CF]: https://aws.amazon.com/cloudfront/ +[S3]: https://aws.amazon.com/s3/ +[CloudWatch]: https://aws.amazon.com/cloudwatch/ +[Bootstrap]: http://getbootstrap.com +[RR]: https://github.com/ReactTraining/react-router +[React]: https://facebook.github.io/react/ +[DynamoDB]: https://aws.amazon.com/dynamodb/ +[APIG]: https://aws.amazon.com/api-gateway/ +[Lambda]: https://aws.amazon.com/lambda/ +[Stripe]: https://stripe.com +[Seed]: https://seed.run +[Netlify]: https://netlify.com +[GitHub]: https://github.com +[Sentry]: https://sentry.io diff --git a/_chapters/pl/what-is-aws-lambda.md b/_chapters/pl/what-is-aws-lambda.md new file mode 100644 index 0000000000..10de4ba790 --- /dev/null +++ b/_chapters/pl/what-is-aws-lambda.md @@ -0,0 +1,92 @@ +--- +layout: post +title: Co to jest AWS Lambda? +date: 2020-10-29 18:00:00 +lang: pl +ref: what-is-aws-lambda +description: AWS Lambda to usługa obliczeniowa serverless dostępna na Amazon Web Services. Uruchamia fragmenty kodu (zwane funkcjami Lambda) w kontenerach bezstanowych, które są wywoływane na żądanie, aby odpowiedzieć na zdarzenia (takie jak żądania HTTP). Po zakończeniu wykonywania funkcji kontenery są wyłączane. Użytkownicy ponoszą koszt jedynie za czas potrzebny do wykonania funkcji. +comments_id: what-is-aws-lambda/308 +--- + +[AWS Lambda](https://aws.amazon.com/lambda/) (w skrócie Lambda) to usługa obliczeniowa serverless dostępna na AWS. W tym rozdziale będziemy używać Lambdy do budowy naszej aplikacji serverless. Mimo tego, że nie musimy się przejmować szczegółami działania usługi, ważne jest, aby mieć ogólne pojęcie o tym, jak będą wykonywane Twoje funkcje. + +### Specyfikacja Lambdy + +Zacznijmy od szybkiego przyjrzenia się specyfikacji technicznej usługi AWS Lambda. Lambda obsługuje następujące środowiska uruchomieniowe: + +- Node.js 18.x, 16.x,, 14.x +- Java 17, 11, 8 +- Python 3.11, 3.10, 3.9, 3.8,, 3.7 +- .NET 7, 6 +- Go 1.x +- Ruby 3.2, 2.7 +- [Rust](https://docs.aws.amazon.com/lambda/latest/dg/lambda-rust.html) + +Każda funkcja działa w kontenerze z 64-bitowym AMI Amazon Linux. Środowisko wykonawcze posiada: + +- Pamięć RAM: 128MB - 3008MB, dostępna w przyrostach 64 MB +- Efemeryczny dysk: 512MB +- Maksymalny czas wykonania: 900 sekund +- Rozmiar skompresowanego pakietu: 50 MB +- Rozmiar nieskompresowanego pakietu: 250 MB + +Możliwe, że zauważyłeś, że procesor nie jest wymieniony jako część specyfikacji kontenera, z uwagi na to, że nie jest możliwa bezpośrednia kontrola nad nim. Wraz ze zwiększeniem pamięci RAM zwiększa się również ilość procesorów. + +Przestrzeń na dysku efemerycznym jest dostępna w postaci katalogu `/tmp`. Możesz go używać jedynie do tymczasowego przechowywania, ponieważ kolejne wywołania nie będą miały do niego dostępu. W dalszej części powiemy nieco więcej na temat bezstanowej natury funkcji Lambda. + +Określony czas wykonania wskazuje na to, że funkcja Lambda może działać maksymalnie przez 900 sekund, tzn. 15 minut. Co za tym idzie - Lambda nie jest przeznaczona do długotrwałych procesów. + +Rozmiar pakietu odnosi się do całego kodu niezbędnego do uruchomienia funkcji. Obejmuje to również wszelkie zależności (w przypadku Node.js jest to katalog `node_modules/'), które Twoja funkcja potrzebuje zaimportować. Maksymalny rozmiar pakietu nieskompresowanego to 250 MB i 50 MB po skompresowaniu. Poniżej przyjrzymy się procesowi pakowania. + +### Funkcje Lambda + +A więc, tak wygląda funkcja Lambda (wersja Node.js). + +![Anatomia funkcji Lambda obraz](/assets/anatomy-of-a-lambda-function.png) + +Tutaj `myHandler` jest nazwą naszej funkcji Lambda. Obiekt `event` zawiera wszystkie informacje o zdarzeniu, które wywołało tę Lambdę. W przypadku żądania HTTP będzie to informacja o konkretnym żądaniu HTTP. Obiekt `context` zawiera informacje o środowisku uruchomieniowym, w którym nasza funkcja Lambda jest wykonywana. Po wykonaniu całego kodu zdefiniowanego wewnatrz funkcji Lambda wywoływana jest funkcja `callback` z otrzymanym wynikiem (lub błędem), którym to AWS odpowie na żądanie HTTP. + +### Pakowanie funkcji + +Funkcje Lambda należy spakować i przesłać do AWS. Oznacza to zwykle proces kompresji funkcji i wszystkich jej zależności oraz przesłania ich do wiadra S3. Należy również powiadomić AWS, że chcesz użyć tego pakietu, gdy ma miejsce określone zdarzenie. Aby ułatwić cały ten proces, używamy [frameworka Serverless] (https://serverless.com). Omówimy to szczegółowo w dalszej części tego przewodnika. + +### Model wykonawczy + +Kontener (i wykorzystywane przez niego zasoby), w którym działa nasza funkcja, jest w całości zarządzany przez AWS. Jest wywoływany, gdy ma miejsce zdarzenie i wyłączany, jeśli nie jest używany. Jeśli podczas obsługi jednego zdarzenia wysyłane są nowe żądania, do ich obsługi zostanie przydzielony nowy kontener. Oznacza to, że jeśli mamy do czynienia z gwałtownym wzrostem liczby żądań, dostawca chmury zwyczajnie utworzy wiele kontenerów z naszą funkcją, aby móc obsłużyć te żadania. + +Ma to pewne interesujące konsekwencje. Po pierwsze, nasze funkcje są bezstanowe. Po drugie, każde żądanie (lub zdarzenie) jest obsługiwane przez jedną instancję funkcji Lambda. Oznacza to, że nie możesz obsługiwać jednoczesnych żądań w swoim kodzie. AWS tworzy kontener w momencie gdy pojawia się nowe żądanie, niemniej jednak dokonuje tu pewnej optymalizacji. Kontener pozostaje aktywny przez kilka minut (5 - 15 minut w zależności od obciążenia), dzięki czemu może odpowiadać na kolejne żądania bez cold startu. + +### Funkcje bezstanowe + +Opisany powyżej model wykonawczy sprawia, że funkcje Lambda są faktycznie bezstanowe. Oznacza to, że za każdym razem, gdy funkcja Lambda jest wyzwalana przez zdarzenie, jest ona wywoływana w zupełnie nowym środowisku. Nie mamy dostępu do kontekstu wykonawczego poprzedniego zdarzenia. + +Jednak ze względu na wspomnianą powyżej optymalizację, kompletny kod Lambdy wykonywany jest jedynie podczas tworzenia nowego kontenera. Pamiętaj, że nasze funkcje działają w kontenerach, tak więc kiedy funkcja jest wywoływana po raz pierwszy, zostanie wykonany kompletny zadany kod źródłowy, łącznie z właściwą funkcją (handlerem). Jeśli kontener jest nadal dostępny dla kolejnych żądań, zostanie wywołany jedynie handler, a kod wokół niego zostanie pominięty. + +Posługując się przykładem, poniższa metoda `createNewDbConnection` jest wywoływana jedynie podczas tworzenia nowego kontenera, a nie za każdym razem, gdy wywoływana jest funkcja Lambda. Z kolei funkcja `myHandler` jest wywoływana przy każdym wywołaniu. + +```js +var dbConnection = createNewDbConnection(); + +exports.myHandler = function (event, context, callback) { + var result = dbConnection.makeQuery(); + callback(null, result); +}; +``` + +Ten efekt cache'owania kontenerów dotyczy również katalogu `/tmp`, o którym wspomnialiśmy powyżej. Będzie on dostępny tak długo, jak długo kontener pozostanie aktywny. + +Jak zapewne się domyślasz, nie jest to niezawodny sposób na uczynienie naszych funkcji Lambda stanowymi. Powodem jest fakt, że zwyczajnie nie mamy kontroli nad procesem wywoływania Lambdy oraz cache'owania jej kontenerów. + +### Stawki + +Na koniec, funkcje Lambda są rozliczane tylko za czas potrzebny do wykonania funkcji. Czas jest liczony od momentu rozpoczęcia wykonywania do momentu otrzymania wyniku lub przerwania, oraz jest zaokrąglany w górę do najbliższych 100 ms. + +Zwróć uwagę, że mimo tego, że AWS może zachować kontener z funkcją Lambda po zakończeniu jej wykonania nie zostaniesz za to obciążony. + +Lambda ma spory pakiet darmowy, dlatego też jest mało prawdopodobne, że przekroczysz jego limit podczas pracy z tym przewodnikiem. + +Bezpłatny pakiet Lambdy obejmuje 1 mln żądań miesięcznie i 400 000 GB-sekund czasu obliczeniowego miesięcznie. Po przekroczeniu limitu kosztuje 0,20 USD za 1 milion żądań i 0,00001667 USD za każdą GB-sekundę. Liczba GB-sekund zależy od zużycia pamięci przez funkcję Lambda. Więcej informacji możesz znaleźć na [stronie z cennikiem Lambdy] (https://aws.amazon.com/lambda/pricing/). + +Z doświadczenia wiemy, że koszty związane z wykorzystaniem Lambdy stanowią zwykle najmniejszą cześć kosztów naszej infrastruktury. + +Następnie przyjrzyjmy się dokładniej zaletom modelu serverless, w tym całkowitemu kosztowi uruchomienia naszej aplikacji demo. diff --git a/_chapters/pl/what-is-serverless.md b/_chapters/pl/what-is-serverless.md new file mode 100644 index 0000000000..409b46a9ff --- /dev/null +++ b/_chapters/pl/what-is-serverless.md @@ -0,0 +1,53 @@ +--- +layout: post +title: Co to jest serverless? +date: 2020-10-23 11:00:00 +lang: pl +ref: what-is-serverless +description: Serverless odnosi się do aplikacji, w których zarządzanie oraz alokacja serwerów i zasobów jest całkowicie zarządzana przez dostawcę chmury. Rozliczenie kosztów opiera się na rzeczywistym zużyciu zasobów. +comments_id: what-is-serverless/27 +--- + +Zazwyczaj budujemy i wdrażamy aplikacje webowe, w których mamy pewien stopień kontroli nad żądaniami HTTP kierowanymi do serwera. Aplikacja działa na naszym serwerze, a my odpowiadamy za zapewnienie niezbędnych zasobów i zarządzanie nimi. Z tym podejściem wiąże się kilka problemów. + +1. Jesteśmy obciążani kosztami utrzymania serwera nawet wtedy, gdy nie obsługujemy żadnych żądań. + +2. Jesteśmy odpowiedzialni za dostępność i utrzymanie serwera oraz wszystkich jego zasobów. + +3. Jesteśmy również odpowiedzialni za stosowanie odpowiednich aktualizacji zabezpieczeń na serwerze. + +4. Wraz ze wzrostem obciążenia musimy zarządzać skalowaniem naszego serwera w górę. A co za tym idzie, musimy zarządzać skalowaniem w dół, gdy nie mamy tak dużego obciążenia. + +W przypadku mniejszych firm i indywidualnych deweloperów może to się okazać trudne do wykonania. W rezultacie nie skupiamy się na zadaniach ważniejszych: budowaniu i utrzymaniu aplikacji. W większych organizacjach zajmuje się tym zespół ds. infrastruktury i zwykle nie jest to obowiązkiem samego dewelopera. Procesy związane z zarządzaniem serwerami mogą niestety spowolnić prace nad aplikacją, ponieważ nie możesz po prostu zbudować aplikacji bez współpracy z zespołem ds. infrastruktury, który pomoże Ci rozpocząć pracę. Jako deweloperzy szukaliśmy rozwiązania tego problemu, i właśnie tu pojawia się technologia serverless. + +### Serverless computing + +Serverless computing (w skrócie serverless) to model, w którym dostawca chmury (AWS, Azure, czy Google Cloud) jest odpowiedzialny za wykonanie fragmentu kodu poprzez dynamiczną alokację zasobów. Naliczanie opłat odbywa się jedynie za zasoby faktycznie wykorzystane do uruchomienia kodu. Kod jest zwykle uruchamiany w kontenerach bezstanowych, które mogą być wyzwalane przez różne zdarzenia, w tym żądania http, wydarzenia z bazy danych, usługi kolejkowania, alarmy monitorowania, ładowanie plików, zaplanowane zdarzenia (zadania cron) itp. Kod do wykonania wysyłany do dostawcy chmury ma zwykle postać funkcji. Stąd serverless jest czasami określane jako _„Functions as a Service”_ lub _„FaaS”_. Poniżej przedstawiono ofertę FaaS głównych dostawców chmury: + +- AWS: [AWS Lambda](https://aws.amazon.com/lambda/) +- Microsoft Azure: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) +- Google Cloud: [Cloud Functions](https://cloud.google.com/functions/) + +Pomimo że infrastruktura w serverless jest niewidoczna dla dewelopera, serwery nadal są wykorzystywane do wykonywania naszych funkcji. + +Jako, że Twój kod będzie wykonywany właśnie w ramach pojedynczych funkcji, jest kilka pojęć, które musimy wyjaśnić. + +### Mikroserwisy + +Największą zmianą, przed którą stoimy przechodząc do świata serverless, jest to, że architektura naszej aplikacji musi mieć postać funkcji. Być może jesteś przyzwyczajony do wdrażania aplikacji jako pojedynczej monolitycznej aplikacji Rails lub Express. Tymczasem w świecie serverless zazwyczaj wymagane jest przyjęcie architektury opartej na mikrousługach. Możesz obejść ten wymóg, uruchamiając całą aplikację za pomocą jednej funkcji jako monolit i samodzielnie obsługując routing. Niemniej jednak, nie jest to zalecane, ponieważ znacznie lepiej jest tworzyć funkcje o małych rozmiarach. Wyjaśnimy to poniżej. + +### Funkcje bezstanowe + +Twoje funkcje są uruchamiane w bezpiecznych, (prawie) bezstanowych kontenerach. Oznacza to, że nie będziesz w stanie wykonać kodu na serwerze aplikacji długo po zakończeniu zdarzenia wyzwalającego, lub używając kontekstu poprzedniego wykonania do obsługi nowego żądania. Musisz założyć, że Twoja funkcja jest wywoływana za każdym razem w nowym kontenerze. + +Istnieją jednak pewne niuanse z tym związane; omówimy je w rozdziale [Co to jest AWS Lambda?]({% link _chapters/what-is-aws-lambda.md %}). + +### Cold start + +Z uwagi na to, że funkcje uruchamiane są w kontenerze, który jest tworzony na żądanie, aby odpowiedzieć na zdarzenie, wiąże się z tym pewne opóźnienie. Jest to tzw. _cold start_ (zimny start). Twój kontener może być aktywny jeszcze przez chwilę po zakończeniu wykonywania funkcji. Jeśli w tym czasie zostanie wyzwolone inne zdarzenie, funkcja zareaguje znacznie szybciej; jest to tzw. _warm start_ (ciepły start). + +Czas trwania cold startu zależy od implementacji danego dostawcy chmury. W przypadku AWS Lambda może wynieść od kilkuset milisekund do kilku sekund. Czas może się różnić w zależności od używanego środowiska uruchomieniowego (lub języka), rozmiaru funkcji (jako pakietu) i oczywiście od danego dostawcy chmury. Cold starty znacznie się poprawiły na przestrzeni lat, jako że dostawcom usług w chmurze udało się zoptymalizować czasy opóźnienia przesyłu. + +Oprócz optymalizacji funkcji możesz wykorzystać proste tricki, jak na przykład pomocniczą, zaplanowaną funkcję, która będzie wywoływać Twoje właściwe funkcje co kilka minut, i tym samym utrzyma je w trybie rozgrzanym. [Framework Serverless](https://serverless.com), którego będziemy używać w tym samouczku, ma kilka wtyczek, które [pomagają utrzymać funkcje w trybie rozgrzanym](https://github.com/FidelLimited/serverless-plugin-warmup). + +Teraz, gdy mamy lepsze pojęcie o serverless, przyjrzyjmy się dokładniej czym jest funkcja Lambda i w jaki sposób będzie wykonywany Twój kod. diff --git a/_chapters/pl/who-is-this-guide-for.md b/_chapters/pl/who-is-this-guide-for.md new file mode 100644 index 0000000000..e8a90f2a39 --- /dev/null +++ b/_chapters/pl/who-is-this-guide-for.md @@ -0,0 +1,18 @@ +--- +layout: post +title: Dla kogo jest ten przewodnik? +date: 2020-10-14 09:25:00 +lang: pl +ref: who-is-this-guide-for +comments_id: who-is-this-guide-for/96 +--- + +Ten przewodnik jest przeznaczony dla full-stack deweloperów lub deweloperów, którzy chcą tworzyć full-stackowe aplikacje bezserwerowe. Udostępniając przewodnik krok po kroku zarówno po części frontendowej, jak i backendowej, mamy nadzieję, że udało nam się uwzględnić wszystkie aspekty tworzenia aplikacji bezserwerowych. W sieci istnieje sporo innych samouczków, mimo to uważamy, że przydatne byłoby posiadanie jednego punktu odniesienia dla całego procesu. Ten przewodnik ma służyć jako źródło informacji o tym, jak tworzyć i wdrażać aplikacje bezserwerowe, w przeciwieństwie do omówienia najlepszego możliwego rozwiązania. + +Możliwe, że jesteś deweloperem backendowym, który chciałby dowiedzieć się więcej o frontendowej części tworzenia aplikacji bezserwerowych lub deweloperem frontendowym, który chciałby dowiedzieć się więcej o backendzie. W tym przewodniku powinieneś znaleźć to, czego potrzebujesz. + +Na tą chwilę kierujemy się wyłączenie do deweloperów używających języka JavaScript, ale w przyszłości być może skupimy się na innych językach i środowiskach. Mimo wszystko uważamy, że jest to dobry punkt wyjścia, ponieważ używanie jednego języka (JavaScript) i środowiska (Node.js) do budowania całej aplikacji może okazać się korzystne dla full-stack dewelopera. + +W ramach uwagi osobistej, podejście bezserwerowe jest dla nas wielką rewelacją, dlatego też chcieliśmy stworzyć i udostępnić materiał, w którym zawarlibyśmy, to czego się nauczyliśmy. Więcej o nas przeczytasz [**tutaj**]({{ site.sst_url }}). [Zobacz też próbkę tego, co inni stworzyli za pomocą SST]({% link showcase.md %}). + +Zacznijmy od przyjrzenia się temu, co będziemy omawiać. diff --git a/_chapters/pt/add-support-for-es6-es7-javascript.md b/_chapters/pt/add-support-for-es6-es7-javascript.md new file mode 100644 index 0000000000..17101111ce --- /dev/null +++ b/_chapters/pt/add-support-for-es6-es7-javascript.md @@ -0,0 +1,109 @@ +--- +layout: post +title: Add Support for ES6/ES7 JavaScript +date: 2016-12-29 12:00:00 +lang: pt +ref: add-support-for-es6-and-typescript +redirect_from: /chapters/add-support-for-es6-javascript.html +description: O AWS Lambda suporta até o Node.js v8.10 e para usarmos as funções import/exports no nosso projeto, nós vamos precisar do Babel e Webpack 4 para transpilar o código. Podemos fazer isso utilizando o plugin serverless-webpack. Vamos usar o serverless-nodejs-starter para começar. +context: true +code: backend +comments_id: add-support-for-es6-es7-javascript/128 +--- + +O AWS Lambda possui suporte para o Node.js v8.10. A sintaxe suportada é um pouco diferente do que quando comparado com o frontend em React que vamos trabalhar bem em breve. É uma boa prática usar funcionalidades do ES similares entre os códigos das duas partes do projeto - especificamente, nós utilizaremos imports/exports em nossas funções. Para fazer isso, vamos transpilar nosso código usando o [Babel](https://babeljs.io) e o [Webpack 4](https://webpack.github.io). O Serverless Framework também suporta plugins que fazem isso automaticamente, então utilizaremos o [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack). + +Isso já foi adicionado no capítulo anterior usando o [`serverless-nodejs-starter`]({% link _archives/serverless-nodejs-starter.md %}). Nós usamos esse modelo por algumas razões específicas: + +- Usar uma versão similar do JavaScript no frontend e no backend +- Manter os mesmos números de linhas de código depois de transpilado para facilitar correção de erros +- Permitir que o backend da API seja executado localmente +- Adicionar suporte para testes unitários + +Nós instalamos esse modelo usando o comando `serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name my-project`. Ele faz com que o Serverless Framework use o [modelo](https://github.com/AnomalyInnovations/serverless-nodejs-starter) como um template para nosso projeto. + +Neste capítulo vamos ver com mais detalhes sobre o que foi feito, assim você pode alterar o que quiser no futuro caso precise. + +### Serverless Webpack + +O processo de transpilação que converte nosso código para o Node v8.10 é feito pelo plugin serverless-webpack. Esse plugin foi adicionado no arquivo `serverless.yml`. Vamos ver com mais detalhes. + +{%change%} Abra o `serverless.yml` e substitua tudo pelas linhas abaixo. + +```yaml +service: notes-app-api + +# Uso do plugin serverless-webpack que transpila o código para ES6 +plugins: + - serverless-webpack + - serverless-offline + +# Configuração do serverless-webpack +# Habilitar o auto-packing de módulos externos +custom: + webpack: + webpackConfig: ./webpack.config.js + includeModules: true + +provider: + name: aws + runtime: nodejs8.10 + stage: prod + region: us-east-1 +``` + +A opção `service` é muito importante. Nós estamos nomeando nosso serviço como `notes-app-api`. O Serverless Framework cria sua stack na AWS usando esse mesmo nome. Isso significa que se você alterar o nome e fazer o deploy, isso fará com que seja criado outro projeto com o novo nome. + +Note que o plugin `serverless-webpack` foi incluído e o arquivo `webpack.config.js` contém suas configurações. + +Olhe abaixo como seu `webpack.config.js` deve parecer. Você não precisa fazer nenhuma alteração nele, estamos apenas explorando e entendendo como funciona. + +```js +const slsw = require("serverless-webpack") +const nodeExternals = require("webpack-node-externals") + +module.exports = { + entry: slsw.lib.entries, + target: "node", + // Gerar os sourcemaps para mensagens de erro + devtool: "source-map", + // Tendo em vista que o 'aws-sdk' é incompatível com o webpack, + // nós excluímos todas as dependências + externals: [nodeExternals()], + mode: slsw.lib.webpack.isLocal ? "development" : "production", + optimization: { + // Não queremos minimizar nosso código por agora. + minimize: false + }, + performance: { + // Desabilita warnings sobre o tamanho das entry points + hints: false + }, + // Executar o babel em todos arquivos .js e pular todos existentes na pasta node_modules + module: { + rules: [ + { + test: /\.js$/, + loader: "babel-loader", + include: __dirname, + exclude: /node_modules/ + } + ] + } +} +``` + +A parte principal desse configuração é no atributo `entry` que vamos gerar automaticamente usando `slsw.lib.entries` que é parte do plugin `serverless-webpack`. Ele pega automaticamente todas nossas funções e as empacota. Também utilizamos o `babel-loader` em cada um para transpilar nosso código. Outra coisa para se notar é que estamos usando `nodeExternals` porque não queremos que os o Webpack tente fazer alterações nos arquivos do módulo `aws-sdk`, pois ele não é compatível. + +Finalmente, vamos dar uma olhada nas configurações do Babel. Lembre-se que você não pecisa alterar nada aqui. Abra o arquivo `.babelrc` que está na raíz do projeto. Ele deve se parecer com o código abaixo. + +```json +{ + "plugins": ["source-map-support", "transform-runtime"], + "presets": [["env", { "node": "8.10" }], "stage-3"] +} +``` + +Nesse arquivo estamos dizendo ao Babel para transpilar nosso código para o Node v8.10. + +E agora estamos prontos para construir de fato nosso backend. diff --git a/_chapters/pt/create-a-cognito-test-user.md b/_chapters/pt/create-a-cognito-test-user.md new file mode 100644 index 0000000000..6c2a54b994 --- /dev/null +++ b/_chapters/pt/create-a-cognito-test-user.md @@ -0,0 +1,39 @@ +--- +layout: post +title: Crie um usuário de teste no Cognito +date: 2016-12-28 12:00:00 +lang: pt +ref: create-a-cognito-test-user +description: Para testar o grupo de usuários do Cognito, nós vamos criar um usuário teste. Podemos criar um usuário pelo AWS CLI usando os comandos aws cognito-idp sign-up e admin-confirm-sign-up. +context: true +comments_id: create-a-cognito-test-user/126 +--- + +Neste capítulo, vamos criar um usuário teste para nosso grupo de usuários do Cognito. Nós vamos precisar desse usuário para testar a funcionalidade de autenticação do nosso aplicativo no futuro. + +### Criando um usuároio + +Primeiro, vamos usar o AWS CLI para registrar um usuário com seu email e senha. + +{%change%} Execute em seu terminal. + +```bash +$ aws cognito-idp sign-up \ + --region REGIAO_COGNITO \ + --client-id ID_CLIENTE_COGNITO \ + --username admin@example.com \ + --password Passw0rd! +``` + +O usuário está criado no grupo de usuários. Entretanto, antes de podermos usar o usuário para autenticar no grupo, a conta precisa ser verificada. Vamos agora verificar a conta usando um comando de administrador. + +{%change%} Execute em seu terminal. + +```bash +$ aws cognito-idp admin-confirm-sign-up \ + --region REGIAO_COGNITO \ + --user-pool-id ID_CLIENTE_COGNITO \ + --username admin@example.com +``` + +Agora nosso usuário de teste está pronto. A seguir vamos configurar o framework Serverless para criar nossa API de backend. diff --git a/_chapters/pt/create-a-cognito-user-pool.md b/_chapters/pt/create-a-cognito-user-pool.md new file mode 100644 index 0000000000..538c22ac70 --- /dev/null +++ b/_chapters/pt/create-a-cognito-user-pool.md @@ -0,0 +1,87 @@ +--- +layout: post +title: Crie um grupo de usuários no Cognito +date: 2016-12-28 00:00:00 +lang: pt +ref: create-a-cognito-user-pool +description: O grupo de usuários do Amazon Cognito lida com o cadastro e login de usuários para apps web e mobile. Nós vamos criar um grupo de usuários no Cognito para armazenar e gerenciar os usuários do nosso app serverless. Vamos usar o e-mail como usuário na hora do login e vamos configurar nosso app como um cliente do nosso grupo de usuários. +context: true +comments_id: create-a-cognito-user-pool/148 +--- + +Nosso app de anotações precisa de gerenciar contas de usuários e processo de autenticação de um modo seguro e confiável. Para isso, vamos utilizar o [Amazon Cognito](https://aws.amazon.com/pt/cognito/). + +O grupo de usuários do Cognito facilita muito na hora de desenvolvedores adicionarem funcionalidades de cadastro e login nos seus aplicativos web ou mobile. O Cognito serve como um provedor de identidades e mantêm um diretório de usuários. Além de suportar cadastros e login, fornece um provisionamento de tokens de identidade para usuários registrados. + +Nesse capítulo, nós vamos criar um grupo de usuários para nosso app de anotações. + +### Criando um grupo de usuários + +No [Console da AWS](https://console.aws.amazon.com), selecione **Cognito** da lista de serviços. + +![Tela de seleção do Cognito](/assets/cognito-user-pool/select-cognito-service.png) + +Selecione **Gerenciar grupos de usuários**. + +![Tela de seleção gerenciar grupos de usuários no Cognito](/assets/cognito-user-pool/select-manage-your-user-pools.png) + +Selecione **Criar um grupo de usuários**. + +![Tela de seleção criar um grupo de usuários](/assets/cognito-user-pool/select-create-a-user-pool.png) + +Digite um **Nome do grupo** e selecione **Revisar padrões**. + +![Tela de preenchimento do novo grupo de usuários](/assets/cognito-user-pool/fill-in-user-pool-info.png) + +Selecione **Escolher atributos de nome de usuário...**. + +![Tela Escolher atributos de nome de usuário... ](/assets/cognito-user-pool/choose-username-attributes.png) + +Agora selecione **Endereço de e-mail ou número de telefone** e **Permitir endereços de e-mail**. Isso fará com que os usuários cadastrem-se e façam login com seu e-mail, sem precisar de um nome de usuário. + +![Tela Endereço de e-mail ou número de telefone](/assets/cognito-user-pool/select-email-address-as-username.png) + +Role até o fim da página e clique em **Próxima etapa**. + +![Tela próxima etapa](/assets/cognito-user-pool/select-next-step-attributes.png) + +Selecione **Revisar** no painel esquerdo e tenha certeza de que **Atributos de nome de usuário** está marcado como **email**. + +![Tela de revisão do grupo de usuários](/assets/cognito-user-pool/review-user-pool-settings.png) + +Agora clique em **Criar grupo** no final da página. + +![Tela criar grupo](/assets/cognito-user-pool/select-create-pool.png) + +Seu grupo de usuários foi criado. Salve em algum lugar o **ID do grupo** e **ARN do grupo** pois vamos precisar deles mais tarde. Também lembre-se da região em que o grupo foi criado - neste caso é a `us-east-1`. + +![Tela de grupo de usuários criado](/assets/cognito-user-pool/user-pool-created.png) + +### Criando um cliente de aplicativo + +Selecione **Clientes de aplicativo** do painel na esquerda. + +![Tela Clientes de aplicativo](/assets/cognito-user-pool/select-user-pool-apps.png) + +Selecione **Adicionar cliente de aplicativo**. + +![Tela adicionar cliente de aplicativo](/assets/cognito-user-pool/select-add-an-app.png) + +Digite **Nome do cliente de aplicativo**, desmarque **Gerar segredo do cliente**, selecione **Habilitar API de entrada para autenticação baseada em servidor (ADMIN_NO_SRP_AUTH)** e clique em **Criar cliente de aplicativo**. + +- **Gerar segredo do cliente**: aplicativos com um segredo do cliente não são suportados pelo SDK do Javascript. Sendo assim, devemos desmarcar essa opção. +- **Habilitar API de entrada para autenticação baseada em servidor**: requisito da AWS CLI para gerenciar o grupo de usuários pela interface da linha de comando. No próximo capítulo vamos criar um usuário de teste através da linha de comando. + +![Tela de preenchimento do cliente de aplicativo](/assets/cognito-user-pool/fill-user-pool-app-info.png) + +Seu cliente de aplicativo foi criado. Salve o **ID do cliente de aplicativo** pois precisaremos dele nos próximos capítulos. + +![Tela de cliente de aplicativo criado](/assets/cognito-user-pool/user-pool-app-created.png) + +### Criando um nome de domínio + +Finalmente, selecione **Nome do domínio** do painel esquerdo. digite um nome de domínio único e clique em **Salvar as alterações** + +![Tela de criação de nome do domínio](/assets/cognito-user-pool/user-pool-domain-name.png) + +Agora nosso grupo de usuários do Cognito está pronto. Ele vai conter um diretório com os usuários do nosso app de anotações. Também vamos utilizá-lo para autenticar acessos a nossa API. A seguir vamos configurar um usuário teste no grupo de usuários. diff --git a/_chapters/pt/create-a-dynamodb-table.md b/_chapters/pt/create-a-dynamodb-table.md new file mode 100644 index 0000000000..c6d2803c2a --- /dev/null +++ b/_chapters/pt/create-a-dynamodb-table.md @@ -0,0 +1,61 @@ +--- +layout: post +title: Crie uma tabela no DynamoDB +date: 2016-12-27 00:00:00 +lang: pt +ref: create-a-dynamodb-table +description: O Amazon DynamoDB é um banco de dados NoSQL totalmente gerenciado que vamos utilizar no backend de nossa API serverless. O DynamoDB armazena dados em tabelas e cada uma possui uma chave primária que não pode ser mudada. Também vamos provisionar os throughputs de leitura e gravação para nossa tabela. +context: true +comments_id: create-a-dynamodb-table/139 +--- + +Para construir o backend do nosso app, é sensato começar pensando como os dados serão armazenados. Nós vamos utilizar o [DynamoDB](https://aws.amazon.com/dynamodb/) para isso. + +### Sobre o DynamoDB + +O Amazon DynamoDB é um banco de dados NoSQL totalmente gerenciado que possui uma alta performance e ao mesmo tempo previsível, com escalabilidade consistente. Assim como outros bancos de dados, o DynamoDB usa tabelas. Cada tabela contém vários itens e cada item é composto por um ou mais atributos. Nós vamos passar pelos conceitos básicos nos caítulos seguintes, porém, para ter uma visão melhor, comece [por este excelente guia](https://www.dynamodbguide.com) (em inglês). + +### Criando uma tabela + +Primeiro, faça o login no [console da AWS](https://console.aws.amazon.com) e selecione **DynamoDB** da lista de serviços. + +![Tela para seleção do DynamoDB](/assets/dynamodb/select-dynamodb-service.png) + +Selecione **Criar tabela**. + +![Tela para criação da tabela no DynamoDB](/assets/dynamodb/create-dynamodb-table.png) + +Digite o **Nome da tabela** e a **Chave primária** como na tela abaixo. Tenha certeza de que os valores `userId` e `noteId` seguem a nomenclatura em camel case. + +![Tela de criação da chave primária](/assets/dynamodb/set-table-primary-key.png) + +Cada tabela no DynamoDB contém uma chave primária, que não deve ser alterar após criada. A chave primária identifica unicamente um item na tabela, sendo que nenhum item possui a mesma chave. No caso do DynamoDB, são suportadas dois tipos de chaves primárias: + +- Chave de partição +- Chave de partição e chave de classificação + +Nós vamos usar a chave primária composta para que nos dê uma flexibilidade melhor na hora de realizar consultas. Por exemplo, se você consultar apenas pelo valor do `userId`, o DynamoDB vai retornar todas as notas daquele usuário. Você também pode consultar pelo `userId` junto com o `noteId` para consultar uma nota em particular. + +Para entender mais sobre como funcionam indexes no DynamoDB, você pode ler mais sobre aqui: [Componentes principais do DynamoDB][dynamodb-components] + +Se você ver uma mensagem semelhante a da imagem abaixo, desmarque a opção **Usar configurações padrão**. + +![tela de aviso do Auto Scaling IAM Role](/assets/dynamodb/auto-scaling-iam-role-warning.png) + +Role até o final da página e marque a opção **Função vinculada ao serviço do Auto Scaling do DynamoDB** e depois clique no botão **Criar**. + +![Set Table Provisioned Capacity screenshot](/assets/dynamodb/set-table-provisioned-capacity.png) + +Caso não veja a mensagem como mostrado acima, deixe marcado a opção **Usar configurações padrão** e clique no botão **Criar**. + +Note que por padrão, a capacidade provisionada é 5 unidades de leitura e 5 de gravação. Quando você cria uma tabela, você configura quanto de capacidade provisionada quer reservar para leitura e gravação. O DynamoDB vai reservar os recursos necessários para cumprir a capacidade configurada enquanto garante uma performance consistente e de baixa latência. Uma unidade de leitura consegue ler até 8 KB por segundo e uma capacidade de gravação salva até 1 KB de informação por segundo. Você pode mudar sua capacidade provisionada mais tarde, tendo a opção de aumentar ou diminuir assim que necessário. + +A tabela `notes` foi criada. Caso a tela fique parada na mensagem **A tabela está sendo criada** , atualize a página manualmente. + +![Tela de seleção de tabelas do DynamoDB](/assets/dynamodb/dynamodb-table-created.png) + +É uma boa prática configurar backups para sua tabela, especialmente se planeja usar o projeto em produção. Vamos falar sobre isso no capítulo extra, [Backups no DynamoDB]({% link _archives/backups-in-dynamodb.md %}). + +Agora vamos configurar um bucket no S3 para receber upload de arquivos. + +[dynamodb-components]: https://docs.aws.amazon.com/pt_br/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html diff --git a/_chapters/pt/create-an-s3-bucket-for-file-uploads.md b/_chapters/pt/create-an-s3-bucket-for-file-uploads.md new file mode 100644 index 0000000000..59f3429871 --- /dev/null +++ b/_chapters/pt/create-an-s3-bucket-for-file-uploads.md @@ -0,0 +1,83 @@ +--- +layout: post +title: Crie um bucket no S3 para upload de arquivos +date: 2016-12-27 00:00:00 +lang: pt +ref: create-an-s3-bucket-for-file-uploads +description: Para permitir que os usuários façam upload de arquivos para nosso app serverless, vamos usar o Amazon S3. O S3 permite o upload de arquivos e usa buckets para organizá-los. Nós vamos criar um bucket e ativar o CORS (cross-origin resource sharing) que é necessário para que nosso app em React.js consiga fazer uploads nele. +redirect_from: /chapters/create-a-s3-bucket-for-file-uploads.html +context: true +comments_id: create-an-s3-bucket-for-file-uploads/150 +--- + +Agora que temos nossa tabela no DynamoDB pronta, vamos configurar a parte que lida com upload de arquivos. Precisamos permitir uploads pois cada anotação pode ter um arquivo como anexo. + +[O Amazon S3](https://aws.amazon.com/pt/s3/) (Simple Storage Service) fornece um serviço de armazenamento pela interface web como REST. Você pode armazenar qualquer arquivo no S3 incluindo imagens, vídeos, etc. Os arquivos (objetos) são organizados em buckets que são identificados com um nome único, atribuído por uma chave única por usuário. + +Neste capítulo, nós vamos criar um bucket no S3 que será utilizado para armazenar os arquivos que cada usuário fez o upload pelo nosso app. + +### Criar um Bucket + +Primeiro, faça o login no [Console da AWS](https://console.aws.amazon.com) e selecione o **S3** na lista de serviços. + +![Tela de seleção do S3](/assets/s3/select-s3-service.png) + +Selecione **Criar bucket**. + +![Tela para criar bucket no S3](/assets/s3/select-create-bucket.png) + +Digite um nome para o bucket e selecione uma região. Depois clique no botão **Criar**. + +- **Nomes do buckets** são globalmente únicos, então você deve criar um nome único ainda não utilizado. +- **Região** é uma região demográfica onde os arquivos são armazenados fisicamente em servidores. No tutorial será utilizado a **Leste dos EUA (Norte da Virgínia)**, mas se estiver no Brasil, recomenda-se utilizar a **América do Sul (São Paulo)** + +Guarde o nome da região que utilizar, pois vamos precisar disso mais tarde. + +![Tela de informações do bucket S3](/assets/s3/enter-s3-bucket-info.png) + +Siga os próximos passos e aceite as configurações padrões clicando em **Próximo** e clique em **Criar bucket** no último passo. + +![Tela de propriedades do bucket](/assets/s3/set-s3-bucket-properties.png) +![Tela de permissões do bucket](/assets/s3/set-s3-bucket-permissions.png) +![Tela de review de criação do bucket](/assets/s3/review-s3-bucket.png) + +### Habilitar CORS + +No app de anotações, os usuários farão upload de arquivos para o bucket que acabamos de criar. Nosso app será hospedado em um domínio customizado e vai estabelecer uma comunicação entre domínios no ato do upload. Por padrão, S3 não permite acesso ao bucket a partir de domínios diferentes. Entretanto, o CORS permite que isso aconteça, basta ativá-lo no bucket. + +Selecione o bucket que acabamos de criar (clique bem em cima do nome). + +![Tela de seleção de buckets no S3](/assets/s3/select-created-s3-bucket.png) + +Selecione a aba the **Permissões**, e clique em **Configuração de CORS**. + +![Tela de permissões do bucket no S3](/assets/s3/select-s3-bucket-cors-configuration.png) + +Coloque essas configurações no editor que aparece e depois clique em **Salvar**. + +```json +[ + { + "AllowedMethods": [ + "GET", + "PUT", + "POST", + "HEAD", + "DELETE" + ], + "AllowedOrigins": [ + "*" + ], + "AllowedHeaders": [ + "*" + ], + "MaxAgeSeconds": 3000 + } +] +``` + +Você também pode editar essa configuração para utilizar seu próprio domínio ou uma lista que queira permitir o acesso, uma vez que desejar usar em produção. + +![Tela de edição das políticas de CORS](/assets/s3/save-s3-bucket-cors-configuration.png) + +Agora que temos nosso bucket no S3 pronto, vamos configurar autenticação de usuários. diff --git a/_chapters/pt/how-to-get-help.md b/_chapters/pt/how-to-get-help.md index bf00f8f699..b8aaa7b076 100644 --- a/_chapters/pt/how-to-get-help.md +++ b/_chapters/pt/how-to-get-help.md @@ -12,7 +12,7 @@ Caso você tenha algum problema em algum passo específico, nós queremos ter ce - Nós usamos um [fórum]({{ site.forum_url }}) como sessão de comentários e com isso já ajudamos várias pessoas a resolver seus problemas no passado. Então, tenha certeza que sua dúvida ainda não foi sanada antes de fazer um novo comentário. - Poste no fórum específico do capítulo que você está tendo problemas para mantermos a organização. -![Serverless Stack Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) +![SST Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) O guia inteiro está hospedado no [GitHub]({{ site.github_repo }}). Se você achar algum erro, você pode: diff --git a/_chapters/pt/setup-the-serverless-framework.md b/_chapters/pt/setup-the-serverless-framework.md new file mode 100644 index 0000000000..cb1dbdbb58 --- /dev/null +++ b/_chapters/pt/setup-the-serverless-framework.md @@ -0,0 +1,68 @@ +--- +layout: post +title: Set up the Serverless Framework +date: 2016-12-29 00:00:00 +lang: pt +ref: setup-the-serverless-framework +description: Para criar a API do nosso backend serverless usando AWS Lambda e API Gateway, vamos usar o Serverless Framework (https://serverless.com). Este framework ajuda desenvolvedores a construírem e gerenciar aplicações serverless na AWS e outros provedores de computação em nuvem. Vamos instalar o Serverless Framework CLI pelo NPM e usá-lo para criar um novo projeto. +context: true +code: backend +comments_id: set-up-the-serverless-framework/145 +--- + +Vamos utilizar [AWS Lambda](https://aws.amazon.com/lambda/) e [Amazon API Gateway](https://aws.amazon.com/api-gateway/) para criar nosso backend. AWS Lambda é um serviço que permite executar códigos na nuvem sem se preocupar com servidores. Você paga apenas pelo tempo em que a função é executada e não há cobrança de tempo inativo. O API Gateway permite que desenvolvedores criem, publiquem, mantenham e monitorem APIs de forma segura. Usar Lambda e configurar o API Gateway sem nenhum tipo de framework pode ser uma tarefa árdua, por isso vamos uilizar o [Serverless Framework](https://serverless.com) para simplificar o processo. + +O Serverless Framework dá possibilidade aos desenvolvedores implantarem o backend de suas aplicações como funções independentes baseadas em eventos, que são mais tarde enviadas para o Lambda. O framework também configura o Lambda para executar o código como resposta a requests de HTTP em forma de eventos utilizando o API Gateway. + +Neste capítulo nós vamos configurar o Serverless Framework em nosso ambiente de desenvolvimento local. + +### Instalando Serverless + +{%change%} Instala o Serverless globalmente. + +```bash +$ npm install serverless -g +``` + +O comando acima precisa do [NPM](https://www.npmjs.com) instalado, um gerenciador de pacotes. Acesse [esse link](https://docs.npmjs.com/getting-started/installing-node) se precisar de ajuda para instalar. + +{%change%} No diretório onde vai colocar os arquivos do projeto, crie um projeto base do Node.js. Nós vamos ver alguns detalhes desse projeto no próximo capítulo. + +```bash +$ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name notes-app-api +``` + +{%change%} Vá até o diretório do backend de nossa API. + +```bash +$ cd notes-app-api +``` + +Agora o diretório deve contar novos arquivos, incluindo **handler.js** e **serverless.yml**. + +- **handler.js** arquivo que contém o código para os serviços e funções que vamos implantar no Lambda. +- **serverless.yml** arquivo que contém configurações para os serviços da AWS, cujo o Serverless vai configurar de acordo com as instruções contidas aqui. + +Também temos um diretório `tests/` onde nós podemos adicionar testes unitários. + +### Instalando pacotes Node.js + +O projeto inicial requisita algumas dependências que são listadas no arquivo `package.json`. + +{%change%} Na raíz do projeto, execute. + +```bash +$ npm install +``` + +{%change%} Agora vamos instalar alguns outros pacotes específicos para nosso backend. + +```bash +$ npm install aws-sdk --save-dev +$ npm install uuid --save +``` + +- **aws-sdk** é um pacote para comunicar com os serviços da AWS. +- **uuid** serve para gerar ids únicas. Precisamos dele para salvar itens no DynamoDB. + +O projeto incial que estamos usando é compatível com a versão do JavaScript que estaremos usando no nosso frontend mais tarde. Em seguida, vamos ver como fazer isso. diff --git a/_chapters/pt/what-does-this-guide-cover.md b/_chapters/pt/what-does-this-guide-cover.md index 02696a36fe..058c7b7e32 100644 --- a/_chapters/pt/what-does-this-guide-cover.md +++ b/_chapters/pt/what-does-this-guide-cover.md @@ -8,7 +8,7 @@ context: true comments_id: what-does-this-guide-cover/83 --- -Para entendermos os principais conceitos envolvidos na construção de aplicações web, nós iremos criar um aplicativo de notas chamado [**Scratch**](https://demo2.serverless-stack.com). +Para entendermos os principais conceitos envolvidos na construção de aplicações web, nós iremos criar um aplicativo de notas chamado [**Scratch**](https://demo2.sst.dev). ![Completed app desktop screenshot](/assets/completed-app-desktop.png) @@ -57,7 +57,7 @@ Você vai precisar do [Node.js v8.10+ e NPM v5.5+](https://nodejs.org/en/). Voc ### Como esse guia é estruturado -O guia é separado em duas partes. Ambas são relativamente "únicas". A primeira parte cobre o básico enquanto a segunda parte cobre os tópicos mais avançados e um pouco de automação do setup. Nós lançamos esse guia no começo de 2017 apenas com a primeira parte. A comunidade Serverless Stack cresceu bastante nesse meio tempo e a maioria dos nossos leitores usaram o que foi feito nesse guia para aumentar o tamanho de seus negócios. +O guia é separado em duas partes. Ambas são relativamente "únicas". A primeira parte cobre o básico enquanto a segunda parte cobre os tópicos mais avançados e um pouco de automação do setup. Nós lançamos esse guia no começo de 2017 apenas com a primeira parte. A comunidade SST cresceu bastante nesse meio tempo e a maioria dos nossos leitores usaram o que foi feito nesse guia para aumentar o tamanho de seus negócios. Então nós decidimos extender esse guia e adicionar uma segunda parte a ele. Essa sendo destinada para pessoas que prentendem usar essa tecnologia para seus próprios projetos. Essa segunda parte ensina a automatizar todas as partes manuais da parte 1 e ajuda você a criar um fluxo de trabalho completo para produção. @@ -90,7 +90,7 @@ Para o frontend: #### Parte II -Focando nos leitores que estão procurando uma maneira de utilizar o Serverless Stack para os projetos do dia-a-dia. Nós automatizamos todos os passos da primeira etapa. A seguir é o que nós vamos cobrir, por ordem. +Focando nos leitores que estão procurando uma maneira de utilizar o SST para os projetos do dia-a-dia. Nós automatizamos todos os passos da primeira etapa. A seguir é o que nós vamos cobrir, por ordem. Para o backend: @@ -111,7 +111,7 @@ Para o frontend - Ambientes no Create React App - Aceitando pagamento com cartão de crédito no React - Automatizando as entregas com Netlify -- Configurando domínios customizados atrvés do Netlify +- Configurando domínios customizados através do Netlify Nós acreditamos que tudo isso vai lhe dar uma ótima base para a criação de uma aplicação com Serverless para o mundo real. Se existe algum outro conceito ou tecnologias que você gostaria que nós cobríssemos nesse guia, sinta-se a vontade para comentar no nosso [forum]({{ site.forum_url }}) (em inglês). diff --git a/_chapters/pt/what-is-aws-lambda.md b/_chapters/pt/what-is-aws-lambda.md index 24610c1726..f3a8c023ec 100644 --- a/_chapters/pt/what-is-aws-lambda.md +++ b/_chapters/pt/what-is-aws-lambda.md @@ -8,18 +8,19 @@ description: AWS Lambda é o serviço de Serverless oferecido pela Amazon Web Se comments_id: what-is-aws-lambda/308 --- - [AWS Lambda](https://aws.amazon.com/lambda/) (ou Lambda para resumir as coisas) é o serviço de Serverless oferecido pela AWS. Nesse capítulo vamos usar Lambda para montar nossa aplicação Serverless. E, enquanto não precisarmos conhecer a fundo sobre como a Lambda funciona, é importante conhecer como as funções serão executadas. ### Especificações das funções Lambda Vamos começar com uma visão por cima das especificações técnicas da AWS Lambda. As funções suportam as seguintes linguagens: -- Node.js: v8.10 e v6.10 -- Java 8 -- Python: 3.6 e 2.7 -- .NET Core: 1.0.1 e 2.0 +- Node.js 18.x, 16.x, e 14.x +- Java 17, 11 e 8 +- Python 3.11, 3.10, 3.9, 3.8, e 3.7 +- .NET 7 e 6 - Go 1.x +- Ruby 3.2 e 2.7 +- [Rust](https://docs.aws.amazon.com/pt_br/lambda/latest/dg/lambda-rust.html) Cada função executa dentro de um container com uma distro Linux própria da Amazon chamada Amazon AMI com arquitetura 64-bit. E o ambiente de execução pode possuir as seguintes especificações: @@ -63,10 +64,10 @@ Entretanto, devido a otimização citada anteriormente, a função Lambda atual Por exemplo, o metódo `createNewDbConnection` abaixo é chamado apenas uma vez por instância de container e não toda vez que a função Lambda é chamada. Por outro lado, a função `myHandler` é executado em cada request. -``` javascript +```js var dbConnection = createNewDbConnection(); -exports.myHandler = function(event, context, callback) { +exports.myHandler = function (event, context, callback) { var result = dbConnection.makeQuery(); callback(null, result); }; diff --git a/_chapters/pt/who-is-this-guide-for.md b/_chapters/pt/who-is-this-guide-for.md index 50c1260c4a..0a88266f91 100644 --- a/_chapters/pt/who-is-this-guide-for.md +++ b/_chapters/pt/who-is-this-guide-for.md @@ -12,6 +12,26 @@ Esse guia foi feito para desenvolvedores full-stack ou desenvolvedores que desej Talvez você seja um desenvolvedor backend querendo aprender mais sobre a parte frontend de aplicações Serverless, ou um desenvolvedor frontend que gostaria de aprender mais sobre backend. Esse guia servirá para ambos os casos. -Por hora, apenas vamos abordar o desenvolvimento com JavaScript. Futuramente talvez abordemos outras linguagens e ambientes. Porém, para começar, nós achamos muito benéfico para um desenvolvedor full-stack utilizar apenas uma linguagem (JavaScript) e ambiente (NodeJs) para a construção de uma aplicação completa. +Pessoalmente, a ideia do Serverless foi uma enorme revelação para nós e isso nos fez criar um guia com qual poderíamos compartilhar o que aprendemos. Você pode saber mais sobre nós [**aqui**]({{ site.sst_url }}). E [veja alguns exemplos de pessoas que construíram aplicações com SST clicando aqui]({% link showcase.md %}) -Pessoalmente, a ideia do Serverless foi uma enorme revelação para nós e isso nos fez criar um guia com qual poderíamos compartilhar o que aprendemos. Você pode ser mais sobre nós [**aqui**]({% link about.md %}). E [veja alguns exemplos de pessoas que construiram aplicações com a stack Serverless]({% link showcase.md %}) +Por hora, apenas vamos abordar o desenvolvimento com JavaScript/TypeScript. Futuramente talvez abordemos outras linguagens e ambientes. Porém, para começar, nós achamos muito benéfico para um desenvolvedor full-stack utilizar apenas uma linguagem (TypeScript) e ambiente (Node.js) para a construção de uma aplicação completa. + +### Por que TypeScript + +Nós usamos TypeScript desde o frontend, backend e até a criação da nossa infraestrutura. Se você não estiver familiar com TypeScript talvez você esteja pensando por que tipagem estática importa. + +Uma grande vantagem em usar tipagem estática em todo o código é que seu editor de código consegue autocompletar e mostrar opções inválidas no seu código. Isso é muito útil quando você esta começando. Porém, isso também pode ser útil quando você está configurando sua infraestrutura através de código. + +Deixando toda essa benevolência do autocomplete de lado, tipagem estática acaba sendo um ponto crítico para ajudar na manutenibilidade de um projeto. Isso importa muito se você pretende trabalhar no mesmo projeto, com o mesmo código, por anos. + +Deve ser fácil para você e seu time fazer modificações em partes do seu projeto após muito tempo sem mexer nele. TypeScript permite que você faça isso! Seu projeto não vai ser mais tão _frágil_ e você não terá medo de fazer modificações. + +#### TypeScript do jeito fácil + +Se você não está acostumado com TypeScript, você deve estar pensando _"Então eu vou ter que escrever todos esses tipos extras para minhas coisas?"_ ou _"Toda essa tipagem não vai deixar meu código verboso e assustador?"_. + +Essas preocupações são válidas. Mas acontece que, se as bibliotecas que você está usando são feitas para o uso junto ao TypeScript, você não irá ter tantas tipagens extras no seu código. Na verdade, como você vai ver nesse tutorial, você vai ter todos os benefícios de um projeto tipado com um código que quase parece com o JavaScript normal. + +Além disso, o TypeScript pode ser adotado gradativamente. Isso significa que você pode usar o nosso projeto base de TypeScript e adicionar JavaScript a ele. Fazer isso não é recomendável, porém isso pode ser uma opção. + +Vamos começar dando uma olhada sobre o que vamos contemplar nesse guia a seguir. diff --git a/_chapters/pt/why-create-serverless-apps.md b/_chapters/pt/why-create-serverless-apps.md index b3c1d28142..ad1d1dd616 100644 --- a/_chapters/pt/why-create-serverless-apps.md +++ b/_chapters/pt/why-create-serverless-apps.md @@ -8,15 +8,15 @@ description: Aplicações Serverless são fáceis de manter e escalar, tendo em comments_id: why-create-serverless-apps/87 --- -É muito importante entender porque vale a pena criar aplicações Serverless. Aqui temos algumas boas razões do porque aplicações Serverless tem certa vantagem em cima de modelos tradicionais de hospedar aplicações. +É muito importante entender porque vale a pena criar aplicações Serverless. Aqui temos algumas boas razões do porque aplicações Serverless têm certas vantagens em cima de modelos tradicionais de hospedar aplicações. 1. Baixa manutenção 2. Baixo custo 3. Fácil de escalar -De longe, o maior benefício, é que você só tera de se preocupar com o seu código e nada mais. E o resultado da baixa manutenção é que você não precisará gerenciar servidores. Você não irá precisar checar se seu servidor está executando da maneira correto ou que você está seguindo todas as questões de segurança para ele. +De longe, o maior benefício, é que você só tera de se preocupar com o seu código e nada mais. E o resultado da baixa manutenção é que você não precisará gerenciar servidores. Você não irá precisar checar se seu servidor está executando da maneira correta ou que você está seguindo todas as questões de segurança para ele. -O principal motivo é o preço de se executar aplicações Serverless, onde você efetivamente só vai pagar algo quando acontecer uma requisição. Tendo em vista esse cenário, enquanto sua aplicação não está sendo usada nada será cobrado. Vamos fazer uma rápida análise de quanto nos custará para executar uma simples aplicação de notas. No nosso cenário, vamos assumir uma média de 1000 usuários ativos por dia fazendo uma média de 20 requisições por dia para a nossa API e armazenando cerca de 10MB de arquivos no S3. +O principal motivo é o preço de se executar aplicações Serverless, onde você efetivamente só vai pagar algo quando acontecer uma requisição. Tendo em vista esse cenário, enquanto sua aplicação não está sendo usada, nada será cobrado. Vamos fazer uma rápida análise de quanto nos custará para executar uma simples aplicação de notas. No nosso cenário, vamos assumir uma média de 1000 usuários ativos por dia fazendo uma média de 20 requisições por dia para a nossa API e armazenando cerca de 10MB de arquivos no S3. {: .cost-table } | Serviço | Taxa | Custo | @@ -36,8 +36,8 @@ O principal motivo é o preço de se executar aplicações Serverless, onde voc [3] DynamoDB oferece 25GB/mês de armazenamento gratuito. [4] S3 oferece 1GB gratuito de transferência. -Chegamos em um valor de $6.10 (cerca de R$ 22,67 na cotação atual do dólar em relação ao real). Adicionalmente, um domínio .com custa $12 por ano (cerca de R$ 30 dependendo do site), esse último sendo o mais caro. Mas tenha em mente que essas estimativas não são exatas e podem ter inúmeras variações. O uso no mundo real podem seguir por outros caminhos. Entretanto, essas estimativas conseguem nos mostrar o quão barato pode ser ter uma aplicação Serverless. +Chegamos em um valor de $6.10 (cerca de R$ 22,67 na cotação atual do dólar em relação ao real). Adicionalmente, um domínio .com custa $12 por ano (cerca de R$ 30 dependendo do site), esse último sendo o mais caro. Mas tenha em mente que essas estimativas não são exatas e podem ter inúmeras variações. O uso no mundo real pode seguir por outros caminhos. Entretanto, essas estimativas conseguem nos mostrar o quão barato pode ser ter uma aplicação Serverless. -Por fim, todo o esquema de escalonamento se deve em parte pelo DynamoDB que nos entrega escalonamento infinito e a Lambda que escala conforme a demanda. Claro que o nosso frontend é apenas uma simples página estática e isso garantirá que nossa aplicação responda quase que instantaneamente graças ao CloudFront. +Por fim, todo o esquema de escalonamento se deve em parte pelo DynamoDB que nos entrega escalonamento infinito e à Lambda, que escala conforme a demanda. Claro que o nosso frontend é apenas uma simples página estática e isso garantirá que nossa aplicação responda quase que instantaneamente graças ao CloudFront. Perfeito! Agora que você está convencido que é uma boa criar aplicações Serverless, mãos na massa! diff --git a/_chapters/purchase-a-domain-with-route-53.md b/_chapters/purchase-a-domain-with-route-53.md new file mode 100644 index 0000000000..94c1149c64 --- /dev/null +++ b/_chapters/purchase-a-domain-with-route-53.md @@ -0,0 +1,41 @@ +--- +layout: post +title: Purchase a Domain with Route 53 +date: 2017-02-08 01:00:00 +lang: en +description: To host our full-stack serverless app under our own domain name in AWS we are going to purchase a domain using Route 53. +comments_id: purchase-a-domain-with-route-53/1867 +ref: purchase-a-domain-with-route-53 +--- + +To host our app on our own custom domain, we'll be using [Amazon Route 53](https://aws.amazon.com/route53/). + +If you are following this guide but are not ready to purchase a new domain, you can skip this chapter. + +On the other hand, if you have an existing domain that is not on AWS, follow these docs to [move it over to Amazon Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). + +Let's get started. In your [AWS Console](https://console.aws.amazon.com) head over to the Route 53 section in the list of services. + +![Select Route 53 service screenshot](/assets/select-route-53-service.png) + +Type in your domain in the **Register domain** section and click **Check**. + +![Search available domain screenshot](/assets/search-available-domain.png) + +After checking its availability, click **Add to cart**. + +![Add domain to cart screenshot](/assets/add-domain-to-cart.png) + +And hit **Continue** at the bottom of the page. + +![Continue to contact details screenshot](/assets/continue-to-contact-detials.png) + +Fill in your contact details and hit **Continue** once again. + +![Continue to confirm details screenshot](/assets/continue-to-confirm-detials.png) + +Finally, review your details and confirm the purchase by hitting **Complete Purchase**. + +![Confirm domain purchase screenshot](/assets/confirm-domain-purchase.png) + +Next, let's use this domain in our serverless app. diff --git a/_chapters/redirect-on-login-and-logout.md b/_chapters/redirect-on-login-and-logout.md index b6f59b85b8..e5ffa43db9 100644 --- a/_chapters/redirect-on-login-and-logout.md +++ b/_chapters/redirect-on-login-and-logout.md @@ -4,8 +4,7 @@ title: Redirect on Login and Logout date: 2017-01-17 00:00:00 lang: en ref: redirect-on-login-and-logout -description: To ensure that the user is redirected after logging in and logging out of our React.js app, we are going to use the withRouter higher-order component from React Router v4. And we’ll use the history.push method to navigate the app. -context: true +description: To ensure that the user is redirected after logging in and logging out of our React.js app, we are going to use the useNavigate React hook from React Router. And we’ll use the nav.push method to navigate the app. comments_id: redirect-on-login-and-logout/154 --- @@ -14,75 +13,79 @@ To complete the login flow we are going to need to do two more things. 1. Redirect the user to the homepage after they login. 2. And redirect them back to the login page after they logout. -We are going to use the `history.push` method that comes with React Router v4. +We are going to use the `useNavigate` hook that comes with React Router. This will allow us to use the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History){:target="_blank"}. ### Redirect to Home on Login -Since our `Login` component is rendered using a `Route`, it adds the router props to it. So we can redirect using the `this.props.history.push` method. +{%change%} First, initialize `useNavigate` hook in the beginning of `src/containers/Login.tsx`. -``` javascript -this.props.history.push("/"); +```tsx +const nav = useNavigate(); ``` -Update the `handleSubmit` method in `src/containers/Login.js` to look like this: +Make sure to add it below the `export default function Login() {` line. -``` javascript -handleSubmit = async event => { +{%change%} Then update the `handleSubmit` method in `src/containers/Login.tsx` to look like this: + +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); try { - await Auth.signIn(this.state.email, this.state.password); - this.props.userHasAuthenticated(true); - this.props.history.push("/"); - } catch (e) { - alert(e.message); + await Auth.signIn(email, password); + userHasAuthenticated(true); + nav("/"); + } catch (error) { + if (error instanceof Error) { + alert(error.message); + } else { + alert(String(error)); + } } } ``` -Now if you head over to your browser and try logging in, you should be redirected to the homepage after you've been logged in. - -![React Router v4 redirect home after login screenshot](/assets/redirect-home-after-login.png) +{%change%} Also, import `useNavigate` from React Router in the header of `src/containers/Login.tsx`. -### Redirect to Login After Logout +```tsx +import { useNavigate } from "react-router-dom"; +``` -Now we'll do something very similar for the logout process. However, the `App` component does not have access to the router props directly since it is not rendered inside a `Route` component. To be able to use the router props in our `App` component we will need to use the `withRouter` [Higher-Order Component](https://facebook.github.io/react/docs/higher-order-components.html) (or HOC). You can read more about the `withRouter` HOC [here](https://reacttraining.com/react-router/web/api/withRouter). +Now if you head over to your browser and try logging in, you should be redirected to the homepage after you've been logged in. -To use this HOC, we'll change the way we export our App component. +![React Router v6 redirect home after login screenshot](/assets/redirect-home-after-login.png) -Replace the following line in `src/App.js`. +### Redirect to Login After Logout -``` coffee -export default App; -``` +Now we'll do something very similar for the logout process. -With this. +{%change%} Add the `useNavigate` hook in the beginning of `App` component in `src/App.tsx`. -``` coffee -export default withRouter(App); +```tsx +const nav = useNavigate(); ``` -And import `withRouter` by replacing the `import { Link }` line in the header of `src/App.js` with this: +{%change%} Import `useNavigate` from React Router in the header of `src/App.tsx`. -``` coffee -import { Link, withRouter } from "react-router-dom"; +```tsx +import { useNavigate } from "react-router-dom"; ``` -Add the following to the bottom of the `handleLogout` method in our `src/App.js`. +{%change%} Add the following to the bottom of the `handleLogout` function in our `src/App.tsx`. -``` coffee -this.props.history.push("/login"); +```tsx +nav("/login"); ``` -So our `handleLogout` method should now look like this. +So our `handleLogout` function should now look like this. -``` coffee -handleLogout = async event => { +```tsx +async function handleLogout() { await Auth.signOut(); - this.userHasAuthenticated(false); + userHasAuthenticated(false); - this.props.history.push("/login"); + nav("/login"); } ``` @@ -90,4 +93,4 @@ This redirects us back to the login page once the user logs out. Now if you switch over to your browser and try logging out, you should be redirected to the login page. -You might have noticed while testing this flow that since the login call has a bit of a delay, we might need to give some feedback to the user that the login call is in progress. Let's do that next. +You might have noticed while testing this flow that since the login call has a bit of a delay, we might need to give some feedback to the user that the login call is in progress. Also, we are not doing a whole lot with the errors that the `Auth` package might throw. Let's look at those next. diff --git a/_chapters/redirect-on-login.md b/_chapters/redirect-on-login.md index d1a10e276e..a864e1efab 100644 --- a/_chapters/redirect-on-login.md +++ b/_chapters/redirect-on-login.md @@ -3,8 +3,7 @@ layout: post title: Redirect on Login date: 2017-02-04 00:00:00 lang: en -description: To make sure that our React.js redirects a user to the right page after they login, we are going to use the React Router v4 Redirect component. -context: true +description: To make sure that our React.js redirects a user to the right page after they login, we are going to use the React Router useNavigate hook. comments_id: redirect-on-login/24 ref: redirect-on-login --- @@ -13,20 +12,16 @@ Our secured pages redirect to the login page when the user is not logged in, wit Let's start by adding a method to read the `redirect` URL from the querystring. -Add the following method to your `src/components/UnauthenticatedRoute.js` below the imports. +{%change%} Add the following method to your `src/components/UnauthenticatedRoute.tsx` below the imports and interface. -``` coffee -function querystring(name, url = window.location.href) { - name = name.replace(/[[]]/g, "\\$&"); - - const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)", "i"); +```tsx +function querystring(name: string, url = window.location.href) { + const parsedName = name.replace(/[[]]/g, "\\$&"); + const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`, "i"); const results = regex.exec(url); - if (!results) { - return null; - } - if (!results[2]) { - return ""; + if (!results || !results[2]) { + return false; } return decodeURIComponent(results[2].replace(/\+/g, " ")); @@ -37,31 +32,52 @@ This method takes the querystring param we want to read and returns it. Now let's update our component to use this parameter when it redirects. -Replace our current `export default ({ component: C, props: cProps, ...rest }) => {` method with the following. +{%change%} Replace our current `UnauthenticatedRoute` function component with the following. -``` coffee -export default ({ component: C, props: cProps, ...rest }) => { +```tsx +export default function UnauthenticatedRoute(props: Props) { + const { isAuthenticated } = useAppContext(); + const { children } = props; const redirect = querystring("redirect"); - return ( - - !cProps.isAuthenticated - ? - : } - /> - ); -}; + + if (isAuthenticated) { + return ; + } + + return cloneElement(children, props); +} +``` + +{%change%} And remove the following from the `handleSubmit` method in `src/containers/Login.tsx`. + +```tsx +nav("/"); ``` -And remove the following from the `handleSubmit` method in `src/containers/Login.js`. +{%change%} Also, remove the hook declaration. -``` coffee -this.props.history.push("/"); +```tsx +const nav = useNavigate(); +``` + +{%change%} Finally, remove the import. + +```jsx +import { useNavigate } from "react-router-dom"; ``` Now our login page should redirect after we login. -And that's it! Our app is ready to go live. Let's look at how we are going to deploy it using our serverless setup. +### Commit the Changes + +{%change%} Let's commit our code so far and push it to GitHub. + +```bash +$ git add . +$ git commit -m "Building our React app" +$ git push +``` + +And that's it! Our app is ready to go live. + +Next we'll be deploying our serverless app to production. And we'll do it using our own domain! diff --git a/_chapters/render-the-note-form.md b/_chapters/render-the-note-form.md index ac8b16f547..fc07062310 100644 --- a/_chapters/render-the-note-form.md +++ b/_chapters/render-the-note-form.md @@ -4,46 +4,46 @@ title: Render the Note Form date: 2017-01-29 00:00:00 lang: en description: We are going to render a user’s note in a form in our React.js app. To render the form fields, we’ll use React-Bootstrap’s FormGroup and FormControl components. -context: true comments_id: render-the-note-form/140 ref: render-the-note-form --- -Now that our container loads a note on `componentDidMount`, let's go ahead and render the form that we'll use to edit it. +Now that our container loads a note using the `useEffect` method, let's go ahead and render the form that we'll use to edit it. -Replace our placeholder `render` method in `src/containers/Notes.js` with the following. +{%change%} Replace our placeholder `return` statement in `src/containers/Notes.tsx` with the following. -``` coffee -validateForm() { - return this.state.content.length > 0; +```tsx +function validateForm() { + return content.length > 0; } -formatFilename(str) { +function formatFilename(str: string) { return str.replace(/^\w+-/, ""); } -handleChange = event => { - this.setState({ - [event.target.id]: event.target.value - }); +function handleFileChange(event: React.ChangeEvent) { + if (event.currentTarget.files === null) return; + file.current = event.currentTarget.files[0]; } -handleFileChange = event => { - this.file = event.target.files[0]; -} +async function handleSubmit(event: React.FormEvent) { + let attachment; -handleSubmit = async event => { event.preventDefault(); - if (this.file && this.file.size > config.MAX_ATTACHMENT_SIZE) { - alert(`Please pick a file smaller than ${config.MAX_ATTACHMENT_SIZE/1000000} MB.`); + if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) { + alert( + `Please pick a file smaller than ${ + config.MAX_ATTACHMENT_SIZE / 1000000 + } MB.` + ); return; } - this.setState({ isLoading: true }); + setIsLoading(true); } -handleDelete = async event => { +async function handleDelete(event: React.FormEvent) { event.preventDefault(); const confirmed = window.confirm( @@ -54,116 +54,110 @@ handleDelete = async event => { return; } - this.setState({ isDeleting: true }); + setIsDeleting(true); } -render() { - return ( -
- {this.state.note && - - - + {note && ( + + + + setContent(e.target.value)} /> - - {this.state.note.attachment && - - Attachment - + + + Attachment + {note.attachment && ( +

- {this.formatFilename(this.state.note.attachment)} + {formatFilename(note.attachment)} - - } - - {!this.state.note.attachment && - Attachment} - - - - - } -

- ); -} +

+ )} + + + + + Save + + + Delete + + + + + )} +
+); ``` +{%change%} To complete this, let's add `isLoading` and `isDeleting` below the state and ref declarations at the top of our `Notes` component function. -We are doing a few things here: - -1. We render our form only when `this.state.note` is available. - -2. Inside the form we conditionally render the part where we display the attachment by using `this.state.note.attachment`. - -3. We format the attachment URL using `formatFilename` by stripping the timestamp we had added to the filename while uploading it. - -4. We also added a delete button to allow users to delete the note. And just like the submit button it too needs a flag that signals that the call is in progress. We call it `isDeleting`. - -5. We handle attachments with a file input exactly like we did in the `NewNote` component. - -6. Our delete button also confirms with the user if they want to delete the note using the browser's `confirm` dialog. - -To complete this code, let's add `isLoading` and `isDeleting` to the state. +```tsx +const [isLoading, setIsLoading] = useState(false); +const [isDeleting, setIsDeleting] = useState(false); +``` -So our new initial state in the `constructor` looks like so. +{%change%} Replace the `const [note, setNote]` definition with the right type. -``` javascript -this.state = { - isLoading: null, - isDeleting: null, - note: null, - content: "", - attachmentURL: null -}; +```tsx +const [note, setNote] = useState(null); ``` -Let's also add some styles by adding the following to `src/containers/Notes.css`. - -``` css -.Notes form { - padding-bottom: 15px; -} +{%change%} Let's also add some styles by adding the following to `src/containers/Notes.css`. +```css .Notes form textarea { height: 300px; - font-size: 24px; + font-size: 1.5rem; } ``` -Also, let's include the React-Bootstrap components that we are using here by adding the following to our header. And our styles, the `LoaderButton`, and the `config`. +{%change%} And finally, let's add the imports. -``` javascript -import { FormGroup, FormControl, ControlLabel } from "react-bootstrap"; -import LoaderButton from "../components/LoaderButton"; +```js import config from "../config"; +import Form from "react-bootstrap/Form"; +import { NoteType } from "../types/note"; +import Stack from "react-bootstrap/Stack"; +import LoaderButton from "../components/LoaderButton"; import "./Notes.css"; ``` +We are doing a few things here: + +1. We render our form only when the `note` state variable is set. + +2. Inside the form we conditionally render the part where we display the attachment by using `note.attachment`. + +3. We format the attachment URL using `formatFilename` by stripping the timestamp we had added to the filename while uploading it. + +4. We also added a delete button to allow users to delete the note. And just like the submit button it too needs a flag that signals that the call is in progress. We call it `isDeleting`. + +5. We handle attachments with a file input exactly like we did in the `NewNote` component. + +6. Our delete button also confirms with the user if they want to delete the note using the browser's `confirm` dialog. + And that's it. If you switch over to your browser, you should see the note loaded. -![Notes page loaded screenshot](/assets/notes-page-loaded.png) +![Notes page loaded screenshot](/assets/part2/notes-page-loaded.png) Next, we'll look at saving the changes we make to our note. diff --git a/_chapters/report-api-errors-in-react.md b/_chapters/report-api-errors-in-react.md new file mode 100644 index 0000000000..4c3cc10de2 --- /dev/null +++ b/_chapters/report-api-errors-in-react.md @@ -0,0 +1,93 @@ +--- +layout: post +title: Report API Errors in React +date: 2020-04-03 00:00:00 +lang: en +description: In this chapter we look at how to report AWS Amplify API errors in our React app to Sentry. We use the config object from Axios to log the API endpoint that triggered the error. +comments_id: report-api-errors-in-react/1731 +ref: report-api-errors-in-react +--- + +Now that we have our [React app configured with Sentry]({% link _chapters/setup-error-reporting-in-react.md %}), let's go ahead and start sending it some errors. + +So far we've been using the `onError` method in `src/lib/errorLib.ts` to handle errors. Recall that it doesn't do a whole lot outside of alerting the error. + +```typescript +export function onError(error) { + let message = error.toString(); + + // Auth errors + if (!(error instanceof Error) && error.message) { + message = error.message; + } + + alert(message); +} +``` + +For most errors we simply alert the error message. But Amplify's Auth package doesn't throw `Error` objects, it throws objects with a couple of properties, including the `message`. So we alert that instead. + +For API errors we want to report both the error and the API endpoint that caused the error. On the other hand, for Auth errors we need to create an `Error` object because Sentry needs actual errors sent to it. + +{%change%} Replace the `onError` method in `src/lib/errorLib.ts` with the following: + +```typescript +export function onError(error: any) { + if (error === "No current user") { + // discard auth errors from non-logged-in user + return; + } + + let errorInfo = {} as ErrorInfoType + let message = String(error); + // typesafe version of our unknown error, always going to + // become an object for logging. + let err = {} + + if (error instanceof Error) { + // It is an error, we can go forth and report it. + err = error; + } else { + if (!(error instanceof Error) + && typeof error === 'object' + && error !== null) { + // At least it's an object, let's use it. + err = error; + // Let's cast it as an ErrorInfoType so we can check + // a couple more things. + errorInfo = error as ErrorInfoType; + + // If it has a message, assume auth error from Amplify Auth + if ('message' in errorInfo + && typeof errorInfo.message === 'string') { + message = errorInfo.message; + error = new Error(message); + } + + // Found Config, Assume API error from Amplify Axios + if ('config' in errorInfo + && typeof errorInfo.config === 'object' + && 'url' in errorInfo.config + ) { + errorInfo.url = errorInfo.config['url']; + } + } + + // If nothing else, make a new error using message from + // the start of all this. + if (typeof error !== 'object') { + err = new Error(message); + } + } + + logError(err, errorInfo); + + alert(message); +} +``` + +You'll notice that in the case of an Auth error we create an `Error` object and add the object that we get as the `errorInfo`. For API errors, Amplify uses [Axios](https://github.com/axios/axios). This has a `config` object that contains the API endpoint that generated the error. + +We report this to Sentry by calling `logError(error, errorInfo)` that we added in the [previous chapter]({% link _chapters/setup-error-reporting-in-react.md %}). And just as before we simply alert the message to the user. It would be a good idea to further customize what you show the user. But we'll leave this as an exercise for you. + +This handles all the expected errors in our React app. However, there are a lot of other things that can go wrong while rendering our app. To handle them we are going to setup a [React Error Boundary](https://reactjs.org/docs/error-boundaries.html) in the next chapter. diff --git a/_chapters/review-our-app-architecture.md b/_chapters/review-our-app-architecture.md new file mode 100644 index 0000000000..ffa9f60350 --- /dev/null +++ b/_chapters/review-our-app-architecture.md @@ -0,0 +1,28 @@ +--- +layout: post +title: Review Our App Architecture +date: 2020-10-28 00:00:00 +lang: en +ref: review-our-app-architecture +description: In this chapter we'll do a quick overview of the serverless API that we are about to build. We'll be using the DynamoDB table and S3 bucket that we previously created. +comments_id: review-our-app-architecture/2178 +--- + +So far we've [deployed our simple Hello World API]({% link _chapters/create-a-hello-world-api.md %}), [created a database (DynamoDB)]({% link _chapters/create-a-dynamodb-table-in-sst.md %}), and [created an S3 bucket for file uploads]({% link _chapters/create-an-s3-bucket-in-sst.md %}). We are ready to start working on our backend API but let's get a quick sense of how the aforementioned pieces fit together. + +### Notes App API Architecture + +Our notes app backend will look something like this. + +![Serverless public API architecture](/assets/diagrams/serverless-public-api-architecture.png) + +There are a couple of things of note here: + +1. Our database is not exposed publicly and is only invoked by our Lambda functions. +2. But our users will be uploading files directly to the S3 bucket that we created. + +The second point is something that is different from a lot of traditional server based architectures. We are typically used to uploading the files to our server and then moving them to a file server. But here we will be directly uploading it to our S3 bucket. We will look at this in more detail when we look at file uploads. + +In the coming sections will also be looking at how we can secure access to these resources. We will be setting it up such that only our authenticated users will be allowed to access these resources. + +Now that we have a good idea of how our app will be architected, let's get to work! diff --git a/_chapters/s3-as-a-serverless-service.md b/_chapters/s3-as-a-serverless-service.md deleted file mode 100644 index d8c1b62f6f..0000000000 --- a/_chapters/s3-as-a-serverless-service.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -layout: post -title: S3 as a Serverless Service -description: To use CloudFormation cross-stack references for S3 in Serverless we need to "Export" the S3 bucket name using the "Ref" and the ARN of the S3 bucket using "Fn::GetAtt". -date: 2018-04-02 15:00:00 -context: true -code: mono-repo -comments_id: s3-as-a-serverless-service/407 ---- - -Just as we did with [DynamoDB in the last chapter]({% link _chapters/dynamodb-as-a-serverless-service.md %}), we'll look at splitting S3 into a separate Serverless service. It should be noted that for our simple note taking application, it does make too much sense to split S3 into its own service. But it is useful to go over the case to better understand cross-stack references in Serverless. - -In the [example repo]({{ site.backend_mono_github_repo }}), you'll notice that we have a `uploads` service in the `services/` directory. And the `serverless.yml` in this service looks like the following. - -``` yml -service: notes-app-mono-uploads - -custom: - # Our stage is based on what is passed in when running serverless - # commands. Or falls back to what we have set in the provider section. - stage: ${opt:stage, self:provider.stage} - -provider: - name: aws - runtime: nodejs8.10 - stage: dev - region: us-east-1 - -resources: - Resources: - S3Bucket: - Type: AWS::S3::Bucket - Properties: - # Set the CORS policy - CorsConfiguration: - CorsRules: - - - AllowedOrigins: - - '*' - AllowedHeaders: - - '*' - AllowedMethods: - - GET - - PUT - - POST - - DELETE - - HEAD - MaxAge: 3000 - - # Print out the name of the bucket that is created - Outputs: - AttachmentsBucketArn: - Value: - Fn::GetAtt: - - S3Bucket - - Arn - Export: - Name: ${self:custom.stage}-AttachmentsBucketArn - - AttachmentsBucketName: - Value: - Ref: S3Bucket - Export: - Name: ${self:custom.stage}-AttachmentsBucket -``` - -Most of the `Resources:` section should be fairly straightforward and is based on [Part II of this guide]({% link _chapters/configure-s3-in-serverless.md %}). So let's go over the cross-stack exports in the `Outputs:` section. - -1. Just as in the [DynamoDB service]({% link _chapters/dynamodb-as-a-serverless-service.md %}), we are exporting the [ARN]({% link _chapters/what-is-an-arn.md %}) (`AttachmentsBucketArn`) and the name of the bucket (`AttachmentsBucketName`). - -2. The names of the exported values is based on the stage: `${self:custom.stage}-AttachmentsBucketArn` and `${self:custom.stage}-AttachmentsBucket`. - -3. We can get the ARN by using the `Fn::GetAtt` function by passing in the ref (`S3Bucket`) and the attribute we need (`Arn`). - -4. And finally, we can get the bucket name by just using the ref (`S3Bucket`). Note that unlike the DynamoDB table name, the S3 bucket name is auto-generated. So while we could get away with not exporting the DynamoDB table name; in the case of S3, we need to export it. - -Now that we have the main infrastructure pieces created, let's take a look at our APIs next. For illustrative purposes we are going to create two separate API services and look at how to group them under the same API Gateway domain. diff --git a/_chapters/save-changes-to-a-note.md b/_chapters/save-changes-to-a-note.md index ae2409e3e4..dfeca3d3a0 100644 --- a/_chapters/save-changes-to-a-note.md +++ b/_chapters/save-changes-to-a-note.md @@ -3,71 +3,75 @@ layout: post title: Save Changes to a Note date: 2017-01-30 00:00:00 lang: en -description: For a user to be able to edit a note in our React.js app, we need to make a PUT request to our severless backend API using AWS Amplify. We also need to allow them to upload files directly to S3 and add that as an attachment to the note. -context: true +description: For a user to be able to edit a note in our React.js app, we need to make a PUT request to our serverless backend API using AWS Amplify. We also need to allow them to upload files directly to S3 and add that as an attachment to the note. comments_id: save-changes-to-a-note/131 ref: save-changes-to-a-note --- Now that our note loads into our form, let's work on saving the changes we make to that note. -Replace the `handleSubmit` method in `src/containers/Notes.js` with the following. +{%change%} Replace the `handleSubmit` function in `src/containers/Notes.tsx` with the following. -``` coffee -saveNote(note) { - return API.put("notes", `/notes/${this.props.match.params.id}`, { - body: note +```tsx +function saveNote(note: NoteType) { + return API.put("notes", `/notes/${id}`, { + body: note, }); } -handleSubmit = async event => { +async function handleSubmit(event: React.FormEvent) { let attachment; event.preventDefault(); - if (this.file && this.file.size > config.MAX_ATTACHMENT_SIZE) { - alert(`Please pick a file smaller than ${config.MAX_ATTACHMENT_SIZE/1000000} MB.`); + if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) { + alert( + `Please pick a file smaller than ${ + config.MAX_ATTACHMENT_SIZE / 1000000 + } MB.` + ); return; } - this.setState({ isLoading: true }); + setIsLoading(true); try { - if (this.file) { - attachment = await s3Upload(this.file); + if (file.current) { + attachment = await s3Upload(file.current); + } else if (note && note.attachment) { + attachment = note.attachment; } - await this.saveNote({ - content: this.state.content, - attachment: attachment || this.state.note.attachment + await saveNote({ + content: content, + attachment: attachment, }); - this.props.history.push("/"); + nav("/"); } catch (e) { - alert(e); - this.setState({ isLoading: false }); + onError(e); + setIsLoading(false); } } - ``` -And include our `s3Upload` helper method in the header: +{%change%} And include our `s3Upload` helper method in the header: -``` javascript -import { s3Upload } from "../libs/awsLib"; +```tsx +import { s3Upload } from "../lib/awsLib"; ``` The code above is doing a couple of things that should be very similar to what we did in the `NewNote` container. -1. If there is a file to upload we call `s3Upload` to upload it and save the key we get from S3. +1. If there is a file to upload we call `s3Upload` to upload it and save the key we get from S3. If there isn't then we simply save the existing attachment object, `note.attachment`. -2. We save the note by making a `PUT` request with the note object to `/notes/:id` where we get the `id` from `this.props.match.params.id`. We use the `API.put()` method from AWS Amplify. +2. We save the note by making a `PUT` request with the note object to `/notes/:id` where we get the `id` from the `useParams` hook. We use the `API.put()` method from AWS Amplify. 3. And on success we redirect the user to the homepage. Let's switch over to our browser and give it a try by saving some changes. -![Notes page saving screenshot](/assets/notes-page-saving.png) +![Notes page saving screenshot](/assets/part2/notes-page-saving.png) -You might have noticed that we are not deleting the old attachment when we upload a new one. To keep things simple, we are leaving that bit of detail up to you. It should be pretty straightforward. Check the [AWS Amplify API Docs](https://aws.github.io/aws-amplify/api/classes/storageclass.html#remove) on how to a delete file from S3. +You might have noticed that we are not deleting the old attachment when we upload a new one. To keep things simple, we are leaving that bit of detail up to you. It should be pretty straightforward. Check the [AWS Amplify API Docs](https://aws.github.io/aws-amplify/api/classes/storageclass.html#remove){:target="_blank"} on how to a delete file from S3. Next up, let's allow users to delete their note. diff --git a/_chapters/secure-our-serverless-apis.md b/_chapters/secure-our-serverless-apis.md new file mode 100644 index 0000000000..277bcad506 --- /dev/null +++ b/_chapters/secure-our-serverless-apis.md @@ -0,0 +1,149 @@ +--- +layout: post +title: Secure Our Serverless APIs +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we'll secure our serverless APIs by only allowing authenticated users to connect. We'll get the user id in our Lambda functions from the Cognito Identity Pool identityId. +redirect_from: /chapters/test-the-configured-apis.html +ref: secure-our-serverless-apis +comments_id: secure-our-serverless-apis/2467 +--- + +Now that our APIs have been [secured with Cognito User Pool and Identity Pool]({% link _chapters/adding-auth-to-our-serverless-app.md %}), we are ready to use the authenticated user's info in our Lambda functions. + +Recall that we've been hard coding our user ids so far (with user id `123`). We'll need to grab the real user id from the Lambda function event. + +### Cognito Identity Id + +Recall the function signature of a Lambda function: + +```ts +export async function main(event: APIGatewayProxyEvent, context: Context) {} +``` + +Or the refactored version that we are using: + +```ts +export const main = Util.handler(async (event) => {}); +``` + +So far we've used the `event` object to get the path parameters (`event.pathParameters`) and request body (`event.body`). + +Now we'll get the id of the authenticated user. + +```ts +event.requestContext.authorizer?.iam.cognitoIdentity.identityId; +``` + +This is an id that's assigned to our user by our Cognito Identity Pool. + +You'll also recall that so far all of our APIs are hard coded to interact with a single user. + +```ts +userId: "123", // The id of the author +``` + +Let's change that. + +{%change%} Replace the above line in `packages/functions/src/create.ts` with. + +```ts +userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, +``` + +{%change%} Do the same in these files: + +- `packages/functions/src/get.ts` +- `packages/functions/src/update.ts` +- `packages/functions/src/delete.ts` + +{%change%} In `packages/functions/src/list.ts` find this line instead. + +```ts +":userId": "123", +``` + +{%change%} And replace it with. + +```ts +":userId": event.requestContext.authorizer?.iam.cognitoIdentity.identityId, +``` + +Keep in mind that the `userId` above is the Federated Identity id (or Identity Pool user id). This is not the user id that is assigned in our User Pool. If you want to use the user's User Pool user Id instead, have a look at the [Mapping Cognito Identity Id and User Pool Id]({% link _archives/mapping-cognito-identity-id-and-user-pool-id.md %}){:target="_blank"} chapter. + +To test these changes we cannot use the `curl` command anymore. We'll need to generate a set of authentication headers to make our requests. Let's do that next. + +## Test the APIs + +Let's quickly test our APIs with authentication. + +To be able to hit our API endpoints securely, we need to follow these steps. + +1. Authenticate against our User Pool and acquire a user token. +2. With the user token get temporary IAM credentials from our Identity Pool. +3. Use the IAM credentials to sign our API request with [Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html){:target="_blank"}. + +These steps can be a bit tricky to do by hand. So we created a simple tool called [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test){:target="_blank"}. + +```bash +$ npx aws-api-gateway-cli-test \ +--user-pool-id='' \ +--app-client-id='' \ +--cognito-region='' \ +--identity-pool-id='' \ +--invoke-url='' \ +--api-gateway-region='' \ +--username='admin@example.com' \ +--password='Passw0rd!' \ +--path-template='/notes' \ +--method='POST' \ +--body='{"content":"hello world","attachment":"hello.jpg"}' +``` + +We need to pass in quite a bit of our info to complete the above steps. + +- Use the username and password of the user created above. +- Replace `USER_POOL_ID`, `USER_POOL_CLIENT_ID`, `COGNITO_REGION`, and `IDENTITY_POOL_ID` with the `UserPool`, `UserPoolClient`, `Region`, and `IdentityPool` from our [previous chapter]({% link _chapters/adding-auth-to-our-serverless-app.md %}). +- Replace the `API_ENDPOINT` with the `Api` from back when we [created our API]({% link _chapters/create-a-hello-world-api.md %}). +- And for the `API_REGION` you can use the same `Region` as we used above. Since our entire app is deployed to the same region. + +While this might look intimidating, just keep in mind that behind the scenes all we are doing is generating some security headers before making a basic HTTP request. We won't need to do this when we connect from our React.js app. + +{%info%} +If you are on Windows, you can use the command below. The spaces between each option are very important. + +```bash +$ npx aws-api-gateway-cli-test --username admin@example.com --password Passw0rd! --user-pool-id --app-client-id --cognito-region --identity-pool-id --invoke-url --api-gateway-region --path-template /notes --method POST --body "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}" +``` +{%endinfo%} + +If the command is successful, the response will look similar to this. + +```bash +Authenticating with User Pool +Getting temporary credentials +Making API request +{ + status: 200, + statusText: 'OK', + data: { + userId: 'us-east-1:06d418dd-b55b-4f7d-9af4-5d067a69106e', + noteId: 'b5199840-c0e5-11ec-a5e8-61c040911d73', + content: 'hello world', + attachment: 'hello.jpg', + createdAt: 1650485336004 + } +} +``` + +### Commit the Changes + +{%change%} Let's commit and push our changes to GitHub. + +```bash +$ git add . +$ git commit -m "Securing the API" +$ git push +``` + +We’ve now got a serverless API that’s secure and handles user authentication. In the next section we are going to look at how we can work with 3rd party APIs in serverless. And how to handle secrets! diff --git a/_chapters/set-custom-domains-through-seed.md b/_chapters/set-custom-domains-through-seed.md deleted file mode 100644 index 0c1d77ef17..0000000000 --- a/_chapters/set-custom-domains-through-seed.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: post -title: Set Custom Domains Through Seed -date: 2018-03-15 00:00:00 -lang: en -description: We will use the Seed console to configure our API Gateway endpoints in our Serverless project with custom domains. To configure a stage with a custom domain go to the stage settings, select the Route 53 domain, a sub-domain, and the base path. -context: true -ref: set-custom-domains-through-seed -comments_id: set-custom-domains-through-seed/178 ---- - -Our serverless API uses API Gateway and it gives us some auto-generated endpoints. We would like to configure them to use a scheme like `api.my-domain.com` or something similar. This can take a few different steps through the AWS Console, but it is pretty straightforward to configure through [Seed](https://seed.run). - -From our **prod** stage, click on **View Resources**. - -![Prod stage view deployment screenshot](/assets/part2/prod-stage-view-deployment.png) - -This shows you a list of the API endpoints and Lambda functions that are a part of this deployment. Now click on **Settings**. - -![Prod stage deployment screenshot](/assets/part2/prod-stage-deployment.png) - -And hit **Update Custom Domain**. - -![Custom domain panel prod screenshot](/assets/part2/custom-domain-panel-prod.png) - -In the first part of the tutorial we had added our domain to Route 53. If you haven't done so you can [read more about it here](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). Hit **Select a domain** and you should see a list of all your Route 53 domains. Select the one you intend to use. And fill in the sub-domain and base path. For example, you could use `api.my-domain.com/prod`; where `api` is the sub-domain and `prod` is the base path. - -And hit **Update**. - -![Custom domain details prod screenshot](/assets/part2/custom-domain-details-prod.png) - -Seed will now go through and configure the domain for this API Gateway endpoint, create the SSL certificate and attach it to the domain. This process can take up to 40 mins. - -While we wait, we can do the same for our `dev` stage. Go into the **dev** stage > click **View Deployment** > click **Settings** > and hit **Update Custom Domain**. And select the domain, sub-domain, and base path. In our case we'll use something like `api.my-domain.com/dev`. - -![Custom domain details dev screenshot](/assets/part2/custom-domain-details-dev.png) - -Hit **Update** and wait for the changes to take place. - -Once complete, we are ready to test our fully-configured serverless API backend! diff --git a/_chapters/setting-up-the-sst-console.md b/_chapters/setting-up-the-sst-console.md new file mode 100644 index 0000000000..f8c9aa24aa --- /dev/null +++ b/_chapters/setting-up-the-sst-console.md @@ -0,0 +1,78 @@ +--- +layout: post +title: Setting up the SST Console +date: 2024-07-24 00:00:00 +lang: en +redirect_from: /chapters/setting-up-your-project-on-seed.html +description: In this chapter we'll set up the SST Console so we can manage, monitor, and autodeploy our apps. +ref: setting-up-the-sst-console +comments_id: setting-up-the-sst-console/2957 +--- + +We are going to set up the [SST Console]({{ site.sst_url }}/docs/console/){:target="_blank"} to auto-deploy our app and manage our environments. + +Start by [**signing up for a free account here**]({{ site.console_url }}){:target="_blank"}. + +![Create new SST Console account](/assets/part2/create-new-sst-console-account.png) + +Let's **create your workspace**. + +![Create SST Console workspace](/assets/part2/create-sst-console-workspace.png) + +Next, **connect your AWS account**. + +![Connect AWS account in SST Console](/assets/part2/connect-aws-account-in-sst-console.png) + +This will send you to the AWS Console and ask you to create a CloudFormation stack. + +![AWS create CloudFormation stack](/assets/part2/aws-create-cloudformation-stack.png) + +This stack needs to be in **us-east-1**. So make sure you use the dropdown at the top right to check that you are in the right region. + +![AWS Console check region](/assets/part2/aws-console-check-region.png) + +Scroll down, **confirm** the checkbox at the bottom and click **Create stack**. + +![AWS click create CloudFormation stack](/assets/part2/aws-click-create-cloudformation-stack.png) + +It will take a couple of minutes to create the stack. + +![AWS CloudFormation stack create complete](/assets/part2/aws-cloudformation-stack-create-complete.png) + +Once complete, head back to the SST Console. It'll take a minute to scan your AWS account for your SST apps. + +While it's doing that, let's link our GitHub. Click on **Manage workspace**, scroll down to the **Integrations**, and enable **GitHub**. + +![Enable GitHub integration in SST Console](/assets/part2/enable-github-integration-in-sst-console.png) + +You'll be asked to select where you want to install the SST Console integration. You can either pick your personal account or any organizations you are a part of. This is where your notes app repo has been created. + +Once you select where you want to install it, scroll down and click **Install**. + +![Install SST Console in GitHub](/assets/part2/install-sst-console-in-github.png) + +Now your GitHub integration should be enabled. And hopefully the Console should be done scanning your AWS account. You should see your notes app with your personal stage. + +![Notes app in SST Console](/assets/part2/notes-app-in-sst-console.png) + +Here you can see the resources in your stage, the logs from your functions, and any issues that have been detected. For now, let's head over to the **Settings** > **Autodeploy** > pick your repo > and click **Select**. + +![Select GitHub repo SST Console](/assets/part2/select-github-repo-sst-console.png) + +Let's create a couple of environments. This tells the SST Console when to auto-deploy your app. + +![GitHub repo selected SST Console](/assets/part2/github-repo-selected-sst-console.png) + +We are going to create two environments. Starting with a **Branch environment**. Use **production** as the name, select your AWS account, and click **Add Environment**. + +![Create branch environment SST Console](/assets/part2/create-branch-environment-sst-console.png) + +Do the same for a **PR environment**. + +![Create PR environment SST Console](/assets/part2/create-pr-environment-sst-console.png) + +The two above environments tell the Console that any stage with the name `production` or starting with `pr-` should be auto-deployed to the given AWS account. By default, the stage names are derived from the name of the branch. + +So if you _git push_ to a branch called `production`, the SST Console will auto-deploy that to a stage called `production`. + +Let's do that next. diff --git a/_chapters/setting-up-your-project-on-netlify.md b/_chapters/setting-up-your-project-on-netlify.md deleted file mode 100644 index 677b9abed2..0000000000 --- a/_chapters/setting-up-your-project-on-netlify.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: post -title: Setting up Your Project on Netlify -date: 2018-03-27 00:00:00 -lang: en -description: To automate deployments for our Create React App on Netlify start by signing up for a free account and adding your Git repository. -context: true -ref: setting-up-your-project-on-netlify -comments_id: setting-up-your-project-on-netlify/190 ---- - -Now we are going to set our React app on [Netlify](https://www.netlify.com). Start by [creating a free account](https://app.netlify.com/signup). - -![Signup for Netlify screenshot](/assets/part2/signup-for-netlify.png) - -Next, create a new site by hitting the **New site from Git** button. - -![Hit new site from git button screenshot](/assets/part2/hit-new-site-from-git-button.png) - -Pick **GitHub** as your provider. - -![Select GitHub as provider screenshot](/assets/part2/select-github-as-provider.png) - -Then pick your project from the list. - -![Select GitHub repo from list screenshot](/assets/part2/select-github-repo-from-list.png) - -It'll default the branch to `master`. We can now deploy our app! Hit **Deploy site**. - -![Hit Deploy site screenshot](/assets/part2/hit-deploy-site.png) - -This should be deploying our app. Once it is done, click on the deployment. - -![View deployed site screenshot](/assets/part2/view-deployed-site.png) - -And you should see your app in action! - -![Netlify deployed notes app screenshot](/assets/part2/netlify-deployed-notes-app.png) - -Of course, it is hosted on a Netlify URL. We'll change that by configuring custom domains next. diff --git a/_chapters/setting-up-your-project-on-seed.md b/_chapters/setting-up-your-project-on-seed.md deleted file mode 100644 index 4e27083f8a..0000000000 --- a/_chapters/setting-up-your-project-on-seed.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -layout: post -title: Setting up Your Project on Seed -date: 2018-03-12 00:00:00 -lang: en -description: To automate our Serverless deployments, we will use a service called Seed (https://seed.run). We will sign up for a free account, add our project repository, and set our AWS IAM credentials. -context: true -ref: setting-up-your-project-on-seed -comments_id: setting-up-your-project-on-seed/175 ---- - -We are going to use [Seed](https://seed.run) to automate our serverless deployments and manage our environments. - -Start by signing up for a free account [here](https://console.seed.run/signup-account). - -![Create new Seed account screenshot](/assets/part2/create-new-seed-account.png) - -Let's **Add your first app**. - -![Add your first Seed app screenshot](/assets/part2/add-your-first-seed-app.png) - -Now to add your project, select **GitHub** as your git provider. You'll be asked to give Seed permission to your GitHub account. - -![Select Git provider screenshot](/assets/part2/select-git-provider.png) - -Select the repo we've been using so far. - -Next, Seed will scan your repos for a `serverless.yml`. Hit **Add Service** to confirm this. - -![Serverless.yml detected screenshot](/assets/part2/serverless-yml-detected.png) - -Note that, if your `serverless.yml` is not in your project root, you will need to change the path. - -Seed deploys to your AWS account on your behalf. You should create a separate IAM user with exact permissions that your project needs. You can read more about this [here](https://seed.run/docs/customizing-your-iam-policy). But for now we'll simply use the one we've used in this tutorial. - -Run the following command. - -``` bash -$ cat ~/.aws/credentials -``` - -The output should look something like this. - -``` -[default] -aws_access_key_id = YOUR_IAM_ACCESS_KEY -aws_secret_access_key = YOUR_IAM_SECRET_KEY -``` - -Seed will also create a couple of stages (or environments) for you. By default, it'll create a **dev** and a **prod** stage using the same AWS credentials. You can customize these but for us this is perfect. - -Fill in the credentials and click **Add a New App**. - -![Add AWS IAM credentials screenshot](/assets/part2/add-aws-iam-credentials.png) - -You new app is ready to go! - -![View new Seed app screenshot](/assets/part2/view-new-seed-app.png) - -You'll notice a few things here. First, we have a service called **notes-app-2-api**. It's picking up the service name from our `serverless.yml`. You can choose to change this by clicking on the service and editing its name. A Serverless app can have multiple services within it. A service (roughly speaking) is a reference to a `serverless.yml` file. In our case we have one service in the root of our repo. You'll also notice the two stages that have been created. - -Now before we proceed to deploying our app, we need to enable running unit tests as a part of our build process. You'll recall that we had added a couple of tests back in the [unit tests]({% link _chapters/unit-tests-in-serverless.md %}) chapter. And we want to run those before we deploy our app. - -To do this, hit the **Settings** link and click **Enable Unit Tests**. - -![Click Enable Unit Tests in Seed screenshot](/assets/part2/click-enable-unit-tsts-in-seed.png) - -Back in our pipeline, you'll notice that our **dev** stage is hooked up to master. This means that any commits to master will trigger a build in dev. - -Click on **dev**. - -![Click dev stage in Seed project screenshot](/assets/part2/click-dev-stage-in-seed-project.png) - -You'll see that we haven't deployed to this stage yet. - -![Dev stage in Seed project screenshot](/assets/part2/dev-stage-in-seed-project.png) - -However, before we do that, we'll need to add our secret environment variables. diff --git a/_chapters/setup-a-stripe-account.md b/_chapters/setup-a-stripe-account.md index ddbab95d53..0c3d9e4fbd 100644 --- a/_chapters/setup-a-stripe-account.md +++ b/_chapters/setup-a-stripe-account.md @@ -1,34 +1,42 @@ --- layout: post title: Setup a Stripe Account -date: 2018-03-06 00:00:00 +date: 2021-08-23 00:00:00 lang: en description: We are going to use Stripe to process our credit card payments. To do this let's first create a free Stripe account. -context: true ref: setup-a-stripe-account comments_id: setup-a-stripe-account/169 --- -Let's start by creating a free Stripe account. Head over to [Stripe](https://dashboard.stripe.com/register) and register for an account. -![Create a Stripe account screenshot](/assets/part2/create-a-stripe-account.png) +So far we've created a basic CRUD (create, read, update, and delete) API. We are going to make a small addition to this by adding an endpoint that works with a 3rd party API. This section is also going to illustrate how to work with environment variables and how to accept credit card payments using Stripe. + +A common extension of the notes app (that we've noticed) is to add a billing API that works with Stripe. In the case of our notes app we are going to allow our users to pay a fee for storing a certain number of notes. The flow is going to look something like this: + +1. The user is going to select the number of notes they want to store and puts in their credit card information. + +2. We are going to generate a one time token by calling the Stripe SDK on the frontend to verify that the credit card info is valid. -Once signed in, click the **Developers** link on the left. +3. We will then call an API passing in the number of notes and the generated token. -![Stripe dashboard screenshot](/assets/part2/stripe-dashboard.png) +4. The API will take the number of notes, figure out how much to charge (based on our pricing plan), and call the Stripe API to charge our user. -And hit **API keys**. +We aren't going to do much else in the way of storing this info in our database. We'll leave that as an exercise for the reader. -![Developer section in Stripe dashboard screenshot](/assets/part2/developer-section-in-stripe-dashboard.png) +### Sign up for Stripe + +Let's start by creating a free Stripe account. Head over to [Stripe](https://dashboard.stripe.com/register) and register for an account. + +![Create a Stripe account screenshot](/assets/part2/create-a-stripe-account.png) -The first thing to note here is that we are working with a test version of API keys. To create the live version, you'd need to verify your email address and business details to activate your account. For the purpose of this guide we'll continue working with our test version. +Once signed in with a confirmed account, you will be able to use the developer tools. -The second thing to note is that we need to generate the **Publishable key** and the **Secret key**. The Publishable key is what we are going to use in our frontend client with the Stripe SDK. And the Secret key is what we are going to use in our API when asking Stripe to charge our user. As denoted, the Publishable key is public while the Secret key needs to stay private. +![Stripe dashboard screenshot](/assets/part2/developer-section-in-stripe-dashboard.png) -Hit the **Reveal test key token**. +The first thing to do is switch to test mode. This is important because we don't want to charge our credit card every time we test our app. -![Stripe dashboard Stripe API keys screenshot](/assets/part2/stripe-dashboard-stripe-api-keys.png) +The second thing to note is that Stripe has automatically generated a test and live **Publishable key** and a test and live **Secret key**. The Publishable key is what we are going to use in our frontend client with the Stripe SDK. And the Secret key is what we are going to use in our API when asking Stripe to charge our user. As denoted, the Publishable key is public while the Secret key needs to stay private. -Make a note of both the **Publishable key** and the **Secret key**. We are going to be using these later. +Make a note of both the **Publishable test key** and the **Secret test key**. We are going to be using these later. -Next let's create our billing API. +Next, let's use this in our SST app. diff --git a/_chapters/setup-an-error-boundary-in-react.md b/_chapters/setup-an-error-boundary-in-react.md new file mode 100644 index 0000000000..042301f304 --- /dev/null +++ b/_chapters/setup-an-error-boundary-in-react.md @@ -0,0 +1,181 @@ +--- +layout: post +title: Setup an Error Boundary in React +date: 2020-04-03 00:00:00 +lang: en +description: In this chapter we look at how to handle unexpected errors in our React app using an Error Boundary component. It lets us catch any errors, log it to Sentry, and show a fallback UI. +comments_id: setup-an-error-boundary-in-react/1732 +ref: setup-an-error-boundary-in-react +--- + +In the previous chapter we looked at how to [report API errors to Sentry in our React app]({% link _chapters/report-api-errors-in-react.md %}). Now let's report all those unexpected errors that might happen using a [React Error Boundary](https://reactjs.org/docs/error-boundaries.html). + +An Error Boundary is a component that allows us to catch any errors that might happen in the child components tree, log those errors, and show a fallback UI. + +### Create an Error Boundary + +It's incredibly straightforward to setup. So let's get started. + +{%change%} Add the following to `src/components/ErrorBoundary.tsx` in your `frontend/` directory. + +```tsx +import React from "react"; +import {logError} from "../lib/errorLib"; +import "./ErrorBoundary.css"; + +export default class ErrorBoundary extends React.Component { + state = { hasError: false }; + + static getDerivedStateFromError(_error: unknown) { + return { hasError: true }; + } + + componentDidCatch(error: Error, errorInfo: any) { + logError(error, errorInfo); + } + + render() { + if (this.state.hasError) { + return
+

Sorry there was a problem loading this page

+
; + } else { + return this.props.children; + } + } +} + +``` + +The key part of this component is the `componentDidCatch` and `getDerivedStateFromError` methods. These get triggered when any of the child components have an unhandled error. We set the internal state, `hasError` to `true` to display our fallback UI. And we report the error to Sentry by calling `logError` with the `error` and `errorInfo` that comes with it. + +Let's include some simple styles for this. + +{%change%} Create a `src/components/ErrorBoundary.css` file and add: + +```css +.ErrorBoundary { + padding-top: 100px; +} +``` + +The styles we are using are very similar to our `NotFound` component. We use that when a user navigates to a page that we don't have a route for. + +### Use the Error Boundary + +To use the Error Boundary component that we created, we'll need to add it to our app component. + +{%change%} Find the following in `src/App.tsx`. + +{% raw %} + +```tsx + + + +``` + +{% endraw %} + +{%change%} And wrap it with our `ErrorBoundary`: + +{% raw %} + +```tsx + + + + + +``` + +{% endraw %} + +{%change%} Also, make sure to import it in the header of `src/App.js`. + +```tsx +import ErrorBoundary from "./components/ErrorBoundary"; +``` + +And that's it! Now an unhandled error in our containers will show a nice error message. While reporting the error to Sentry. + +### Commit the Changes + +{%change%} Let's commit these to Git (but don't push yet). + +```bash +$ git add .;git commit -m "Adding React error reporting"; +``` + +### Test the Error Boundary + +Before we move on, let's do a quick test. + +Replace the following in `src/containers/Home.tsx`. + +```tsx +{isAuthenticated ? renderNotes() : renderLander()} +``` + +With these faulty lines: + +{% raw %} + +```tsx +{isAuthenticated ? renderNotes() : renderLander()} +{ isAuthenticated.none.no } +``` + +{% endraw %} + +Now in your browser you should see something like this. + +![React error message](/assets/monitor-debug-errors/react-error-message.png) + +{%note%} +You'll need to have the SST local development environment (`npm start`) and React local environment (`npm run start`) running. +{%endnote%} + +While developing, React doesn't show your Error Boundary fallback UI by default. To view that, hit the **close** button on the top right. + +![React Error Boundary fallback UI](/assets/monitor-debug-errors/react-error-boundary-fallback-ui.png) + +Since we are developing locally, we don't report this error to Sentry. But let's do a quick test to make sure it's hooked up properly. + +Replace the following from the top of `src/lib/errorLib.js`. + +```js +const isLocal = process.env.NODE_ENV === "development"; +``` + +With: + +```js +const isLocal = false; +``` + +Now if we head over to our browser, we should see the error as before. And we should see the error being reported to Sentry as well! It might take a moment or two before it shows up. + +![First error in Sentry](/assets/monitor-debug-errors/first-error-in-sentry.png) + +And if you click through, you can see the error in detail. + +![Error details in Sentry](/assets/monitor-debug-errors/error-boundary-error-details-in-sentry.png) + +Now our React app is ready to handle the errors that are thrown its way! + +Let's cleanup all the testing changes we made above. + +```bash +$ git checkout . +``` + +### Push the Changes + +{%change%} Let's also push these changes to GitHub and deploy our app. + +```bash +$ git push +``` + +Next, let's look at how to handle errors in our serverless app. diff --git a/_chapters/setup-bootstrap.md b/_chapters/setup-bootstrap.md index 56e9f32efe..d649c5b458 100644 --- a/_chapters/setup-bootstrap.md +++ b/_chapters/setup-bootstrap.md @@ -4,48 +4,52 @@ title: Set up Bootstrap date: 2017-01-09 00:00:00 lang: en ref: set-up-bootstrap -description: Bootstrap is a UI framework that makes it easy to build consistent responsive web apps. We are going to use Bootstrap with our React.js project using the React-Bootstrap library. React-Bootstrap allows you to use Bootstrap in the form of standard React components. -context: true +description: Bootstrap is a UI framework that makes it easy to build consistent responsive web apps. We are going to use Bootstrap with our React.js project using the React Bootstrap and the Bootstrap icons from the React Icons package. React Bootstrap and React Icons allow you to use them as standard React components. comments_id: set-up-bootstrap/118 --- -A big part of writing web applications is having a UI Kit to help create the interface of the application. We are going to use [Bootstrap](http://getbootstrap.com) for our note taking app. While Bootstrap can be used directly with React; the preferred way is to use it with the [React-Bootstrap](https://react-bootstrap.github.io) package. This makes our markup a lot simpler to implement and understand. +A big part of writing web applications is having a UI Kit to help create the interface of the application. We are going to use [Bootstrap](http://getbootstrap.com){:target="_blank"} for our note taking app. While Bootstrap can be used directly with React; the preferred way is to use it with the [React Bootstrap](https://react-bootstrap.github.io){:target="_blank"} package. This makes our markup a lot simpler to implement and understand. + +We also need a couple of icons in our application. We'll be using the [React Icons](https://react-icons.github.io/react-icons/){:target="_blank"} package for this. It allows us to include icons in our React app as standard React components. ### Installing React Bootstrap -Run the following command in your working directory. +{%change%} Run the following command **in your `packages/frontend/` directory**. -``` bash -$ npm install react-bootstrap@0.32.4 --save +```bash +$ npm install bootstrap react-bootstrap react-icons +$ npm install -D @types/bootstrap @types/react-bootstrap ``` -This installs the NPM package and adds the dependency to your `package.json`. +This installs the packages and dependencies to the `package.json` of your React app. ### Add Bootstrap Styles -React Bootstrap uses the standard Bootstrap v3 styles; so just add the following styles to your `public/index.html`. +React Bootstrap uses the standard Bootstrap styles, so just import the style sheet. + +{%change%} **Add it above** the `import "./index.css"` line in `src/main.tsx`. -``` html - +```typescript +import "bootstrap/dist/css/bootstrap.min.css"; ``` We'll also tweak the styles of the form fields so that the mobile browser does not zoom in on them on focus. We just need them to have a minimum font size of `16px` to prevent the zoom. -To do that, let's add the following to our `src/index.css`. +{%change%} To do that, let's add the following to our `src/index.css`. -``` css +```css select.form-control, textarea.form-control, input.form-control { - font-size: 16px; + font-size: 1rem; } -input[type=file] { +input[type="file"] { width: 100%; } ``` We are also setting the width of the input type file to prevent the page on mobile from overflowing and adding a scrollbar. -Now if you head over to your browser, you might notice that the styles have shifted a bit. This is because Bootstrap includes [Normalize.css](http://necolas.github.io/normalize.css/) to have a more consistent styles across browsers. +Now if you head over to your browser, you might notice that the styles have shifted a bit. This is because Bootstrap includes [Normalize.css](http://necolas.github.io/normalize.css/){:target="_blank"} to have a more consistent styles across browsers. Next, we are going to create a few routes for our application and set up the React Router. diff --git a/_chapters/setup-custom-fonts.md b/_chapters/setup-custom-fonts.md index 382f6513f6..14377bb207 100644 --- a/_chapters/setup-custom-fonts.md +++ b/_chapters/setup-custom-fonts.md @@ -5,24 +5,27 @@ date: 2017-01-08 00:00:00 lang: en ref: set-up-custom-fonts description: To use custom fonts in our React.js project we are going to use Google Fonts and include it in our public/index.html. -context: true comments_id: set-up-custom-fonts/81 --- -Custom Fonts are now an almost standard part of modern web applications. We'll be setting it up for our note taking app using [Google Fonts](https://fonts.google.com). +Custom Fonts are now an almost standard part of modern web applications. We'll be setting it up for our note taking app using [Google Fonts](https://fonts.google.com){:target="_blank"}. This also gives us a chance to explore the structure of our newly created React.js app. ### Include Google Fonts -For our project we'll be using the combination of a Serif ([PT Serif](https://fonts.google.com/specimen/PT+Serif)) and Sans-Serif ([Open Sans](https://fonts.google.com/specimen/Open+Sans)) typeface. They will be served out through Google Fonts and can be used directly without having to host them on our end. +For our project we'll be using the combination of a Serif ([PT Serif](https://fonts.google.com/specimen/PT+Serif){:target="_blank"}) and Sans-Serif ([Open Sans](https://fonts.google.com/specimen/Open+Sans){:target="_blank"}) typeface. They will be served out through Google Fonts and can be used directly without having to host them on our end. Let's first include them in the HTML. Our React.js app is using a single HTML file. -Go ahead and edit `public/index.html` and add the following line in the `` section of the HTML to include the two typefaces. +{%change%} Edit `public/index.html` and add the following line in the `` section of the HTML to include the two typefaces. ``` html - + ``` Here we are referencing all the 5 different weights (300, 400, 600, 700, and 800) of the Open Sans typeface. @@ -31,21 +34,21 @@ Here we are referencing all the 5 different weights (300, 400, 600, 700, and 800 Now we are ready to add our newly added fonts to our stylesheets. Create React App helps separate the styles for our individual components and has a master stylesheet for the project located in `src/index.css`. -Let's change the current font in `src/index.css` for the `body` tag to the following. +{%change%} Let's replace the current styles in `src/index.css` for the `body` tag to the following. ``` css body { margin: 0; padding: 0; - font-family: "Open Sans", sans-serif; - font-size: 16px; color: #333; - -webkit-font-smoothing: antialiased; + font-size: 16px; -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: "Open Sans", sans-serif; } ``` -And let's change the fonts for the header tags to our new Serif font by adding this block to the css file. +{%change%} And let's change the fonts for the header tags to our new Serif font by adding this block to the css file. ``` css h1, h2, h3, h4, h5, h6 { @@ -55,6 +58,6 @@ h1, h2, h3, h4, h5, h6 { Now if you just flip over to your browser with our new app, you should see the new fonts update automatically; thanks to the live reloading. -![Custom fonts updated screenshot](/assets/custom-fonts-updated.png) +![Custom fonts updated screenshot](/assets/part2/custom-fonts-updated.png) We'll stay on the theme of adding styles and set up our project with Bootstrap to ensure that we have a consistent UI Kit to work with while building our app. diff --git a/_chapters/setup-error-logging-in-serverless.md b/_chapters/setup-error-logging-in-serverless.md new file mode 100644 index 0000000000..db11548e9b --- /dev/null +++ b/_chapters/setup-error-logging-in-serverless.md @@ -0,0 +1,202 @@ +--- +layout: post +title: Setup Error Logging in Serverless +date: 2020-04-01 00:00:00 +lang: en +description: In this chapter we'll look at how to handle errors in our Lambda functions. We'll also handle Lambda timeouts and enable logging for the AWS SDK. +comments_id: setup-error-logging-in-serverless/1733 +redirect_from: /chapters/monitoring-deployments-in-seed.html +ref: setup-error-logging-in-serverless +--- + +Now that we have our React app configured to report errors, let's move on to our serverless backend. Our React app is reporting API errors (and other unexpected errors) with the API endpoint that caused the error. We want to use that info to be able to debug on the backend and figure out what's going on. + +To do this, we'll setup the error logging in our backend to catch: + +- Errors in our code +- Errors while calling AWS services +- Unexpected errors like Lambda functions timing out or running out of memory + +We are going to look at how to setup a debugging framework to catch the above errors, and have enough context for us to easily pinpoint and fix the issue. We'll be using [CloudWatch](https://aws.amazon.com/cloudwatch/) to write our logs, and we'll be using the log viewer in [Seed](https://seed.run) to view them. + +### Setup a Debug Lib + +Let's start by adding some code to help us with that. + +{%change%} Create a `packages/core/src/debug.ts` file from your project root with the following. + +```typescript +import util from "util"; +import AWS from "aws-sdk"; +import {APIGatewayEvent} from "aws-lambda"; + +export interface LogType { + [key: string | symbol]: Date | string; +} + +let logs:Array; + +// Log AWS SDK calls +AWS.config.logger = { log: debug }; + +export default function debug(...args: Array) { + logs.push({ + date: new Date(), + string: util.format.apply(null, [...args]), + }); +} + +export function init(event: APIGatewayEvent) { + logs = []; + + // Log API event + debug("API event", { + body: event.body, + pathParameters: event.pathParameters, + queryStringParameters: event.queryStringParameters, + }); +} + +export function flush(error: unknown) { + logs.forEach(({ date, string }) => console.debug(date, string)); + console.error(error); +} +``` + +We are doing a few things of note in this simple helper. + +- **Enable AWS SDK logging** + + We start by enabling logging for the AWS SDK. We do so by running `AWS.config.logger = { log: debug }`. This is telling the AWS SDK to log using our logger, the `debug()` method (we'll look at this below). So when you make a call to an AWS service, ie. a query call to the DynamoDB table `dev-notes`, this will log: + + ```bash + [AWS dynamodb 200 0.296s 0 retries] query({ TableName: 'dev-notes', + KeyConditionExpression: 'userId = :userId', + ExpressionAttributeValues: { ':userId': { S: 'USER-SUB-1234' } } }) + ``` + + Note, we only want to log this info when there is an error. We'll look at how we accomplish this below. + +- **Log API request info** + + We initialize our debugger by calling `init()`. We log the API request info, including the path parameters, query string parameters, and request body. We do so using our internal `debug()` method. + +- **Log only on error** + + We log messages using our special `debug()` method. Debug messages logged using this method only get printed out when we call the `flush()` method. This allows us to log very detailed contextual information about what was being done leading up to the error. We can log: + + - Arguments and return values for function calls. + - And, request/response data for HTTP requests made. + + We only want to print out debug messages to the console when we run into an error. This helps us reduce clutter in the case of successful requests. And, keeps our CloudWatch costs low! + + To do this, we store the log info (when calling `debug()`) in memory inside the `logs` array. And when we call `flush()` (in the case of an error), we `console.debug()` all those stored log messages. + +So in our Lambda function code, if we want to log some debug information that only gets printed out if we have an error, we'll do the following: + +```typescript +debug( + "This stores the message and prints to CloudWatch if Lambda function later throws an exception" +); +``` + +In contrast, if we always want to log to CloudWatch, we'll: + +```typescript +console.log("This prints a message in CloudWatch prefixed with INFO"); +console.warn("This prints a message in CloudWatch prefixed with WARN"); +console.error("This prints a message in CloudWatch prefixed with ERROR"); +``` + +Now let's use the debug library in our Lambda functions. + +### Setup Handler Lib + +You'll recall that all our Lambda functions are wrapped using a `handler()` method. We use this to format what our Lambda functions return as their HTTP response. It also, handles any errors that our Lambda functions throws. + +We'll use the debug lib that we added above to improve our error handling. + +{%change%} Replace our `packages/core/src/handler.ts` with the following. + +```typescript +import { Context, APIGatewayEvent } from 'aws-lambda'; +import * as debug from "./debug"; + +export default function handler(lambda: Function) { + return async function (event: APIGatewayEvent, context: Context) { + let body, statusCode; + + // Start debugger + debug.init(event); + + try { + // Run the Lambda + body = await lambda(event, context); + statusCode = 200; + } catch (error) { + // Print debug messages + debug.flush(error); + + if (error instanceof Error) { + body = {error: error.message}; + } else { + body = {error: String(error)}; + } + statusCode = 500; + } + + // Return HTTP response + return { + statusCode, + body: JSON.stringify(body), + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, + }; + }; +} +``` + +This should be fairly straightforward: + +1. We initialize our debugger by calling `debug.init()`. +2. We run our Lambda function. +3. We format the success response. +4. In the case of an error, we first write out our debug logs by calling `debug.flush(e)`. Where `e` is the error that caused our Lambda function to fail. +5. We format and return our HTTP response. + +### Using the Error Handler + +You might recall the way we are currently using the above error handler in our Lambda functions. + +```typescript +export const main = handler((event, context) => { + // Do some work + const a = 1 + 1; + // Return a result + return { result: a }; +}); +``` + +We wrap all of our Lambda functions using the error handler. + +Note that, the `handler.ts` needs to be **imported before we import anything else**. This is because the `debug.js` that it imports needs to initialize AWS SDK logging before it's used anywhere else. + +{%change%} Go check and make sure you have imported handler first. + +### Commit the Code + +Let's push our changes + +{%change%} Let's commit the code we have so far. + +```bash +$ git add . +$ git commit -m "Adding serverless error logging" +$ git push +``` + +This should deploy our changes to production. + +And that's pretty much it! With these simple steps, we are now ready to look at some examples of how to debug our serverless app. diff --git a/_chapters/setup-error-reporting-in-react.md b/_chapters/setup-error-reporting-in-react.md new file mode 100644 index 0000000000..11c147a978 --- /dev/null +++ b/_chapters/setup-error-reporting-in-react.md @@ -0,0 +1,111 @@ +--- +layout: post +title: Setup Error Reporting in React +date: 2020-04-03 00:00:00 +lang: en +description: In this chapter we signup for a free Sentry account and set it up in our React app. We also configure our app to not report any errors when we are developing locally. +comments_id: setup-error-reporting-in-react/1734 +ref: setup-error-reporting-in-react +--- + +Let's start by setting up error reporting in React. To do so, we'll be using [Sentry](https://sentry.io). Sentry is a great service for reporting and debugging errors. And it comes with a very generous free tier. + +In this chapter we'll sign up for a free Sentry account and configure it in our React app. And in the coming chapters we'll be reporting the various frontend errors to it. + +Let's get started. + +### Create a Sentry Account + +Head over to [Sentry](https://sentry.io) and hit **Get Started**. + +![Sentry landing page](/assets/monitor-debug-errors/sentry-landing-page.png) + +Then enter your info and hit **Create Your Account**. + +![Sentry create an account](/assets/monitor-debug-errors/sentry-create-an-account.png) + +Next hit **Create project**. + +![Sentry hit create project](/assets/monitor-debug-errors/sentry-hit-create-project.png) + +For the type of project, select **React**. + +![Sentry select React project](/assets/monitor-debug-errors/sentry-select-react-project.png) + +Give your project a name. + +![Sentry name React project](/assets/monitor-debug-errors/sentry-name-react-project.png) + +And that's it. Scroll down and copy the `Sentry.init` line. + +![Sentry init code snippet](/assets/monitor-debug-errors/sentry-init-code-snippet.png) + +### Install Sentry + +{%change%} Now head over to the React `frontend/` directory and install Sentry. + +```bash +$ pnpm add @sentry/react --save +``` + +We are going to be using Sentry across our app. So it makes sense to keep all the Sentry related code in one place. + +{%change%} Add the following to the top of your `src/lib/errorLib.ts`. + +```typescript +import * as Sentry from "@sentry/react"; +import config from "../config"; + +export interface ErrorInfoType { + [key: string | symbol]: string; +} + +const isLocal = process.env.NODE_ENV === "development"; + +export function initSentry() { + if (isLocal) { + return; + } + + Sentry.init({ dsn: config.SENTRY_DSN }); +} + +export function logError(error: unknown, errorInfo: ErrorInfoType | null = null) { + if (isLocal) { + return; + } + + Sentry.withScope((scope) => { + errorInfo && scope.setExtras(errorInfo); + Sentry.captureException(error); + }); +} +``` + +{%change%} Add the `SENTRY_DSN` below the `const config = {` line in `frontend/src/config.ts`. + +```typescript +SENTRY_DSN: "https://your-dsn-id-here@sentry.io/123456", +``` + +Make sure to replace `https://your-dsn-id-here@sentry.io/123456` with the line we copied from the Sentry dashboard above. + +We are using the `isLocal` flag to conditionally enable Sentry because we don't want to report errors when we are developing locally. Even though we all know that we _rarely_ ever make mistakes while developing… + +The `logError` method is what we are going to call when we want to report an error to Sentry. It takes: + +- An Error object in `error`. +- And, an object with key-value pairs of additional info in `errorInfo`. + +Next, let's initialize our app with Sentry. + +{%change%} Add the following to the end of the imports in `src/index.tsx`. + +```typescript +import { initSentry } from "./lib/errorLib"; + +initSentry() + +``` + +Now we are ready to start reporting errors in our React app! Let's start with the API errors. diff --git a/_chapters/setup-secure-pages.md b/_chapters/setup-secure-pages.md index 4a8ac288c8..39ce61b471 100644 --- a/_chapters/setup-secure-pages.md +++ b/_chapters/setup-secure-pages.md @@ -11,7 +11,7 @@ We are almost done putting together our app. All the pages are done but there ar We also have a couple of pages that need to behave in sort of the same way. We want the user to be redirected to the homepage if they type in the login (`/login`) or signup (`/signup`) URL. Currently, the login and sign up page end up loading even though the user is already logged in. -There are many ways to solve the above problems. The simplest would be to just check the conditions in our containers and redirect. But since we have a few containers that need the same logic we can create a special route (like the `AppliedRoute` from the [Add the session to the state]({% link _chapters/add-the-session-to-the-state.md %}) chapter) for it. +There are many ways to solve the above problems. The simplest would be to just check the conditions in our containers and redirect. But since we have a few containers that need the same logic we can create a special route for it. We are going to create two different route components to fix the problem we have. diff --git a/_chapters/setup-ssl.md b/_chapters/setup-ssl.md deleted file mode 100644 index 7c01de2c02..0000000000 --- a/_chapters/setup-ssl.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -layout: post -title: Set up SSL -date: 2017-02-11 00:00:00 -lang: en -description: We want to enable SSL or HTTPS for our React.js app on AWS. To do so we are going to request a certificate using the Certificate Manager service from AWS. We are then going to use the new certificate in our CloudFront Distributions. -context: true -comments_id: comments-for-set-up-ssl/133 -ref: setup-ssl ---- - -Now that our app is being served through our domain, let's add a layer of security to it by switching to HTTPS. AWS makes this fairly easy to do, thanks to Certificate Manager. - -### Request a Certificate - -Select **Certificate Manager** from the list of services in your [AWS Console](https://console.aws.amazon.com). Ensure that you are in the **US East (N. Virginia)** region. This is because a certificate needs to be from this region for it to [work with CloudFront](http://docs.aws.amazon.com/acm/latest/userguide/acm-regions.html). - -![Select Certificate Manager service screenshot](/assets/select-certificate-manager-service.png) - -If this is your first certificate, you'll need to hit **Get started**. If not then hit **Request a certificate** from the top. - -![Get started with Certificate Manager screenshot](/assets/get-started-certificate-manager.png) - -And type in the name of our domain. Hit **Add another name to this certificate** and add our www version of our domain as well. Hit **Review and request** once you are done. - -![Add domain names to certificate screenshot](/assets/add-domain-names-to-certificate.png) - -Now to confirm that we control the domain, select the **DNS validation** method and hit **Review**. - -![Select dns validation for certificate screenshot](/assets/select-dns-validation-for-certificate.png) - -On the validation screen expand the two domains we are trying to validate. - -![Expand dns validation details screenshot](/assets/expand-dns-validation-details.png) - -Since we control the domain through Route 53, we can directly create the DNS record through here by hitting **Create record in Route 53**. - -![Create Route 53 dns record screenshot](/assets/create-route-53-dns-record.png) - -And confirm that you want the record to be created by hitting **Create**. - -![Confirm Route 53 dns record screenshot](/assets/confirm-route-53-dns-record.png) - -Also, make sure to do this for the other domain. - -The process of creating a DNS record and validating it can take around 30 minutes. - -Next, we'll associate this certificate with our CloudFront Distributions. - -### Update CloudFront Distributions with Certificate - -Open up our first CloudFront Distribution from our list of distributions and hit the **Edit** button. - -![Select CloudFront Distribution screenshot](/assets/select-cloudfront-Distribution.png) - -Now switch the **SSL Certificate** to **Custom SSL Certificate** and select the certificate we just created from the drop down. And scroll down to the bottom and hit **Yes, Edit**. - -![Select custom SSL Certificate screenshot](/assets/select-custom-ssl-certificate.png) - -Next, head over to the **Behaviors** tab from the top. - -![Select Behaviors tab screenshot](/assets/select-behaviors-tab.png) - -And select the only one we have and hit **Edit**. - -![Edit Distribution Behavior screenshot](/assets/edit-distribution-behavior.png) - -Then switch the **Viewer Protocol Policy** to **Redirect HTTP to HTTPS**. And scroll down to the bottom and hit **Yes, Edit**. - -![Switch Viewer Protocol Policy screenshot](/assets/switch-viewer-protocol-policy.png) - -Now let's do the same for our other CloudFront Distribution. - -![Select custom SSL Certificate screenshot](/assets/select-custom-ssl-certificate-2.png) - -But leave the **Viewer Protocol Policy** as **HTTP and HTTPS**. This is because we want our users to go straight to the HTTPS version of our non-www domain. As opposed to redirecting to the HTTPS version of our www domain before redirecting again. - -![Dont switch Viewer Protocol Policy for www distribution screenshot](/assets/dont-switch-viewer-protocol-policy-for-www-distribution.png) - -### Update S3 Redirect Bucket - -The S3 Redirect Bucket that we created in the last chapter is redirecting to the HTTP version of our non-www domain. We should switch this to the HTTPS version to prevent the extra redirect. - -Open up the S3 Redirect Bucket we created in the last chapter. Head over to the **Properties** tab and select **Static website hosting**. - -![Open S3 Redirect Bucket Properties screenshot](/assets/open-s3-redirect-bucket-properties.png) - -Change the **Protocol** to **https** and hit **Save**. - -![Change S3 Redirect to HTTPS screenshot](/assets/change-s3-redirect-to-https.png) - -And that's it. Our app should be served out on our domain through HTTPS. - -![App live with certificate screenshot](/assets/app-live-with-certificate.png) - -Next up, let's look at the process of deploying updates to our app. diff --git a/_chapters/setup-the-serverless-framework.md b/_chapters/setup-the-serverless-framework.md deleted file mode 100644 index a0ce663cc8..0000000000 --- a/_chapters/setup-the-serverless-framework.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -layout: post -title: Set up the Serverless Framework -date: 2016-12-29 00:00:00 -lang: en -ref: setup-the-serverless-framework -description: To create our serverless backend API using AWS Lambda and API Gateway, we are going to use the Serverless Framework (https://serverless.com). Serverless Framework helps developers build and manage serverless apps on AWS and other cloud providers. We can install the Serverless Framework CLI from it’s NPM package and use it to create a new Serverless Framework project. -context: true -comments_id: set-up-the-serverless-framework/145 ---- - -We are going to be using [AWS Lambda](https://aws.amazon.com/lambda/) and [Amazon API Gateway](https://aws.amazon.com/api-gateway/) to create our backend. AWS Lambda is a compute service that lets you run code without provisioning or managing servers. You pay only for the compute time you consume - there is no charge when your code is not running. And API Gateway makes it easy for developers to create, publish, maintain, monitor, and secure APIs. Working directly with AWS Lambda and configuring API Gateway can be a bit cumbersome; so we are going to use the [Serverless Framework](https://serverless.com) to help us with it. - -The Serverless Framework enables developers to deploy backend applications as independent functions that will be deployed to AWS Lambda. It also configures AWS Lambda to run your code in response to HTTP requests using Amazon API Gateway. - -In this chapter, we are going to set up the Serverless Framework on our local development environment. - -### Install Serverless - -Install Serverless globally. - -``` bash -$ npm install serverless -g -``` - -The above command needs [NPM](https://www.npmjs.com), a package manager for JavaScript. Follow [this](https://docs.npmjs.com/getting-started/installing-node) if you need help installing NPM. - -In your working directory; create a project using a Node.js starter. We'll go over some of the details of this starter project in the next chapter. - -``` bash -$ serverless install --url https://github.com/AnomalyInnovations/serverless-nodejs-starter --name notes-app-api -``` - -Go into the directory for our backend api project. - -``` bash -$ cd notes-app-api -``` - -Now the directory should contain a few files including, the **handler.js** and **serverless.yml**. - -- **handler.js** file contains actual code for the services/functions that will be deployed to AWS Lambda. -- **serverless.yml** file contains the configuration on what AWS services Serverless will provision and how to configure them. - -We also have a `tests/` directory where we can add our unit tests. - -### Install Node.js packages - -The starter project relies on a few dependencies that are listed in the `package.json`. - -At the root of the project, run. - -``` bash -$ npm install -``` - -Next, we'll install a couple of other packages specifically for our backend. - -``` bash -$ npm install aws-sdk --save-dev -$ npm install uuid --save -``` - -- **aws-sdk** allows us to talk to the various AWS services. -- **uuid** generates unique ids. We need this for storing things to DynamoDB. - -The starter project that we are using allows us to use the version of JavaScript that we'll be using in our frontend app later. Let's look at exactly how it does this. diff --git a/_chapters/si/call-the-create-api.md b/_chapters/si/call-the-create-api.md new file mode 100644 index 0000000000..c58ecc3fed --- /dev/null +++ b/_chapters/si/call-the-create-api.md @@ -0,0 +1,64 @@ +--- +layout: post +title: API එකක් සෑදීම සඳහා +date: 2017-01-23 00:00:00 +lang: si +ref: call-the-create-api +description: අපගේ පරිශීලකයින්ට අපගේ React.js යෙදුමේ සටහනක් සෑදීමට ඉඩ දීම සඳහා, අපි අපගේ පෝරමය serverless API backend සමඟ සම්බන්ධ කළ යුතුයි. මේ සඳහා අපි භාවිතා කිරීමට යන්නේ AWS Amplify's API මොඩියුලය. +comments_id: call-the-create-api/124 +--- + +දැන් අපේ මූලික නිර්‍මාණ සටහන් පත්‍රය ක්‍රියාත්මක වන බැවින් එය අපේ API වෙත සම්බන්ධ කරමු. අපි ටික වේලාවකට පසු S3 වෙත උඩුගත කරමු. AWS IAM භාවිතා කර අපේ APIs ආරක්‍ෂා කර ඇති අතර අපගේ සත්‍යාපන සපයන්නා වන්නේ Cognito User Pool. ස්තූතිවන්ත වන්නට, ලොග් වී ඇති පරිශීලක සැසිය භාවිතා කිරීමෙන් Amplify අප වෙනුවෙන් මෙය රැකබලා ගනී. + +{%change%} පහත දැක්වෙන දේ `src/containers/NewNote.js` හි header ට එකතු කිරීමෙන් `API` මොඩියුලය ඇතුළත් කරමු. + +```js +import { API } from "aws-amplify"; +``` + +{%change%} තවද අපගේ `handleSubmit` function ය පහත සඳහන් දෑ සමඟ ප්‍රතිස්ථාපනය කරන්න. + +```js +async function handleSubmit(event) { + event.preventDefault(); + + if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) { + alert( + `Please pick a file smaller than ${ + config.MAX_ATTACHMENT_SIZE / 1000000 + } MB.` + ); + return; + } + + setIsLoading(true); + + try { + await createNote({ content }); + nav("/"); + } catch (e) { + onError(e); + setIsLoading(false); + } +} + +function createNote(note) { + return API.post("notes", "/notes", { + body: note, + }); +} +``` + +මෙය සරල කරුණු කිහිපයක් කරයි. + +1. `/notes` වෙත POST request කිරීමෙන් සහ අපගේ note object පාස් වීමෙන් අපි 'createNote' හි අපගේ create call ලබා දෙන්නෙමු. `API.post()` method සඳහා වන පළමු arguments දෙක `notes` සහ`/notes` බව සලකන්න. එයට හේතුව නම් [Configure AWS Amplify]({% link _chapters/configure-aws-amplify.md %}) පරිච්ඡේදයේ අපි මෙම API කට්ටලය `notes` නමින් හැඳින්වූ බැවිනි. + +2. මේ වන විට note object සරලව note අන්තර්ගතයයි. අපි දැනට මෙම note නිර්මාණය කරන්නේ ඇමුණුමකින් තොරවය. + +3. අවසාන වශයෙන්, note සෑදීමෙන් පසු අපි අපේ homepage හරවා යවමු. + +එපමණයි; ඔබ ඔබේ බ්‍රව්සරය වෙත මාරු වී ඔබේ පෝරමය ඉදිරිපත් කිරීමට උත්සාහ කරන්නේ නම්, එය අපේ homepage සාර්ථකව යා යුතුය. + +![New note created screenshot](/assets/new-note-created.png) + +ඊළඟට අපි අපේ ගොනුව S3 වෙත උඩුගත කර අපේ note ට ඇමුණුමක් එකතු කරමු. diff --git a/_chapters/si/how-to-get-help.md b/_chapters/si/how-to-get-help.md new file mode 100644 index 0000000000..aa06ae806c --- /dev/null +++ b/_chapters/si/how-to-get-help.md @@ -0,0 +1,20 @@ +--- +layout: post +title: උපකාර ලබා ගන්නේ කෙසේද? +lang: si +ref: how-to-get-help +date: 2016-12-23 00:00:00 +comments_id: how-to-get-help/95 +--- + +ඔබට කිසියම් පියවරකදී ගැටළුවක් ඇතිවූයේ නම්, අපි ඔබට එය තේරුම් ගැනීමට සහ විසඳා ගැනීමට සහය වීමට සිටින බව තහවුරු කරගැනීමට අවශ්‍ය ය. උපකාර ලබා ගැනීමට මාර්ග කිහිපයක් තිබේ. + +- අපි අපේ අදහස් ප්‍රකාශ කිරීමේ මාධ්‍යය ලෙස [Discourse පර්ෂද මාතෘකා](({{ site.forum_url }})) භාවිත කරන අතර සමහරක් ගැටළු විසඳා ගැනීමට අතීතයේ සහය වී ඇත්තෙමු. එමනිසා, වෙනත් අයෙකු ඔබට ඇති ගැටළුවටම මුහුණ දී ඇත්දැයි දැනගැනීම සඳහා සෑම පරිච්ජෙදයක්ම යටතේ ඇති අදහස් පිරික්සා බැලීමට සහතික වන්න. +- ඔබේ ගැටළුව, විස්තර සහිතව අදාළ පරිච්ජේදය යටතේ ඇති අදහස්වල පළ කරන්න. අපගෙන් අයෙකු ප්‍රතිචාර දක්වනු ඇත. + +![SST හි Discourse පර්ෂදයේ තිර රූපය](/assets/serverless-stack-discourse-forums.png) + +මෙම සමස්ත මාර්ගෝපදේශය ම [GitHub]({{ site.github_repo }}) හි රඳවා පවතී. එමනිසා ඔබ දෝෂයක් සොයා ගන්නා සෑමවිටම ඔබට පහත යමක් කළ හැකිය: + +- [නව ගැටළුවක්]({{ site.github_repo }}/issues/new) විවෘත කරන්න. +- හෝ ඔබ අක්ෂර වින්‍යාස දෝෂයක් සොයා ගත්තේ නම්, පිටුව සංස්කරණය කර වෙනස්කම් භාර ගැනීමේ ඉල්ලීමක් ඉදිරිපත් කරන්න! diff --git a/_chapters/si/what-does-this-guide-cover.md b/_chapters/si/what-does-this-guide-cover.md new file mode 100644 index 0000000000..80f7ed3312 --- /dev/null +++ b/_chapters/si/what-does-this-guide-cover.md @@ -0,0 +1,152 @@ +--- +layout: post +ref: what-does-this-guide-cover +title: මෙම මාර්ගෝපදේශය ආවරණය කරන්නේ මොනවාද? +date: 2016-12-22 00:00:00 +lang: si +ref: what-does-this-guide-cover +comments_id: what-does-this-guide-cover/83 +--- + +වෙබ් යෙදුමක් ගොඩනැගීමේ ප්‍රධාන සංකල්ප පුහුණු වීම සඳහා අපි මෙහිදී [**Scratch**](https://demo2.sst.dev) නම් සරල සටහන් තබා ගැනීමේ යෙදුමක් නිර්මාණය කරන්නෙමු. කෙසේ නමුත්, වෙනත් නිබන්ධන මෙන් නොව අපගේ අරමුණ වන්නේ නිකුත් කිරීමට සුදුසු තත්වයේ සම්පූර්ණ අන්ත යෙදුමක් නිර්මාණය කිරීමට අවශ්‍ය වන්නේ මොනවද යන්න පිළිබඳ ගැඹුරින් සොයා බැලීමටයි. + +![සම්පූර්ණ කළ යෙදුම මේස පරිගණක තිරයකදී දිස්වන අයුරු දැක්වෙන තිර රූපය](/assets/completed-app-desktop.png) + +සමපුර්ණ කළ යෙදුමේ ජංගම තිර රූපය + +එය සම්පූර්ණයෙන් ම JavaScript වලින් රචිත, serverless යෙදුම් ක්‍රමලේඛ අතුරුමුහුණත් මඟින් බලගැන්වෙන තනි පිටුවේ යෙදුමකි. මෙහි [ඉදිරි අන්තය]({{ site.frontend_github_repo }}) සහ [පසු අන්තය]({{ site.backend_github_repo }}) සඳහා සම්පූර්ණ කේතය අන්තර්ගත ය. එය සාපේක්ෂව සරල යෙදුමක් වන නමුත් අපි පහත අවශ්‍යතා සම්පූර්ණ කිරීමට බලාපොරොත්තු වන්නෙමු. + +- පරිශීලකයන්ට ලියාපදිංචි වීමට සහ තමන්ගේ ගිණුම්වලට පිවිසීමට ඉඩ දිය යුතුය. +- පරිශීලකයන්ට කිසියම් අන්තර්ගතයක් සහිතව සටහන් නිර්මාණය කළ හැකි විය යුතුය. +- සෑම සටහනකට ම උඩුගත කල ලේඛනයක්, ඇමුණුමක් ලෙස පැවතිය හැක. +- පරිශීලකයන්ට තම සටහන් සහ ඇමුණුම් සංස්කරණය කිරීමට ඉඩ දිය යුතුය. +- පරිශීලකයන්ට තම සටහන් මකා දැමිය හැකිය. +- මෙම යෙදුම, ණයපත් ගෙවීම් හැසිරවීමට සමත් විය යුතුය +- අභිරුචි වසම් නාමයක HTTPS ඔස්සේ, යෙදුම පරිශීලකයන්ට පිරිනැමිය යුතුය. +- පසු අන්තයේ ක්‍රමලේඛ අතුරුමුහුණත් ආරක්ෂාකාරී වීමට අවශ්‍ය ය. +- යෙදුම විවිධ තිර ප්‍රමාණයන්ට සංවේදී වීමට අවශ්‍ය ය. +- අප `git push` කරන විට යෙදුමෙහි නව පිටපතක් නිකුත් වීමට අවශ්‍ය ය. +- අපට යෙදුම පිළිබඳ පරීක්ෂාවෙන් සිටීමට සහ දෝෂ නිදොස්කරණය කිරීමට හැකි විය යුතුය. + +අප මේ සඳහා AWS වේදිකාව භාවිත කරනු ඇත. අපි ඉදිරියේදී තවත් සමහර වේදිකාවන් ද ආවරණය කරනු ඇත, නමුත් AWS වේදිකාව තෝරා ගැනීම හොඳ ආරම්භයක් යයි අපි තීරණය කළෙමු. + +### තාක්ෂණයන් සහ සේවාවන් + +අපේ serverless යෙදුම ගොඩනැගීම සඳහා අපි පහත තාක්ෂණ සහ සේවා සමූහය භාවිත කරනවා. + +- අපගේ serverless යෙදුම් ක්‍රමලේඛ අතුරුමුහුණත සඳහා [Lambda][Lambda] & [API Gateway][APIG] +- අපේ දත්ත ගබඩාව සඳහා [DynamoDB][DynamoDB] +- පරිශීලක සත්‍යාපනය සහ අපගේ ක්‍රමලේඛ අතුරුමුහුණත් ආරක්ෂාව සඳහා [Cognito][Cognito] +- අපේ යෙදුම රැඳවීමට සහ ලේඛන උඩුගත කිරීම සඳහා [S3][S3] +- අපේ යෙදුම බාහිරයට පිරිනැමීම සඳහා [CloudFront][CF] +- අපේ වසම් නාමය සඳහා [Route 53][R53] +- SSL සඳහා [Certificate Manager][CM] +- Lambda සහ ක්‍රමලේඛ අතුරුමුහුණත් ප්‍රවේශ සටහන් සඳහා [CloudWatch][CloudWatch] +- අපේ තනි පිටුවේ යෙදුම සඳහා [React.js][React] +- මාර්ගනය සඳහා [React Router][RR] +- පරිශීලක අතුරුමුහුණත් කට්ටලය සඳහා [Bootstrap][Bootstrap] +- ණයපත් ගෙවීම් හැසිරවීම සඳහා [Stripe][Stripe] +- නිකුත් කිරීම් ස්වයංක්‍රීයකරණය සඳහා [Seed][Seed] +- React නිකුත් කිරීම් ස්වයංක්‍රීයකරණය කිරීම සඳහා [Netlify][Netlify] +- අපගේ ව්‍යාපෘති කේතය රඳවා තැබීම සඳහා [GitHub][GitHub] +- දෝෂ වාර්තාකරණය සඳහා [Sentry][Sentry] + +අපි ඉහත සේවාවන් සඳහා ඒවායේ නොමිල අවධීන් භාවිත කරන්නෙමු. එවිට ඔබට එම සේවාවන් සඳහා නොමිලයේ ලියාපදිංචි විය හැකිය. මෙය ඇත්තෙන්ම ඔබේ යෙදුම රැඳවීම සඳහා නව අභිරුචි වසම් නාමයක් මිල දී ගැනීම කෙරෙහි නම් වලංගු නොවේ. එසේම AWS හි ගිණුමක් සැදීමේදී ඔබට ණයපත් තොරතුරු ඇතුළත් කිරීමට ද සිදුවේ. එමනිසා, ඔබ මෙම නිබන්ධනයෙන් අප ආවරණය කරන ප්‍රමාණයට වඩා AWS සම්පත් ප්‍රමාණයක් භාවිත කරන්නේ නම් ඔබෙන් ගාස්තුවක් අයවිය හැකිය. + +ඉහත ලැයිස්තුව මඳක් වික්ෂිප්තකාරී වුවත්, අපි, ඔබ මෙම නිබන්ධනය සම්පූර්ණ කිරීමෙන් **සැබෑ ලෝකයේ**, **ආරක්‍ෂිත** සහ **පූර්ණ ක්‍රියාකාරී** වෙබ් යෙදුම් නිර්මාණය කිරීමට සුදානම් බව සහතික වන්නට උත්සාහ කරන්නෙමු. කණගාටු නොවන්න, අප ඔබට මේ සඳහා සහය වනු ඇත. + +### අවශ්‍යතා + +ඔබට මෙම මාර්ගෝපදේශය අනුගමනය කිරීමට දේවල් කිහිපයක් අවශ්‍ය වේ: + +- [Node v8.10+ සහ NPM v5.5+](https://nodejs.org/en/) ඔබේ පරිගණකයේ ස්ථාපනය කර තිබීම +- නොමිල [GitHub ගිණුම](https://github.com/join) ක්. +- සහ විධාන පෙළ භාවිත කරන්නේ කෙසේද යන්න පිළිබඳ මුලික දැනුම + +### මෙම මාර්ගෝපදේශය සැකසී ඇත්තේ කෙසේද? + +මෙම මාර්ගෝපදේශය දළ වශයෙන් කොටස් කිහිපයකට බෙදා ඇත: + +1. **මූලිකාංග** + + අපි මෙහි දී ඔබේ පළමු පූර්ණ අන්ත යෙදුම නිර්මාණය කරන්නේ කෙසේදැයි විස්තර කරන්නෙමු. මෙම පරිච්ජේද පසු අන්තය(Serverless) සහ ඉදිරි අන්තය(React) අනුව දළ වශයෙන් බෙදා වෙන්කර තිබේ.තවද අපි මෙහි දී ඔබේ serverless යෙදුම සහ React යෙදුම නිකුත් කරන්නේ කෙසේද යන්න පිළිබඳව ද කතා කරන්නෙමු. + + මාර්ගෝපදේශයේ මෙම කොටස මුළුමුලින් ම සම්පූර්ණ කෙරෙන පරිදි ප්‍රවේශමෙන් සැලසුම් කර ඇත. අපි සෑම පියවරක්ම විස්තරාත්මක ව සාකච්ජා කරන අතර ඔබේ පළමු යෙදුම නිර්මාණය කිරීමට ඔබට සහය වීම සඳහා තිර ජායාරූප විශාල ප්‍රමාණයක් ද සතුව ඇත්තෙමු. + +2. **හොඳම භාවිතයන්** + + අපි 2017 දී මෙම මාර්ගෝපදේශය පළමු කොටස පමණක් සහිතව නිකුත් කළෙමු. ප්‍රජාව වර්ධනය වී ඇති අතර අපගේ කියවන්නන් බොහෝ දෙනෙක් මෙහි විස්තර කෙරෙන සැකසීම භාවිත කර ඔවුනගේ කටයුතු බලගන්වන යෙදුම් නිර්මාණය කර තිබේ. මෙම කොටසේදී අපි, නිකුත් කළ යෙදුම් සඳහා වන හොඳම භාවිතයන් ආවරණය කරන්නෙමු. මේවා ඔබට ඇත්තෙන්ම වැදගත් වන්නේ ඔබේ යෙදුමේ කේත ප්‍රමාණය වැඩි වන විට හෝ ඔබේ යෙදුමේ ක්‍රමලේඛන කණ්ඩායම වැඩි වන විටදී ය. + + මෙම කොටසේ පරිච්ජේද සාපේක්ෂව ස්වාධීන වන අතර නිශ්චිත මාතෘකා වටා දෝලනය වීමට නැඹුරුවක් දක්වයි. + +3. **යොමුව** + + අවසානයේ, අප සතුව විවිධ මාතෘකා යටතේ වන පරිච්ජේද එකතුවක් ඇත. අපි මේවාට මාර්ගෝපදේශයේ අතරමැදදී යොමු වනු ඇත. ඉහත සඳහන් කොටස් දෙකටම අත්‍යවශයෙන් අයත් නොවන මාතෘකා ආවරණය කිරීමට අපි මෙය යොදා ගන්නෙමු + +#### ඔබේ පළමු Serverless යෙදුම ගොඩනැගීම + +මෙම මාර්ගෝපදේශනයේ පළමු කොටස සටහන් යෙදුම නිර්මාණය කිරීමට සහ එය නිකුත් කිරීමට ඔබට සහය වෙයි. අපි සියලුම මුලිකංග ආවරණය කරන්නෙමු. සෑම සේවාවක්ම ඔබ විසින්ම නිර්මාණය කෙරෙනු ඇත. පහත දැක්වෙන්නේ කුමක් කුමන පිළිවෙලින් ආවරණය වන්නේ ද යන්නයි. + +පසු අන්තය සඳහා: + +- ඔබේ AWS ගිණුම ගැළපෙන පරිදි සකසා ගැනීම +- DynamoDB භාවිතයෙන් ඔබේ දත්ත ගබඩාව සකසා ගැනීම +- ලේඛන උඩුගත කිරීම සඳහා S3 සකසා ගැනීම +- පරිශීලක ගිණුම් කළමනාකරණය සඳහා Cognito පරිශීලක තටාක සකසා ගැනීම +- අපගේ ලේඛන/ගොනු උඩුගත කිරීම් ආරක්ෂා කිරීම සඳහා Cognito අනන්‍යතා තටාකය සකසා ගැනීම +- Lambda සහ ක්‍රමලේඛ අතුරුමුහුණත් ද්වාරය සමඟ වැඩ කිරීම සඳහා Serverless ක්‍රමලේඛ රාමුව සකසා ගැනීම +- විවිධ පසු අන්ත ක්‍රමලේඛ අතුරුමුහුණත් රචනා කිරීම +- බාහිර ක්‍රමලේඛ අතුරුමුහුණත් භාවිත කිරීම (Stripe) +- විධාන පෙළ ඔස්සේ ඔබේ යෙදුම නිකුත් කිරීම + +ඉදිරි අන්තය සඳහා: + +- Create React App භාවිතයෙන් අපේ ව්‍යාපෘතිය සකසා ගැනීම +- Bootstrap භාවිතයෙන් කැමතිම අයිකන, අකුරු විලාසිතා සහ පරිශීලක අතුරුමුහුණත් කට්ටලයක් එක් කිරීම +- React-Router භාවිතයෙන් මාර්ග සැකසීම +- AWS Cognito SDK භාවිතයෙන් පරිශීලකයන් ලියාපදිංචි කිරීම සහ පිවිසවීම +- අපේ සටහන් කළමනාකරණය කිරීම සඳහා පසු අන්තය සඳහා වන සම්බන්ධකය +- ලේඛන උඩුගත කිරීම සඳහා AWS JS SDK භාවිත කිරීම +- React හි ණයපත් ගෙවීම් පිළිගැනීම +- Create React App හි පරිසරයන් +- Netlify භාවිතයෙන් ඔබේ ඉදිරි අන්තය නිකුත් කිරීම +- Netlify ඔස්සේ අභිරුචි වසම් වින්‍යාසකරණය + +පසු අන්ත නිකුත් කිරීම් ස්වයංක්‍රීයකරණය කිරීම: + +- කේතය ඔස්සේ DynamoDB සකසා ගැනීම +- කේතය ඔස්සේ S3 ගැළපෙන පරිදි සකසා ගැනීම +- කේතය ඔස්සේ Cognito පරිශීලක තටාකය ගැළපෙන පරිදි සකසා ගැනීම +- කේතය ඔස්සේ Cognito අනන්‍යතා තටාකය ගැළපෙන පරිදි සකසා ගැනීම +- Serverless ක්‍රමලේඛ රාමුවෙහි පාරිසරික විචල්‍යයන් +- Serverless ක්‍රමලේඛ රාමුවෙහි රහස්‍ය විචල්‍යයන් සමඟ කටයුතු කිරීම +- Serverless හි ඒකක පරීක්ෂාවන් +- Seed භාවිතයෙන් නිකුත් කිරීම් ස්වයංක්‍රීයකරණය කිරීම +- Seed ඔස්සේ අභිරුචි වසම් නාම ගැළපෙන පරිදි සකසා ගැනීම + +Serverless යෙදුම් පරීක්ෂාව සහ නිදොස්කරණය: + +- Sentry භාවිතයෙන් React හි දෝෂ වාර්තාකරණය සකසා ගැනීම +- React හි දෝෂ සීමාවක් ගැළපෙන පරිදි සකසා ගැනීම +- අපේ Serverless ක්‍රමලේඛ අතුරුමුහුණත් වලට දෝෂ සටහන්කරණය එකතු කිරීම +- පොදු Serverless දෝෂ සඳහා නිදොස්කරණ ක්‍රියා පිළිවෙල ආවරණය කිරීම + +මෙය ඔබට පිරිපුන් නිමාවෙන් යුත් පූර්ණ අන්ත Serverless යෙදුම් ගොඩනැගීම සඳහා හොඳ පදනමක් වනු ඇතැයි අපි සිතමු. පහත ඒවාට අමතරව අප විසින් ආවරණය කළ යුතු යයි ඔබට හැඟෙන වෙනයම් සංකල්ප හෝ තාක්ෂණයන් වේ නම්, නොපැකිලව අපගේ [පර්ෂදයන්හි]({{ site.forum_url }}) දී අප වෙත දැනගැනීමට සලස්වන්න. + +[Cognito]: https://aws.amazon.com/cognito/ +[CM]: https://aws.amazon.com/certificate-manager +[R53]: https://aws.amazon.com/route53/ +[CF]: https://aws.amazon.com/cloudfront/ +[S3]: https://aws.amazon.com/s3/ +[CloudWatch]: https://aws.amazon.com/cloudwatch/ +[Bootstrap]: http://getbootstrap.com +[RR]: https://github.com/ReactTraining/react-router +[React]: https://facebook.github.io/react/ +[DynamoDB]: https://aws.amazon.com/dynamodb/ +[APIG]: https://aws.amazon.com/api-gateway/ +[Lambda]: https://aws.amazon.com/lambda/ +[Stripe]: https://stripe.com +[Seed]: https://seed.run +[Netlify]: https://netlify.com +[GitHub]: https://github.com +[Sentry]: https://sentry.io diff --git a/_chapters/si/who-is-this-guide-for.md b/_chapters/si/who-is-this-guide-for.md new file mode 100644 index 0000000000..a3bb5386ce --- /dev/null +++ b/_chapters/si/who-is-this-guide-for.md @@ -0,0 +1,18 @@ +--- +layout: post +title: මෙම නිබන්ධනය කා සඳහා ද? +date: 2020-08-22 00:00:00 +lang: si +ref: who-is-this-guide-for +comments_id: who-is-this-guide-for/96 +--- + +මේ නිබන්ධනය serverless යෙදුම් හදන්න ඉගෙනගන්න කැමති මෘදුකාංග සංවර්ධකයන් වෙනුවෙන් නිර්මාණය කර තිබෙනවා. frontend සහ backend පැති දෙකම ආවරණය වෙන විදිහට පියවරෙන් පියවර අනුගමනය කළ හැකි මඟපෙන්වීමක් ලබා දීම තුළින් serverless යෙදුම් නිර්මාණය කිරීම පිළිබඳ විවිධ අංශ ආවරණය වනු ඇතැයි අපි බලාපොරොත්තු වෙනවා. මේ ගැන අන්තර්ජාලයේ විවිධ තැන්වල තවත් නිබන්ධන තිබුණත් මේ ආකාරයේ serverless යෙදුම් සංවර්ධනය පිළිබඳ සම්පූර්ණ නිබන්ධනයක් තිබීම ඉතා ප්‍රයෝජනවත්. මේ නිබන්ධනය මූලිකවම, serverless යෙදුම් සංවර්ධනය පිළිබද සම්මතයන් පැහැදිලි කිරීමට වඩා හුදෙක් serverless යෙදුම් සංවර්ධනය කර අන්තර්ජාලයේ පලකිරීම පිලිබඳ දැනගත යුතු සියලු තොරතුරු ආවරණය කිරීම අරමුණු කොට නිර්මාණය කොට තිබෙනවා. + +ඔබ සමහර විට backend පැත්ත දන්න, නමුත් serverless යෙදුම් වල frontend පැත්ත ගැන වැඩිදුර ඉගෙනගන්න කැමති කෙනෙක් වෙන්න පුළුවන්. එහෙමත් නැත්නම් frontend පැත්ත දන්න නමුත් backend පැත්ත ගැන වැඩිදුර දැනගන්න කැමති කෙනෙක් වෙන්නත් පුළුවන්. මේ කොහොම වුණත්, මේ නිබන්ධනය ඔබේ ඉගෙනුම් අවශ්‍යතා සම්පූර්ණ කරනු ඇති. + +දැනට මේ නිබන්ධනය සම්පූර්ණයෙන්ම නිර්මාණය කරලා තියෙන්නේ JavaScript සංවර්ධකයන් වෙනුවෙන්. අපි ඉස්සරහට වෙනත් පරිගණක භාෂා ගැනත් අවධානය යොමු කරාවි. නමුත් JavaScript යොදා ගැනීම හොද ආරම්භයක් කියලා අපි හිතනවා, මොකද JavaScript භාවිත කරලා සම්පුර්ණයෙන්ම වෙබ් අඩවියක ඉදිරි අන්තය (frontend) සහ පසු අන්තය (backend) යන දෙකම සංවර්ධනය කරන්න පුළුවන් නිසා. + +පුද්ගලිකව, serverless විදිහට වෙබ් යෙදුම් නිර්මාණය කිරීම අපි කළ දැවැන්ත අනාවරණයක්. ඒ නිසා අපි ඉගෙනගත් දේ බෙදාගත හැකි යමක් නිර්මාණය කරන්න අපිට අවශ්‍ය වුණා. ඔබට අපි ගැන [**මෙතනින්**]({{ site.sst_url }}) වැඩිදුර දැනගන්න පුළුවන් සහ මේ තාක්ෂණය භාවිත කරලා හදපු ආකෘති යෙදුම් ගැන [මෙතනින්]({% link showcase.md %}) ගිහින් බලන්නත් පුළුවන්. + +ඉතින්, මේ නිබන්ධනයෙන් ආවරණය වෙන්නේ මොනවද කියලා දැනගෙන ම අපි ඉගෙනීම ආරම්භ කරමු. diff --git a/_chapters/signup-with-aws-cognito.md b/_chapters/signup-with-aws-cognito.md index 515b3e8fec..577c0dd076 100644 --- a/_chapters/signup-with-aws-cognito.md +++ b/_chapters/signup-with-aws-cognito.md @@ -5,72 +5,84 @@ date: 2017-01-21 00:00:00 lang: en ref: signup-with-aws-cognito description: To implement a signup form in our React.js app using Amazon Cognito we are going to use AWS Amplify. We are going to call the Auth.signUp() method to sign a user up and call the Auth.confirmSignUp() method with the confirmation code to complete the process. -context: true comments_id: signup-with-aws-cognito/130 --- -Now let's go ahead and implement the `handleSubmit` and `handleConfirmationSubmit` methods and connect it up with our AWS Cognito setup. +Now let's go ahead and implement the `handleSubmit` and `handleConfirmationSubmit` functions and connect it up with our AWS Cognito setup. -Replace our `handleSubmit` and `handleConfirmationSubmit` methods in `src/containers/Signup.js` with the following. +{%change%} Replace our `handleSubmit` and `handleConfirmationSubmit` functions in `src/containers/Signup.tsx` with the following. -``` javascript -handleSubmit = async event => { +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); - - this.setState({ isLoading: true }); - + setIsLoading(true); try { const newUser = await Auth.signUp({ - username: this.state.email, - password: this.state.password - }); - this.setState({ - newUser + username: fields.email, + password: fields.password, }); + setIsLoading(false); + setNewUser(newUser); } catch (e) { - alert(e.message); + onError(e); + setIsLoading(false); } - - this.setState({ isLoading: false }); } -handleConfirmationSubmit = async event => { +async function handleConfirmationSubmit( + event: React.FormEvent +) { event.preventDefault(); - - this.setState({ isLoading: true }); - + setIsLoading(true); try { - await Auth.confirmSignUp(this.state.email, this.state.confirmationCode); - await Auth.signIn(this.state.email, this.state.password); - - this.props.userHasAuthenticated(true); - this.props.history.push("/"); + await Auth.confirmSignUp(fields.email, fields.confirmationCode); + await Auth.signIn(fields.email, fields.password); + userHasAuthenticated(true); + nav("/"); } catch (e) { - alert(e.message); - this.setState({ isLoading: false }); + onError(e); + setIsLoading(false); } } ``` -Also, include the Amplify Auth in our header. +{%change%} Also, include the Amplify `Auth`, `onError`, and `ISignUpResult` type in our header. -``` javascript +```tsx import { Auth } from "aws-amplify"; +import { onError } from "../lib/errorLib"; +import { ISignUpResult } from "amazon-cognito-identity-js"; +``` + +Let's use the right type for the new user object. + +{%change%} Replace the `const [newUser, setNewUser]` line with. + +```typescript +const [newUser, setNewUser] = useState(null); +``` + +Let's install the npm package. + +{%change%} Run the following **in the `packages/frontend/` directory**. + +```bash +$ npm install amazon-cognito-identity-js ``` The flow here is pretty simple: -1. In `handleSubmit` we make a call to signup a user. This creates a new user object. +1. In `handleSubmit` we make a call to signup a user using `Auth.signUp()`. This creates a new user object. -2. Save that user object to the state as `newUser`. +2. Save that user object to the state using `setNewUser`. -3. In `handleConfirmationSubmit` use the confirmation code to confirm the user. +3. In `handleConfirmationSubmit` use the confirmation code to confirm the user with `Auth.confirmSignUp()`. 4. With the user now confirmed, Cognito now knows that we have a new user that can login to our app. -5. Use the email and password to authenticate exactly the same way we did in the login page. +5. Use the email and password to authenticate exactly the same way we did in the login page. By calling `Auth.signIn()`. -6. Update the App's state using the `userHasAuthenticated` method. +6. Update the App's context using the `userHasAuthenticated` function. 7. Finally, redirect to the homepage. @@ -80,9 +92,9 @@ Now if you were to switch over to your browser and try signing up for a new acco A quick note on the signup flow here. If the user refreshes their page at the confirm step, they won't be able to get back and confirm that account. It forces them to create a new account instead. We are keeping things intentionally simple but here are a couple of hints on how to fix it. -1. Check for the `UsernameExistsException` in the `handleSubmit` method's `catch` block. +1. Check for the `UsernameExistsException` in the `handleSubmit` function's `catch` block. -2. Use the `Auth.resendSignUp()` method to resend the code if the user has not been previously confirmed. Here is a link to the [Amplify API docs](https://aws.github.io/aws-amplify/api/classes/authclass.html#resendsignup). +2. Use the `Auth.resendSignUp()` method to resend the code if the user has not been previously confirmed. Here is a link to the [Amplify API docs](https://aws.github.io/aws-amplify/api/classes/authclass.html#resendsignup){:target="_blank"}. 3. Confirm the code just as we did before. @@ -92,13 +104,15 @@ Now while developing you might run into cases where you need to manually confirm ```bash aws cognito-idp admin-confirm-sign-up \ - --region YOUR_COGNITO_REGION \ - --user-pool-id YOUR_COGNITO_USER_POOL_ID \ - --username YOUR_USER_EMAIL + --region \ + --user-pool-id \ + --username ``` -Just be sure to use your Cognito User Pool Id and the email you used to create the account. +Just be sure to use your Cognito `USER_POOL_ID` and the _email address_ you used to create the account. -If you would like to allow your users to change their email or password, you can refer to our [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}). +{%info%} +If you would like to allow your users to change their email or password, you can refer to our [Extra Credit series of chapters on user management]({% link _archives/manage-user-accounts-in-aws-amplify.md %}){:target="_blank"}. +{%endinfo%} Next up, we are going to create our first note. diff --git a/_chapters/staying-up-to-date.md b/_chapters/staying-up-to-date.md index aee10b22e0..e344ca907c 100644 --- a/_chapters/staying-up-to-date.md +++ b/_chapters/staying-up-to-date.md @@ -2,21 +2,21 @@ layout: post title: Staying up to date lang: en -description: Keep up-to-date with Serverless Stack by subscribing to our email newsletter. +description: Keep up-to-date with SST by subscribing to our email newsletter. date: 2018-04-02 00:00:00 ref: staying-up-to-date comments_id: staying-up-to-date/28 --- We made this guide open source to make sure that the content is kept up to date and accurate with the help of the community. We are also adding new chapters based on the needs of the community and the feedback we receive. -To help people stay up to date with the changes, we run the Serverless Stack newsletter. The newsletter is a: +To help people stay up to date with the changes, we run the [SST Newsletter]({% link newsletter.md %}). The newsletter is a: - Short plain text email -- Outlines the recent updates to Serverless Stack -- Never sent out more than once a week +- Outlines the recent updates to SST +- Never sent out more than once a month - One click unsubscribe -- And you get the entire guide as a 400 page PDF +- And you get the entire guide as a 300 page ebook You can also follow us on Twitter. -Subscribe +Subscribe diff --git a/_chapters/test-the-billing-api.md b/_chapters/test-the-billing-api.md deleted file mode 100644 index 0459667f9e..0000000000 --- a/_chapters/test-the-billing-api.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -layout: post -title: Test the Billing API -date: 2018-03-09 00:00:00 -lang: en -description: To test our Serverless Stripe billing API, we are going to mock the Lambda HTTP event. Pass in the Stripe test token and call the "serverless invoke local" command. -ref: test-the-billing-api -comments_id: test-the-billing-api/172 ---- - -Now that we have our billing API all set up, let's do a quick test in our local environment. - -Create a `mocks/billing-event.json` file and add the following. - -``` json -{ - "body": "{\"source\":\"tok_visa\",\"storage\":21}", - "requestContext": { - "identity": { - "cognitoIdentityId": "USER-SUB-1234" - } - } -} -``` - -We are going to be testing with a Stripe test token called `tok_visa` and with `21` as the number of notes we want to store. You can read more about the Stripe test cards and tokens in the [Stripe API Docs here](https://stripe.com/docs/testing#cards). - -Let's now invoke our billing API by running the following in our project root. - -``` bash -$ serverless invoke local --function billing --path mocks/billing-event.json -``` - -The response should look similar to this. - -``` json -{ - "statusCode": 200, - "headers": { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": true - }, - "body": "{\"status\":true}" -} -``` - -### Commit the Changes - -Let's commit these to Git. - -``` bash -$ git add . -$ git commit -m "Adding a mock event for the billing API" -``` - -Now that we have our new billing API ready. Let's look at how to setup unit tests to ensure that our business logic has been configured correctly. diff --git a/_chapters/test-the-configured-apis.md b/_chapters/test-the-configured-apis.md deleted file mode 100644 index 9297f64e43..0000000000 --- a/_chapters/test-the-configured-apis.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -layout: post -title: Test the Configured APIs -date: 2018-03-16 00:00:00 -lang: en -description: Test the Serverless APIs that have been deployed using Seed. To do so use the "aws-api-gateway-cli-test" NPM package and test both the production and dev environments. -ref: test-the-configured-apis -comments_id: test-the-configured-apis/179 ---- - -Now we have two sets of APIs (prod and dev), let's quickly test them to make sure they are working fine before we plug our frontend into them. Back in the [Test the APIs]({% link _chapters/test-the-apis.md %}) chapter, we used a simple utility called [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test). - -Before we do the test let's create a test user for both the environments. We'll be following the exact same steps as the [Create a Cognito test user]({% link _chapters/create-a-cognito-test-user.md %}) chapter. - -### Create Test Users - -We are going to use the AWS CLI for this. - -In your terminal, run. - -``` bash -$ aws cognito-idp sign-up \ - --region YOUR_DEV_COGNITO_REGION \ - --client-id YOUR_DEV_COGNITO_APP_CLIENT_ID \ - --username admin@example.com \ - --password Passw0rd! -``` - -Refer back to the [Deploying through Seed]({% link _chapters/deploying-through-seed.md %}) chapter to look up the **dev** version of your Cognito App Client Id. And replace `YOUR_DEV_COGNITO_REGION` with the region that you deployed to. - -Next we'll confirm the user through the Cognito Admin CLI. - -``` bash -$ aws cognito-idp admin-confirm-sign-up \ - --region YOUR_DEV_COGNITO_REGION \ - --user-pool-id YOUR_DEV_COGNITO_USER_POOL_ID \ - --username admin@example.com -``` - -Again, replace `YOUR_DEV_COGNITO_USER_POOL_ID` with the **dev** version of your Cognito User Pool Id from the [Deploying through Seed]({% link _chapters/deploying-through-seed.md %}) chapter and the region from the previous command. - -Let's quickly do the same with **prod** versions as well. - -In your terminal, run. - -``` bash -$ aws cognito-idp sign-up \ - --region YOUR_PROD_COGNITO_REGION \ - --client-id YOUR_PROD_COGNITO_APP_CLIENT_ID \ - --username admin@example.com \ - --password Passw0rd! -``` - -Here use your prod version of your Cognito details. - -And confirm the user. - -``` bash -$ aws cognito-idp admin-confirm-sign-up \ - --region YOUR_PROD_COGNITO_REGION \ - --user-pool-id YOUR_PROD_COGNITO_USER_POOL_ID \ - --username admin@example.com -``` - -Make sure to use the prod versions here as well. - -Now we are ready to test our APIs. - -### Test the API - -Let's test our dev endpoint. Run the following command: - -``` bash -$ npx aws-api-gateway-cli-test \ ---username='admin@example.com' \ ---password='Passw0rd!' \ ---user-pool-id='YOUR_DEV_COGNITO_USER_POOL_ID' \ ---app-client-id='YOUR_DEV_COGNITO_APP_CLIENT_ID' \ ---cognito-region='YOUR_DEV_COGNITO_REGION' \ ---identity-pool-id='YOUR_DEV_IDENTITY_POOL_ID' \ ---invoke-url='YOUR_DEV_API_GATEWAY_URL' \ ---api-gateway-region='YOUR_DEV_API_GATEWAY_REGION' \ ---path-template='/notes' \ ---method='POST' \ ---body='{"content":"hello world","attachment":"hello.jpg"}' -``` - -Refer back to the [Deploying through Seed]({% link _chapters/deploying-through-seed.md %}) chapter for these: - -- `YOUR_DEV_COGNITO_USER_POOL_ID` and `YOUR_DEV_COGNITO_APP_CLIENT_ID` are all related to your Cognito User Pool. -- `YOUR_DEV_IDENTITY_POOL_ID` is for your Cognito Identity Pool. -- And `YOUR_DEV_API_GATEWAY_URL` is your API Gateway endpoint. It looks something likes this `https://ly55wbovq4.execute-api.us-east-1.amazonaws.com/dev`. But if you have configured it with a custom domain use the one from the [Set custom domains through Seed]({% link _chapters/set-custom-domains-through-seed.md %}) chapter. -- Finally, the `YOUR_DEV_API_GATEWAY_REGION` and `YOUR_DEV_COGNITO_REGION` is the region you deployed to. In our case it is `us-east-1`. - -If the command is successful, it'll look something like this. - -``` bash -Authenticating with User Pool -Getting temporary credentials -Making API request -{ status: 200, - statusText: 'OK', - data: - { userId: 'us-east-1:9bdc031d-ee9e-4ffa-9a2d-123456789', - noteId: '8f7da030-650b-11e7-a661-123456789', - content: 'hello world', - attachment: 'hello.jpg', - createdAt: 1499648598452 } } -``` - -Also run the same command for prod. Make sure to use the prod versions. - -``` bash -$ npx aws-api-gateway-cli-test \ ---username='admin@example.com' \ ---password='Passw0rd!' \ ---user-pool-id='YOUR_PROD_COGNITO_USER_POOL_ID' \ ---app-client-id='YOUR_PROD_COGNITO_APP_CLIENT_ID' \ ---cognito-region='YOUR_PROD_COGNITO_REGION' \ ---identity-pool-id='YOUR_PROD_IDENTITY_POOL_ID' \ ---invoke-url='YOUR_PROD_API_GATEWAY_URL' \ ---api-gateway-region='YOUR_PROD_API_GATEWAY_REGION' \ ---path-template='/notes' \ ---method='POST' \ ---body='{"content":"hello world","attachment":"hello.jpg"}' -``` - -And you should see it give a similar output as dev. - -Now that our APIs our tested we are ready to plug these into our frontend. But before we do that, let's do a quick test to see what will happen if we make a mistake and push some faulty code to production. diff --git a/_chapters/translations.md b/_chapters/translations.md index 933d511f14..8fde3573ae 100644 --- a/_chapters/translations.md +++ b/_chapters/translations.md @@ -9,9 +9,9 @@ comments_id: comments-translations/788 Our guide is available in several languages thanks to contributions by our incredible readers. You can view the translated versions of a chapter by clicking on the links below the chapter title. -![Chapter translation links Screenshot](/assets/chapter-translation-links.png) +![Chapter translation links Screenshot](/assets/part2/chapter-translation-links.png) -Below is a list of all the chapters that are available in multiple languages. If you are interested in helping with our translation efforts, leave us a [comment here](https://discourse.serverless-stack.com/t/help-us-translate-serverless-stack/596/15). +Below is a list of all the chapters that are available in multiple languages. If you are interested in helping with our translation efforts, leave us a [comment here](https://discourse.sst.dev/t/help-us-translate-serverless-stack/596/15). --- @@ -33,7 +33,7 @@ Below is a list of all the chapters that are available in multiple languages. If --- -A big thanks to our contributors for helping make Serverless Stack more accessible! +A big thanks to our contributors for helping make SST more accessible! - [Bernardo Bugmann](https://github.com/bernardobugmann) - [Sebastian Gutierrez](https://github.com/pepas24) diff --git a/_chapters/unexpected-errors-in-lambda-functions.md b/_chapters/unexpected-errors-in-lambda-functions.md new file mode 100644 index 0000000000..56b8a10e8c --- /dev/null +++ b/_chapters/unexpected-errors-in-lambda-functions.md @@ -0,0 +1,147 @@ +--- +layout: post +title: Unexpected Errors in Lambda Functions +date: 2020-04-06 00:00:00 +lang: en +description: In this chapter we look at how to debug unexpected errors in your Lambda functions for your serverless app. These errors include timeout errors and when your Lambda function runs out of memory. +comments_id: unexpected-errors-in-lambda-functions/1735 +ref: unexpected-errors-in-lambda-functions +--- + +Previously, we looked at [how to debug errors in our Lambda function code]({% link _chapters/logic-errors-in-lambda-functions.md %}). In this chapter let's look at how to debug some unexpected errors. Starting with the case of a Lambda function timing out. + +### Debugging Lambda Timeouts + +Our Lambda functions often make API requests to interact with other services. In our notes app, we talk to DynamoDB to store and fetch data; and we also talk to Stripe to process payments. When we make an API request, there is the chance the HTTP connection times out or the remote service takes too long to respond. We are going to look at how to detect and debug the issue. The default timeout for Lambda functions are 6 seconds. So let's simulate a timeout using `setTimeout`. + +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of + // the item to be retrieved + Key: { + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Set a timeout + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Return the retrieved item + return result.Item; +}); +``` + +{%change%} Let's commit this code. + +```bash +$ git add . +$ git commit -m "Adding a timeout" +$ git push +``` + +Head over to your Seed dashboard, select the **prod** stage in the pipeline and deploy the `debug` branch. + +![Deploy debug branch in Seed](/assets/monitor-debug-errors/deploy-debug-branch-in-seed.png) + +On your notes app, try and select a note. You will notice the page tries to load for a couple of seconds, and then fails with an error alert. + +![Timeout error in notes app note page](/assets/monitor-debug-errors/timeout-error-in-notes-app-note-page.png) + +You'll get an error alert in Sentry. And if you head over to the **Issues** tab in Seed you'll notice a new error — `Lambda Timeout Error`. + +If you click on the new error, you'll notice that the request took 6006.18ms. And since the Lambda timeout is 6 seconds by default. This means that the function timed out. + +![Timeout error details in Seed](/assets/monitor-debug-errors/timeout-error-details-in-seed.png) + +To drill into this issue further, add a `console.log` in your Lambda function. This messages will show in the request log and it'll give you a sense of where the timeout is taking place. + +Next let's look at what happens when our Lambda function runs out of memory. + +### Debugging Out of Memory Errors + +By default, a Lambda function has 1024MB of memory. You can assign any amount of memory between 128MB and 3008MB in 64MB increments. So in our code, let's try and allocate more memory till it runs out of memory. + +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +function allocMem():Array { + let bigList: Array = Array(4096000).fill(1); + return bigList.concat(allocMem()); +} + +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of + // the item to be retrieved + Key: { + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + allocMem(); + + // Return the retrieved item + return result.Item; +}); +``` + +Now we'll set our Lambda function to use the lowest memory allowed. + +{%change%} Add the following below the `defaults: {` line in your `stacks/ApiStack.ts`. + +```typescript +memorySize: 128, +``` + +{%change%} Let's commit this. + +```bash +$ git add . +$ git commit -m "Adding a memory error" +$ git push +``` + +Head over to your Seed dashboard and deploy it. Then, in your notes app, try and load a note. It should fail with an error alert. + +Just as before, you'll see the error in Sentry. And head over to new issue in Seed. + + !`[Memory error details in Seed]`(/assets/monitor-debug-errors/memory-error-details-in-seed.png) + +Note the request took all of 128MB of memory. Click to expand the request. + +You'll see `exited with error: signal: killed Runtime.ExitError`. This is printed out by Lambda runtime indicating the runtime was killed. This means that you should give your function more memory or that your code is leaking memory. + +Next, we'll look at how to debug errors that happen outside your Lambda function handler code. diff --git a/_chapters/unit-tests-in-serverless.md b/_chapters/unit-tests-in-serverless.md index 6cd8e5d80c..f3556013ec 100644 --- a/_chapters/unit-tests-in-serverless.md +++ b/_chapters/unit-tests-in-serverless.md @@ -1,55 +1,32 @@ --- layout: post title: Unit Tests in Serverless -date: 2018-03-10 00:00:00 +date: 2021-07-17 00:00:00 lang: en -description: To test our business logic in Serverless, we will use Jest to add unit tests to our project. We can run these tests using the "npm test" command. -context: true -code: backend +description: In this chapter we look at how to write unit tests in our serverless apps using SST's CLI. ref: unit-tests-in-serverless comments_id: unit-tests-in-serverless/173 --- -So we have some simple business logic that figures out exactly how much to charge our user based on the number of notes they want to store. We want to make sure that we test all the possible cases for this before we start charging people. To do this we are going to configure unit tests for our Serverless Framework project. +In this chapter we'll look at how to write unit tests for our serverless app. Typically you might want to test some of your _business logic_. -We are going to use [Jest](https://facebook.github.io/jest/) for this and it is already a part of [our starter project](https://github.com/AnomalyInnovations/serverless-nodejs-starter). +The template we are using comes with a setup to help with that. It uses [Vitest](https://vitest.dev){:target="_blank"} for this. -However, if you are starting a new Serverless Framework project. Add Jest to your dev dependencies by running the following. +### Writing Tests -``` bash -$ npm install --save-dev jest -``` - -And update the `scripts` block in your `package.json` with the following: - -``` -"scripts": { - "test": "jest" -}, -``` - -This will allow you to run your tests using the command `npm test`. - -Alternatively, if you are using the [serverless-bundle](https://github.com/AnomalyInnovations/serverless-bundle) plugin to package your functions, it comes with a built-in script to transpile your code and run your tests. Add the following to your `package.json` instead. - -``` -"scripts": { - "test": "serverless-bundle test" -}, -``` - -### Add Unit Tests +We are going to test the business logic that we added in the [previous chapter]({% link _chapters/add-an-api-to-handle-billing.md %}) to compute how much to bill a user. -Now create a new file in `tests/billing.test.js` and add the following. +{%change%} Create a new file in `packages/core/src/billing/test/index.test.ts` and add the following. -``` js -import { calculateCost } from "../libs/billing-lib"; +```ts +import { test, expect } from "vitest"; +import { Billing } from "../"; test("Lowest tier", () => { const storage = 10; const cost = 4000; - const expectedCost = calculateCost(storage); + const expectedCost = Billing.compute(storage); expect(cost).toEqual(expectedCost); }); @@ -58,7 +35,7 @@ test("Middle tier", () => { const storage = 100; const cost = 20000; - const expectedCost = calculateCost(storage); + const expectedCost = Billing.compute(storage); expect(cost).toEqual(expectedCost); }); @@ -67,54 +44,52 @@ test("Highest tier", () => { const storage = 101; const cost = 10100; - const expectedCost = calculateCost(storage); + const expectedCost = Billing.compute(storage); expect(cost).toEqual(expectedCost); }); ``` -This should be straightforward. We are adding 3 tests. They are testing the different tiers of our pricing structure. We test the case where a user is trying to store 10, 100, and 101 notes. And comparing the calculated cost to the one we are expecting. You can read more about using Jest in the [Jest docs here](https://facebook.github.io/jest/docs/en/getting-started.html). +This should be straightforward. We are adding 3 tests. They are testing the different tiers of our pricing structure. We test the case where a user is trying to store 10, 100, and 101 notes. And comparing the calculated cost to the one we are expecting. + +### Run Tests -### Run tests +Now let's run these tests. -And we can run our tests by using the following command in the root of our project. +{%change%} Run the following in the **`packages/core/` directory**. -``` bash +```bash $ npm test ``` You should see something like this: -``` bash - PASS tests/billing.test.js - ✓ Lowest tier (4ms) +```bash +✓ src/billing/test/index.test.ts (3) + ✓ Lowest tier ✓ Middle tier - ✓ Highest tier (1ms) + ✓ Highest tier -Test Suites: 1 passed, 1 total -Tests: 3 passed, 3 total -Snapshots: 0 total -Time: 1.665s -Ran all test suites. +Test Files 1 passed (1) + Tests 3 passed (3) ``` -And that's it! We have unit tests all configured. +Internally this is running `sst shell vitest`. The [`sst shell`]({{ site.sst_url }}/docs/reference/cli/#shell){:target="_blank"} CLI connects any linked resources. This ensures that your tests have the same kind of access as the rest of your application code. + +{%info%} +You'll need to Ctrl-C to quit the test runner. It's useful to have when you are working on them as it'll reload your tests. +{%endinfo%} + +And that's it! We have unit tests all configured. These tests are fairly simple but should give you an idea of how to add more in the future. ### Commit the Changes -Let's commit these changes. +{%change%} Let's commit our changes and push it to GitHub. -``` bash +```bash $ git add . $ git commit -m "Adding unit tests" -``` - -### Push the Changes - -We are done making changes to our project, so let's go ahead and push them to GitHub. - -``` bash $ git push ``` -Next we'll use our Git repo to automate our deployments. This will ensure that when we push our changes to Git, it will run our tests, and deploy them for us automatically. We'll also learn to configure multiple environments. +Now we are almost ready to move on to our frontend. But before we do, we need to ensure that our backend is configured so that our React app will be able to connect to it. diff --git a/_chapters/update-the-app.md b/_chapters/update-the-app.md deleted file mode 100644 index f79ace32a0..0000000000 --- a/_chapters/update-the-app.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -layout: post -title: Update the App -date: 2017-02-13 00:00:00 -lang: en -description: Tutorial on how to make changes to your React.js single page application. -code: frontend_part1 -comments_id: comments-for-update-the-app/43 -ref: update-the-app ---- - -Let's make a couple of quick changes to test the process of deploying updates to our app. - -We are going to add a Login and Signup button to our lander to give users a clear call to action. - -To do this update our `renderLander` method in `src/containers/Home.js`. - -``` coffee -renderLander() { - return ( -
-

Scratch

-

A simple note taking app

-
- - Login - - - Signup - -
-
- ); -} -``` - -And import the `Link` component from React-Router in the header. - -``` javascript -import { Link } from "react-router-dom"; -``` - -Also, add a couple of styles to `src/containers/Home.css`. - -``` css -.Home .lander div { - padding-top: 20px; -} -.Home .lander div a:first-child { - margin-right: 20px; -} -``` - -And our lander should look something like this. - -![App updated lander screenshot](/assets/app-updated-lander.png) - -Next, let's deploy these updates. diff --git a/_chapters/upload-a-file-to-s3.md b/_chapters/upload-a-file-to-s3.md index c43211322d..d4a8a56fe8 100644 --- a/_chapters/upload-a-file-to-s3.md +++ b/_chapters/upload-a-file-to-s3.md @@ -5,39 +5,31 @@ date: 2017-01-24 00:00:00 lang: en ref: upload-a-file-to-s3 description: We want users to be able to upload a file in our React.js app and add it as an attachment to their note. To upload files to S3 directly from our React.js app we are going to use AWS Amplify's Storage.put() method. -context: true comments_id: comments-for-upload-a-file-to-s3/123 --- Let's now add an attachment to our note. The flow we are using here is very simple. 1. The user selects a file to upload. -2. The file is uploaded to S3 under the user's folder and we get a key back. +2. The file is uploaded to S3 under the user's folder and we get a key back. 3. Create a note with the file key as the attachment. -We are going to use the Storage module that AWS Amplify has. If you recall, that back in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}) chapter we allow a logged in user access to a folder inside our S3 Bucket. AWS Amplify stores directly to this folder if we want to *privately* store a file. +We are going to use the Storage module that AWS Amplify has. If you recall, that back in the [Create a Cognito identity pool]({% link _archives/create-a-cognito-identity-pool.md %}){:target="_blank"} chapter we allow a logged in user access to a folder inside our S3 Bucket. AWS Amplify stores directly to this folder if we want to _privately_ store a file. Also, just looking ahead a bit; we will be uploading files when a note is created and when a note is edited. So let's create a simple convenience method to help with that. - ### Upload to S3 -Create a `src/libs/` directory for this. - -``` bash -$ mkdir src/libs/ -``` +{%change%} Create `src/lib/awsLib.ts` and add the following: -Add the following to `src/libs/awsLib.js`. - -``` coffee +```typescript import { Storage } from "aws-amplify"; -export async function s3Upload(file) { +export async function s3Upload(file: File) { const filename = `${Date.now()}-${file.name}`; const stored = await Storage.vault.put(filename, file, { - contentType: file.type + contentType: file.type, }); return stored.key; @@ -58,40 +50,41 @@ The above method does a couple of things. Now that we have our upload methods ready, let's call them from the create note method. -Replace the `handleSubmit` method in `src/containers/NewNote.js` with the following. +{%change%} Replace the `handleSubmit` method in `src/containers/NewNote.tsx` with the following. -``` javascript -handleSubmit = async event => { +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); - if (this.file && this.file.size > config.MAX_ATTACHMENT_SIZE) { - alert(`Please pick a file smaller than ${config.MAX_ATTACHMENT_SIZE/1000000} MB.`); + if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) { + alert( + `Please pick a file smaller than ${ + config.MAX_ATTACHMENT_SIZE / 1000000 + } MB.` + ); return; } - this.setState({ isLoading: true }); + setIsLoading(true); try { - const attachment = this.file - ? await s3Upload(this.file) - : null; - - await this.createNote({ - attachment, - content: this.state.content - }); - this.props.history.push("/"); + const attachment = file.current + ? await s3Upload(file.current) + : undefined; + + await createNote({ content, attachment }); + nav("/"); } catch (e) { - alert(e); - this.setState({ isLoading: false }); + onError(e); + setIsLoading(false); } } ``` -And make sure to include `s3Upload` by adding the following to the header of `src/containers/NewNote.js`. +{%change%} And make sure to include `s3Upload` by adding the following to the header of `src/containers/NewNote.tsx`. -``` javascript -import { s3Upload } from "../libs/awsLib"; +```tsx +import { s3Upload } from "../lib/awsLib"; ``` The change we've made in the `handleSubmit` is that: diff --git a/_chapters/use-environment-variables-in-lambda-functions.md b/_chapters/use-environment-variables-in-lambda-functions.md deleted file mode 100644 index db0b2b3540..0000000000 --- a/_chapters/use-environment-variables-in-lambda-functions.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -layout: post -title: Use Environment Variables in Lambda Functions -date: 2018-03-03 00:00:00 -lang: en -description: To configure our Serverless Framework project to deploy our resources as infrastructure as code, we need to make a couple of changes to our Lambda functions. Instead of hardcoding the resources, we can reference them using the "process.env" variable. -ref: use-environment-variables-in-lambda-functions -comments_id: use-environment-variables-in-lambda-functions/166 ---- - -Back in the [Configure DynamoDB in Serverless]({% link _chapters/configure-dynamodb-in-serverless.md %}) chapter, we are creating our table through CloudFormation. The table that is created is based on the stage we are currently in. This means that in our Lambda functions when we talk to our database, we cannot simply hard code the table names. Since, in the `dev` stage it would be called `dev-notes` and in `prod` it'll be called `prod-notes`. - -This requires us to use environment variables in our Lambda functions to figure out which table we should be talking to. Currently, if you pull up `create.js` you'll notice the following section. - -``` js -const params = { - TableName: "notes", - Item: { - userId: event.requestContext.identity.cognitoIdentityId, - noteId: uuid.v1(), - content: data.content, - attachment: data.attachment, - createdAt: new Date().getTime() - } -}; -``` - -We need to change the `TableName: "notes"` line to use the relevant table name. In the [Configure DynamoDB in Serverless]({% link _chapters/configure-dynamodb-in-serverless.md %}) chapter, we also added `tableName:` to our `serverless.yml` under the `environment:` block. - -``` yml -# These environment variables are made available to our functions -# under process.env. -environment: - tableName: ${self:custom.tableName} -``` - -As we noted there, we can reference this in our Lambda functions as `process.env.tableName`. - -So let's go ahead and make the change. - -Replace this line in `create.js`. - -``` -TableName: "notes", -``` - -with this: - -``` -TableName: process.env.tableName, -``` - -Similarly, in the `get.js`, replace this: - -``` -TableName: "notes", -``` - -with this: - -``` -TableName: process.env.tableName, -``` - -In `list.js`, replace this: - -``` -TableName: "notes", -``` - -with this: - -``` -TableName: process.env.tableName, -``` - -Also in `update.js`, replace this: - -``` -TableName: "notes", -``` - -with this: - -``` -TableName: process.env.tableName, -``` - -Finally in `delete.js`, replace this: - -``` -TableName: "notes", -``` - -with this: - -``` -TableName: process.env.tableName, -``` - -### Commit Your Code - -Make sure to commit your code using: - -``` bash -$ git add . -$ git commit -m "Use environment variables in our functions" -``` - -Next let's deploy our newly configured serverless backend API. diff --git a/_chapters/use-the-redirect-routes.md b/_chapters/use-the-redirect-routes.md index 3bc3765d56..14589e4bb1 100644 --- a/_chapters/use-the-redirect-routes.md +++ b/_chapters/use-the-redirect-routes.md @@ -4,39 +4,75 @@ title: Use the Redirect Routes date: 2017-02-03 00:00:00 lang: en redirect_from: /chapters/use-the-hoc-in-the-routes.html -description: In our React.js app we can use the AuthenticatedRoute and UnauthenticatedRoute in place of the Routes that we want secured. We’ll do this inside React Router v4’s Switch component. -context: true +description: In our React.js app we can use the AuthenticatedRoute and UnauthenticatedRoute in place of the Routes that we want secured. We’ll do this inside React Router v6’s Routes component. comments_id: use-the-redirect-routes/152 ref: use-the-redirect-routes --- Now that we created the `AuthenticatedRoute` and `UnauthenticatedRoute` in the last chapter, let's use them on the containers we want to secure. -First import them in the header of `src/Routes.js`. +{%change%} First, we switch to our new redirect routes. -``` javascript -import AuthenticatedRoute from "./components/AuthenticatedRoute"; -import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; +So the following routes in `src/Routes.tsx` would be affected. + +```tsx +} /> +} /> +} /> +} /> +} /> ``` -Next, we simply switch to our new redirect routes. +{%change%} They should now look like so: -So the following routes in `src/Routes.js` would be affected. +```tsx + + + + } +/> + + + + } +/> + + + + } +/> + + + + } +/> -``` coffee - - - - + + + + } +/> ``` +{%change%} Then import them in the header of `src/Routes.tsx`. -They should now look like so: - -``` coffee - - - - +```tsx +import AuthenticatedRoute from "./components/AuthenticatedRoute.tsx"; +import UnauthenticatedRoute from "./components/UnauthenticatedRoute.tsx"; ``` And now if we tried to load a note page while not logged in, we would be redirected to the login page with a reference to the note page. diff --git a/_chapters/what-does-this-guide-cover.md b/_chapters/what-does-this-guide-cover.md index 36c95f3b20..3692d66c93 100644 --- a/_chapters/what-does-this-guide-cover.md +++ b/_chapters/what-does-this-guide-cover.md @@ -5,17 +5,22 @@ title: What Does This Guide Cover? date: 2016-12-22 00:00:00 lang: en ref: what-does-this-guide-cover -context: true comments_id: what-does-this-guide-cover/83 --- -To step through the major concepts involved in building web applications, we are going to be building a simple note taking app called [**Scratch**](https://demo2.serverless-stack.com). +To step through the major concepts involved in building web applications, we are going to be building a simple note taking app called [**Scratch**]({{ site.demo_url }}){:target="_blank"}. -![Completed app desktop screenshot](/assets/completed-app-desktop.png) +However, unlike most tutorials out there, our goal is to go into the details of what it takes to build a full-stack application for production. -Completed app mobile screenshot +### Demo App -It is a single page application powered by a serverless API written completely in JavaScript. Here is the complete source for the [backend]({{ site.backend_github_repo }}) and the [frontend]({{ site.frontend_github_repo }}). It is a relatively simple application but we are going to address the following requirements. +The demo app is a single page application powered by a serverless API written completely in TypeScript. + +[![Completed app desktop screenshot](/assets/completed-app-desktop.png)]({{ site.demo_url }}){:target="_blank"} + +![Completed app mobile screenshot](/assets/completed-app-mobile.png){: width="432" } + +It is a relatively simple application but we are going to address the following requirements. - Should allow users to signup and login to their accounts - Users should be able to create notes with some content @@ -26,109 +31,88 @@ It is a single page application powered by a serverless API written completely i - App should be served over HTTPS on a custom domain - The backend APIs need to be secure - The app needs to be responsive +- The app should be deployed when we `git push` + +#### Demo Source + +Here is the complete source of the app we will be building. We recommend bookmarking it and use it as a reference. + +- [**Demo source**]({{ site.sst_demo_repo }}){:target="_blank"} -We'll be using the AWS Platform to build it. We might expand further and cover a few other platforms but we figured the AWS Platform would be a good place to start. +We will be using the AWS Platform to build it. We might expand further and cover a few other platforms but we figured the AWS Platform would be a good place to start. ### Technologies & Services -We'll be using the following set of technologies and services to build our serverless application. - -- [Lambda][Lambda] & [API Gateway][APIG] for our serverless API -- [DynamoDB][DynamoDB] for our database -- [Cognito][Cognito] for user authentication and securing our APIs -- [S3][S3] for hosting our app and file uploads -- [CloudFront][CF] for serving out our app -- [Route 53][R53] for our domain -- [Certificate Manager][CM] for SSL -- [React.js][React] for our single page app -- [React Router][RR] for routing -- [Bootstrap][Bootstrap] for the UI Kit -- [Stripe][Stripe] for processing credit card payments -- [Seed][Seed] for automating Serverless deployments -- [Netlify][Netlify] for automating React deployments -- [GitHub][GitHub] for hosting our project repos. +We will be using the following set of technologies and services to build our serverless application. + +- [AWS](https://aws.amazon.com) + - [S3][S3]{:target="_blank"} for file uploads + - [DynamoDB][DynamoDB]{:target="_blank"} for our database + - [Lambda][Lambda]{:target="_blank"} & [API Gateway][APIG]{:target="_blank"} for our serverless API + - [Cognito][Cognito]{:target="_blank"} for user authentication and management +- [React][React]{:target="_blank"} for our frontend + - [Bootstrap][Bootstrap]{:target="_blank"} for the UI Kit + - [React Router][RR]{:target="_blank"} for routing + - [Vite][Vite]{:target="_blank"} for building our single page app +- [Vitest][Vitest]{:target="_blank"} for our unit tests +- [GitHub][GitHub]{:target="_blank"} for hosting our project repos +- [Stripe][Stripe]{:target="_blank"} for processing credit card payments We are going to be using the **free tiers** for the above services. So you should be able to sign up for them for free. This of course does not apply to purchasing a new domain to host your app. Also for AWS, you are required to put in a credit card while creating an account. So if you happen to be creating resources above and beyond what we cover in this tutorial, you might end up getting charged. -While the list above might look daunting, we are trying to ensure that upon completing the guide you'll be ready to build **real-world**, **secure**, and **fully-functional** web apps. And don't worry we'll be around to help! +While the list above might look daunting, we are trying to ensure that upon completing the guide you will be ready to build **real-world**, **secure**, and **fully-functional** web apps. And don't worry we will be around to help! ### Requirements -You need [Node v8.10+ and NPM v5.5+](https://nodejs.org/en/). You also need to have basic knowledge of how to use the command line. +You just need a couple of things to work through this guide: -### How This Guide Is Structured - -The guide is split into two separate parts. They are both relatively standalone. The first part covers the basics while the second covers a couple of advanced topics along with a way to automate the setup. We launched this guide in early 2017 with just the first part. The Serverless Stack community has grown and many of our readers have used the setup described in this guide to build apps that power their businesses. - -So we decided to extend the guide and add a second part to it. This is targeting folks that are intending to use this setup for their projects. It automates all the manual steps from part 1 and helps you create a production ready workflow that you can use for all your serverless projects. Here is what we cover in the two parts. +- [Node v20 and npm v10](https://nodejs.org/en/download/package-manager){:target="_blank"} +- A [GitHub account](https://github.com/join){:target="_blank"} +- Basic knowledge of JavaScript and TypeScript +- And basic knowledge of how to use the command line -#### Part I +### How This Guide Is Structured -Create the notes application and deploy it. We cover all the basics. Each service is created by hand. Here is what is covered in order. +The guide is split roughly into a couple of parts: For the backend: - Configure your AWS account - Create your database using DynamoDB - Set up S3 for file uploads -- Set up Cognito User Pools to manage user accounts -- Set up Cognito Identity Pool to secure our file uploads -- Set up the Serverless Framework to work with Lambda & API Gateway - Write the various backend APIs +- Set up Cognito User Pools to manage user accounts +- Set up Cognito Identity Pool to secure our resources +- Working with secrets +- Adding unit tests For the frontend: - Set up our project with Create React App - Add favicons, fonts, and a UI Kit using Bootstrap -- Set up routes using React-Router -- Use AWS Cognito SDK to login and signup users +- Set up routes using React Router +- Use AWS Cognito with Amplify to login and signup users - Plugin to the backend APIs to manage our notes -- Use the AWS JS SDK to upload files -- Create an S3 bucket to upload our app -- Configure CloudFront to serve out our app -- Point our domain with Route 53 to CloudFront -- Set up SSL to serve our app over HTTPS +- Use the AWS Amplify to upload files +- Accepting credit cards with the Stripe React SDK -#### Part II +Deploying to prod: -Aimed at folks who are looking to use the Serverless Stack for their day-to-day projects. We automate all the steps from the first part. Here is what is covered in order. +- Use a custom domain for your app +- Deploy your app when you push to git -For the backend: - -- Configure DynamoDB through code -- Configure S3 through code -- Configure Cognito User Pool through code -- Configure Cognito Identity Pool through code -- Environment variables in Serverless Framework -- Working with the Stripe API -- Working with secrets in Serverless Framework -- Unit tests in Serverless -- Automating deployments using Seed -- Configuring custom domains through Seed -- Monitoring deployments through Seed - -For the frontend - -- Environments in Create React App -- Accepting credit card payments in React -- Automating deployments using Netlify -- Configure custom domains through Netlify - -We think this will give you a good foundation on building full-stack production ready serverless applications. If there are any other concepts or technologies you'd like us to cover, feel free to let us know on our [forums]({{ site.forum_url }}). +We believe this will give you a good foundation on building full-stack production ready serverless applications. If there are any other concepts or technologies you'd like us to cover, feel free to let us know on [Discord]({{ site.discord_invite_url }}){:target="_blank"} . +[APIG]: https://aws.amazon.com/api-gateway/ +[Bootstrap]: http://getbootstrap.com/ [Cognito]: https://aws.amazon.com/cognito/ -[CM]: https://aws.amazon.com/certificate-manager -[R53]: https://aws.amazon.com/route53/ -[CF]: https://aws.amazon.com/cloudfront/ -[S3]: https://aws.amazon.com/s3/ -[Bootstrap]: http://getbootstrap.com -[RR]: https://github.com/ReactTraining/react-router -[React]: https://facebook.github.io/react/ [DynamoDB]: https://aws.amazon.com/dynamodb/ -[APIG]: https://aws.amazon.com/api-gateway/ +[GitHub]: https://github.com/ [Lambda]: https://aws.amazon.com/lambda/ -[Stripe]: https://stripe.com -[Seed]: https://seed.run -[Netlify]: https://netlify.com -[GitHub]: https://github.com +[RR]: https://reactrouter.com/ +[Vite]: https://vitejs.dev +[Vitest]: https://vitest.dev +[React]: https://facebook.github.io/react/ +[S3]: https://aws.amazon.com/s3/ +[Stripe]: https://stripe.com/ diff --git a/_chapters/what-is-aws-lambda.md b/_chapters/what-is-aws-lambda.md index b2b6e4eaad..6088a9cac6 100644 --- a/_chapters/what-is-aws-lambda.md +++ b/_chapters/what-is-aws-lambda.md @@ -8,68 +8,90 @@ description: AWS Lambda is a serverless computing service provided by Amazon Web comments_id: what-is-aws-lambda/308 --- -[AWS Lambda](https://aws.amazon.com/lambda/) (or Lambda for short) is a serverless computing service provided by AWS. In this chapter we are going to be using Lambda to build our serverless application. And while we don't need to deal with the internals of how Lambda works, it's important to have a general idea of how your functions will be executed. +[AWS Lambda](https://aws.amazon.com/lambda/){:target="_blank"} (or Lambda for short) is a serverless computing service provided by AWS. In this chapter we are going to be using Lambda to build our serverless application. And while we don't need to deal with the internals of how Lambda works, it's important to have a general idea of how your functions will be executed. + ### Lambda Specs Let's start by quickly looking at the technical specifications of AWS Lambda. Lambda supports the following runtimes. -- Node.js: v10.15 and v8.10 -- Java 8 -- Python: 3.7, 3.6, and 2.7 -- .NET Core: 1.0.1 and 2.1 +- Node.js 18.x, 16.x, and 14.x +- Java 17, 11 and 8 +- Python 3.11, 3.10, 3.9, 3.8, and 3.7 +- .NET 7 and 6 - Go 1.x -- Ruby 2.5 -- Rust +- Ruby 3.2 and 2.7 +- [Rust](https://docs.aws.amazon.com/lambda/latest/dg/lambda-rust.html) + +{%info%} +[.NET Core 2.2 and 3.0 are supported through custom runtimes](https://aws.amazon.com/blogs/developer/announcing-amazon-lambda-runtimesupport/){:target="_blank"}. +{%endinfo%} + +[Check out the AWS docs to learn more about the available runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){:target="_blank"}. Each function runs inside a container with a 64-bit Amazon Linux AMI. And the execution environment has: -- Memory: 128MB - 3008MB, in 64 MB increments -- Ephemeral disk space: 512MB +- Memory: 128MB - 10240MB, in 1 MB increments +- Ephemeral disk space: 512MB - 10240MB, in 1 MB increments - Max execution duration: 900 seconds - Compressed package size: 50MB - Uncompressed package size: 250MB +- Container image package size: 10GB You might notice that CPU is not mentioned as a part of the container specification. This is because you cannot control the CPU directly. As you increase the memory, the CPU is increased as well. -The ephemeral disk space is available in the form of the `/tmp` directory. You can only use this space for temporary storage since subsequent invocations will not have access to this. We'll talk a bit more on the stateless nature of the Lambda functions below. +The ephemeral disk space is available in the form of the `/tmp` directory. You can only use this space for temporary storage since subsequent invocations will not have access to this. We will talk a bit more on the stateless nature of the Lambda functions below. The execution duration means that your Lambda function can run for a maximum of 900 seconds or 15 minutes. This means that Lambda isn't meant for long running processes. -The package size refers to all your code necessary to run your function. This includes any dependencies (`node_modules/` directory in case of Node.js) that your function might import. There is a limit of 250MB on the uncompressed package and a 50MB limit once it has been compressed. We'll take a look at the packaging process below. +The package size refers to all your code necessary to run your function. This includes any dependencies (`node_modules/` directory in case of Node.js) that your function might import. There is a limit of 250MB on the uncompressed package and a 50MB limit once it has been compressed. If you need more space, you can package your container as a Docker image which can be up to 10GB. We will take a look at the packaging process below. ### Lambda Function -Finally here is what a Lambda function (a Node.js version) looks like. +Finally here is what a Lambda function using Node.js looks like. + +```js +export const handler = async (event, context) => { + // Do work -![Anatomy of a Lambda Function image](/assets/anatomy-of-a-lambda-function.png) + return { + statusCode: 200, + body: "Hello World!" + }; +}; +``` -Here `myHandler` is the name of our Lambda function. The `event` object contains all the information about the event that triggered this Lambda. In the case of an HTTP request it'll be information about the specific HTTP request. The `context` object contains info about the runtime our Lambda function is executing in. After we do all the work inside our Lambda function, we simply call the `callback` function with the results (or the error) and AWS will respond to the HTTP request with it. +Here `handler` is the name of our Lambda function. It's an `async` function. The `event` object contains all the information about the event that triggered this Lambda. In the case of an HTTP request it'll be information about the specific HTTP request. The `context` object contains info about the runtime our Lambda function is executing in. + +After we do all the work inside our Lambda function, we simply return. If this function is connected to an API Gateway, you can return the response HTTP status code and body. ### Packaging Functions -Lambda functions need to be packaged and sent to AWS. This is usually a process of compressing the function and all its dependencies and uploading it to a S3 bucket. And letting AWS know that you want to use this package when a specific event takes place. To help us with this process we use the [Serverless Framework](https://serverless.com). We'll go over this in detail later on in this guide. +Lambda functions need to be packaged and sent to AWS. This is usually a process of compressing the function and all its dependencies and uploading it to an S3 bucket. And letting AWS know that you want to use this package when a specific event takes place. To help us with this process we use the [SST]({{ site.sst_url }}). We will go over this in detail later on in this guide. ### Execution Model The container (and the resources used by it) that runs our function is managed completely by AWS. It is brought up when an event takes place and is turned off if it is not being used. If additional requests are made while the original event is being served, a new container is brought up to serve a request. This means that if we are undergoing a usage spike, the cloud provider simply creates multiple instances of the container with our function to serve those requests. -This has some interesting implications. Firstly, our functions are effectively stateless. Secondly, each request (or event) is served by a single instance of a Lambda function. This means that you are not going to be handling concurrent requests in your code. AWS brings up a container whenever there is a new request. It does make some optimizations here. It will hang on to the container for a few minutes (5 - 15mins depending on the load) so it can respond to subsequent requests without a cold start. +This has some interesting implications. Firstly, our functions are effectively stateless. Secondly, each request (or event) is served by a single instance of a Lambda function. This means that you are not going to be handling concurrent requests in your code. AWS brings up a container whenever there is a new request. It does make some optimizations here. It will hang on to the container for a few minutes (5 - 15 mins depending on the load) so it can respond to subsequent requests without a cold start. ### Stateless Functions The above execution model makes Lambda functions effectively stateless. This means that every time your Lambda function is triggered by an event it is invoked in a completely new environment. You don't have access to the execution context of the previous event. -However, due to the optimization noted above, the actual Lambda function is invoked only once per container instantiation. Recall that our functions are run inside containers. So when a function is first invoked, all the code in our handler function gets executed and the handler function gets invoked. If the container is still available for subsequent requests, your function will get invoked and not the code around it. +However, as noted in the optimization above, AWS will hang on to an existing container for a few minutes and use that to respond to any requests. So for that container instance, the code around the Lambda function will only be invoked once. While the actual Lambda function will be invoked for each request. -For example, the `createNewDbConnection` method below is called once per container instantiation and not every time the Lambda function is invoked. The `myHandler` function on the other hand is called on every invocation. +For example, the `createNewDbConnection` method below is called once per container instance and not every time the Lambda function is invoked. The `handler` function on the other hand is called on every invocation. -``` javascript +```js var dbConnection = createNewDbConnection(); -exports.myHandler = function(event, context, callback) { +export const handler = async (event, context) => { var result = dbConnection.makeQuery(); - callback(null, result); + return { + statusCode: 200, + body: JSON.stringify(result) + }; }; ``` @@ -79,15 +101,14 @@ Now you can guess that this isn't a very reliable way to make our Lambda functio ### Pricing -Finally, Lambda functions are billed only for the time it takes to execute your function. And it is calculated from the time it begins executing till when it returns or terminates. It is rounded up to the nearest 100ms. +Finally, Lambda functions are billed only for the time it takes to execute your function. And it is calculated from the time it begins executing till when it returns or terminates. It is rounded up to the nearest 1ms. Note that while AWS might keep the container with your Lambda function around after it has completed; you are not going to be charged for this. Lambda comes with a very generous free tier and it is unlikely that you will go over this while working on this guide. -The Lambda free tier includes 1M free requests per month and 400,000 GB-seconds of compute time per month. Past this, it costs $0.20 per 1 million requests and $0.00001667 for every GB-seconds. The GB-seconds is based on the memory consumption of the Lambda function. For further details check out the [Lambda pricing page](https://aws.amazon.com/lambda/pricing/). +The Lambda free tier includes 1M free requests per month and 400,000 GB-seconds of compute time per month. Past this, it costs $0.20 per 1 million requests and $0.00001667 for every GB-seconds. The GB-seconds is based on the memory consumption of the Lambda function. You can save up to 17% by purchasing AWS Compute Savings Plans in exchange for a 1 or 3 year commitment. For further details check out the [Lambda pricing page](https://aws.amazon.com/lambda/pricing/){:target="_blank"}. In our experience, Lambda is usually the least expensive part of our infrastructure costs. Next, let's take a deeper look into the advantages of serverless, including the total cost of running our demo app. - diff --git a/_chapters/what-is-infrastructure-as-code.md b/_chapters/what-is-infrastructure-as-code.md index 0a16b92cf8..a9e5f483b6 100644 --- a/_chapters/what-is-infrastructure-as-code.md +++ b/_chapters/what-is-infrastructure-as-code.md @@ -3,18 +3,102 @@ layout: post title: What Is Infrastructure as Code date: 2018-02-26 00:00:00 lang: en -description: Infrastructure as code in Serverless is a way of programmatically defining the resources your project is going to use. In the case of Serverless Framework, these are defined in the serverless.yml. -context: true +description: Infrastructure as Code or IaC is a process of automating the management of infrastructure through code, rather than doing it manually through a console or user interface. ref: what-is-infrastructure-as-code comments_id: what-is-infrastructure-as-code/161 --- -[Serverless Framework](https://serverless.com) converts your `serverless.yml` into a [CloudFormation](https://aws.amazon.com/cloudformation) template. This is a description of the infrastructure that you are trying to configure as a part of your serverless project. In our case we were describing the Lambda functions and API Gateway endpoints that we were trying to configure. +[SST]({{ site.sst_url }}){:target="_blank"} converts your infrastructure code into a series of API calls to your cloud providers. Behind the scenes it uses [Pulumi](https://www.pulumi.com/){:target="_blank"} and [Terraform](https://www.terraform.io/){:target="_blank"}, more on this below. Your SST config is a description of the infrastructure that you are trying to create as a part of your project. In our case we'll be defining Lambda functions, API Gateway endpoints, DynamoDB tables, S3 buckets, etc. -However, in Part I we created our DynamoDB table, Cognito User Pool, S3 uploads bucket, and Cognito Identity Pool through the AWS Console. You might be wondering if this too can be configure programmatically, instead of doing them manually through the console. It definitely can! +While you can configure this using the [AWS console](https://aws.amazon.com/console/){:target="_blank"}, you'll need to do a whole lot of clicking around. It's much better to configure our infrastructure programmatically. -This general pattern is called **Infrastructure as code** and it has some massive benefits. Firstly, it allows us to simply replicate our setup with a couple of simple commands. Secondly, it is not as error prone as doing it by hand. We know a few of you have run into configuration related issues by simply following the steps in the tutorial. Additionally, describing our entire infrastructure as code allows us to create multiple environments with ease. For example, you can create a dev environment where you can make and test all your changes as you work on it. And this can be kept separate from your production environment that your users are interacting with. +This general pattern is called **Infrastructure as code** and it has some massive benefits. Firstly, it allows us to completely automate the entire process. All you need is a config and a CLI to create your entire app. Secondly, it's not as error prone as doing it by hand. -In the next few chapters we are going to configure our various infrastructure pieces through our `serverless.yml`. +Additionally, describing our entire infrastructure as code allows us to create multiple environments with ease. For example, you can create a dev environment where you can make and test all your changes as you work on it. And this can be kept separate from the production environment that your users are interacting with. -Let's start by configuring our DynamoDB in our `serverless.yml`. +### Terraform + +[Terraform](https://www.terraform.io/){:target="_blank"} is a large open source project that maintains _providers_ for all the cloud providers out there. Each provider includes resources that allow you to define almost everything the cloud provider has. + +Terraform uses something called [HCL](https://developer.hashicorp.com/terraform/language/syntax/configuration) to define resources. For example, here's what the Terraform definition of a DynamoDB table looks like. + +```hcl +resource "aws_dynamodb_table" "example" { + name = "example-table" + billing_mode = "PAY_PER_REQUEST" + + attribute { + name = "id" + type = "S" + } + + attribute { + name = "sort_key" + type = "N" + } + + hash_key = "id" + range_key = "sort_key" + + tags = { + Name = "example-table" + Environment = "production" + } +} +``` + +### Pulumi + +One of the problems with the definition above is that as you work with more complex applications, your definitions start to get really large. + +And while HCL (or YAML, or JSON) are easy to get started, it can be hard to reuse and compose them. To fix this [Pulumi](https://www.pulumi.com/){:target="_blank"} takes these providers and translates them into TypeScript (and other programming languages). + +So in Pulumi the same DynamoDB would look something like this. + +```ts +import * as aws from "@pulumi/aws"; + +const table = new aws.dynamodb.Table("exampleTable", { + attributes: [ + { name: "id", type: "S" }, + { name: "sort_key", type: "N" }, + ], + hashKey: "id", + rangeKey: "sort_key", + billingMode: "PAY_PER_REQUEST", + tags: { + Name: "example-table", + Environment: "production", + }, +}); +``` + +### Problems with traditional IaC + +Traditional IaC, like the Terraform and Pulumi definition above, are made up of low level resources. This has a couple of implications: + +1. You need to understand how each of these low level resources work. You need to know what the properties of a resource does. +2. You need a lot of these low level resources. For example, to deploy a Next.js app on AWS, you need around 70 of these low level resources. + +This makes IaC really intimidating for most developers. Since you need very specific AWS knowledge to even deploy a simple CRUD app. As a result, IaC has been traditionally only used by DevOps or Platform engineers. + +Additionally, traditional IaC tools don't help you with local development. They are only concerned with how you define and deploy your infrastructure. + +To fix this, we created SST. SST has high level components that wrap around these resources with sane defaults, so creating a Next.js app is as simple as. + +```ts +new sst.aws.Nextjs("MyWeb"); +``` + +And it comes with a full local development environment with the `sst dev` command. + +### Working with IaC + +If you have not worked with IaC before, it might feel unfamiliar at first. But as long as you remember a couple of things you'll be fine. + +1. SST **automatically manages** the resources in AWS defined in your app. +2. You don’t need to **make any manual changes** to them in your AWS Console. + +You can learn more about the [SST workflow]({{ site.sst_url }}/docs/workflow){:target="_blank"}. + +Now we are ready to create our first SST app. diff --git a/_chapters/what-is-serverless.md b/_chapters/what-is-serverless.md index 8925b200ea..e4b2afac20 100644 --- a/_chapters/what-is-serverless.md +++ b/_chapters/what-is-serverless.md @@ -24,9 +24,9 @@ For smaller companies and individual developers this can be a lot to handle. Thi Serverless computing (or serverless for short), is an execution model where the cloud provider (AWS, Azure, or Google Cloud) is responsible for executing a piece of code by dynamically allocating the resources. And only charging for the amount of resources used to run the code. The code is typically run inside stateless containers that can be triggered by a variety of events including http requests, database events, queuing services, monitoring alerts, file uploads, scheduled events (cron jobs), etc. The code that is sent to the cloud provider for execution is usually in the form of a function. Hence serverless is sometimes referred to as _"Functions as a Service"_ or _"FaaS"_. Following are the FaaS offerings of the major cloud providers: -- AWS: [AWS Lambda](https://aws.amazon.com/lambda/) -- Microsoft Azure: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) -- Google Cloud: [Cloud Functions](https://cloud.google.com/functions/) +- AWS: [AWS Lambda](https://aws.amazon.com/lambda/){:target="_blank"} +- Microsoft Azure: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/){:target="_blank"} +- Google Cloud: [Cloud Functions](https://cloud.google.com/functions/){:target="_blank"} While serverless abstracts the underlying infrastructure away from the developer, servers are still involved in executing our functions. @@ -40,7 +40,7 @@ The biggest change that we are faced with while transitioning to a serverless wo Your functions are typically run inside secure (almost) stateless containers. This means that you won't be able to run code in your application server that executes long after an event has completed or uses a prior execution context to serve a request. You have to effectively assume that your function is invoked in a new container every single time. -There are some subtleties to this and we will discuss in the [What is AWS Lambda]({% link _chapters/what-is-aws-lambda.md %}) chapter. +There are some subtleties to this and we will discuss in the next chapter. ### Cold Starts @@ -48,6 +48,6 @@ Since your functions are run inside a container that is brought up on demand to The duration of cold starts depends on the implementation of the specific cloud provider. On AWS Lambda it can range from anywhere between a few hundred milliseconds to a few seconds. It can depend on the runtime (or language) used, the size of the function (as a package), and of course the cloud provider in question. Cold starts have drastically improved over the years as cloud providers have gotten much better at optimizing for lower latency times. -Aside from optimizing your functions, you can use simple tricks like a separate scheduled function to invoke your function every few minutes to keep it warm. [Serverless Framework](https://serverless.com) which we are going to be using in this tutorial has a few plugins to [help keep your functions warm](https://github.com/FidelLimited/serverless-plugin-warmup). +Aside from optimizing your functions, you can use simple tricks like a separate scheduled function to invoke your function every few minutes to keep it warm. [SST]({{ site.url }}), which we are going to be using in this tutorial, has a pre-built Cron component to help with this. Now that we have a good idea of serverless computing, let's take a deeper look at what a Lambda function is and how your code will be executed. diff --git a/_chapters/what-is-sst.md b/_chapters/what-is-sst.md new file mode 100644 index 0000000000..e542e3d31f --- /dev/null +++ b/_chapters/what-is-sst.md @@ -0,0 +1,26 @@ +--- +layout: post +title: What is SST? +date: 2021-08-17 00:00:00 +lang: en +description: SST is a framework that makes it easy to build modern full-stack applications on your own infrastructure. +ref: what-is-sst +comments_id: comments-for-what-is-sst/2468 +--- + +We are going to be using [AWS Lambda](https://aws.amazon.com/lambda/){:target="_blank"}, [Amazon API Gateway](https://aws.amazon.com/api-gateway/){:target="_blank"}, and a host of other AWS services to create our application. AWS Lambda is a compute service that lets you run code without provisioning or managing servers. You pay only for the compute time you consume - there is no charge when your code is not running. But working directly with AWS Lambda, API Gateway, and the other AWS services can be a bit cumbersome. + +Since these services run on AWS, it can be tricky to test and debug them locally. And a big part of building serverless applications, is being able to define our infrastructure as code. This means that we want our infrastructure to be created programmatically. We don't want to have to click through the AWS Console to create our infrastructure. + +To solve these issues we created the [SST]({{ site.sst_github_repo }}){:target="_blank"}. + +SST makes it easy to build full-stack applications by allowing developers to: + +1. Define their _entire_ infrastructure in code +2. Use [higher-level components]({{ site.sst_url }}/docs/components){:target="_blank"} designed for modern full-stack apps +3. Test their applications [Live]({{ site.sst_url }}/docs/live){:target="_blank"} +4. Debugging with your IDEs +5. Manage their apps with a [web based dashboard]({{ site.sst_url }}/docs/console){:target="_blank"} +6. Deploy to multiple environments and regions + +Before we start creating our application, let's look at the _infrastructure as code_ concept in a bit more detail. diff --git a/_chapters/who-is-this-guide-for.md b/_chapters/who-is-this-guide-for.md index 31aef79f0a..95a12f2d79 100644 --- a/_chapters/who-is-this-guide-for.md +++ b/_chapters/who-is-this-guide-for.md @@ -4,7 +4,6 @@ title: Who Is This Guide For? date: 2016-12-21 00:00:00 lang: en ref: who-is-this-guide-for -context: true comments_id: who-is-this-guide-for/96 --- @@ -12,8 +11,26 @@ This guide is meant for full-stack developers or developers that would like to b So you might be a backend developer who would like to learn more about the frontend portion of building serverless apps or a frontend developer that would like to learn more about the backend; this guide should have you covered. -We are also catering this solely towards JavaScript developers for now. We might target other languages and environments in the future. But we think this is a good starting point because it can be really beneficial as a full-stack developer to use a single language (JavaScript) and environment (Node.js) to build your entire application. +On a personal note, the serverless approach has been a giant revelation for us and we wanted to create a resource where we could share what we've learned. You can read more about us [**here**]({{ site.sst_url }}). And [check out a sample of what folks have built with SST]({% link showcase.md %}). -On a personal note, the serverless approach has been a giant revelation for us and we wanted to create a resource where we could share what we've learned. You can read more about us [**here**]({% link about.md %}). And [check out a sample of what folks have built with Serverless Stack]({% link showcase.md %}). +We are also catering this solely towards JavaScript/TypeScript developers for now. We might target other languages and environments in the future. But we think this is a good starting point because it can be really beneficial as a full-stack developer to use a single language (TypeScript) and environment (Node.js) to build your entire application. + +### Why TypeScript + +We use TypeScript across the board for this guide from the frontend, to the backend, all the way to creating our infrastructure. If you are not familiar with TypeScript you might be wondering why does typesafety matter. + +One big advantage is that of using a fully typesafe setup is that your code editor can autocomplete and point out any invalid options in your code. This is really useful when you are first starting out. But it's also useful when you are working with configuring infrastructure through code. + +Aside from all the autocomplete goodness, typesafety ends up being critical for the maintainability of codebases. This matters if you are planning to work with the same codebase for years to come. + +It should be easy for your team to come in and make changes to parts of your codebase that have not been worked on for a long time. TypeScript allows you to do this! Your codebase no longer feels _brittle_ and you are not afraid to make changes. + +#### TypeScript made easy + +If you are not used to TypeScript, you might be wondering, _"Don't I have to write all these extra types for things?"_ or _"Doesn't TypeScript make my code really verbose and scary?"_. + +These are valid concerns. But it turns out, if the libraries you are using are designed well for TypeScript, you won't need a lot of extra type definitions in your code. In fact, as you'll see in this tutorial, you'll get all the benefits of a fully typesafe codebase with code that looks almost like regular JavaScript. + +Also, TypeScript can be gradually adopted. Meaning that you can use our TypeScript starter while adding JavaScript files to it. We don't recommend doing this, but that's always an option for you. Let's start by looking at what we'll be covering. diff --git a/_chapters/working-with-3rd-party-apis.md b/_chapters/working-with-3rd-party-apis.md deleted file mode 100644 index a13a374f6a..0000000000 --- a/_chapters/working-with-3rd-party-apis.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: post -title: Working with 3rd Party APIs -date: 2018-03-05 00:00:00 -lang: en -description: To learn how to use a 3rd party API in our AWS Lambda functions, we are going to create a billing API using Stripe. -ref: working-with-3rd-party-apis -comments_id: working-with-3rd-party-apis/168 ---- - -In the first part of the tutorial, we created a basic CRUD API. We are going to make a small addition to this by adding an endpoint that works with a 3rd party API. This section is also going to illustrate how to work with secret environment variables and how to accept credit card payments using Stripe. - -A common extension of Serverless Stack (that we have noticed) is to add a billing API that works with Stripe. In the case of our notes app we are going to allow our users to pay a fee for storing a certain number of notes. The flow is going to look something like this: - -1. The user is going to select the number of notes he wants to store and puts in his credit card information. - -2. We are going to generate a one time token by calling the Stripe SDK on the frontend to verify that the credit card info is valid. - -3. We will then call an API passing in the number of notes and the generated token. - -4. The API will take the number of notes, figure out how much to charge (based on our pricing plan), and call the Stripe API to charge our user. - -We aren't going to do much else in the way of storing this info in our database. We'll leave that as an exercise for the reader. - -Let's get started with first setting up our Stripe account. diff --git a/_chapters/wrapping-up.md b/_chapters/wrapping-up.md index c73ca74e61..2bb0d0964a 100644 --- a/_chapters/wrapping-up.md +++ b/_chapters/wrapping-up.md @@ -5,21 +5,30 @@ lang: en description: Wrapping up the tutorial and going over the next steps. date: 2018-03-30 00:00:00 ref: wrapping-up +code: sst_full comments_id: comments-for-wrapping-up/100 --- Congratulations on completing the guide! +### App in Prod + We've covered how to build and deploy our backend serverless API and our frontend serverless app. And not only does it work well on the desktop. -![App update live screenshot](/assets/app-update-live.png) +![App update live screenshot](/assets/part2/app-update-live.png) It's mobile optimized as well! -Mobile app homescreen screenshot +![Completed app mobile screenshot](/assets/part2/completed-app-mobile-lander.png){: width="432" } + +--- + +We hope what you've learned here can be adapted to fit the use case you have in mind. + +We'd love to hear from you about your experience following this guide. Please send us any comments or feedback you might have, via [email](mailto:{{ site.email }}). And [star our repo on GitHub]({{ site.sst_github_repo }}){:target="_blank"}. -The entire workflow and the ideas covered in this guide are in production at a number of companies. We hope what you've learned here can be adapted to fit the use case you have in mind. We are going to be covering a few other topics in the future while we keep this guide up to date. +Star our GitHub repo -We'd love to hear from you about your experience following this guide. Please send us any comments or feedback you might have, via [email](mailto:{{ site.email }}). We'd love to feature your comments here. Also, if you'd like us to cover any of the chapters or concepts in a bit more detail, feel free to [let us know](mailto:{{ site.email }}). +Also, we'd love to feature what you've built with SST, please [send us a URL and brief description](mailto:{{ site.email }}). Thank you and we hope you found this guide helpful! diff --git a/_chapters/zh/how-to-get-help.md b/_chapters/zh/how-to-get-help.md new file mode 100644 index 0000000000..a6fc509395 --- /dev/null +++ b/_chapters/zh/how-to-get-help.md @@ -0,0 +1,19 @@ +--- +layout: post +title: 如何获取帮助? +lang: zh +ref: how-to-get-help +date: 2016-12-23 00:00:00 +comments_id: how-to-get-help/95 +--- + +一旦你发现自己在某个特定步骤出现问题,我们希望确保我们能够帮助你修复它并使你弄明白。这里有一些获得帮助的方法。 + +- 我们使用 [ Discourse 论坛主题]({{ site.forum_url }})作为我们的评论,我们过去已经帮助解决了很多问题。因此,请确保查看每一章节下的评论,看是否有其他人遇到了和你一样的问题。 +- 在特定章节的评论中发布详细描述你问题的信息,我们中有人将会回应。 + +![SST Discourse Forums screenshot](/assets/serverless-stack-discourse-forums.png) + +完整的指南托管在 [GitHub]({{ site.github_repo }})上。因此,如果你发现了错误,你可以随时: +- 打开一个 [新的话题]({{ site.github_repo }}/issues/new) +- 或者如果你已经发现了排印错误,编辑该页并提交一个 PR diff --git a/_chapters/zh/what-does-this-guide-cover.md b/_chapters/zh/what-does-this-guide-cover.md new file mode 100644 index 0000000000..544eeca700 --- /dev/null +++ b/_chapters/zh/what-does-this-guide-cover.md @@ -0,0 +1,135 @@ +--- +layout: post +ref: what-does-this-guide-cover +title: 本指南涵盖哪些内容? +date: 2016-12-22 00:00:00 +lang: zh +ref: what-does-this-guide-cover +context: true +comments_id: what-does-this-guide-cover/83 +--- + +为了逐步了解构建 web 应用程序所涉及的主要概念,我们将要构建一个叫做 [**Scratch**](https://demo2.sst.dev) 的简单笔记程序。 + +![Completed app desktop screenshot](/assets/completed-app-desktop.png) + +Completed app mobile screenshot + +这是一个单页面应用,完全使用 JavaScript 编写的 serverless API 驱动。这里是全部的[后端]({{ site.backend_github_repo }})和[前端]({{ site.frontend_github_repo }})源代码。它是一个相对简单的应用,但我们将要满足如下需求: + +- 应该允许用户注册和登录他们的账户 +- 用户应该能够创建包含某些内容的笔记 +- 每一篇笔记还能够上传一个文件作为附件 +- 允许用户修改他们的笔记和附件 +- 用户还能够删除他们的笔记 +- 该应用应该能处理信用卡付款 +- 该应用应该通过自定义域名上的 HTTPS 提供服务 +- 后端 API 需要是安全的 +- 该应用需要响应及时 + +我们将使用 AWS 的平台来构建它。我们可能会进一步扩展并涵盖一些其他的平台,但我们认为 AWS 的平台将会是一个不错的起点。 + +### 技术 & 服务 + +我们将使用以下的技术和服务来构建我们的 serverless 应用。 + +- 使用 [Lambda][Lambda] & [API Gateway][APIG] 来构建我们的 serverless API +- 使用 [DynamoDB][DynamoDB] 作为我们的数据库 +- 使用 [Cognito][Cognito] 做用户身份认证和保护我们的 API +- 使用 [S3][S3] 托管我们的应用和上传的文件 +- 使用 [CloudFront][CF] 提供我们的应用 +- 使用 [Route 53][R53] 来解析我们的域名 +- 使用 [Certificate Manager][CM] 提供 SSL +- 使用 [React.js][React] 编写我们的单页面应用 +- 使用 [React Router][RR] 来路由 +- 使用 [Bootstrap][Bootstrap] 作为 UI 工具包 +- 使用 [Stripe][Stripe] 处理信用卡付款 +- 使用 [Seed][Seed] 自动化部署 Serverless 应用 +- 使用 [Netlify][Netlify] 自动化部署 React 应用 +- 使用 [GitHub][GitHub] 托管我们的工程仓库 + +我们将使用以上服务的 **免费套餐**。因此你应该免费注册 AWS 以获取它们。当然,这不适用于购买一个新域名来托管你的应用。当在 AWS 上创建账户的时候,你需要绑定一个信用卡账户。 +因此,如果你恰巧要创建超过我们在此教程中涵盖的资源,那你最终可能会被收取费用。 + +尽管上面列出的内容可能看起来令人生畏,但我们正在努力确保完成本指南后,你将可以构建一个 **真正的**,**安全的**,和**全功能的** web 应用。不用担心,我们会随时帮助你! + +### 要求 + +你需要 [Node v8.10+ and NPM v5.5+](https://nodejs.org/en/)。你还需要有一些如何使用命令行的基本知识。 + +### 本指南是如何组织的 + +本指南分为两个单独的部分,它们都是相对独立的。第一部分介绍了基础知识,第二部分介绍了一些高级主题已经自动设置的方法。我们在2017年初发布了本指南的第一部分。Serverless 栈社区已经发展壮大,我们的很多读者已经使用本指南中描述的设置来构建驱动他们业务的应用。 + +因此,我们决定扩展该指南,给它增加第二部分。这是针对打算在项目中使用此设置的人们的。它使第一部分中的所有手工步骤自动化,帮助你创建一个准生产级工作流,你可以使用到你所有的 serverless 项目中。这是我们在两部分中介绍的内容。 + +#### 第一部分 + +创建笔记应用并进行部署。我们涵盖了所有的基础知识。每个服务都是手工创建的。这是按顺序介绍的内容: + +对于后端: + +- 配置你的 AWS 账户 +- 使用 DynamoDB 创建你的数据库 +- 设置 S3 进行文件上传 +- 设置 Cognito 用户池来管理用户账户 +- 设置 Cognito 身份池以保护我们的文件上传 +- 设置 Serverless 框架以与 Lambda 和 API Gateway 一起使用 +- 编写各种后端 API + +对于前端: + +- 使用 Create React App 来设置我们的项目 +- 使用 Bootstrap 增加 favicons,字体和 UI 工具包 +- 使用 React-Router 来设置路由 +- 使用 AWS Cognito SDK 来实现用户注册和登录 +- 插入后端 API 以管理我们的笔记应用 +- 使用 AWS JS SDK 来上传文件 +- 创建一个 S3 桶来上传我们的应用 +- 配置 CloudFront 以提供我们的应用 +- 使用 Route 53 将我们的域名指向 CloudFront +- 设置 SSL 以通过 HTTPS 提供我们应用的服务 + +#### 第二部分 + +面对希望将 Serverless 栈用于日常项目的人们,我们将第一部分中所有的步骤进行了自动化。这里是按顺序涵盖的内容: +We automate all the steps from the first part. Here is what is covered in order. + +对于后端: + +- 通过代码配置 DynamoDB +- 通过代码配置 S3 +- 通过代码配置 Cognito 用户池 +- 通过代码配置 Cognito 身份池 +- Serverless 框架中的环境变量 +- 使用 Stripe API +- 在 Serverless 框架中使用秘钥 +- Serverless 中的单元测试 +- 使用 Seed 进行自动部署 +- 通过 Seed 配置自定义域名 +- 通过 Seed 监控部署 + +对于前端 + +- Create React App 中的环境变量 +- 在 React 中接受信用卡支付 +- 使用 Netlify 进行自动部署 +- 通过 Netlify 配置自定义域名 + +我们认为,这将会为你在构建全栈准生产级 serverless 应用方面奠定良好的基础。如果你希望我们涵盖任何其他的概念和技术,请在我们的论坛上告知我们。 + +[Cognito]: https://aws.amazon.com/cognito/ +[CM]: https://aws.amazon.com/certificate-manager +[R53]: https://aws.amazon.com/route53/ +[CF]: https://aws.amazon.com/cloudfront/ +[S3]: https://aws.amazon.com/s3/ +[Bootstrap]: http://getbootstrap.com +[RR]: https://github.com/ReactTraining/react-router +[React]: https://facebook.github.io/react/ +[DynamoDB]: https://aws.amazon.com/dynamodb/ +[APIG]: https://aws.amazon.com/api-gateway/ +[Lambda]: https://aws.amazon.com/lambda/ +[Stripe]: https://stripe.com +[Seed]: https://seed.run +[Netlify]: https://netlify.com +[GitHub]: https://github.com diff --git a/_chapters/zh/what-is-serverless.md b/_chapters/zh/what-is-serverless.md new file mode 100644 index 0000000000..d4a5f5636f --- /dev/null +++ b/_chapters/zh/what-is-serverless.md @@ -0,0 +1,50 @@ +--- +layout: post +title: 什么是无服务器? +date: 2016-12-23 12:00:00 +lang: zh +ref: what-is-serverless +description: 无服务器指的是由云服务商完全管理服务器和资源的管理和分配的应用程序。计费则基于这些资源的实际消耗。 +comments_id: what-is-serverless/27 +--- + +传统上,我们已经构建并部署了 web 应用程序,对这些应用程序,我们可以对服务器发出的 HTTP 请求进行一定程度的控制。我们的应用程序运行在该服务器上,我们负责为其配置和管理资源。但这会产生一些问题: + +1、即使没有处理任何请求,我们也要保持服务器正常运行。 + +2、我们负责服务器及其所有资源的正常运行及维护。 + +3、我们还负责对服务器进行适当的安全更新。 + +4、随着使用量的扩张,我们还需要管理服务器的扩展,结果是,当我们没有太多使用量时,我们要将其减少。 + +对于较小的公司和个人开发者而言,这可能需要处理很多工作。这使得我们无法专注于我们更重要的工作,构建和维护实际的应用程序。在大型组织中,这是由基础设施团队处理的,并且通常这不是开发者个人的责任。但是,为此所需的过程最终可能减慢开发时间。因为你不能不与基础架构团队合作来帮助你启动和运行而直接继续构建应用程序。作为开发者,我们一直在寻找一种解决这些问题的方法,这就是无服务器的来源。 + + +### 无服务器计算 + +无服务器计算(或简称 serverless),是一种执行模型,在该模型中,云服务商(AWS,Azure 或 Google Cloud)负责通过动态分配资源来执行一段代码,并且仅收取运行代码所使用资源的费用。该代码通常运行在无状态的容器中,能够被包括 HTTP 请求、数据库事件、队列服务、监控报警、文件上传、调度事件(cron 任务)等各种事件触发。被发送到云服务商执行的代码通常是以函数的形式,因此,无服务器计算有时是指 "函数即服务" 或者 FAAS。以下是主要云服务商提供的 FAAS 产品: + +- AWS: [AWS Lambda](https://aws.amazon.com/lambda/) +- Microsoft Azure: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) +- Google Cloud: [Cloud Functions](https://cloud.google.com/functions/) + +尽管无服务器计算对开发人员抽象了底层基础设施,但服务器仍然参与执行我们的函数。由于你的代码将要作为独立的函数执行,我们需要知道以下几点: + +#### 微服务 + +当向无服务器计算的世界过度时,我们面临的最大变化是我们的程序需要被组织成函数的形式。你可能习惯于将你的应用程序部署为单个 Rails 或 Express 整体应用程序。但是在无服务器计算的世界里,你通常需要采用更多基于微服务的架构。你可以通过在单个函数中作为一个整体运行你的整个应用程序并自行处理路由来解决此问题。但不建议这样做,因为减小你的函数的大小更好。我们将在下面讨论。 + +#### 无状态函数 + +你的函数通常运行在安全的(几乎是)无状态容器中,这意味着你将无法在一个事件已经完成后长时间执行的应用服务器中运行代码,或者无法使用先前的执行上下文为请求提供服务。你不得不有效地假定你的函数每次都在一个新容器中被调用。 对此有一些微妙之处,我们将会在 [什么是 AWS Lambda]({% link _chapters/what-is-aws-lambda.md %}) 一章中进行讨论。 + +#### 冷启动 + +由于你的函数在需要响应事件的容器中运行,因此存在一定的延时。这被称为"冷启动"。当你的函数执行完成后,你的容器可能会保留一段时间。如果另一个事件在此时被触发,则它的响应速度要快得多,这通常被称为"热启动"。 + +冷启动的持续时间取决于特定云服务商的实现。在 AWS Lambda 上,它的范围从几百毫秒到几秒不等。它可能取决于使用的运行时(或编程语言)、函数(以包的形式)的大小,当然,还取决于所讨论的云服务商。多年以来,随着云服务商在优化时延方面变得越来越出色,冷启动已经大为改善。 + +除了优化你的函数,你还可以使用一些简单的技巧,例如使用单独的调度函数每隔几分钟来调用你的函数以使其保持运行状态。我们在此教程中使用的 [Serverless Framework](https://serverless.com) 中,有一些插件能够[帮助保持你的函数处于运行状态](https://github.com/FidelLimited/serverless-plugin-warmup)。 + +既然我们对无服务器计算有了一些了解,让我们更深入地了解什么是 Lambda 函数以及你的代码是如何被执行的。 diff --git a/_chapters/zh/who-is-this-guide-for.md b/_chapters/zh/who-is-this-guide-for.md new file mode 100644 index 0000000000..8f823036a4 --- /dev/null +++ b/_chapters/zh/who-is-this-guide-for.md @@ -0,0 +1,20 @@ +--- +layout: post +title: 本指南适用于谁? +date: 2016-12-21 00:00:00 +lang: zh +ref: who-is-this-guide-for +context: true +comments_id: who-is-this-guide-for/96 +--- + +本指南适用于全栈开发人员或希望构建全栈无服务器应用程序的开发人员。通过为前端和后端都提供一个循序渐进的指南,我们希望它能解决构建无服务器应用程序的所有不同方面。互联网上还有很多其他的教程,但我们认为为整个过程提供一个参考点会很有用。本指南旨在作为一种学习如何构建和部署无服务器应用程序的资源,而不是为此提供最佳的可能方法。 + +因此,您可能是一个想要了解有关构建无服务器应用程序的前端部分更多信息的后端开发者,或一个想要了解有关后端的更多信息的前端开发者,本指南应该涵盖了您。 + +目前,我们仅将其提供给 JavaScript 开发人员。我们将来可能会针对其他的语言和环境。但我们认为这是一个很好的起点,因为对于使用单一语言(JavaScript)和环境(Node.js)来构建整个应用程序的全栈开发人员而言,它确实是有益的 。 + +就个人而言,无服务器方法对我们来说是一个巨大的启示,并且,我们希望创建一种资源,我们能够在这里分享我们所学到的知识。你可以阅读更多关于[**我们**]({{ site.sst_url }})的信息。此外,查看[一个用 SST 构建的示例]({% link showcase.md %})。 + +让我们先来看一下我们要介绍的内容。 + diff --git a/_config.yml b/_config.yml index e7da001928..31b5d16cfa 100644 --- a/_config.yml +++ b/_config.yml @@ -13,16 +13,30 @@ # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. # You can create any custom variable you would like, and they will be accessible # in the templates via {{ site.myvariable }}. -title: Serverless Stack -email: contact@anoma.ly +title: SST Guide +email: hello@sst.dev +jobs_email: jobs@sst.dev description: > # this means to ignore newlines until "baseurl:" - Building a Full-Stack App - with Serverless and React + Learn how to build modern full-stack apps on AWS. +description_full: > + The most widely read resource for building full-stack serverless apps on AWS. baseurl: "" # the subpath of your site, e.g. /blog -url: "https://serverless-stack.com" # the base hostname & protocol for your site, e.g. http://example.com -logo: /assets/og-image.jpg +url: "https://guide.sst.dev" # the base hostname & protocol for your site, e.g. http://example.com -github_repo: "https://github.com/AnomalyInnovations/serverless-stack-com" +sst_url: "https://sst.dev" + +exclude: + - .idea + - LICENSE + - README.md + - package.json + - sst.config.ts + - pnpm-lock.yaml + - CONTRIBUTING.md + +demo_url: "https://demo.sst.dev" + +github_repo: "https://github.com/sst/guide" github_edit_prefix: "/edit/master/" github_history_prefix: "/commits/master/" github_issues_prefix: "/issues/" @@ -31,32 +45,65 @@ backend_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-dem frontend_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-demo-client" frontend_fb_login_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-demo-fb-login-client" frontend_user_mgmt_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client" -backend_mono_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-demo-mono-api" +backend_ext_api_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-demo-ext-api" +backend_ext_resources_github_repo: "https://github.com/AnomalyInnovations/serverless-stack-demo-ext-resources" -newsletter_signup_form: "https://emailoctopus.com/lists/1c11b9a8-1500-11e8-a3c9-06b79b628af2/forms/subscribe" +sst_github_repo: "https://github.com/sst/sst" +sst_github_examples_prefix: "/tree/master/examples/" +sst_demo_repo: "https://github.com/sst/demo-notes-app" + +seed_url: "https://seed.run" +survey_url: "https://serverless-stack.typeform.com/to/xiPdCW" google_analytics: "UA-3536629-11" -forum_url: "https://discourse.serverless-stack.com" +v2_url: "https://v2.sst.dev" +v1_conf_url: "https://v1conf.sst.dev" +console_url: "https://console.sst.dev" +old_console_url: "https://old.console.sst.dev" +forum_url: "https://discourse.sst.dev" forum_thread_prefix: "/t/" patreon_url: "https://www.patreon.com/serverless_stack" -twitter_url: "https://twitter.com/anomaly_inv" +github_sponsor_url: "https://github.com/sponsors/jayair" +twitter: "SST_dev" +twitter_url: "https://twitter.com/SST_dev" +slack_invite_url: "https://sst.dev/slack" +discord_invite_url: "https://sst.dev/discord" +linkedin_url: "https://www.linkedin.com/company/sst-dev" +youtube_url: "https://www.youtube.com/c/sst-dev" + +social_cards_url: "https://social-cards.sst.dev" + +stats: + newsletter: "90,000" + newsletter_short: "90k" + discord: "11k" + github: "21000" + github_short: "21k" collections: chapters: output: true + archives: + output: true + examples: + output: true # Build settings markdown: kramdown #theme: minima plugins: - - jekyll-seo-tag - jekyll-sitemap - jekyll-redirect-from - # Front Matter defaults -defaults: - - scope: - path: "" - values: - image: /assets/og-image.jpg +#defaults: +# - scope: +# path: "" +# values: +# image: serverless-stack-lander/index.png + + +# Disable redirects plugin from creating +# redirects.json +redirect_from: + json: false diff --git a/_data/archiveslist.yml b/_data/archiveslist.yml new file mode 100644 index 0000000000..48e94168ff --- /dev/null +++ b/_data/archiveslist.yml @@ -0,0 +1,283 @@ +# # +# # Archived Chapters +# # + + +# +# Best Practices Section +# + +best-practices-intro: + title: Introduction + chapters: + - page: Best practices for building serverless apps + url: /archives/best-practices-for-building-serverless-apps.html + +organize-serverless-apps: + title: Organize a serverless app + chapters: + - page: Organizing serverless projects + url: /archives/organizing-serverless-projects.html + - page: Cross-stack references in serverless + url: /archives/cross-stack-references-in-serverless.html + - page: Share code between services + url: /archives/share-code-between-services.html + - page: Share an API endpoint between services + url: /archives/share-an-api-endpoint-between-services.html + - page: Deploy a serverless app with dependencies + url: /archives/deploy-a-serverless-app-with-dependencies.html + +configure-environments: + title: Manage environments + chapters: + - page: Environments in serverless apps + url: /archives/environments-in-serverless-apps.html + - page: Structure environments across AWS accounts + url: /archives/structure-environments-across-aws-accounts.html + - page: Manage AWS accounts using AWS Organizations + url: /archives/manage-aws-accounts-using-aws-organizations.html + - page: Parameterize serverless resources names + url: /archives/parameterize-serverless-resources-names.html + - page: Deploying to multiple AWS accounts + url: /archives/deploying-to-multiple-aws-accounts.html + subchapters: + - page: Deploy the resources repo + url: /archives/deploy-the-resources-repo.html + - page: Deploy the API services repo + url: /archives/deploy-the-api-services-repo.html + - page: Manage environment related config + url: /archives/manage-environment-related-config.html + - page: Storing secrets in serverless apps + url: /archives/storing-secrets-in-serverless-apps.html + - page: Share Route 53 domains across AWS accounts + url: /archives/share-route-53-domains-across-aws-accounts.html + - page: Monitor usage for environments + url: /archives/monitor-usage-for-environments.html + +development-lifecycle: + title: Development lifecycle + chapters: + - page: Working on serverless apps + url: /archives/working-on-serverless-apps.html + - page: Invoke Lambda functions locally + url: /archives/invoke-lambda-functions-locally.html + - page: Invoke API Gateway endpoints locally + url: /archives/invoke-api-gateway-endpoints-locally.html + - page: Creating feature environments + url: /archives/creating-feature-environments.html + - page: Creating Pull Request environments + url: /archives/creating-pull-request-environments.html + - page: Promoting to production + url: /archives/promoting-to-production.html + - page: Rollback changes + url: /archives/rollback-changes.html + - page: Deploying only updated services + url: /archives/deploying-only-updated-services.html + +observability: + title: Observability + chapters: + - page: Tracing serverless apps with X-Ray + url: /archives/tracing-serverless-apps-with-x-ray.html + +best-practices-conclusion: + title: Conclusion + chapters: + - page: Wrapping up the best practices + url: /archives/wrapping-up-the-best-practices.html + +# +# Serverless Framework Section +# + +setup-serverless: + title: Setting up a serverless app + chapters: + - page: Set up the Serverless Framework + url: /archives/setup-the-serverless-framework.html + subchapters: + - page: Add support for ES6 and TypeScript + url: /archives/add-support-for-es6-and-typescript.html + - page: Deploy your Hello World API + url: /archives/deploy-your-hello-world-api.html + +setup-backend: + title: Create your AWS resources + chapters: + - page: Create a DynamoDB table + url: /archives/create-a-dynamodb-table.html + - page: Create an S3 bucket for file uploads + url: /archives/create-an-s3-bucket-for-file-uploads.html + +build-api: + title: Building a serverless API + chapters: + - page: Add a create note API + url: /archives/add-a-create-note-api.html + - page: Add a get note API + url: /archives/add-a-get-note-api.html + - page: Add a list all the notes API + url: /archives/add-a-list-all-the-notes-api.html + - page: Add an update note API + url: /archives/add-an-update-note-api.html + - page: Add a delete note API + url: /archives/add-a-delete-note-api.html + - page: Deploy the APIs + url: /archives/deploy-the-apis.html + +users-auth: + title: Users and authentication + chapters: + - page: Create a Cognito user pool + url: /archives/create-a-cognito-user-pool.html + subchapters: + - page: Create a Cognito test user + url: /archives/create-a-cognito-test-user.html + - page: Create a Cognito identity pool + url: /archives/create-a-cognito-identity-pool.html + - page: Secure the APIs + url: /archives/secure-the-apis.html + - page: Test the APIs + url: /archives/test-the-apis.html + +third-party-apis: + title: Secrets and 3rd party APIs + chapters: + - page: Working with 3rd party APIs + url: /archives/working-with-3rd-party-apis.html + subchapters: + - page: Add a billing API + url: /archives/add-a-billing-api.html + - page: Load secrets from .env + url: /archives/load-secrets-from-env.html + - page: Test the billing API + url: /archives/test-the-billing-api.html + +infrastructure-as-code: + title: Infrastructure as Code + chapters: + - page: Configure DynamoDB in serverless + url: /archives/configure-dynamodb-in-serverless.html + - page: Configure S3 in serverless + url: /archives/configure-s3-in-serverless.html + - page: Configure Cognito User Pool in serverless + url: /archives/configure-cognito-user-pool-in-serverless.html + - page: Configure Cognito Identity Pool in serverless + url: /archives/configure-cognito-identity-pool-in-serverless.html + - page: Handle API Gateway CORS errors + url: /archives/handle-api-gateway-cors-errors.html + - page: Deploy your serverless infrastructure + url: /archives/deploy-your-serverless-infrastructure.html + +# +# Extra Credits Section +# + +extra-backend: + title: Serverless + chapters: + - page: What is IAM + url: /archives/what-is-iam.html + - page: What is an ARN + url: /archives/what-is-an-arn.html + - page: What is AWS CDK? + url: /archives/what-is-aws-cdk.html + - page: What is AWS AppSync + url: /archives/what-is-aws-appsync.html + - page: Cognito user pool vs identity pool + url: /archives/cognito-user-pool-vs-identity-pool.html + - page: API Gateway and Lambda Logs + url: /archives/api-gateway-and-lambda-logs.html + - page: Debugging serverless API Issues + url: /archives/debugging-serverless-api-issues.html + - page: Serverless environment variables + url: /archives/serverless-environment-variables.html + - page: Stages in Serverless Framework + url: /archives/stages-in-serverless-framework.html + - page: Backups in DynamoDB + url: /archives/backups-in-dynamodb.html + - page: Configure multiple AWS profiles + url: /archives/configure-multiple-aws-profiles.html + - page: Customize the serverless IAM Policy + url: /archives/customize-the-serverless-iam-policy.html + - page: Mapping Cognito Identity Id and User Pool Id + url: /archives/mapping-cognito-identity-id-and-user-pool-id.html + - page: Connect to API Gateway with IAM auth + url: /archives/connect-to-api-gateway-with-iam-auth.html + - page: Serverless Node.js Starter + url: /archives/serverless-nodejs-starter.html + - page: Set custom domains through Seed + url: /archives/set-custom-domains-through-seed.html + - page: Using AWS CDK with Serverless Framework + url: /archives/using-aws-cdk-with-serverless-framework.html + - page: Package Lambdas with serverless-bundle + url: /archives/package-lambdas-with-serverless-bundle.html + - page: Dynamically generate social share images with serverless + url: /archives/dynamically-generate-social-share-images-with-serverless.html + - page: Using Lerna and Yarn Workspaces with serverless + url: /archives/using-lerna-and-yarn-workspaces-with-serverless.html + +extra-auth: + title: Authentication + chapters: + - page: How to add authentication to a serverless app + url: /archives/how-to-add-authentication-to-a-serverless-app.html + subchapters: + - page: Using Cognito to add authentication to a serverless app + url: /archives/using-cognito-to-add-authentication-to-a-serverless-app.html + +extra-frontend: + title: React + chapters: + - page: Understanding React Hooks + url: /archives/understanding-react-hooks.html + - page: Code Splitting in Create React App + url: /archives/code-splitting-in-create-react-app.html + - page: Environments in Create React App + url: /archives/environments-in-create-react-app.html + - page: Setting serverless environments variables in a React app + url: /archives/setting-serverless-environments-variables-in-a-react-app.html + - page: Deploying a React app to AWS + url: /archives/deploying-a-react-app-to-aws.html + subchapters: + - page: Create an S3 bucket + url: /archives/create-an-s3-bucket.html + - page: Deploy to S3 + url: /archives/deploy-to-s3.html + - page: Create a CloudFront distribution + url: /archives/create-a-cloudfront-distribution.html + - page: Set up a Custom Domain With SSL + url: /archives/setup-a-custom-domain-with-ssl.html + - page: Set up your domain with CloudFront + url: /archives/setup-your-domain-with-cloudfront.html + - page: Set up www domain redirect + url: /archives/setup-www-domain-redirect.html + - page: Deploy updates + url: /archives/deploy-updates.html + - page: Deploying a React app to Netlify + url: /archives/deploying-a-react-app-to-netlify.html + subchapters: + - page: Setting up your project on Netlify + url: /archives/setting-up-your-project-on-netlify.html + - page: Custom domains in Netlify + url: /archives/custom-domain-in-netlify.html + - page: Creating a CI/CD pipeline for React + url: /archives/creating-a-ci-cd-pipeline-for-react.html + - page: Manage environments in Create React App + url: /archives/manage-environments-in-create-react-app.html + - page: Create a Netlify build script + url: /archives/create-a-netlify-build-script.html + - page: Frontend workflow in Netlify + url: /archives/frontend-workflow-in-netlify.html + - page: Manage User Accounts in AWS Amplify + url: /archives/manage-user-accounts-in-aws-amplify.html + subchapters: + - page: Handle Forgot and Reset Password + url: /archives/handle-forgot-and-reset-password.html + - page: Allow Users to Change Passwords + url: /archives/allow-users-to-change-passwords.html + - page: Allow Users to Change Their Email + url: /archives/allow-users-to-change-their-email.html + - page: Facebook Login with Cognito using AWS Amplify + url: /archives/facebook-login-with-cognito-using-aws-amplify.html + diff --git a/_data/authors.yml b/_data/authors.yml new file mode 100644 index 0000000000..7dacfa29f6 --- /dev/null +++ b/_data/authors.yml @@ -0,0 +1,13 @@ +frank: + name: Frank + picture: /assets/lander/profiles/frank.png + linkedin: fanjiewang + twitter: fanjiewang + github: fwang + +jay: + name: Jay + picture: /assets/lander/profiles/jay.png + linkedin: jayair + twitter: jayair + github: jayair diff --git a/_data/changelog.yml b/_data/changelog.yml index 507b2f4e2b..6d1415ee15 100644 --- a/_data/changelog.yml +++ b/_data/changelog.yml @@ -1,4 +1,72 @@ current: + title: "Flip Ion to v3" + desc: "Aug 20, 2024: Update SST version from Ion to v3." + +v9-0: + title: "Migrate to Ion" + desc: "Jul 25, 2024: Migrating away from CDK to Terraform with SST Ion v0.1." + +v8-0-1: + title: "Updating Seed to IAM roles" + desc: "Sep 15, 2023: Minor changes to the setting up your project on Seed chapter." + +v8-0: + title: "Updating to TypeScript" + desc: "Aug 31, 2023: Using TS by default, switching to pnpm, and using Vite instead of Create React App. And archiving old chapters." + +v7-4: + title: "Upgrading to SST v2.5" + desc: "Apr 9, 2023: Updating the guide and code to use the latest `sst bind`." + +v7-3: + title: "Upgrading to SST v2" + desc: "Mar 10, 2023: Updating the guide and code snippets to SST v2." + +v7-2: + title: "Upgrading to SST v1" + desc: "May 24, 2022: Updating the guide and code snippets to SST v1." + +v7-1: + title: "Upgrading to CDK v2" + desc: "Feb 2, 2022: Updating SST version for CDK v2 upgrade." + +v7-0-3: + title: "Renaming lib to stacks" + desc: "Sep 24, 2021: Renaming lib to stacks in the SST app." + +v7-0: + title: "Creating separate SST and Serverless Framework sections" + desc: "Aug 25, 2021: Creating a separate SST version and Serverless Framework version of the guide." + +v6-1: + title: "Adding Extra Credit AppSync and Auth chapters" + desc: "Aug 3, 2021: Adding Extra Credit chapters on AppSync and Auth." + +v6-0: + title: "Upgrading Bootstrap and reorganizing chapters" + desc: "Nov 11, 2020: Upgrading to Bootstrap 4 and React Bootstrap 1.4. Also, a major reorganizing of the chapters." + +v5-0-2: + title: "Fixing encoding issue in eBook" + desc: "Oct 23, 2020: Generating new eBook version to fix encoding issues." + +v5-0-1: + title: "Updating to new eBook format" + desc: "Oct 21, 2020: Generating new eBook using Pandoc." + +v5-0: + title: "Using CDK to configure infrastructure resources" + desc: "Oct 7, 2020: Moving from CloudFormation to AWS CDK to configure infrastructure resources. And using SST to deploy CDK alongside Serverless Framework." + +v4-1: + title: "Adding new monitoring and debugging section" + desc: "Apr 8, 2020: Adding a new section on monitoring and debugging full-stack Serverless apps. Updating React Router. Using React Context to manage app state." + +v4-0: + title: "New edition of SST" + desc: "Oct 8, 2019: Adding a new section for Serverless best practices. Updating to React Hooks. Reorganizing chapters. Updating backend to Node 10." + +v3-4: title: "Updating to serverless-bundle and on-demand DynamoDB" desc: "Jul 18, 2019: Updating to serverless-bundle plugin and On-Demand Capacity for DynamoDB." diff --git a/_data/chapterlist.yml b/_data/chapterlist.yml index f75ddc6a34..7eace9c334 100644 --- a/_data/chapterlist.yml +++ b/_data/chapterlist.yml @@ -8,14 +8,14 @@ preface: - page: How to get help? url: /chapters/how-to-get-help.html -intro-part1: +intro: title: Introduction chapters: - - page: What is Serverless? + - page: What is serverless? url: /chapters/what-is-serverless.html - page: What is AWS Lambda? url: /chapters/what-is-aws-lambda.html - - page: Why create Serverless apps? + - page: Why create serverless apps? url: /chapters/why-create-serverless-apps.html setup-aws: @@ -25,63 +25,82 @@ setup-aws: url: /chapters/create-an-aws-account.html - page: Create an IAM user url: /chapters/create-an-iam-user.html - subchapters: - - page: What is IAM - url: /chapters/what-is-iam.html - - page: What is an ARN - url: /chapters/what-is-an-arn.html - page: Configure the AWS CLI url: /chapters/configure-the-aws-cli.html -setup-backend: - title: Setting up the Serverless Backend +setup-sst: + title: Setting up an SST app chapters: - - page: Create a DynamoDB table - url: /chapters/create-a-dynamodb-table.html - - page: Create an S3 bucket for file uploads - url: /chapters/create-an-s3-bucket-for-file-uploads.html - - page: Create a Cognito user pool - url: /chapters/create-a-cognito-user-pool.html - subchapters: - - page: Create a Cognito test user - url: /chapters/create-a-cognito-test-user.html - - page: Set up the Serverless Framework - url: /chapters/setup-the-serverless-framework.html + - page: What is SST? + url: /chapters/what-is-sst.html subchapters: - - page: Add support for ES6/ES7 JavaScript - url: /chapters/add-support-for-es6-es7-javascript.html + - page: What is Infrastructure as Code? + url: /chapters/what-is-infrastructure-as-code.html + - page: Create an SST app + url: /chapters/create-an-sst-app.html + - page: Create a Hello World API + url: /chapters/create-a-hello-world-api.html -build-api: - title: Building a Serverless REST API +setup-sst-backend: + title: Create your AWS resources chapters: - - page: Add a create note API - url: /chapters/add-a-create-note-api.html - - page: Add a get note API - url: /chapters/add-a-get-note-api.html - - page: Add a list all the notes API - url: /chapters/add-a-list-all-the-notes-api.html - - page: Add an update note API - url: /chapters/add-an-update-note-api.html - - page: Add a delete note API - url: /chapters/add-a-delete-note-api.html - - page: Handle API Gateway CORS errors - url: /chapters/handle-api-gateway-cors-errors.html + - page: Create a DynamoDB Table in SST + url: /chapters/create-a-dynamodb-table-in-sst.html + - page: Create an S3 bucket in SST + url: /chapters/create-an-s3-bucket-in-sst.html -deploy-backend: - title: Deploying the Backend +build-sst-api: + title: Building a serverless API chapters: - - page: Deploy the APIs - url: /chapters/deploy-the-apis.html - - page: Create a Cognito identity pool - url: /chapters/create-a-cognito-identity-pool.html - subchapters: - - page: Cognito user pool vs identity pool - url: /chapters/cognito-user-pool-vs-identity-pool.html - - page: Test the APIs - url: /chapters/test-the-apis.html + - page: Review our app architecture + url: /chapters/review-our-app-architecture.html + - page: Add an API to create a note + url: /chapters/add-an-api-to-create-a-note.html + - page: Add an API to get a note + url: /chapters/add-an-api-to-get-a-note.html + - page: Add an API to list all the notes + url: /chapters/add-an-api-to-list-all-the-notes.html + - page: Add an API to update a note + url: /chapters/add-an-api-to-update-a-note.html + - page: Add an API to delete a note + url: /chapters/add-an-api-to-delete-a-note.html + +add-auth-stack: + title: Users and authentication + chapters: + - page: Auth in serverless apps + url: /chapters/auth-in-serverless-apps.html + - page: Adding auth to our serverless app + url: /chapters/adding-auth-to-our-serverless-app.html + - page: Secure our serverless APIs + url: /chapters/secure-our-serverless-apis.html + +handling-secrets: + title: Working with secrets + chapters: + - page: Setup a Stripe account + url: /chapters/setup-a-stripe-account.html + - page: Handling secrets in SST + url: /chapters/handling-secrets-in-sst.html + - page: Add an API to handle billing + url: /chapters/add-an-api-to-handle-billing.html + +unit-tests: + title: Serverless unit tests + chapters: + - page: Unit tests in serverless + url: /chapters/unit-tests-in-serverless.html + +cors-sst: + title: CORS in serverless + chapters: + - page: Handle CORS in serverless APIs + url: /chapters/handle-cors-in-serverless-apis.html + - page: Handle CORS in S3 for File Uploads + url: /chapters/handle-cors-in-s3-for-file-uploads.html setup-react: - title: Setting up a React App + title: Setting up a React app chapters: - page: Create a new React.js app url: /chapters/create-a-new-reactjs-app.html @@ -92,6 +111,10 @@ setup-react: url: /chapters/setup-custom-fonts.html - page: Set up Bootstrap url: /chapters/setup-bootstrap.html + +react-routes: + title: Routes in React + chapters: - page: Handle routes with React Router url: /chapters/handle-routes-with-react-router.html subchapters: @@ -101,12 +124,12 @@ setup-react: url: /chapters/adding-links-in-the-navbar.html - page: Handle 404s url: /chapters/handle-404s.html - - page: Configure AWS Amplify - url: /chapters/configure-aws-amplify.html -build-react: - title: Building a React App +setup-amplify: + title: Adding auth to a React app chapters: + - page: Configure AWS Amplify + url: /chapters/configure-aws-amplify.html - page: Create a login page url: /chapters/create-a-login-page.html subchapters: @@ -122,6 +145,8 @@ build-react: url: /chapters/redirect-on-login-and-logout.html - page: Give feedback while logging in url: /chapters/give-feedback-while-logging-in.html + - page: Create a Custom React Hook to Handle Form Fields + url: /chapters/create-a-custom-react-hook-to-handle-form-fields.html - page: Create a signup page url: /chapters/create-a-signup-page.html subchapters: @@ -129,6 +154,10 @@ build-react: url: /chapters/create-the-signup-form.html - page: Signup with AWS Cognito url: /chapters/signup-with-aws-cognito.html + +build-react: + title: Building a React app + chapters: - page: Add the create note page url: /chapters/add-the-create-note-page.html subchapters: @@ -150,128 +179,6 @@ build-react: url: /chapters/save-changes-to-a-note.html - page: Delete a note url: /chapters/delete-a-note.html - - page: Set up secure pages - url: /chapters/setup-secure-pages.html - subchapters: - - page: Create a route that redirects - url: /chapters/create-a-route-that-redirects.html - - page: Use the redirect routes - url: /chapters/use-the-redirect-routes.html - - page: Redirect on login - url: /chapters/redirect-on-login.html - -deploy-react: - title: Deploying a React app on AWS - chapters: - - page: Deploy the Frontend - url: /chapters/deploy-the-frontend.html - subchapters: - - page: Create an S3 bucket - url: /chapters/create-an-s3-bucket.html - - page: Deploy to S3 - url: /chapters/deploy-to-s3.html - - page: Create a CloudFront distribution - url: /chapters/create-a-cloudfront-distribution.html - - page: Set up your domain with CloudFront - url: /chapters/setup-your-domain-with-cloudfront.html - - page: Set up www domain redirect - url: /chapters/setup-www-domain-redirect.html - - page: Set up SSL - url: /chapters/setup-ssl.html - - page: Deploy updates - url: /chapters/deploy-updates.html - subchapters: - - page: Update the app - url: /chapters/update-the-app.html - - page: Deploy again - url: /chapters/deploy-again.html - -intro-part2: - title: Introduction - chapters: - - page: Getting production ready - url: /chapters/getting-production-ready.html - -new-backend: - title: Create a new backend - chapters: - - page: Initialize the backend repo - url: /chapters/initialize-the-backend-repo.html - subchapters: - - page: Organize the backend repo - url: /chapters/organize-the-backend-repo.html - -infrastructure-code: - title: Infrastructure as Code - chapters: - - page: What is Infrastructure as Code? - url: /chapters/what-is-infrastructure-as-code.html - subchapters: - - page: Configure DynamoDB in Serverless - url: /chapters/configure-dynamodb-in-serverless.html - - page: Configure S3 in Serverless - url: /chapters/configure-s3-in-serverless.html - - page: Configure Cognito User Pool in Serverless - url: /chapters/configure-cognito-user-pool-in-serverless.html - - page: Configure Cognito Identity Pool in Serverless - url: /chapters/configure-cognito-identity-pool-in-serverless.html - - page: Use environment variables in Lambda functions - url: /chapters/use-environment-variables-in-lambda-functions.html - - page: Deploy your Serverless infrastructure - url: /chapters/deploy-your-serverless-infrastructure.html - -stripe-api: - title: Adding a Stripe billing API - chapters: - - page: Working with 3rd party APIs - url: /chapters/working-with-3rd-party-apis.html - subchapters: - - page: Setup a Stripe account - url: /chapters/setup-a-stripe-account.html - - page: Add a billing API - url: /chapters/add-a-billing-api.html - - page: Load secrets from env.yml - url: /chapters/load-secrets-from-env-yml.html - - page: Test the billing API - url: /chapters/test-the-billing-api.html - -unit-tests: - title: Adding unit tests - chapters: - - page: Unit tests in Serverless - url: /chapters/unit-tests-in-serverless.html - -serverless-deployments: - title: Automating Serverless deployments - chapters: - - page: Automating Serverless deployments - url: /chapters/automating-serverless-deployments.html - subchapters: - - page: Setting up your project on Seed - url: /chapters/setting-up-your-project-on-seed.html - - page: Configure secrets in Seed - url: /chapters/configure-secrets-in-seed.html - - page: Deploying through Seed - url: /chapters/deploying-through-seed.html - - page: Set custom domains through Seed - url: /chapters/set-custom-domains-through-seed.html - - page: Test the configured APIs - url: /chapters/test-the-configured-apis.html - - page: Monitoring deployments in Seed - url: /chapters/monitoring-deployments-in-seed.html - -connect-frontend: - title: Connect to the new backend - chapters: - - page: Initialize the frontend repo - url: /chapters/initialize-the-frontend-repo.html - subchapters: - - page: Manage environments in Create React App - url: /chapters/manage-environments-in-create-react-app.html - -add-billing-form: - title: Adding a billing form - chapters: - page: Create a settings page url: /chapters/create-a-settings-page.html subchapters: @@ -282,27 +189,73 @@ add-billing-form: - page: Connect the billing form url: /chapters/connect-the-billing-form.html -react-deployments: - title: Automating React App deployments +secure-pages: + title: Securing React pages + chapters: + - page: Set up secure pages + url: /chapters/setup-secure-pages.html + - page: Create a route that redirects + url: /chapters/create-a-route-that-redirects.html + - page: Use the redirect routes + url: /chapters/use-the-redirect-routes.html + - page: Redirect on login + url: /chapters/redirect-on-login.html + +custom-domains: + title: Using Custom Domains + chapters: + - page: Getting production ready + url: /chapters/getting-production-ready.html + - page: Purchase a domain with Route 53 + url: /chapters/purchase-a-domain-with-route-53.html + - page: Custom domains in serverless APIs + url: /chapters/custom-domains-in-serverless-apis.html + - page: Custom domains for React apps on AWS + url: /chapters/custom-domains-for-react-apps-on-aws.html + +automating-deployments: + title: Automating deployments chapters: - - page: Automating React Deployments - url: /chapters/automating-react-deployments.html - subchapters: - - page: Create a build script - url: /chapters/create-a-build-script.html - - page: Setting up your project on Netlify - url: /chapters/setting-up-your-project-on-netlify.html - - page: Custom Domains in Netlify - url: /chapters/custom-domain-in-netlify.html - - page: Frontend workflow - url: /chapters/frontend-workflow.html + - page: Creating a CI/CD pipeline for serverless + url: /chapters/creating-a-ci-cd-pipeline-for-serverless.html + subchapters: + - page: Setting up the SST Console + url: /chapters/setting-up-the-sst-console.html +# - page: Configure secrets in Seed +# url: /chapters/configure-secrets-in-seed.html + - page: Deploying through the SST Console + url: /chapters/deploying-through-the-sst-console.html + +#monitor-debug-errors: +# title: Monitoring and debugging +# chapters: +# - page: Debugging full-stack serverless apps +# url: /chapters/debugging-full-stack-serverless-apps.html +# - page: Setup error reporting in React +# url: /chapters/setup-error-reporting-in-react.html +# subchapters: +# - page: Report API errors in React +# url: /chapters/report-api-errors-in-react.html +# - page: Configure an error boundary in React +# url: /chapters/setup-an-error-boundary-in-react.html +# - page: Setup error logging in serverless +# url: /chapters/setup-error-logging-in-serverless.html +# subchapters: +# - page: Logic errors in Lambda functions +# url: /chapters/logic-errors-in-lambda-functions.html +# - page: Unexpected errors in Lambda functions +# url: /chapters/unexpected-errors-in-lambda-functions.html +# - page: Errors outside Lambda functions +# url: /chapters/errors-outside-lambda-functions.html +# - page: Errors in API Gateway +# url: /chapters/errors-in-api-gateway.html conclusion: title: Conclusion chapters: - page: Wrapping up url: /chapters/wrapping-up.html - - page: Futher reading + - page: Further reading url: /chapters/further-reading.html - page: Translations url: /chapters/translations.html @@ -312,71 +265,3 @@ conclusion: url: /chapters/changelog.html - page: Staying up to date url: /chapters/staying-up-to-date.html - -extra-sls-architecture: - title: Serverless architecture - chapters: - - page: Organizing Serverless projects - url: /chapters/organizing-serverless-projects.html - - page: Cross-stack references in Serverless - url: /chapters/cross-stack-references-in-serverless.html - subchapters: - - page: DynamoDB as a Serverless service - url: /chapters/dynamodb-as-a-serverless-service.html - - page: S3 as a Serverless service - url: /chapters/s3-as-a-serverless-service.html - - page: API Gateway domains across services - url: /chapters/api-gateway-domains-across-services.html - - page: Cognito as a Serverless service - url: /chapters/cognito-as-a-serverless-service.html - - page: Deploying multiple services in Serverless - url: /chapters/deploying-multiple-services-in-serverless.html - -extra-backend: - title: Backend - chapters: - - page: API Gateway and Lambda Logs - url: /chapters/api-gateway-and-lambda-logs.html - - page: Debugging Serverless API Issues - url: /chapters/debugging-serverless-api-issues.html - - page: Serverless environment variables - url: /chapters/serverless-environment-variables.html - - page: Stages in Serverless Framework - url: /chapters/stages-in-serverless-framework.html - - page: Backups in DynamoDB - url: /chapters/backups-in-dynamodb.html - - page: Configure multiple AWS profiles - url: /chapters/configure-multiple-aws-profiles.html - - page: Customize the Serverless IAM Policy - url: /chapters/customize-the-serverless-iam-policy.html - - page: Mapping Cognito Identity Id and User Pool Id - url: /chapters/mapping-cognito-identity-id-and-user-pool-id.html - - page: Connect to API Gateway with IAM auth - url: /chapters/connect-to-api-gateway-with-iam-auth.html - - page: Serverless Node.js Starter - url: /chapters/serverless-nodejs-starter.html - - page: Package Lambdas with serverless-bundle - url: /chapters/package-lambdas-with-serverless-bundle.html - -extra-user-mgmt: - title: User Management - chapters: - - page: Manage User Accounts in AWS Amplify - url: /chapters/manage-user-accounts-in-aws-amplify.html - subchapters: - - page: Handle Forgot and Reset Password - url: /chapters/handle-forgot-and-reset-password.html - - page: Allow Users to Change Passwords - url: /chapters/allow-users-to-change-passwords.html - - page: Allow Users to Change Their Email - url: /chapters/allow-users-to-change-their-email.html - -extra-frontend: - title: Frontend - chapters: - - page: Code Splitting in Create React App - url: /chapters/code-splitting-in-create-react-app.html - - page: Environments in Create React App - url: /chapters/environments-in-create-react-app.html - - page: Facebook Login with Cognito using AWS Amplify - url: /chapters/facebook-login-with-cognito-using-aws-amplify.html diff --git a/_data/languages.yml b/_data/languages.yml index a011e1332c..b22953391a 100644 --- a/_data/languages.yml +++ b/_data/languages.yml @@ -5,3 +5,7 @@ es: Spanish de: German fr: Français id: Indonesian +zh: 中文 +si: සිංහල +pl: Polish + diff --git a/_data/sponsors.yml b/_data/sponsors.yml new file mode 100644 index 0000000000..e60aa013e2 --- /dev/null +++ b/_data/sponsors.yml @@ -0,0 +1,21 @@ +list: + seed: + - link: "https://seed.run" + alt: "Deploy, manage, and monitor Serverless applications." + image: "/assets/sponsors/seed-serverless-ci-cd.png" + width: 435 +# datadog: +# - link: "https://www.datadoghq.com/dg/monitor/serverless-ts/?utm_source=Advertisement&utm_medium=Advertisement&utm_campaign=ServerlessStack-DisplayTshirt" +# alt: "Monitor all your serverless functions in one place" +# image: "/assets/sponsors/datadog-banner.png" +# width: 440 +# thundra: +# - link: "https://www.thundra.io/aws-solution-brief?utm_source=serverlessStack&utm_medium=banner&utm_campaign=Solution%20Brief" +# alt: "Thundra — Application Observability & Security Platform" +# image: "/assets/sponsors/thundra-banner.gif" +# width: 420 +# epsagon: +# - link: "https://epsagon.com/serverless-stack/?utm_source=Advertisement&utm_medium=cpm&utm_campaign=ServerlessStack" +# alt: "Automated & Correlated Tracing for Serverless" +# image: "/assets/sponsors/epsagon-banner.jpg" +# width: 420 diff --git a/_drafts/dynamodb-paging.md b/_drafts/dynamodb-paging.md index 58eea838a7..0ee69231b1 100644 --- a/_drafts/dynamodb-paging.md +++ b/_drafts/dynamodb-paging.md @@ -8,7 +8,7 @@ code: backend comments_id: add-a-create-note-api/125 --- -For the [notes app](https://demo.serverless-stack.com) that we have created, we allow our users to see a list of all the notes they have. But we left out a detail that needs to be handled when a user has a lot of notes. DynamoDB by default returns the amount of results that can fit in 1MB. So in this case where the number of notes exceeds the 1MB query result size, we need to be able to page through the rest of the notes. +For the [notes app](https://demo.sst.dev) that we have created, we allow our users to see a list of all the notes they have. But we left out a detail that needs to be handled when a user has a lot of notes. DynamoDB by default returns the amount of results that can fit in 1MB. So in this case where the number of notes exceeds the 1MB query result size, we need to be able to page through the rest of the notes. In this chapter we are going to look at how to add paging to DynamoDB tables and how to use that in your React app. diff --git a/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md b/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md new file mode 100644 index 0000000000..1940e289b1 --- /dev/null +++ b/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md @@ -0,0 +1,272 @@ +--- +layout: example +title: How to add a custom domain to a serverless API +short_title: Custom Domains +date: 2021-02-25 00:00:00 +lang: en +index: 5 +type: api +description: In this example we will look at how to add a custom domain to a serverless API using SST. We'll be using the Api construct to create an API with a custom domain. +short_desc: Using a custom domain in an API. +repo: rest-api-custom-domain +ref: how-to-add-a-custom-domain-to-a-serverless-api +comments_id: how-to-add-a-custom-domain-to-a-serverless-api/2334 +--- + +In this example we will look at how to add a custom domain to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A domain configured using [Route 53](https://aws.amazon.com/route53/). + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example rest-api-custom-domain +$ cd rest-api-custom-domain +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api-custom-domain", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up an API + +Let's start by setting up an API + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + const stage = app.stage; + + // Create the HTTP API + const api = new Api(stack, "Api", { + customDomain: `${stage}.example.com`, + routes: { + "GET /": "packages/functions/src/lambda.main", + }, + }); + + // Show the API endpoint in output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding a route to it. + +``` +GET / +``` + +We are also configuring a custom domain for the API endpoint. + +```typescript +customDomain: `${stage}.example.com`; +``` + +Our custom domain is based on the stage we are deploying to. So for `dev` it'll be `dev.example.com`. To do this, we are [accessing the properties of the app from the stack]({{ site.v2_url }}/constructs/Stack#accessing-app-properties). + +## Custom domains in Route 53 + +If you are looking to create a new domain, you can [follow this guide to purchase one from Route 53]({% link _chapters/purchase-a-domain-with-route-53.md %}). + +Or if you have a domain hosted on another provider, [read this to migrate it to Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). + +If you already have a domain in Route 53, SST will look for a [hosted zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html) with the name set to the base domain. So for example, if your custom domain is set to `dev.example.com`, SST will look for a hosted zone called `example.com`. If you have it set under a different hosted zone, you'll need to set that explicitly. + +```typescript +const api = new Api(stack, "Api", { + customDomain: { + domainName: "dev.api.example.com", + hostedZone: "example.com", + }, + ... +}); +``` + +## Adding function code + +For this example, we are going to focus on the custom domain. So we are going to keep our Lambda function simple. [Refer to the CRUD example]({% link _examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md %}), if you want to connect your API to a database. + +{%change%} Replace the `packages/functions/src/lambda.ts` with the following. + +```typescript +export async function main() { + const response = { + userId: 1, + id: 1, + title: "delectus aut autem", + completed: false, + }; + + return { + statusCode: 200, + body: JSON.stringify(response), + }; +} +``` + +Note that this function need to be `async` to be invoked by AWS Lambda. Even though, in this case we are doing everything synchronously. + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Deploying your app in this case also means configuring the custom domain. So if you are doing it the first time, it'll take longer to set that up. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-custom-domain-ExampleStack: deploying... + + ✅ dev-rest-api-custom-domain-ExampleStack + + +Stack dev-rest-api-custom-domain-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://9bdtsrrlu1.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. Head over to the following in your browser. Make sure to replace the URL with your API. + +``` +https://9bdtsrrlu1.execute-api.us-east-1.amazonaws.com +``` + +You should see a JSON string printed in the browser. + +Now try hitting our custom domain. + +``` +https://dev.example.com +``` + +You should see the same response again. If the page does not load, don't worry. It can take up to 40 minutes for your custom domain to propagate through DNS. Try again after some time. + +## Making changes + +Let's make a quick change to our API. It would be good if the JSON strings are pretty printed to make them more readable. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +export async function main() { + const response = { + userId: 1, + id: 1, + title: "delectus aut autem", + completed: false, + }; + + return { + statusCode: 200, + body: JSON.stringify(response, null, " "), + }; +} +``` + +Here we are just [adding some spaces](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) to pretty print the JSON. + +If you head back to the custom domain endpoint. + +``` +https://dev.example.com +``` + +You should see the object in a more readable format. + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +Once deployed, you should be able to access that endpoint on the prod custom domain. + +``` +https://prod.example.com +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +This will remove the custom domain mappings as well. + +## Conclusion + +And that's it! You've got a brand new serverless API with a custom domain. A local development environment, to test and make changes. And it's deployed to production with a custom domain as well. So you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md b/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md new file mode 100644 index 0000000000..8b2f90ce66 --- /dev/null +++ b/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md @@ -0,0 +1,373 @@ +--- +layout: example +title: How to add Auth0 authentication to a serverless API +short_title: Auth0 IAM +date: 2021-02-23 00:00:00 +lang: en +index: 5 +type: iam-auth +description: In this example we will look at how to add Auth0 authentication to a serverless API using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a serverless API with Auth0. +repo: api-auth-auth0 +ref: how-to-add-auth0-authentication-to-a-serverless-api +comments_id: how-to-add-auth0-authentication-to-a-serverless-api/2333 +--- + +In this example we will look at how to add [Auth0](https://auth0.com) authentication to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- An [Auth0 account](https://auth0.com) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-auth0 +$ cd api-auth-auth0 +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-auth-auth0", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Api, Cognito } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Api + const api = new Api(stack, "Api", { + defaults: { + authorizer: "iam", + }, + routes: { + "GET /private": "packages/functions/src/private.main", + "GET /public": { + function: "packages/functions/src/public.main", + authorizer: "none", + }, + }, + }); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +To secure our APIs we are adding the authorization type `AWS_IAM`. This means the caller of the API needs to have the right permissions. The first route is a private endpoint. The second is a public endpoint and its authorization type is overriden to `NONE`. + +## Setting up authentication + +Now let's add authentication for our serverless app. + +{%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `domain` and `clientId` with that of your Auth0 app. + +```typescript +// Create auth provider +const auth = new Cognito(stack, "Auth", { + identityPoolFederation: { + auth0: { + domain: "https://myorg.us.auth0.com", + clientId: "UsGRQJJz5sDfPQDs6bhQ9Oc3hNISuVif", + }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html) which relies on Auth0 to authenticate users. And we use the [`attachPermissionsForAuthUsers`]({{ site.v2_url }}/constructs/Auth#attachpermissionsforauthusers) method to allow our logged in users to access our API. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +stack.addOutputs({ + ApiEndpoint: api.url, + IdentityPoolId: auth.cognitoIdentityPoolId, +}); +``` + +We are going to print out the Identity Pool Id for reference. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello user!", + }; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-auth0-ExampleStack: deploying... + + ✅ dev-api-auth-auth0-ExampleStack + + +Stack dev-api-auth-auth0-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://2zy74sn6we.execute-api.us-east-1.amazonaws.com + IdentityPoolId: us-east-1:84340cf1-4f64-496e-87c2-517072e7d5d9 +``` + +The `ApiEndpoint` is the API we just created. Make a note of the `IdentityPoolId`, we'll need that later. + +Now let's try out our public route. Head over to the following in your browser. Make sure to replace the URL with your API. + +``` +https://2zy74sn6we.execute-api.us-east-1.amazonaws.com/public +``` + +You should see the greeting `Hello stranger!`. + +And if you try to visit the private route, you will see `{"message":"Forbidden"}`. + +``` +https://2zy74sn6we.execute-api.us-east-1.amazonaws.com/private +``` + +## Login with Auth0 + +We are going to use Auth0's universal login page to test logging in with Auth0. + +First, we'll configure a callback URL that'll be used by the login page. It'll redirect authenticated users to a page with the authorization code. Head over to your Auth0 app, select **Settings**, and add `http://localhost:5678` to the **Allowed Callback URLS**. We don't need a working URL for now. We just need the code. You can later point this to your frontend application. + +Next, open up your browser and head over to the login page. Replace the `client_id` with your app's `Client ID`. And the domain in the URL with the one for your Auth0 app. + +```text +https://myorg.us.auth0.com/authorize?response_type=code&client_id=UsGRQJJz5sDfPQDs6bhQ9Oc3hNISuVif&redirect_uri=http://localhost:5678&scope=openid%20profile +``` + +Your login page should look something like this. Continue logging in. If you have not setup a user, you can create one in your Auth0 dashboard. + +![Authenticate users using Auth0 Universal Login page](/assets/examples/api-auth-auth0/authenticate-users-using-auth0-universal-login-page.png) + +If the login was successful, the browser will be redirected to the callback URL. Copy the **authorization code** from the URL. + +![Generate authorization code for users logged in with Auth0](/assets/examples/api-auth-auth0/generate-authorization-code-for-users-logged-in-with-auth0.png) + +Next, we need to exchange the user's code for tokens. Replace the `url` domain, `client_id` and `client_secret` with the ones for your Auth0 app. Also, replace the `code` with the **authorization code** from above. + +```bash +$ curl --request POST \ + --url https://myorg.us.auth0.com/oauth/token \ + --data "grant_type=authorization_code&client_id=UsGRQJJz5sDfPQDs6bhQ9Oc3hNISuVif&client_secret=80ExzyYpIsGZ5WwOUkefgk8mg5tZiAdzisdnMEXybD7CQIBGgtZIEp_xVBGGSK6P&code=EvaUxc_3vp-LZXDk&redirect_uri=http://localhost:5678" +``` + +You should get a couple of tokens for the Auth0 user. + +```json +{ + "access_token": "0Yl7bZdnkS2LDBbHkpLBXCU2K3SRilnp", + "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imk0REpDeWhabncydDN1dTd6TlI4USJ9.eyJuaWNrbmFtZSI6IndhbmdmYW5qaWUiLCJuYW1lIjoid2FuZ2ZhbmppZUBnbWFpbC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvMmE5Y2VlMTkxYWI3NjBlZmI3ZTU1ZTBkN2MzNjZiYmI_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZ3YS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wMi0yNFQwNDoxMjoxOC40NzJaIiwiaXNzIjoiaHR0cHM6Ly9zc3QtdGVzdC51cy5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NjAzNTdhNmQ5OGUzZTUwMDZhOWQ3NGEzIiwiYXVkIjoiVXNHUlFKSno1c0RmUFFEczZiaFE5T2MzaE5JU3VWaWUiLCJpYXQiOjE2MTQxNDAyMTksImV4cCI6MTYxNDE3NjIxOX0.KIB9bNHykhcFuMkXGEbu1TlcAp0A6xyze4wSwUh_BscnOlXjcKN-IoN6cgnt7YXUYJa7StN3WSduJJEx_LRpcrrUQw-V3BSGge06RA4bGWXM7S4rdpu4TCG0Lw_V272AKkWIrEGdOBd_Xw-lC8iwX0HXzuZ6-n4gzHPJAzhZ7Io0akkObsvSlQaRKOOXsx-cShWPXa3ZVThSgK5iO00LrsbPMICvvrQVSlwG2XnQDaonUnrXg6kKn0rP_GegoFCAz3buYDGYK__Z7oDaj4chldAqR1FmnJ2X9MfRmpjuX4-94ebicLv7O9fdMHIQQWCgtLmcu4T0mKpR2e3gL_13gQ", + "scope": "openid profile", + "expires_in": 86400, + "token_type": "Bearer" +} +``` + +Next, we need to get the user's Cognito Identity id. Replace `--identity-pool-id` with the `IdentityPoolId` from the `sst dev` log output; and replace the `--logins` with the **domain** of your app and the **id_token** from the previous step. + +```bash +$ aws cognito-identity get-id \ + --identity-pool-id us-east-1:84340cf1-4f64-496e-87c2-517072e7d5d9 \ + --logins myorg.us.auth0.com="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imk0REpDeWhabncydDN1dTd6TlI4USJ9.eyJuaWNrbmFtZSI6IndhbmdmYW5qaWUiLCJuYW1lIjoid2FuZ2ZhbmppZUBnbWFpbC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvMmE5Y2VlMTkxYWI3NjBlZmI3ZTU1ZTBkN2MzNjZiYmI_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZ3YS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wMi0yNFQwNDoxMjoxOC40NzJaIiwiaXNzIjoiaHR0cHM6Ly9zc3QtdGVzdC51cy5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NjAzNTdhNmQ5OGUzZTUwMDZhOWQ3NGEzIiwiYXVkIjoiVXNHUlFKSno1c0RmUFFEczZiaFE5T2MzaE5JU3VWaWUiLCJpYXQiOjE2MTQxNDAyMTksImV4cCI6MTYxNDE3NjIxOX0.KIB9bNHykhcFuMkXGEbu1TlcAp0A6xyze4wSwUh_BscnOlXjcKN-IoN6cgnt7YXUYJa7StN3WSduJJEx_LRpcrrUQw-V3BSGge06RA4bGWXM7S4rdpu4TCG0Lw_V272AKkWIrEGdOBd_Xw-lC8iwX0HXzuZ6-n4gzHPJAzhZ7Io0akkObsvSlQaRKOOXsx-cShWPXa3ZVThSgK5iO00LrsbPMICvvrQVSlwG2XnQDaonUnrXg6kKn0rP_GegoFCAz3buYDGYK__Z7oDaj4chldAqR1FmnJ2X9MfRmpjuX4-94ebicLv7O9fdMHIQQWCgtLmcu4T0mKpR2e3gL_13gQ" +``` + +This should give you an identity id for the Auth0 user. + +```json +{ + "IdentityId": "us-east-1:46625265-9c97-420f-a826-15dbc812a008" +} +``` + +Now we'll use that to get the IAM credentials for the identity user. Use the same `--logins` option as the command above. + +```bash +$ aws cognito-identity get-credentials-for-identity \ + --identity-id us-east-1:46625265-9c97-420f-a826-15dbc812a008 \ + --logins myorg.us.auth0.com="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imk0REpDeWhabncydDN1dTd6TlI4USJ9.eyJuaWNrbmFtZSI6IndhbmdmYW5qaWUiLCJuYW1lIjoid2FuZ2ZhbmppZUBnbWFpbC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvMmE5Y2VlMTkxYWI3NjBlZmI3ZTU1ZTBkN2MzNjZiYmI_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZ3YS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wMi0yNFQwNDoxMjoxOC40NzJaIiwiaXNzIjoiaHR0cHM6Ly9zc3QtdGVzdC51cy5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NjAzNTdhNmQ5OGUzZTUwMDZhOWQ3NGEzIiwiYXVkIjoiVXNHUlFKSno1c0RmUFFEczZiaFE5T2MzaE5JU3VWaWUiLCJpYXQiOjE2MTQxNDAyMTksImV4cCI6MTYxNDE3NjIxOX0.KIB9bNHykhcFuMkXGEbu1TlcAp0A6xyze4wSwUh_BscnOlXjcKN-IoN6cgnt7YXUYJa7StN3WSduJJEx_LRpcrrUQw-V3BSGge06RA4bGWXM7S4rdpu4TCG0Lw_V272AKkWIrEGdOBd_Xw-lC8iwX0HXzuZ6-n4gzHPJAzhZ7Io0akkObsvSlQaRKOOXsx-cShWPXa3ZVThSgK5iO00LrsbPMICvvrQVSlwG2XnQDaonUnrXg6kKn0rP_GegoFCAz3buYDGYK__Z7oDaj4chldAqR1FmnJ2X9MfRmpjuX4-94ebicLv7O9fdMHIQQWCgtLmcu4T0mKpR2e3gL_13gQ" +``` + +The temporary IAM credentials should look something like this. + +```json +{ + "IdentityId": "us-east-1:46625265-9c97-420f-a826-15dbc812a008", + "Credentials": { + "AccessKeyId": "ASIARUIS6Q2MOT2D7LGE", + "SecretKey": "r9xMZBe7KXqKYUTBPuGR8jziNrkD8XpL5g2r9Pgw", + "SessionToken": "IQoJb3JpZ2luX2VjEHYaCXVzLWVhc3QtMSJHMEUCIA/0ccZZvhjSPnoXkzJ/TUiSPXB2ON/1Qnn2/omfQOQLAiEA+qjuBHYwZvHG8Q9cfjd/0yloUkh5pkEUzEiCjjaa5FYq6QMIbhACGgwxMTIyNDU3Njk4ODAiDGDpiBCuNOBhkktiHyrGA7A8scWUjxzwUKaEAzYdWOwxDdYxA21wPc3Bz2NSJlscwHQP0AjmZ3aPmEREgwhi92/5SGETFINbJSRDs9dsJ+hrArHpSyoOp6UmXX/48q8b9BbWKB2qeF/kIPMG+1urwgTLn7I9cmYNH0LUHLJ0/EaRVxFo/hUTnTiPsDZCD9X96WxvO+cfjhmpAdCTR8MjxUl4k18grIWzPBkNAJwS1D+zIuoQTQPiIN6e25pWi3Mi+wXxgz+ToBFiPeybl3Q9qHOH0gQipss5eYrMFYaRWS3k6eOLCZoTOA4T/sMoJMweGwT2V33C1/o95W0LXCwYuAWg9bdUC71DHtc9bPY1NCAWqQcnxQabziZkOFTW5aLeDsY53TDPFoYiQ8lUrmDLhZSU3MsBcXVtPsvI5MPmoIqyf62ccd8VJo7idS5yyobZz9Ku7/jG/ZmU5S0jdpjWIVqBGNd5aG4R6Vf41FqMN0bEcz2qQBRFTeRg+UDQTv6Hc0kM943iXXBNdzVptivlkEV/fN5NN8sC5zXOafWUMJ8raQhPOAvWTVPIo8aXfAlKzcAqA/8bzJOzeEADcW71XGABOSzhy5TQayqWVrIX8ksBWMmFcSMwqJSDgQY6hAINr+bYzf+Vp1knGBWE52ArJAWzcss9UQU+b0kXripIvFpbdSCn3Yz4+kHKmmgvLKCEGo2k+zJW8TP+j+f3PsQinCB1VHpLpL2G+rx4aK/wMZ48ALY/rIK8KcYArnmjga5IT/PC/4cRW0z1vCucGQibKZ5skF0tUnpLb3BNwGP42NrtoaFkHPmihRpvvpS93iHX8HavIkpEzNcgkKzCcL3tdWXlnN9Hx/CI1kpb4ubzYaAQYiuURYKrFySzkaAJAvSkCO0ZjG342YHe9V+WEC/VBDRJllSiPBAnWaWrDsymafAKUA3HylrvKAetXaK8sSwGQ3DfkJ6GedJTel3FDN8jzZOo9A==", + "Expiration": "2021-02-08T01:20:40-05:00" + } +} +``` + +Let's make a call to the private route using the credentials. The API request needs to be [signed with AWS SigV4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). We are going to use [Insomnia](https://insomnia.rest) to help us sign and make this request. + +Make sure to replace the **Access Key Id**, **Secret Access Key**, and **Session Token** with the ones from the temporary credentials. And replace the **Region** for your API endpoint. In our case the region is `us-east-1`. + +``` +https://2zy74sn6we.execute-api.us-east-1.amazonaws.com +``` + +![Invoke Auth0 authenticated API Gateway route](/assets/examples/api-auth-auth0/invoke-auth0-authenticated-api-gateway-route.png) + +You should now see. + +``` +Hello user! +``` + +The above process might seem fairly tedious. But once we integrate it into our frontend app, we'll be able to use something like [AWS Amplify]({% link _chapters/configure-aws-amplify.md %}) to handle these steps for us. + +## Making changes + +Let's make a quick change to our private route and print out the caller's user id. + +{%change%} Replace `packages/functions/src/private.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.iam.cognitoIdentity.identityId}!`, + }; +}; +``` + +We are getting the user id from the event object. + +If you head back to Insomnia and hit the `/private` endpoint again. + +![Get caller identity id in Auth0 authenticated route](/assets/examples/api-auth-auth0/get-caller-identity-id-in-auth0-authenticated-route.png) + +You should see the user id. Note, this matches the identity id from the step where we generated the temporary IAM credentials. + +``` +Hello us-east-1:46625265-9c97-420f-a826-15dbc812a008! +``` + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Auth0. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md b/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md new file mode 100644 index 0000000000..3530d946e9 --- /dev/null +++ b/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md @@ -0,0 +1,350 @@ +--- +layout: example +title: How to add Cognito authentication to a serverless API +short_title: Cognito IAM +date: 2021-02-08 00:00:00 +lang: en +index: 1 +type: iam-auth +description: In this example we will look at how to add Cognito User Pool authentication to a serverless API using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating with Cognito User Pool and Identity Pool. +repo: api-auth-cognito +ref: how-to-add-cognito-authentication-to-a-serverless-api +comments_id: how-to-add-cognito-authentication-to-a-serverless-api/2316 +--- + +In this example we will look at how to add [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) authentication to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-cognito +$ cd api-auth-cognito +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-auth-cognito", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, Cognito, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Api + const api = new Api(stack, "Api", { + defaults: { + authorizer: "iam", + }, + routes: { + "GET /private": "packages/functions/src/private.main", + "GET /public": { + function: "packages/functions/src/public.main", + authorizer: "none", + }, + }, + }); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +By default, all routes have the authorization type `AWS_IAM`. This means the caller of the API needs to have the required IAM permissions. The first is a private endpoint. The second is a public endpoint and its authorization type is overriden to `NONE`. + +## Setting up authentication + +{%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create auth provider +const auth = new Cognito(stack, "Auth", { + login: ["email"], +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +This creates a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html); a user directory that manages user sign up and login. We've configured the User Pool to allow users to login with their email and password. + +This also creates a Cognito Identity Pool which assigns IAM permissions to users. We are allowing only the logged in users to have the permission to call the API. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +stack.addOutputs({ + ApiEndpoint: api.url, + UserPoolId: auth.userPoolId, + IdentityPoolId: auth.cognitoIdentityPoolId, + UserPoolClientId: auth.userPoolClientId, +}); +``` + +We are going to print out the resources that we created for reference. + +## Adding function code + +We will create two functions, one for the public route, and one for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello user!", + }; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-cognito-ExampleStack: deploying... + + ✅ dev-api-auth-cognito-ExampleStack + + +Stack dev-api-auth-cognito-ExampleStack + Status: deployed + Outputs: + UserPoolClientId: 4fb69je3470cat29p0nfm3t27k + UserPoolId: us-east-1_e8u3sktE1 + ApiEndpoint: https://12mflx0e8e.execute-api.us-east-1.amazonaws.com + IdentityPoolId: us-east-1:d01df859-f416-4dc2-90ac-0c6fc272d197 +``` + +The `ApiEndpoint` is the API we just created. Make a note of the `UserPoolClientId`, `UserPoolId`, `IdentityPoolId`; we'll need them later. + +Now let's try out our public route. Head over to the following in your browser. Make sure to replace the URL with your API. + +``` +https://12mflx0e8e.execute-api.us-east-1.amazonaws.com/public +``` + +You should see the greeting `Hello stranger!`. + +And if you try to visit the private route, you will see `{"message":"Forbidden"}`. + +``` +https://12mflx0e8e.execute-api.us-east-1.amazonaws.com/private +``` + +## Signing up + +Now to visit the private route, we need to create an account in our User Pool. Usually, we'll have our users sign up for an account through our app. But for this example, we'll use the AWS CLI to sign up a user and confirm their account. + +Use the following command in your terminal. Replace `--client-id` with `UserPoolClientId` from the `sst dev` output above. + +```bash +$ aws cognito-idp sign-up \ + --region us-east-1 \ + --client-id 4fb69je3470cat29p0nfm3t27k \ + --username admin@example.com \ + --password Passw0rd! +``` + +Next we'll verify the user. Replace `--user-pool-id` with `UserPoolId` from the `sst dev` output above. + +```bash +$ aws cognito-idp admin-confirm-sign-up \ + --region us-east-1 \ + --user-pool-id us-east-1_e8u3sktE1 \ + --username admin@example.com +``` + +Now we'll make a request to our private API. Typically, we'll be using our app to do this. But just to test, we'll use the [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test). This makes an authenticated call to our private API using the credentials of the user we just created. + +```bash +$ npx aws-api-gateway-cli-test \ + --username='admin@example.com' \ + --password='Passw0rd!' \ + --user-pool-id='us-east-1_e8u3sktE1' \ + --app-client-id='4fb69je3470cat29p0nfm3t27k' \ + --cognito-region='us-east-1' \ + --identity-pool-id='us-east-1:d01df859-f416-4dc2-90ac-0c6fc272d197' \ + --invoke-url='https://12mflx0e8e.execute-api.us-east-1.amazonaws.com' \ + --api-gateway-region='us-east-1' \ + --path-template='/private' \ + --method='GET' +``` + +Make sure to set the options with the ones in your `sst dev` output. + +- `--user-pool-id` => `UserPoolId` +- `--app-client-id` => `UserPoolClientId` +- `--identity-pool-id` => `IdentityPoolId` +- `--invoke-url` => `ApiEndpoint` +- `--cognito-region` and `--api-gateway-region`, the region in your `sst.config.ts` + +You should now see. + +```bash +{ + status: 200, + statusText: 'OK', + data: 'Hello user!' +} +``` + +The above process might seem fairly tedious. But once we integrate it into our frontend app, we'll be able to use something like [AWS Amplify]({% link _chapters/configure-aws-amplify.md %}) to handle these steps for us. + +## Making changes + +Let's make a quick change to our private route to print out the caller's user id. + +{%change%} Replace `packages/functions/src/private.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.iam.cognitoIdentity.identityId}!`, + }; +}; +``` + +We are getting the user id from the event object. + +If you make the same authenticated request to the `/private` endpoint. + +```bash +$ npx aws-api-gateway-cli-test \ + --username='admin@example.com' \ + --password='Passw0rd!' \ + --user-pool-id='us-east-1_e8u3sktE1' \ + --app-client-id='4fb69je3470cat29p0nfm3t27k' \ + --cognito-region='us-east-1' \ + --identity-pool-id='us-east-1:d01df859-f416-4dc2-90ac-0c6fc272d197' \ + --invoke-url='https://12mflx0e8e.execute-api.us-east-1.amazonaws.com' \ + --api-gateway-region='us-east-1' \ + --path-template='/private' \ + --method='GET' +``` + +You should see the user id. + +```bash +{ + status: 200, + statusText: 'OK', + data: 'Hello us-east-1:6f4e594d-a6ca-4a24-b99b-760913a70a31!' +} +``` + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Cognito. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md b/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md new file mode 100644 index 0000000000..2d50468d9e --- /dev/null +++ b/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md @@ -0,0 +1,338 @@ +--- +layout: example +title: How to add Facebook authentication to a serverless API +short_title: Facebook Auth +date: 2021-02-08 00:00:00 +lang: en +index: 2 +type: iam-auth +description: In this example we will look at how to add Facebook authentication to a serverless API using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a serverless API with Facebook. +repo: api-auth-facebook +ref: how-to-add-facebook-authentication-to-a-serverless-api +comments_id: how-to-add-facebook-authentication-to-a-serverless-api/2317 +--- + +In this example we will look at how to add Facebook authentication to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Facebook app](https://developers.facebook.com/apps) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-facebook +$ cd api-auth-facebook +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-auth-facebook", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, Cognito, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Api + const api = new Api(stack, "Api", { + defaults: { + authorizer: "iam", + }, + routes: { + "GET /private": "packages/functions/src/private.main", + "GET /public": { + function: "packages/functions/src/public.main", + authorizer: "none", + }, + }, + }); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +To secure our APIs we are adding the authorization type `AWS_IAM`. This means the caller of the API needs to have the right permissions. The first route is a private endpoint. The second is a public endpoint and its authorization type is overriden to `NONE`. + +## Setting up authentication + +Now let's add authentication for our serverless app. + +{%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `appId` with that of your Facebook app. + +```typescript +// Create auth provider +const auth = new Cognito(stack, "Auth", { + identityPoolFederation: { + facebook: { appId: "419718329085014" }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html) which relies on Facebook to authenticate users. And we use the [`attachPermissionsForAuthUsers`]({{ site.v2_url }}/constructs/Auth#attachpermissionsforauthusers) method to allow our logged in users to access our API. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +stack.addOutputs({ + ApiEndpoint: api.url, + IdentityPoolId: auth.cognitoIdentityPoolId, +}); +``` + +We are going to print out the resources that we created for reference. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello user!", + }; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-facebook-ExampleStack: deploying... + + ✅ dev-api-auth-facebook-ExampleStack + + +Stack dev-api-auth-facebook-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://2zy74sn6we.execute-api.us-east-1.amazonaws.com + IdentityPoolId: us-east-1:84340cf1-4f64-496e-87c2-517072e7d5d9 +``` + +The `ApiEndpoint` is the API we just created. Make a note of the `IdentityPoolId`, we'll need that later. + +Now let's try out our public route. Head over to the following in your browser. Make sure to replace the URL with your API. + +``` +https://2zy74sn6we.execute-api.us-east-1.amazonaws.com/public +``` + +You should see the greeting `Hello stranger!`. + +And if you try to visit the private route, you will see `{"message":"Forbidden"}`. + +``` +https://2zy74sn6we.execute-api.us-east-1.amazonaws.com/private +``` + +## Login with Facebook + +We are going to use [Facebook's Graph API explorer](https://developers.facebook.com/tools/explorer) to test logging in with Facebook. Head over to — [**developers.facebook.com/tools/explorer**](https://developers.facebook.com/tools/explorer) + +Select your Facebook App and select **Generate Access Token**. Copy the generated access token. + +![Generate access token for users logged in with Facebook](/assets/examples/api-auth-facebook/generate-access-token-for-users-logged-in-with-facebook.png) + +Next, we need to get the user's Cognito Identity id. Replace `--identity-pool-id` with the `IdentityPoolId` from the `sst dev` log output; and replace the `--logins` with the **Access Token** from the previous step. + +```bash +$ aws cognito-identity get-id \ + --identity-pool-id us-east-1:84340cf1-4f64-496e-87c2-517072e7d5d9 \ + --logins graph.facebook.com="EAAF9u0npLFUBAGv7SlHXIMigP0nZBF2LxZA5ZCe3NqZB6Wc6xbWxwHqn64T5QLEsjOZAFhZCLJj1yIsDLPCc9L3TRWZC3SvKf2D1vEZC3FISPWENQ9S5BZA94zxtn6HWQFD8QLMvjt83qOGHeQKZAAtJRgHeuzmd2oGn3jbZBmfYl2rhg3dpEnFhkAmK3lC7BZAEyc0ZD" +``` + +You should get an identity id for the Facebook user. + +```json +{ + "IdentityId": "us-east-1:46625265-9c97-420f-a826-15dbc812a008" +} +``` + +Now we'll need to get the IAM credentials for the identity user. + +```bash +$ aws cognito-identity get-credentials-for-identity \ + --identity-id us-east-1:46625265-9c97-420f-a826-15dbc812a008 \ + --logins graph.facebook.com="EAAF9u0npLFUBAGv7SlHXIMigP0nZBF2LxZA5ZCe3NqZB6Wc6xbWxwHqn64T5QLEsjOZAFhZCLJj1yIsDLPCc9L3TRWZC3SvKf2D1vEZC3FISPWENQ9S5BZA94zxtn6HWQFD8QLMvjt83qOGHeQKZAAtJRgHeuzmd2oGn3jbZBmfYl2rhg3dpEnFhkAmK3lC7BZAEyc0ZD" +``` + +You should get a set of temporary IAM credentials. + +```json +{ + "IdentityId": "us-east-1:46625265-9c97-420f-a826-15dbc812a008", + "Credentials": { + "AccessKeyId": "ASIARUIS6Q2MOT2D7LGE", + "SecretKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "SessionToken": "IQoJb3JpZ2luX2VjEHYaCXVzLWVhc3QtMSJHMEUCIA/0ccZZvhjSPnoXkzJ/TUiSPXB2ON/1Qnn2/omfQOQLAiEA+qjuBHYwZvHG8Q9cfjd/0yloUkh5pkEUzEiCjjaa5FYq6QMIbhACGgwxMTIyNDU3Njk4ODAiDGDpiBCuNOBhkktiHyrGA7A8scWUjxzwUKaEAzYdWOwxDdYxA21wPc3Bz2NSJlscwHQP0AjmZ3aPmEREgwhi92/5SGETFINbJSRDs9dsJ+hrArHpSyoOp6UmXX/48q8b9BbWKB2qeF/kIPMG+1urwgTLn7I9cmYNH0LUHLJ0/EaRVxFo/hUTnTiPsDZCD9X96WxvO+cfjhmpAdCTR8MjxUl4k18grIWzPBkNAJwS1D+zIuoQTQPiIN6e25pWi3Mi+wXxgz+ToBFiPeybl3Q9qHOH0gQipss5eYrMFYaRWS3k6eOLCZoTOA4T/sMoJMweGwT2V33C1/o95W0LXCwYuAWg9bdUC71DHtc9bPY1NCAWqQcnxQabziZkOFTW5aLeDsY53TDPFoYiQ8lUrmDLhZSU3MsBcXVtPsvI5MPmoIqyf62ccd8VJo7idS5yyobZz9Ku7/jG/ZmU5S0jdpjWIVqBGNd5aG4R6Vf41FqMN0bEcz2qQBRFTeRg+UDQTv6Hc0kM943iXXBNdzVptivlkEV/fN5NN8sC5zXOafWUMJ8raQhPOAvWTVPIo8aXfAlKzcAqA/8bzJOzeEADcW71XGABOSzhy5TQayqWVrIX8ksBWMmFcSMwqJSDgQY6hAINr+bYzf+Vp1knGBWE52ArJAWzcss9UQU+b0kXripIvFpbdSCn3Yz4+kHKmmgvLKCEGo2k+zJW8TP+j+f3PsQinCB1VHpLpL2G+rx4aK/wMZ48ALY/rIK8KcYArnmjga5IT/PC/4cRW0z1vCucGQibKZ5skF0tUnpLb3BNwGP42NrtoaFkHPmihRpvvpS93iHX8HavIkpEzNcgkKzCcL3tdWXlnN9Hx/CI1kpb4ubzYaAQYiuURYKrFySzkaAJAvSkCO0ZjG342YHe9V+WEC/VBDRJllSiPBAnWaWrDsymafAKUA3HylrvKAetXaK8sSwGQ3DfkJ6GedJTel3FDN8jzZOo9A==", + "Expiration": "2021-02-08T01:20:40-05:00" + } +} +``` + +Let's make a call to the private route using the credentials. The API request needs to be [signed with AWS SigV4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). We are going to use [Insomnia](https://insomnia.rest) to help us sign and make this request. + +Make sure to replace the **Access Key Id**, **Secret Access Key**, **Region**, and **Session Token** below. In our case the region is `us-east-1`. You can see this in the API URL. + +``` +https://2zy74sn6we.execute-api.us-east-1.amazonaws.com +``` + +![Invoke Facebook authenticated API Gateway route](/assets/examples/api-auth-facebook/invoke-facebook-authenticated-api-gateway-route.png) + +You should now see. + +``` +Hello user! +``` + +The above process might seem fairly tedious. But once we integrate it into our frontend app, we'll be able to use something like [AWS Amplify]({% link _chapters/configure-aws-amplify.md %}) to handle these steps for us. + +## Making changes + +Let's make a quick change to our private route and print out the caller's user id. + +{%change%} Replace `packages/functions/src/private.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.iam.cognitoIdentity.identityId}!`, + }; +}; +``` + +We are getting the user id from the event object. + +If you head back to Insomnia and hit the `/private` endpoint again. + +![Get caller identity id in Facebook authenticated route](/assets/examples/api-auth-facebook/get-caller-identity-id-in-facebook-authenticated-route.png) + +You should see the user id. Note, this matches the identity id that was generated from the step where we generated a set of IAM credentials. + +``` +Hello us-east-1:46625265-9c97-420f-a826-15dbc812a008! +``` + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Facebook. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md b/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md new file mode 100644 index 0000000000..ca27a3df08 --- /dev/null +++ b/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md @@ -0,0 +1,681 @@ +--- +layout: example +title: How to Add Facebook Login to Your Cognito User Pool +short_title: Facebook Auth +date: 2022-05-29 00:00:00 +lang: en +index: 5 +type: jwt-auth +description: In this example we will look at how to add Facebook Login to a Cognito User Pool using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a full-stack serverless app with Facebook. +repo: api-oauth-facebook +ref: how-to-add-facebook-login-to-your-cognito-user-pool +comments_id: how-to-add-facebook-login-to-your-cognito-user-pool/2679 +--- + +In this example, we will look at how to add Facebook Login to Your Cognito User Pool using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Facebook developer account](https://developers.facebook.com/) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-oauth-facebook +$ cd api-oauth-facebook +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-oauth-facebook", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the Cognito + +First, let's create a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) to store the user info using the [`Cognito`]({{ site.v2_url }}/constructs/Cognito) construct + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import * as cognito from "aws-cdk-lib/aws-cognito"; +import { Api, Cognito, StackContext, StaticSite } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create auth + const auth = new Cognito(stack, "Auth", { + cdk: { + userPoolClient: { + supportedIdentityProviders: [ + cognito.UserPoolClientIdentityProvider.FACEBOOK, + ], + oAuth: { + callbackUrls: [ + app.stage === "prod" + ? "prodDomainNameUrl" + : "http://localhost:3000", + ], + logoutUrls: [ + app.stage === "prod" + ? "prodDomainNameUrl" + : "http://localhost:3000", + ], + }, + }, + }, + }); +} +``` + +This creates a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html); a user directory that manages users. We've configured the User Pool to allow users to login with their Facebook account and added the callback and logout URLs. + +Note, we haven't yet set up Facebook OAuth with our user pool, we'll do it next. + +## Setting up Facebook OAuth + +Now let's add Facebook OAuth for our serverless app, to do so we need to create a [Facebook User Pool identity provider](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-social-idp.html) and link it with the user pool we created above. + +Sign In with your Facebook credentials in the developer console and from the **My Apps** menu, choose **Create App**. + +![Create app Facebook console](/assets/examples/api-oauth-facebook/create-app-facebook-console.png) + +Choose **Consumer** as app type and hit **Next**. + +![Facebook app type](/assets/examples/api-oauth-facebook/facebook-app-type.png) + +Give your Facebook app a name and click **Create App**. + +![Basic info to create app](/assets/examples/api-oauth-facebook/basic-info-to-create-app.png) + +On the left navigation bar, choose Settings and then **Basic**. + +{%change%} Create a `.env.local` file in the root and add your Facebook `App ID` and `App secret`. + +```typescript +FACEBOOK_APP_ID= +FACEBOOK_APP_SECRET= +``` + +![Basic settings view](/assets/examples/api-oauth-facebook/basic-settings-view.png) + +{%change%} Add this below the `Cognito` definition in `stacks/ExampleStack.ts`. + +```typescript +// Throw error if App ID & secret are not provided +if (!process.env.FACEBOOK_APP_ID || !process.env.FACEBOOK_APP_SECRET) + throw new Error("Please set FACEBOOK_APP_ID and FACEBOOK_APP_SECRET"); + +// Create a Facebook OAuth provider +const provider = new cognito.UserPoolIdentityProviderFacebook( + stack, + "Facebook", + { + clientId: process.env.FACEBOOK_APP_ID, + clientSecret: process.env.FACEBOOK_APP_SECRET, + userPool: auth.cdk.userPool, + attributeMapping: { + email: cognito.ProviderAttribute.FACEBOOK_EMAIL, + givenName: cognito.ProviderAttribute.FACEBOOK_NAME, + }, + } +); + +// attach the created provider to our userpool +auth.cdk.userPoolClient.node.addDependency(provider); +``` + +This creates a Facebook identity provider with the given scopes and links the created provider to our user pool and Facebook user’s attributes will be mapped to the User Pool user. + +Now let's associate a Cognito domain to the user pool, which can be used for sign-up and sign-in webpages. + +{%change%} Add below code in `stacks/ExampleStack.ts`. + +```typescript +// Create a cognito userpool domain +const domain = auth.cdk.userPool.addDomain("AuthDomain", { + cognitoDomain: { + domainPrefix: `${app.stage}-fb-demo-auth-domain`, + }, +}); +``` + +Note, the `domainPrefix` need to be globally unique across all AWS accounts in a region. + +## Setting up the API + +{%change%} Replace the `Api` definition with the following in `stacks/ExampleStacks.ts`. + +```typescript +// Create a HTTP API +const api = new Api(stack, "Api", { + authorizers: { + userPool: { + type: "user_pool", + userPool: { + id: auth.userPoolId, + clientIds: [auth.userPoolClientId], + }, + }, + }, + defaults: { + authorizer: "userPool", + }, + routes: { + "GET /private": "packages/functions/src/private.handler", + "GET /public": { + function: "packages/functions/src/public.handler", + authorizer: "none", + }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +By default, all routes have the authorization type `JWT`. This means the caller of the API needs to pass in a valid JWT token. The `GET /private` route is a private endpoint. The `GET /public` is a public endpoint and its authorization type is overridden to `NONE`. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello, stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +import { APIGatewayProxyHandlerV2WithJWTAuthorizer } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2WithJWTAuthorizer = async ( + event +) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.jwt.claims.sub}!`, + }; +}; +``` + +## Setting up our React app + +To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Replace the `stack.addOutputs({` call with the following. + +```typescript +// Create a React Static Site +const site = new StaticSite(stack, "Site", { + path: "packages/frontend", + buildOutput: "dist", + buildCommand: "npm run build", + environment: { + VITE_APP_COGNITO_DOMAIN: domain.domainName, + VITE_APP_API_URL: api.url, + VITE_APP_REGION: app.region, + VITE_APP_USER_POOL_ID: auth.userPoolId, + VITE_APP_IDENTITY_POOL_ID: auth.cognitoIdentityPoolId, + VITE_APP_USER_POOL_CLIENT_ID: auth.userPoolClientId, + }, +}); + +// Show the endpoint in the output +stack.addOutputs({ + api_url: api.url, + auth_client_id: auth.userPoolClientId, + auth_domain: `https://${domain.domainName}.auth.${app.region}.amazoncognito.com` + site_url: site.url, +}); +``` + +The construct is pointing to where our React.js app is located. We haven't created our app yet but for now, we'll point to the `packages/frontend` directory. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +We are going to print out the resources that we created for reference. + +## Creating the frontend + +Run these commands in the `packages/` directory to create a basic react project. + +```bash +$ npx create-vite@latest frontend --template react +$ cd frontend +$ npm install +``` + +This sets up our React app in the `packages/frontend` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `packages/frontend/package.json`. + +```bash +"dev": "vite" +``` + +{%change%} With the following: + +```bash +"dev": "sst bind vite" +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-oauth-facebook-ExampleStack: deploying... + + ✅ dev-api-oauth-facebook-ExampleStack + + +Stack dev-api-oauth-facebook-ExampleStack + Status: deployed + Outputs: + api_url: https://v0l1zlpy5f.execute-api.us-east-1.amazonaws.com + auth_client_id: 253t1t5o6jjur88nu4t891eac2 + auth_domain: https://dev-fb-demo-auth-domain.auth.us-east-1.amazoncognito.com + site_url: https://d1567f41smqk8b.cloudfront.net +``` + +On the left navigation bar, click on **Add product**. + +In the **Add products to your app** section choose **Facebook Login** and hit **Set up**. + +![Add products to your app](/assets/examples/api-oauth-facebook/add-products-to-your-app.png) + +Click on **Web**. + +![Facebook quickstart screen](/assets/examples/api-oauth-facebook/facebook-quickstart-screen.png) + +Under **Site URL**, type your user pool domain with the `/oauth2/idpresponse` endpoint. + +![Enter site URL screen](/assets/examples/api-oauth-facebook/enter-site-url-screen.png) + +Type your user pool domain into **App Domains**. + +![Facebook app domains screen](/assets/examples/api-oauth-facebook/facebook-app-domains-screen.png) + +Type your redirect URL into **Valid OAuth Redirect URIs**. It will consist of your user pool domain with the `/oauth2/idpresponse` endpoint. + +![Redirect URLs screen](/assets/examples/api-oauth-facebook/redirect-urls-screen.png) + +The `api_url` is the API we just created. While the `site_url` is where our React app will be hosted. For now, it's just a placeholder website. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button of the `GET /public` to send a `GET` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/api-oauth-google/api-explorer-invocation-response.png) + +You should see a `Hello, stranger!` in the response body. + +And if you try for `GET /private`, you will see `{"message":"Unauthorized"}`. + +## Adding AWS Amplify + +To use our AWS resources on the frontend we are going to use [AWS Amplify](https://aws.amazon.com/amplify/). + +Note, to know more about configuring Amplify with SST check [this chapter]({% link _chapters/configure-aws-amplify.md %}). + +Run the below command to install AWS Amplify in the `packages/frontend/` directory. + +```bash +npm install aws-amplify +``` + +{%change%} Replace `frontend/src/main.jsx` with below code. + +```jsx +/* eslint-disable no-undef */ +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import Amplify from "aws-amplify"; + +Amplify.configure({ + Auth: { + region: import.meta.env.VITE_APP_REGION, + userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, + userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, + mandatorySignIn: false, + oauth: { + domain: `${ + import.meta.env.VITE_APP_COGNITO_DOMAIN + + ".auth." + + import.meta.env.VITE_APP_REGION + + ".amazoncognito.com" + }`, + redirectSignIn: "http://localhost:3000", // Make sure to use the exact URL + redirectSignOut: "http://localhost:3000", // Make sure to use the exact URL + responseType: "token", // or 'token', note that REFRESH token will only be generated when the responseType is code + }, + }, + API: { + endpoints: [ + { + name: "api", + endpoint: import.meta.env.VITE_APP_API_URL, + region: import.meta.env.VITE_APP_REGION, + }, + ], + }, +}); + +ReactDOM.render( + + + , + document.getElementById("root") +); +``` + +## Adding login UI + +{%change%} Replace `frontend/src/App.jsx` with below code. + +{% raw %} + +```jsx +import { Auth, API } from "aws-amplify"; +import React, { useState, useEffect } from "react"; + +const App = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const getUser = async () => { + const user = await Auth.currentUserInfo(); + if (user) setUser(user); + setLoading(false); + }; + + const signIn = async () => + await Auth.federatedSignIn({ + provider: "Facebook", + }); + + const signOut = async () => await Auth.signOut(); + + const publicRequest = async () => { + const response = await API.get("api", "/public"); + alert(JSON.stringify(response)); + }; + + const privateRequest = async () => { + try { + const response = await API.get("api", "/private", { + headers: { + Authorization: `Bearer ${(await Auth.currentSession()) + .getAccessToken() + .getJwtToken()}`, + }, + }); + alert(JSON.stringify(response)); + } catch (error) { + alert(error); + } + }; + + useEffect(() => { + getUser(); + }, []); + + if (loading) return
Loading...
; + + return ( +
+

SST + Cognito + Facebook OAuth + React

+ {user ? ( +
+

Welcome {user.attributes.given_name}!

+

{user.attributes.email}

+ +
+ ) : ( +
+

Not signed in

+ +
+ )} +
+ + +
+
+ ); +}; + +export default App; +``` + +{% endraw %} + +{%change%} Replace `frontend/src/index.css` with the below styles. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 120px; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +.api-section { + width: 100%; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + +.api-section > button { + background-color: darkorange; +} +``` + +Let's start our frontend in development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +npm run dev +``` + +Open up your browser and go to `http://localhost:3000`. + +![Browser view of localhost](/assets/examples/api-oauth-facebook/browser-view-of-localhost.png) + +Note, if you get a blank page add this ` +``` + +There are 2 buttons that invokes the endpoints we created above. + +The **call /public** button invokes **GET /public** route using the `publicRequest` method we created in our frontend. + +Similarly, the **call /private** button invokes **GET /private** route using the `privateRequest` method. + +When you're not logged in and try to click the buttons, you'll see responses like below. + +![public button click without login](/assets/examples/api-oauth-google/public-button-click-without-login.png) + +![private button click without login](/assets/examples/api-oauth-google/private-button-click-without-login.png) + +Once you click on login, you're asked to login through your Facebook account. + +![login button click Facebook login screen](/assets/examples/api-oauth-Facebook/login-button-click-facebook-login-screen.png) + +Once it's done you can check your info. + +![current logged in user info](/assets/examples/api-oauth-facebook/current-logged-in-user-info.png) + +Now that you've authenticated repeat the same steps as you did before, you'll see responses like below. + +![public button click with login](/assets/examples/api-oauth-facebook/public-button-click-with-login.png) + +![private button click with login](/assets/examples/api-oauth-facebook/private-button-click-with-login.png) + +As you can see the private route is only working while we are logged in. + +## Deploying your API + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-api-oauth-facebook-ExampleStack + + +Stack prod-api-oauth-facebook-ExampleStack + Status: deployed + Outputs: + api_url: https://ck198mfop1.execute-api.us-east-1.amazonaws.com + auth_client_id: 875t1t5o6jjur88jd4t891eat5 + auth_domain: https://prod-fb-demo-auth-domain.auth.us-east-1.amazoncognito.com + site_url: https://c1767f41smqkh7.cloudfront.net +``` + +Note, if you get any error like `'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js` replace `vite.config.js` with below code. + +```typescript +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + ... + resolve: { + alias: { + "./runtimeConfig": "./runtimeConfig.browser", + }, + }, + ... +}); +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Facebook. A local development environment, to test. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-facebook-login-to-your-sst-apps.md b/_examples/how-to-add-facebook-login-to-your-sst-apps.md new file mode 100644 index 0000000000..ec6d5e90b5 --- /dev/null +++ b/_examples/how-to-add-facebook-login-to-your-sst-apps.md @@ -0,0 +1,907 @@ +--- +layout: example +title: How to Add Facebook Login to Your Serverless App with SST Auth +short_title: Facebook Auth +date: 2022-10-10 00:00:00 +lang: en +index: 1 +type: sst-auth +description: In this example we will look at how to add Facebook Login to your serverless app using SST Auth. We'll be using the Api, Auth, Table, and StaticSite constructs to create a full-stack app with Facebook authentication. +short_desc: Adding Facebook auth to a full-stack serverless app. +repo: api-sst-auth-facebook +ref: how-to-add-facebook-login-to-your-sst-app-with-sst-auth +comments_id: how-to-add-facebook-login-to-your-sst-app-with-sst-auth/2643 +--- + +In this example, we will look at how to add Facebook Login to your serverless app using [SST Auth]({{ site.v2_url }}/auth). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-sst-auth-facebook +$ cd api-sst-auth-facebook +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-sst-auth-facebook", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of three parts. + +1. `stacks/` — Infrastructure code + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — Application code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Auth flow + +Before we start let's first take a look at the auth flow at a high level. + +![Facebook Auth Flow](/assets/examples/api-sst-auth-facebook/auth-flow.png) + +1. The user clicks on "Sign in with Facebook" in the frontend, and gets redirected to an **Authorize URL** to initiate the auth flow. + +2. This will redirect the user to Facebook and they login to their Facebook account. + +3. Facebook redirects the user back to a **Callback URL** with the user's details. + +4. In the **Callback URL** we: + + Store the user data in our database, create a session token for the user, and redirect to the frontend with the session token. + + The session token is then stored in the cookie or local storage. At this point, the user is authenticated. + +5. From now on, when the user makes API requests, the session token gets passed in through the request header. + +6. The backend API verifies the session token, decodes the user id from it, and looks up the user in the database. + +In this tutorial, we will be implementing each of the above steps. + +## Create a Facebook project + +Before we start, make sure you have a Facebook App with OAuth client credentials. You can follow the steps below to create a new app. + +Head over to the [Facebook Developers console](https://developers.facebook.com), select the **My Apps** on the top right. If you don't have an existing Facebook app, click **Create App**. + +![Facebook Developers Select Create App](/assets/examples/api-sst-auth-facebook/facebook-developer-console-select-create-app.png) + +Select **Consumer**, then click **Next**. + +![Facebook Developers Select App Type](/assets/examples/api-sst-auth-facebook/facebook-developer-console-select-app-type.png) + +Enter an app name, then click **Create app**. + +![Facebook Developers Create App](/assets/examples/api-sst-auth-facebook/facebook-developer-console-create-app.png) + +After the project is created, select **Set up** under **Facebook Login**. + +![Facebook Developers Set Up Facebook Login](/assets/examples/api-sst-auth-facebook/facebook-developer-console-set-up-facebook-login.png) + +Select **Settings** on the left. Then select **Basic**. + +![Facebook Developers Select Settings](/assets/examples/api-sst-auth-facebook/facebook-developer-console-select-settings.png) + +Select **Show** to reveal the **App secret**. + +![Facebook Developers Select Copy Credentials](/assets/examples/api-sst-auth-facebook/facebook-developer-console-copy-credentials.png) + +Make a note of the **App ID** and **App secret**. We will need them in the following steps. + +## Store the secrets + +Since sensitive values should not be defined in the code, we are going to use [Config]({{ site.v2_url }}/config) to help us managing the Facebook app's credentials. + +{%change%} Run in the root. + +```bash +$ npx sst secrets set FACEBOOK_APP_ID 368385265465382 +$ npx sst secrets set FACEBOOK_APP_SECRET 296866b2119ff5afbd84c4ee98dff791 +``` + +Make sure to replace the values with the App ID and secret created in the previous section. + +## Add the authorize URL + +Next, we need to create an **Authorize URL** to initiate the auth flow. + +#### Configure the construct + +We are going to use the [`Auth`]({{ site.v2_url }}/constructs/Auth) construct. It will help us create both the **Authorize URL** and the **Callback URL**. + +{%change%} Add the following below the `Api` construct in `stacks/ExampleStack.ts`. + +```typescript +const auth = new Auth(stack, "auth", { + authenticator: { + handler: "packages/functions/src/auth.handler", + bind: [ + new Config.Secret(stack, "FACEBOOK_APP_ID"), + new Config.Secret(stack, "FACEBOOK_APP_SECRET"), + ], + }, +}); +auth.attach(stack, { + api, + prefix: "/auth", +}); +``` + +Behind the scenes, the `Auth` construct creates a `/auth/*` catch-all route. Both the Authorize and Callback URLs will fall under this route. + +We'll also bind the secrets to the **authenticator** function. It allows the function to access the secret values. + +{%change%} Also remember to import the `Auth` and `Config` construct up top. + +```diff +- import { StackContext, Api } from "sst/constructs"; ++ import { StackContext, Api, Auth, Config } from "sst/constructs"; +``` + +#### Add the auth handler + +Now let's implement the `authenticator` function. + +{%change%} Add a file in `packages/functions/src/auth.ts` with the following. + +```typescript +import { Config } from "sst/node/config"; +import { AuthHandler, FacebookAdapter } from "sst/node/auth"; + +export const handler = AuthHandler({ + providers: { + facebook: FacebookAdapter({ + clientID: Config.FACEBOOK_APP_ID, + clientSecret: Config.FACEBOOK_APP_SECRET, + scope: "openid email", + onSuccess: async (tokenset) => { + return { + statusCode: 200, + body: JSON.stringify(tokenset.claims(), null, 4), + }; + }, + }), + }, +}); +``` + +The `sst/node` package provides helper libraries used in Lambda functions. In the snippet above, we are using the package to create an `AuthHandler` with a `FacebookAdapter` named `facebook`. This creates two routes behind the scenes: + +- **Authorize URL** at `/auth/facebook/authorize` +- **Callback URL** at `/auth/facebook/callback` + +When the Authorize URL is invoked, it will initialize the auth flow and redirects the user to Facebook. + +We are also using the `Config` module to load the Facebook app's credentials stored in the previous step. + +## Set up our React app + +Next, we are going to add a **Sign in with Facebook** button to our frontend. And on click, we will redirect the user to the **Authorize URL**. + +To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Add the following above the `Auth` construct in `stacks/ExampleStack.ts`. + +```typescript +const site = new StaticSite(stack, "site", { + path: "web", + buildCommand: "npm run build", + buildOutput: "dist", + environment: { + VITE_APP_API_URL: api.url, + }, +}); +``` + +{%change%} And add the site URL to `stack.addOutputs`. + +```diff + stack.addOutputs({ + ApiEndpoint: api.url, ++ SiteURL: site.url, + }); +``` + +The construct is pointing to the directory where we are going to add our React.js app. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +{%change%} Also remember to import the `StaticSite` construct up top. + +```diff +- import { StackContext, Api, Auth } from "sst/constructs"; ++ import { StackContext, Api, Auth, Config, StaticSite } from "sst/constructs"; +``` + +## Create the frontend + +{%change%} Run the below commands in our project root to create a basic React project. + +```bash +$ npx create-vite@latest web --template react +$ cd web +$ npm install +``` + +This sets up our React app in the `web/` directory. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `web/package.json`. + +```diff +-"dev": "vite" ++"dev": "sst bind vite" +``` + +## Start our dev environment + +SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +{%change%} Run in the root. + +```bash +$ npm run dev +``` + +The first time you run this command it'll prompt you to enter a stage name. + +```txt +Look like you’re running sst for the first time in this directory. +Please enter a stage name you’d like to use locally. Or hit enter +to use the one based on your AWS credentials (frank): +``` + +You can press enter to use the default stage, or manually enter a stage name. SST uses the stage to namespace all the resources in your application. + +The first time `sst dev` runs, it can take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +After `sst dev` starts up, you will see the following output in your terminal. + +``` +Stack frank-api-sst-auth-facebook-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://2wk0bl6b7i.execute-api.us-east-1.amazonaws.com + SiteURL: https://d54gkw8ds19md.cloudfront.net + + +========================== + Starting Live Lambda Dev +========================== + +SST Console: https://console.sst.dev/api-sst-auth-facebook/frank/local +Debug session started. Listening for requests... +``` + +## Update the Facebook redirect URI + +The `ApiEndpoint` is the API we just created. That means our: + +- **Authorize URL** is `https://2wk0bl6b7i.execute-api.us-east-1.amazonaws.com/auth/facebook/authorize` +- **Callback URL** is `https://2wk0bl6b7i.execute-api.us-east-1.amazonaws.com/auth/facebook/callback` + +And the `SiteURL` is where our React app will be hosted. While in development, it's just a placeholder website. + +Add our **Callback URL** to the **Valid OAuth Redirect URIs** in our Facebook app's Console. + +![Facebook Developers Set OAuth Redirect URIs](/assets/examples/api-sst-auth-facebook/facebook-developer-console-set-oauth-redirect-uris.png) + +## Add the login UI + +{%change%} Replace `web/src/App.jsx` with below code. + +```jsx +const App = () => { + return ( +
+

SST Auth Example

+ +
+ ); +}; + +export default App; +``` + +Let's start our frontend in the development environment. + +{%change%} In the `web/` directory run. + +```bash +$ npm run dev +``` + +Open up your browser and go to the URL it shows. In our case it is: `http://127.0.0.1:5173` + +![Web app not signed in unstyled](/assets/examples/api-sst-auth-facebook/react-site-not-signed-in-unstyled.png) + +Click on `Sign in with Facebook`, and you will be redirected to Facebook to sign in. + +![Facebook Sign in screen](/assets/examples/api-sst-auth-facebook/facebook-sign-in-screen.png) + +Once you are signed in, you will be redirected to the **Callback URL** we created earlier with the user's details. Recall in our `authenticator` handler function, that we are simply printing the user's claims in the `onSuccess` callback. + +![Facebook Sign in callback screen](/assets/examples/api-sst-auth-facebook/facebook-sign-in-callback.png) + +🎉 Sweet! We have just completed steps 1, 2, and 3 of our [Auth Flow](#auth-flow). + +## Create a session token + +Now, let's implement step 4. In the `onSuccess` callback, we will create a session token and pass that back to the frontend. + +#### Define a session type + +First, to make creating and retrieving session typesafe, we'll start by defining our session types. + +{%change%} Add the following above the `AuthHandler` in `packages/functions/src/auth.ts`. + +```typescript +declare module "sst/node/auth" { + export interface SessionTypes { + user: { + userID: string; + }; + } +} +``` + +We are going to keep it simple and create a `user` session type. And it contains a `userId` property. Note that if you have a multi-tenant app, you might want to add something like the `tenantID` as well. + +Also note that as your app grows, you can define multiple session types like an `api_key` session type that represents any server-to-server requests. + +#### Create a session + +Now let's create the session object. + +{%change%} Make the following changes to the `onSuccess` callback. + +```diff + export const handler = AuthHandler({ + providers: { + facebook: FacebookAdapter({ + clientID: Config.FACEBOOK_APP_ID, + clientSecret: Config.FACEBOOK_APP_SECRET, + scope: "openid email", + onSuccess: async (tokenset) => { +- return { +- statusCode: 200, +- body: JSON.stringify(tokenset.claims(), null, 4), +- }; ++ const claims = tokenset.claims(); ++ return Session.parameter({ ++ redirect: "http://127.0.0.1:5173", ++ type: "user", ++ properties: { ++ userID: claims.sub, ++ }, ++ }); + }, + }), + }, + }); +``` + +Remember to replace the `redirect` URL with the URL of your local React app. + +The `Session.parameter` call encrypts the given session object to generate a token. It'll then redirect to the given `redirect` URL with `?token=xxxx` as the query string parameter. + +{%change%} Also import the `Session` up top. + +```diff +- import { AuthHandler, FacebookAdapter } from "sst/node/auth"; ++ import { AuthHandler, FacebookAdapter, Session } from "sst/node/auth"; +``` + +## Use the session + +Now let's use the session token in the frontend. + +#### Store the session token + +Then in the frontend, we will check if the URL contains the `token` query string when the page loads. If it is passed in, we will store it in the local storage, and then redirect the user to the root domain. + +{%change%} Add the following above the `return` in `web/src/App.jsx`. + +```typescript +useEffect(() => { + const search = window.location.search; + const params = new URLSearchParams(search); + const token = params.get("token"); + if (token) { + localStorage.setItem("session", token); + window.location.replace(window.location.origin); + } +}, []); +``` + +#### Load the session + +On page load, we will also check if the session token exists in the local storage. If it does, we want to display the user that is signed in, and have a button for the user to sign out. + +{%change%} Add this above the `useEffect` we just added. + +```typescript +const [session, setSession] = useState(null); + +const getSession = async () => { + const token = localStorage.getItem("session"); + if (token) { + setSession(token); + } +}; + +useEffect(() => { + getSession(); +}, []); +``` + +{%change%} Replace the `return` to conditionally render the page based on `session`. + +```diff + return ( +
+

SST Auth Example

++ {session ? ( ++
++

Yeah! You are signed in.

++ ++
++ ) : ( + ++ )} +
+ ); +``` + +#### Clear the session on logout + +And finally, when the user clicks on `Sign out`, we need to clear the session token from the local storage. + +{%change%} Add the following above the `return`. + +```typescript +const signOut = async () => { + localStorage.removeItem("session"); + setSession(null); +}; +``` + +{%change%} Also, remember to add the imports up top. + +```typescript +import { useEffect, useState } from "react"; +``` + +Let's go back to our browser. Click on **Sign in with Facebook** again. After you authenticate with Facebook, you will be redirected back to the same page with the _"Yeah! You are signed in."_ message. + +![Web app signed in unstyled](/assets/examples/api-sst-auth-facebook/react-site-signed-in-unstyled.png) + +Try refreshing the page, you will remain signed in. This is because the session token is stored in the browser's local storage. + +Let's sign out before continuing with the next section. Click on **Sign out**. + +🎉 Awesome! We have now completed step 4 of our [Auth Flow](#auth-flow). + +Let's move on to steps 5 and 6. We will create a session API that will return the user data given the session token. + +## Store the user data + +So far we haven't been storing the data Facebook's been returning through the **Callback URL**. Let's create a database table to store this. + +#### Create a DynamoDB table + +We'll be using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. + +{%change%} Add the following above the `Api` construct in `stacks/ExampleStack.ts`. + +```typescript +const table = new Table(stack, "users", { + fields: { + userId: "string", + }, + primaryIndex: { partitionKey: "userId" }, +}); +``` + +{%change%} Then let's bind the `table` to the `api`. Make the following changes to the `Api` construct. + +```diff + const api = new Api(stack, "api", { ++ defaults: { ++ function: { ++ bind: [table], ++ }, ++ }, + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); +``` + +{%change%} Import the `Table` construct up top. + +```diff +- import { StackContext, Api, Auth, Config, StaticSite } from "sst/constructs"; ++ import { StackContext, Api, Auth, Config, StaticSite, Table } from "sst/constructs"; +``` + +#### Store the claims + +Now let's update our `authenticator` function to store the user data in the `onSuccess` callback. + +{%change%} Update the `onSuccess` callback in `packages/functions/src/auth.ts`. + +```diff + export const handler = AuthHandler({ + providers: { + facebook: FacebookAdapter({ + clientID: Config.FACEBOOK_APP_ID, + clientSecret: Config.FACEBOOK_APP_SECRET, + scope: "openid email", + onSuccess: async (tokenset) => { + const claims = tokenset.claims(); + ++ const ddb = new DynamoDBClient({}); ++ await ddb.send(new PutItemCommand({ ++ TableName: Table.users.tableName, ++ Item: marshall({ ++ userId: claims.sub, ++ email: claims.email, ++ picture: claims.picture, ++ name: claims.given_name, ++ }), ++ })); + + return Session.parameter({ + redirect: "http://127.0.0.1:5173", + type: "user", + properties: { + userID: claims.sub, + }, + }); + }, + }), + }, + }); +``` + +This is saving the `claims` we get from Facebook in our DynamoDB table. + +{%change%} Also add these imports up top. + +```typescript +import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import { Table } from "sst/node/table"; +``` + +{%change%} And finally install these packages inside the `packages/functions/` directory. + +```bash +npm install --save @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb +``` + +## Fetch the user data + +Now that the user data is stored in the database; let's create an API endpoint that returns the user details given a session token. + +#### Create a session API + +{%change%} Add a `/session` route in the `Api` construct's routes definition in `stacks/ExampleStacks.ts`. + +```diff + routes: { + "GET /": "packages/functions/src/lambda.handler", ++ "GET /session": "packages/functions/src/session.handler", + }, +``` + +{%change%} Add a file at `packages/functions/src/session.ts`. + +```typescript +import { Table } from "sst/node/table"; +import { ApiHandler } from "sst/node/api"; +import { useSession } from "sst/node/auth"; +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; + +export const handler = ApiHandler(async () => { + const session = useSession(); + + // Check user is authenticated + if (session.type !== "user") { + throw new Error("Not authenticated"); + } + + const ddb = new DynamoDBClient({}); + const data = await ddb.send( + new GetItemCommand({ + TableName: Table.users.tableName, + Key: marshall({ + userId: session.properties.userID, + }), + }) + ); + + return { + statusCode: 200, + body: JSON.stringify(unmarshall(data.Item!)), + }; +}); +``` + +The handler calls a [`useSession()`]({{ site.v2_url }}/clients/auth#usesession) hook to decode the session token and retrieve the user's `userID` from the session data. Note that, `useSession` can be called anywhere in your Lambda handler. This works because we are using the [`ApiHandler`]({{ site.v2_url }}/clients/api#apihandler) to wrap our Lambda function. + +We then fetch the user's data from our database table with `userID` being the key. + +Save the changes. And then open up the `sst dev` terminal window. You will be prompted with: + +```bash +Stacks: There are new infrastructure changes. Press ENTER to redeploy. +``` + +Press **ENTER** to deploy the infrastructure changes. + +As we wait, let's update our frontend to make a request to the `/session` API to fetch the user data. + +#### Call the session API + +{%change%} Add the following above the `signOut` function in `web/src/App.jsx`. + +```typescript +const getUserInfo = async (session) => { + try { + const response = await fetch( + `${import.meta.env.VITE_APP_API_URL}/session`, + { + method: "GET", + headers: { + Authorization: `Bearer ${session}`, + }, + } + ); + return response.json(); + } catch (error) { + alert(error); + } +}; +``` + +{%change%} Update the `getSession` function to fetch from the new session API. + +```diff + const getSession = async () => { + const token = localStorage.getItem("session"); + if (token) { +- setSession(token); ++ const user = await getUserInfo(token); ++ if (user) setSession(user); + } ++ setLoading(false); + }; +``` + +And finally, add a loading state to indicate the API is being called. + +{%change%} Add the following below the session `useState` hook. + +```diff + const [session, setSession] = useState(null); ++ const [loading, setLoading] = useState(true); +``` + +## Render the user data + +Let's render the user info. + +{%change%} Update our `return` statement with. + +{% raw %} + +```diff +-
+-

Yeah! You are signed in.

+- +-
++
++

Welcome {session.name}!

++ ++

{session.email}

++ ++
+``` + +{% endraw %} + +Also, let's display a loading sign while waiting for the `/session` API to return. + +{%change%} Add the following above the `return`. + +```diff ++ if (loading) return
Loading...
; + + return ( +
+ ... +``` + +Finally, let's add some basic styles to the page. + +{%change%} Replace `web/src/index.css` with the following. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 100%; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +``` + +Let's go back to our browser. Make sure you are signed out. + +Click on **Sign in with Facebook** again. After you authenticate with Facebook, you will be redirected back to the same page with your details. + +![Web app signed in styled](/assets/examples/api-sst-auth-facebook/react-site-signed-in-styled.png) + +🎉 Congratulations! We have completed the entire [Auth Flow](#auth-flow). + +## Deploy your API + +When deploying to prod, we need to set the Facebook app's credentials in the prod stage as well. + +{%change%} Run in the root. + +```bash +$ npx sst secrets set FACEBOOK_APP_ID 368385265465382 --stage prod +$ npx sst secrets set FACEBOOK_APP_SECRET 296866b2119ff5afbd84c4ee98dff791 --stage prod +``` + +A good practice here is to create two Facebook apps, one for your live users and one for your local development. That way you won’t need to change the URL and you will have an environment where you can test your changes. + +We also need to change our `authenticator` to redirect to the deployed frontend URL instead of `127.0.0.1`. + +{%change%} In `stacks/ExampleStack.ts`, make this change to the `Auth` construct. + +```diff + const auth = new Auth(stack, "auth", { + authenticator: { + handler: "packages/functions/src/auth.handler", + bind: [ + new Config.Secret(stack, "FACEBOOK_APP_ID"), + new Config.Secret(stack, "FACEBOOK_APP_SECRET"), ++ site, + ], + }, + }); +``` + +{%change%} In `packages/functions/src/auth.ts`, change `redirect` to: + +```diff +-redirect: "http://127.0.0.1:5173", ++redirect: process.env.IS_LOCAL ? "http://127.0.0.1:5173" : StaticSite.site.url, +``` + +Note that when we are developing locally via `sst dev`, the `IS_LOCAL` environment variable is set. We will conditionally redirect to `127.0.0.1` or the site's URL depending on `IS_LOCAL`. + +{%change%} Also remember to import the `StaticSite` construct up top. + +```typescript +import { StaticSite } from "sst/node/site"; +``` + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so that when we are developing locally, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash +Stack prod-api-sst-auth-facebook-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://jd8jpfjue6.execute-api.us-east-1.amazonaws.com + SiteURL: https://d36g0g26jff9tr.cloudfront.net +``` + +#### Add the prod redirect URI + +Like we did when we ran `sst dev`; add the `prod` **Callback URL** to the **Authorized redirect URIs** in the Facebook Developers Console. In our case this is — `https://jd8jpfjue6.execute-api.us-east-1.amazonaws.com/auth/facebook/callback` + +![Facebook Developers Set OAuth Redirect URIs For Prod](/assets/examples/api-sst-auth-facebook/facebook-developer-console-set-oauth-redirect-uris-for-prod.png) + +## Conclusion + +And that's it! You've got a brand new serverless full-stack app that supports _Sign in with Facebook_. With a local development environment, to test. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! + +## Clean up + +Optionally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` diff --git a/_examples/how-to-add-github-login-to-your-cognito-user-pool.md b/_examples/how-to-add-github-login-to-your-cognito-user-pool.md new file mode 100644 index 0000000000..3cc408bb14 --- /dev/null +++ b/_examples/how-to-add-github-login-to-your-cognito-user-pool.md @@ -0,0 +1,727 @@ +--- +layout: example +title: How to Add GitHub Login to Your Cognito User Pool +short_title: GitHub Auth +date: 2021-02-08 00:00:00 +lang: en +index: 4 +type: jwt-auth +description: In this example we will look at how to add GitHub Login to a Cognito User Pool using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a full-stack serverless app with GitHub. +repo: api-oauth-github +ref: how-to-add-github-login-to-your-cognito-user-pool +comments_id: how-to-add-github-login-to-your-cognito-user-pool/2649 +--- + +In this example, we will look at how to add GitHub Login to Your Cognito User Pool using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [GitHub OAuth App](https://github.com/settings/applications/new) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-oauth-github +$ cd api-oauth-github +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-oauth-github", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the Cognito + +First, let's create a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) to store the user info using the [`Cognito`]({{ site.v2_url }}/constructs/Cognito) construct. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Api, Cognito, StaticSite } from "sst/constructs"; +import * as cognito from "aws-cdk-lib/aws-cognito"; + +export function ExampleStack({ stack, app }: StackContext) { + const auth = new Cognito(stack, "Auth", { + cdk: { + userPoolClient: { + supportedIdentityProviders: [ + { + name: "GitHub", + }, + ], + oAuth: { + callbackUrls: [ + app.stage === "prod" + ? "https://my-app.com" + : "http://localhost:3000", + ], + logoutUrls: [ + app.stage === "prod" + ? "https://my-app.com" + : "https://localhost:3000", + ], + }, + }, + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + api_endpoint: api.url, + }); +} +``` + +This creates a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html); a user directory that manages users. We've configured the User Pool to allow users to login with their GitHub account and added the callback and logout URLs. + +Note, we haven't yet set up GitHub OAuth with our user pool, we'll do it later. + +## Setting up the API + +{%change%} Replace the `Api` definition with the following in `stacks/ExampleStacks.ts`. + +```typescript +// Create a HTTP API +const api = new Api(stack, "api", { + authorizers: { + userPool: { + type: "user_pool", + userPool: { + id: auth.userPoolId, + clientIds: [auth.userPoolClientId], + }, + }, + }, + defaults: { + authorizer: "none", + }, + routes: { + "GET /public": "packages/functions/src/public.handler", + "GET /user": "packages/functions/src/user.handler", + "POST /token": "packages/functions/src/token.handler", + "GET /private": { + function: "packages/functions/src/private.handler", + authorizer: "userPool", + }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding four routes to it. + +``` +GET /public +POST /token +GET /user +GET /private +``` + +The `GET /public` is a public endpoint, The `GET /private` route have the authorization type `JWT`. This means the caller of the API needs to pass in a valid JWT token and the other two routes are proxy functions to handle GitHub OAuth responses. + +## Adding function code + +Let's create four functions, one handling the public route, one handling the private route and the other for the handling GitHub OAuth responses. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello, stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello, user!", + }; +} +``` + +{%change%} Add a `packages/token.ts`. + +Requesting data from the token endpoint, it will return the following form: `access_token=xxxxxxxxxxxxxxxxxxxxxxx&token_type=bearer`, which is not a JSON. It should be returning a JSON object for OpenID to understand. The below lambda does exactly that. + +The idea for this endpoint is to take the form data sent from AWS Cognito, forward it back to GitHub with the header `accept: application/json` for GitHub API to return back in JSON form instead of **query** form. + +```typescript +import fetch from "node-fetch"; +import parser from "lambda-multipart-parser"; + +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const result = await parser.parse(event); + const token = await ( + await fetch( + `https://github.com/login/oauth/access_token?client_id=${result.client_id}&client_secret=${result.client_secret}&code=${result.code}`, + { + method: "POST", + headers: { + accept: "application/json", + }, + } + ) + ).json(); + + return token; +}; +``` + +Make sure to install the `node-fetch` and `lambda-multipart-parser` packages. + +{%change%} Run the below command in the `packages/functions/` folder. + +```bash +npm install node-fetch lambda-multipart-parser +``` + +{%change%} Add a `packages/user.ts`. + +User info endpoint uses a different authorization scheme: `Authorization: token OAUTH-TOKEN`. But, OpenID will send a `Bearer` scheme so that's we need a proxy to modify it to correct scheme. + +The below lambda gets the Bearer token given by Cognito and modify the header to send token authorization scheme to GitHub and adds a **sub** field into the response for Cognito to map the username. + +```typescript +import fetch from "node-fetch"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const token = await ( + await fetch("https://api.github.com/user", { + method: "GET", + headers: { + authorization: + "token " + event.headers["authorization"].split("Bearer ")[1], + accept: "application/json", + }, + }) + ).json(); + + return { + sub: token.id, + ...token, + }; +}; +``` + +## Setting up GitHub OAuth + +Now let's add GitHub OAuth for our serverless app, to do so we need to create a [GitHub User Pool OIDC IDP](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) and link it with the user pool we created above. + +{%change%} Create a `.env` file in the root and add your GitHub `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` from your [GitHub OAuth App](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app). + +Note, if you haven't created a GitHub OAuth app, follow [this tutorial](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app). + +![GitHub API Credentials](/assets/examples/api-oauth-github/github-api-credentials.png) + +```typescript +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +``` + +{%change%} Add this below the `Auth` definition in `stacks/ExampleStack.ts`. + +```typescript +// Throw error if client ID & secret are not provided +if (!process.env.GITHUB_CLIENT_ID || !process.env.GITHUB_CLIENT_SECRET) + throw new Error("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET"); + +// Create a GitHub OIDC IDP +const idp = new cognito.CfnUserPoolIdentityProvider( + stack, + "GitHubIdentityProvider", + { + providerName: "GitHub", + providerType: "OIDC", + userPoolId: auth.userPoolId, + providerDetails: { + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + attributes_request_method: "GET", + oidc_issuer: "https://github.com", + authorize_scopes: "openid user", + authorize_url: "https://github.com/login/oauth/authorize", + token_url: api.url + "/token", + attributes_url: api.url + "/user", + jwks_uri: api.url + "/token", + }, + attributeMapping: { + email: "email", + name: "name", + picture: "avatar_url", + }, + } +); + +// attach the IDP to the client +if (idp) { + auth.cdk.userPoolClient.node.addDependency(idp); +} +``` + +This creates a GitHub OIDC provider with the given scopes and links the created provider to our user pool and GitHub user’s attributes will be mapped to the User Pool user. + +Now let's associate a Cognito domain to the user pool, which can be used for sign-up and sign-in webpages. + +{%change%} Add below code in `stacks/ExampleStack.ts`. + +```typescript +// Create a cognito userpool domain +const domain = auth.cdk.userPool.addDomain("AuthDomain", { + cognitoDomain: { + domainPrefix: `${app.stage}-github-demo-oauth`, + }, +}); +``` + +Note, the `domainPrefix` need to be globally unique across all AWS accounts in a region. + +## Setting up our React app + +To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +// Create a React Static Site +const site = new StaticSite(stack, "Site", { + path: "packages/frontend", + buildOutput: "dist", + buildCommand: "npm run build", + environment: { + VITE_APP_COGNITO_DOMAIN: domain.domainName, + VITE_APP_STAGE: app.stage, + VITE_APP_API_URL: api.url, + VITE_APP_REGION: app.region, + VITE_APP_USER_POOL_ID: auth.userPoolId, + VITE_APP_IDENTITY_POOL_ID: auth.cognitoIdentityPoolId, + VITE_APP_USER_POOL_CLIENT_ID: auth.userPoolClientId, + }, +}); + +// Show the endpoint in the output +stack.addOutputs({ + api_endpoint: api.url, + auth_client_id: auth.userPoolClientId, + domain: domain.domainName, + site_url: site.url, +}); +``` + +The construct is pointing to where our React.js app is located. We haven't created our app yet but for now, we'll point to the `packages/frontend` directory. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +We are going to print out the resources that we created for reference. + +## Creating the frontend + +Run the below commands in the `packages/` directory to create a basic react project. + +```bash +$ npx create-vite@latest frontend --template react +$ cd frontend +$ npm install +``` + +This sets up our React app in the `packages/frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `frontend/package.json`. + +```bash +"dev": "vite" +``` + +{%change%} With the following: + +```bash +"dev": "sst bind vite" +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-oauth-github-ExampleStack: deploying... + + ✅ dev-api-oauth-github-ExampleStack + + +Stack dev-api-oauth-github-ExampleStack + Status: deployed + Outputs: + api_url: https://v0l1zlpy5f.execute-api.us-east-1.amazonaws.com + auth_client_id: 253t1t5o6jjur88nu4t891eac2 + auth_domain: dev-demo-auth-domain + site_url: https://d1567f41smqk8b.cloudfront.net +``` + +Copy the cognito domain from the terminal output and add it to the **Authorised JavaScript origins** in the GitHub OAuth page. + +Note, if you are not using custom domain, your domain URL will be `https://.auth..amazoncognito.com`. + +And under **Authorised redirect URIs**, append `/oauth2/idpresponse` to your domain URL and add it to the values and click **Update application**. + +![GitHub Credentials](/assets/examples/api-oauth-github/github-credentials.png) + +The `api_endpoint` is the API we just created. While the `site_url` is where our React app will be hosted. For now, it's just a placeholder website. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button of the `GET /public` to send a `GET` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/api-oauth-github/api-explorer-invocation-response.png) + +You should see a `Hello, stranger!` in the response body. + +## Adding AWS Amplify + +To use our AWS resources on the frontend we are going to use [AWS Amplify](https://aws.amazon.com/amplify/). + +Note, to know more about configuring Amplify with SST check [this chapter]({% link _chapters/configure-aws-amplify.md %}). + +Run the below command to install AWS Amplify in the `packages/frontend/` directory. + +```bash +npm install aws-amplify +``` + +{%change%} Replace `frontend/src/main.jsx` with below code. + +```typescript +/* eslint-disable no-undef */ +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import Amplify from "aws-amplify"; + +Amplify.configure({ + Auth: { + region: import.meta.env.VITE_APP_REGION, + userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, + userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, + mandatorySignIn: false, + oauth: { + domain: `${ + import.meta.env.VITE_APP_COGNITO_DOMAIN + + ".auth." + + import.meta.env.VITE_APP_REGION + + ".amazoncognito.com" + }`, + scope: ["email", "profile", "openid", "aws.cognito.signin.user.admin"], + redirectSignIn: + import.meta.env.VITE_APP_STAGE === "prod" + ? "production-url" + : "http://localhost:3000", // Make sure to use the exact URL + redirectSignOut: + import.meta.env.VITE_APP_STAGE === "prod" + ? "production-url" + : "http://localhost:3000", // Make sure to use the exact URL + responseType: "token", + }, + }, + API: { + endpoints: [ + { + name: "api", + endpoint: import.meta.env.VITE_APP_API_URL, + region: import.meta.env.VITE_APP_REGION, + }, + ], + }, +}); + +ReactDOM.render( + + + , + document.getElementById("root") +); +``` + +## Adding login UI + +{%change%} Replace `frontend/src/App.jsx` with below code. + +{% raw %} + +```tsx +import React, { useState, useEffect } from "react"; +import { Auth, API } from "aws-amplify"; + +const App = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const getUser = async () => { + const user = await Auth.currentUserInfo(); + console.log(user); + if (user) setUser(user); + setLoading(false); + }; + + const signIn = async () => + await Auth.federatedSignIn({ + provider: "GitHub", + }); + + const signOut = async () => await Auth.signOut(); + + const publicRequest = async () => { + const response = await API.get("api", "/public"); + alert(JSON.stringify(response)); + }; + + const privateRequest = async () => { + try { + const response = await API.get("api", "/private", { + headers: { + Authorization: `Bearer ${(await Auth.currentSession()) + .getAccessToken() + .getJwtToken()}`, + }, + }); + alert(JSON.stringify(response)); + } catch (error) { + alert(error); + } + }; + + useEffect(() => { + getUser(); + }, []); + + if (loading) return
Loading...
; + + return ( +
+

SST + Cognito + GitHub OAuth + React

+ {user ? ( +
+

Welcome {user.attributes.name}!

+ +

{user.attributes.email}

+ +
+ ) : ( +
+

Not signed in

+ +
+ )} +
+ + +
+
+ ); +}; + +export default App; +``` + +{% endraw %} + +{%change%} Replace `frontend/src/index.css` with the below styles. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 120px; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +.api-section { + width: 100%; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + +.api-section > button { + background-color: darkorange; +} +``` + +Let's start our frontend in development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +npm run dev +``` + +Open up your browser and go to `http://localhost:3000`. + +![Browser view of localhost](/assets/examples/api-oauth-github/browser-view-of-localhost.png) + +There are 2 buttons that invokes the endpoints we created above. + +The **call /public** button invokes **GET /public** route using the `publicRequest` method we created in our frontend. + +Similarly, the **call /private** button invokes **GET /private** route using the `privateRequest` method. + +When you're not logged in and try to click the buttons, you'll see responses like below. + +![public button click without login](/assets/examples/api-oauth-google/public-button-click-without-login.png) + +![private button click without login](/assets/examples/api-oauth-google/private-button-click-without-login.png) + +Once you click on login, you're asked to login through your GitHub account. + +![login button click GitHub login screen](/assets/examples/api-oauth-github/login-button-click-github-login-screen.png) + +Once it's done you can check your info. + +![current logged in user info](/assets/examples/api-oauth-github/current-logged-in-user-info.png) + +Now that you've authenticated repeat the same steps as you did before, you'll see responses like below. + +![public button click with login](/assets/examples/api-oauth-github/public-button-click-with-login.png) + +![private button click with login](/assets/examples/api-oauth-github/private-button-click-with-login.png) + +As you can see the private route is only working while we are logged in. + +## Deploying your API + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-api-oauth-github-ExampleStack + + +Stack prod-api-oauth-github-ExampleStack + Status: deployed + Outputs: + api_url: https://v0l0zspdd7.execute-api.us-east-1.amazonaws.com + auth_client_id: e58t1t5o6jjur88nu4t891eac2 + auth_domain: prod-demo-auth-domain + site_url: https://d1567f41smqksw.cloudfront.net +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with GitHub. A local development environment, to test. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-google-authentication-to-a-serverless-api.md b/_examples/how-to-add-google-authentication-to-a-serverless-api.md new file mode 100644 index 0000000000..f43a69feed --- /dev/null +++ b/_examples/how-to-add-google-authentication-to-a-serverless-api.md @@ -0,0 +1,354 @@ +--- +layout: example +title: How to add Google authentication to a serverless API +short_title: Google Auth +date: 2021-02-08 00:00:00 +lang: en +index: 3 +# type: iam-auth +description: In this example we will look at how to add Google authentication to a serverless API using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a serverless API with Google. +repo: api-auth-google +ref: how-to-add-google-authentication-to-a-serverless-api +comments_id: how-to-add-google-authentication-to-a-serverless-api/2318 +--- + +In this example we will look at how to add Google authentication to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Google API project](https://console.developers.google.com/apis) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-google +$ cd api-auth-google +$ npm install +``` + +By default, our app will be deployed to an environment (or stage) called `dev` and the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; +import { Api } from "sst/constructs"; + +export default { + config(_input) { + return { + name: "api-auth-google", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, Cognito, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Api + const api = new Api(stack, "Api", { + defaults: { + authorizer: "iam", + }, + routes: { + "GET /private": "packages/functions/src/private.handler", + "GET /public": { + function: "packages/functions/src/public.handler", + authorizer: "none", + }, + }, + }); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +To secure our APIs we are adding the authorization type `AWS_IAM`. This means the caller of the API needs to have the right permissions. The first route is a private endpoint. The second is a public endpoint and its authorization type is overriden to `NONE`. + +## Setting up authentication + +Now let's add authentication for our serverless app. + +{%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `clientId` with that of your Google API project. + +```typescript +// Create auth provider +const auth = new Cognito(stack, "Auth", { + identityPoolFederation: { + google: { + clientId: + "38017095028-abcdjaaaidbgt3kfhuoh3n5ts08vodt3.apps.googleusercontent.com", + }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html) which relies on Google to authenticate users. And we use the [`attachPermissionsForAuthUsers`]({{ site.v2_url }}/constructs/Auth#attachpermissionsforauthusers) method to allow our logged in users to access our API. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +stack.addOutputs({ + ApiEndpoint: api.url, + IdentityPoolId: auth.cognitoIdentityPoolId, +}); +``` + +We are going to print out the resources that we created for reference. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello user!", + }; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-google-ExampleStack: deploying... + + ✅ dev-api-auth-google-ExampleStack + + +Stack dev-api-auth-google-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://aueschz6ba.execute-api.us-east-1.amazonaws.com + IdentityPoolId: us-east-1:b6211282-8eac-41d0-a721-945b7be7b586 +``` + +The `ApiEndpoint` is the API we just created. Make a note of the `IdentityPoolId`, we'll need that later. + +Now let's try out our public route. Head over to the following in your browser. Make sure to replace the URL with your API. + +``` +https://aueschz6ba.execute-api.us-east-1.amazonaws.com/public +``` + +You should see the greeting `Hello stranger!`. + +And if you try to visit the private route, you will see `{"message":"Forbidden"}`. + +``` +https://aueschz6ba.execute-api.us-east-1.amazonaws.com/private +``` + +## Login with Google + +We are going to use [Google's OAuth 2.0 Playground](https://developers.google.com/oauthplayground) to test logging in with Google. Head over to — [**developers.google.com/oauthplayground**](https://developers.google.com/oauthplayground) + +Head over to the settings, check **Use your own OAuth credentials**, and enter the **OAuth Client ID** and **OAuth Client secret** for your Google API project. + +![Set Google OAuth Playground Setting](/assets/examples/api-auth-google/set-google-oauth-playground-setting.png) + +Select **Google OAuth2 API v2** from Step 1. Check **userinfo.email**. Then select **Authorize APIs**. + +![Select Google OAuth2 API v2](/assets/examples/api-auth-google/select-google-oauth2-api-v2.png) + +Select **Exchange authorization code for tokens**. + +![Exchange Google authorization code for user access token](/assets/examples/api-auth-google/exchange-authorization-code-for-user-access-token.png) + +Copy the generated **id_token**. + +![Copy access token for users logged in with Google](/assets/examples/api-auth-google/copy-access-token-for-users-logged-in-with-google.png) + +Next, we need to get the user's Cognito Identity id. Replace `--identity-pool-id` with the `IdentityPoolId` from the `sst dev` log output; and replace the `--logins` with the **id_token** from the previous step. + +```bash +$ aws cognito-identity get-id \ + --identity-pool-id us-east-1:b6211282-8eac-41d0-a721-945b7be7b586 \ + --logins accounts.google.com="eyJhbGciOiJSUzI1NiIsImtpZCI6ImZkMjg1ZWQ0ZmViY2IxYWVhZmU3ODA0NjJiYzU2OWQyMzhjNTA2ZDkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDIwNzgwOTk5MzY4NDM3Njg5OTMiLCJlbWFpbCI6IndhbmdmYW5qaWVAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiI0cEFYV2diR0JoNy1NRzUyNEtBUG5BIiwiaWF0IjoxNjEyNzYzMDA1LCJleHAiOjE2MTI3NjY2MDV9.jIukmyMeJNTyOqya2eRWZzgMpUFJQkR2O49NV3-wGhW4sPKJPwKbhhfEMHEadQo5lYgsmQmsTiIrt4uPGMV0MwzvVppJ5iA57x-sc8JeQxezEnI6XVl59mQyuViAnBovCZeOB9nSquBr2KbxmIUvKApGq3E1Z8ksqobB-hzCEl1Jxqxp6aCKWAjJNsIkXpV615O-VYxRbL7Lxpi_1Saethf--PLV3_3kNd_NvsuwJa1CIdLw2fGqt-BUR46sgxICcCn95g9j2wacwBjHDVj_In75Xpecrp0FP-mxW13w9zwO8nWOQcmb4X8guHNd511az-F8r4bGVOy8il0SPoj3yw" +``` + +You should get an identity id for the Google user. + +```json +{ + "IdentityId": "us-east-1:52b11867-4633-4614-ae69-a2872f6a4429" +} +``` + +Now we'll need to get the IAM credentials for the identity user. + +```bash +$ aws cognito-identity get-credentials-for-identity \ + --identity-id us-east-1:52b11867-4633-4614-ae69-a2872f6a4429 \ + --logins accounts.google.com="eyJhbGciOiJSUzI1NiIsImtpZCI6ImZkMjg1ZWQ0ZmViY2IxYWVhZmU3ODA0NjJiYzU2OWQyMzhjNTA2ZDkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDIwNzgwOTk5MzY4NDM3Njg5OTMiLCJlbWFpbCI6IndhbmdmYW5qaWVAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiI0cEFYV2diR0JoNy1NRzUyNEtBUG5BIiwiaWF0IjoxNjEyNzYzMDA1LCJleHAiOjE2MTI3NjY2MDV9.jIukmyMeJNTyOqya2eRWZzgMpUFJQkR2O49NV3-wGhW4sPKJPwKbhhfEMHEadQo5lYgsmQmsTiIrt4uPGMV0MwzvVppJ5iA57x-sc8JeQxezEnI6XVl59mQyuViAnBovCZeOB9nSquBr2KbxmIUvKApGq3E1Z8ksqobB-hzCEl1Jxqxp6aCKWAjJNsIkXpV615O-VYxRbL7Lxpi_1Saethf--PLV3_3kNd_NvsuwJa1CIdLw2fGqt-BUR46sgxICcCn95g9j2wacwBjHDVj_In75Xpecrp0FP-mxW13w9zwO8nWOQcmb4X8guHNd511az-F8r4bGVOy8il0SPoj3yw" +``` + +This should give you a set of temporary IAM credentials. + +```json +{ + "IdentityId": "us-east-1:52b11867-4633-4614-ae69-a2872f6a4429", + "Credentials": { + "AccessKeyId": "ASIARUIS6Q2MERYVMP4Y", + "SecretKey": "/kxZf5+j+ShJE+1iptcdasBt1HVm3q+sA9VjtBjr", + "SessionToken": "IQoJb3JpZ2luX2VjEHcaCXVzLWVhc3QtMSJHMEUCIQCe/hEcayua8aNqS0T9AiJcbcRV3TdRcHbVDJcIdRQG/QIgB+tzHI2K2dSlMmJz6QmTA9W4/lSeoRcX07GJoUg4jqYqpwQIbxACGgwxMTIyNDU3Njk4ODAiDCX9xBhdQx4BF0mu3CqEBFgdvm6VueEpZMKVuCOg5i3PLnLsc58PFVD1iu2omj6cAmn/36ws1kM5BVOJ63hsXWHAzg1HbPrZ+EbgiF30LJWNX58e87Vx3KlpSjzDKLVZM9pH7Rg7JalQK0tmI6TfosffL2RJe7+JjFc0wKujagdKTedM6O15v1/Rkxou0JJ3N0bWSr8GNn2V4A1Xuz9fftkAE2pU2RYCtr8XM0U2s3szyTy61tnwUyddRwRSj3QCxBMRQgifR1bBKRGXzEDC5wwzlWxwH4t13fftlh6YOvp/ri8rZ5O46YLtIUSFzl18olH7ZuXrOjovL+W2Ksygp8ruhq6dFd6/rpSkpcN4CharuIgOQKT/w98ocqbmFOcLcPT97FvK5hZdtDvfOeehfAw40Vso3D3h4609TVpAFbNlsYh4hI4lWT3UazAf5Wwah/7pCwV05xXmGp5TjAdMrZP36Tc0vrt7bIWC7u2GuCKsw2fikj0zUfeIb8gEEL0cyhg0TzPSdZYoWeBf9bnqZqPy77h16bpNlSovHeP+oD+/4VIjw8ZDZg/arSky3dZtnAF9KEHtnS07cBSng8JsaUkhc/DugaB7nH41AuOQzaVfkOc9lnc3i6iDbsT+cJJCdYLtlrCAknCRGs+duX1XKX8Ek3CYvGfD2HqRjKIe9afeWGZJ2NyJJ9x6FmVnJXrLCn+n1jDhqoOBBjqFAhjV4D81AhSut/Z0y0lW+Z3xoD1N0bW5/7G2KQqwxFqa1L5uhvV5uyatgH4a4vHe/r+U1zXA9cIyJvNgreLzIUCHgN8UbgWs9r8rgxeHALGw9elNFdT7fUD5itM4o3rdnWTLBrQlCXlQfs68bsS9ABY6vc/3WLK5XY+7P+SCgFxWTztFLwVXAYqGvxu0cAjO4IC3+vf6MqzgdjRP0xqz9NWzbmax6ups1X2eF1Hjhfit6jMPotahLHZcRiPrPneUT5nOEv+vHGjcY7KOWUisqs8VCTBNvRJjZkD2HHaAoIv7UkVfuNEInt3sOwCu0qlTk6lOX6r8mKiW23vHqiSw/BcODkm3Yw==", + "Expiration": "2021-02-08T02:08:33-05:00" + } +} +``` + +Let's make a call to the private route using the credentials. The API request needs to be [signed with AWS SigV4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). We are going to use [Insomnia](https://insomnia.rest) to help us sign and make this request. + +Make sure to replace the **Access Key Id**, **Secret Access Key**, **Region**, and **Session Token** below. In our case the region is `us-east-1`. You can see this in the API URL. + +``` +https://aueschz6ba.execute-api.us-east-1.amazonaws.com +``` + +![Invoke Google authenticated API Gateway route](/assets/examples/api-auth-google/invoke-google-authenticated-api-gateway-route.png) + +You should now see. + +``` +Hello user! +``` + +The above process might seem fairly tedious. But once we integrate it into our frontend app, we'll be able to use something like [AWS Amplify]({% link _chapters/configure-aws-amplify.md %}) to handle these steps for us. + +## Making changes + +Let's make a quick change to our private route and print out the caller's user id. + +{%change%} Replace `packages/functions/src/private.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.iam.cognitoIdentity.identityId}!`, + }; +}; +``` + +We are getting the user id from the event object. + +If you head back to Insomnia and hit the `/private` endpoint again. + +![Get caller identity id in Google authenticated route](/assets/examples/api-auth-google/get-caller-identity-id-in-google-authenticated-route.png) + +You should see the user id. Note, this matches the identity id that was generated from the step where we generated a set of IAM credentials. + +``` +Hello us-east-1:52b11867-4633-4614-ae69-a2872f6a4429! +``` + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Google. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-google-login-to-your-cognito-user-pool.md b/_examples/how-to-add-google-login-to-your-cognito-user-pool.md new file mode 100644 index 0000000000..54a6aa9094 --- /dev/null +++ b/_examples/how-to-add-google-login-to-your-cognito-user-pool.md @@ -0,0 +1,638 @@ +--- +layout: example +title: How to Add Google Login to Your Cognito User Pool +short_title: Google Auth +date: 2021-02-08 00:00:00 +lang: en +index: 3 +type: jwt-auth +description: In this example we will look at how to add Google Login to a Cognito User Pool using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a full-stack serverless app with Google. +repo: api-oauth-google +ref: how-to-add-google-login-to-your-cognito-user-pool +comments_id: how-to-add-google-login-to-your-cognito-user-pool/2643 +--- + +In this example, we will look at how to add Google Login to Your Cognito User Pool using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Google API project](https://console.developers.google.com/apis) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-oauth-google +$ cd api-oauth-google +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-oauth-google", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the Cognito + +First, let's create a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) to store the user info using the [`Cognito`]({{ site.v2_url }}/constructs/Cognito) construct + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import * as cognito from "aws-cdk-lib/aws-cognito"; +import { Api, Cognito, StackContext, StaticSite } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create auth + const auth = new Cognito(stack, "Auth", { + cdk: { + userPoolClient: { + supportedIdentityProviders: [ + cognito.UserPoolClientIdentityProvider.GOOGLE, + ], + oAuth: { + callbackUrls: [ + app.stage === "prod" + ? "prodDomainNameUrl" + : "http://localhost:3000", + ], + logoutUrls: [ + app.stage === "prod" + ? "prodDomainNameUrl" + : "http://localhost:3000", + ], + }, + }, + }, + }); +} +``` + +This creates a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html); a user directory that manages users. We've configured the User Pool to allow users to login with their Google account and added the callback and logout URLs. + +Note, we haven't yet set up Google OAuth with our user pool, we'll do it next. + +## Setting up Google OAuth + +Now let's add Google OAuth for our serverless app, to do so we need to create a [Google User Pool identity provider](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-social-idp.html) and link it with the user pool we created above. + +{%change%} Create a `.env` file in the root and add your google `clientId` and `clientSecret` from your [Google API project](https://console.developers.google.com/apis). + +![GCP Console API Credentials](/assets/examples/api-oauth-google/gcp-console-api-credentials.png) + +```typescript +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +``` + +{%change%} Add this below the `Cognito` definition in `stacks/ExampleStack.ts`. + +```typescript +// Throw error if client ID & secret are not provided +if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) + throw new Error("Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET"); + +// Create a Google OAuth provider +const provider = new cognito.UserPoolIdentityProviderGoogle(stack, "Google", { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + userPool: auth.cdk.userPool, + scopes: ["profile", "email", "openid"], + attributeMapping: { + email: cognito.ProviderAttribute.GOOGLE_EMAIL, + givenName: cognito.ProviderAttribute.GOOGLE_GIVEN_NAME, + familyName: cognito.ProviderAttribute.GOOGLE_FAMILY_NAME, + profilePicture: cognito.ProviderAttribute.GOOGLE_PICTURE, + }, +}); + +// attach the created provider to our userpool +auth.cdk.userPoolClient.node.addDependency(provider); +``` + +This creates a Google identity provider with the given scopes and links the created provider to our user pool and Google user’s attributes will be mapped to the User Pool user. + +Now let's associate a Cognito domain to the user pool, which can be used for sign-up and sign-in webpages. + +{%change%} Add below code in `stacks/ExampleStack.ts`. + +```typescript +// Create a cognito userpool domain +const domain = auth.cdk.userPool.addDomain("AuthDomain", { + cognitoDomain: { + domainPrefix: `${app.stage}-demo-auth-domain`, + }, +}); +``` + +Note, the `domainPrefix` need to be globally unique across all AWS accounts in a region. + +## Setting up the API + +{%change%} Replace the `Api` definition with the following in `stacks/ExampleStacks.ts`. + +```typescript +// Create a HTTP API +const api = new Api(stack, "Api", { + authorizers: { + userPool: { + type: "user_pool", + userPool: { + id: auth.userPoolId, + clientIds: [auth.userPoolClientId], + }, + }, + }, + defaults: { + authorizer: "userPool", + }, + routes: { + "GET /private": "packages/functions/src/private.handler", + "GET /public": { + function: "packages/functions/src/public.handler", + authorizer: "none", + }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +By default, all routes have the authorization type `JWT`. This means the caller of the API needs to pass in a valid JWT token. The `GET /private` route is a private endpoint. The `GET /public` is a public endpoint and its authorization type is overridden to `NONE`. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello, stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function handler() { + return { + statusCode: 200, + body: "Hello, user!", + }; +} +``` + +## Setting up our React app + +To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +// Create a React Static Site +const site = new StaticSite(stack, "Site", { + path: "packages/frontend", + buildOutput: "dist", + buildCommand: "npm run build", + environment: { + VITE_APP_COGNITO_DOMAIN: domain.domainName, + VITE_APP_API_URL: api.url, + VITE_APP_REGION: app.region, + VITE_APP_USER_POOL_ID: auth.userPoolId, + VITE_APP_IDENTITY_POOL_ID: auth.cognitoIdentityPoolId, + VITE_APP_USER_POOL_CLIENT_ID: auth.userPoolClientId, + }, +}); + +// Show the endpoint in the output +stack.addOutputs({ + api_url: api.url, + auth_client_id: auth.userPoolClientId, + auth_domain: domain.domainName, + site_url: site.url, +}); +``` + +The construct is pointing to where our React.js app is located. We haven't created our app yet but for now, we'll point to the `packages/frontend` directory. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +We are going to print out the resources that we created for reference. + +## Creating the frontend + +Run the below commands in the `packages/` directory to to create a basic react project. + +```bash +$ npx create-vite@latest frontend --template react +$ cd frontend +$ npm install +``` + +This sets up our React app in the `packages/frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `frontend/package.json`. + +```bash +"dev": "vite" +``` + +{%change%} With the following: + +```bash +"dev": "sst bind vite" +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-oauth-google-ExampleStack: deploying... + + ✅ dev-api-oauth-google-ExampleStack + + +Stack dev-api-oauth-google-ExampleStack + Status: deployed + Outputs: + api_url: https://v0l1zlpy5f.execute-api.us-east-1.amazonaws.com + auth_client_id: 253t1t5o6jjur88nu4t891eac2 + auth_domain: dev-demo-auth-domain + site_url: https://d1567f41smqk8b.cloudfront.net +``` + +Copy the cognito domain from the terminal output and add it to the **Authorised JavaScript origins** in the GCP Console. + +Note, if you are not using custom domain, your domain URL will be `https://.auth..amazoncognito.com`. + +And under **Authorised redirect URIs**, append `/oauth2/idpresponse` to your domain URL and add it to the values and click **Save**. + +![GCP Console](/assets/examples/api-oauth-google/gcp-console.png) + +The `api_endpoint` is the API we just created. While the `site_url` is where our React app will be hosted. For now, it's just a placeholder website. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button of the `GET /public` to send a `GET` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/api-oauth-google/api-explorer-invocation-response.png) + +You should see a `Hello, stranger!` in the response body. + +## Adding AWS Amplify + +To use our AWS resources on the frontend we are going to use [AWS Amplify](https://aws.amazon.com/amplify/). + +Note, to know more about configuring Amplify with SST check [this chapter]({% link _chapters/configure-aws-amplify.md %}). + +Run the below command to install AWS Amplify in the `packages/frontend/` directory. + +```bash +npm install aws-amplify +``` + +{%change%} Replace `frontend/src/main.jsx` with below code. + +```jsx +/* eslint-disable no-undef */ +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import Amplify from "aws-amplify"; + +// Configure AWS Amplify with credentials from backend +Amplify.configure({ + Auth: { + region: import.meta.env.VITE_APP_REGION, + userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, + userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, + mandatorySignIn: false, + oauth: { + domain: `${ + import.meta.env.VITE_APP_COGNITO_DOMAIN + + ".auth." + + import.meta.env.VITE_APP_REGION + + ".amazoncognito.com" + }`, + scope: ["email", "profile", "openid", "aws.cognito.signin.user.admin"], + redirectSignIn: + import.meta.env.VITE_APP_API_STAGE === "prod" + ? "production-url" + : "http://localhost:3000", // Make sure to use the exact URL + redirectSignOut: + import.meta.env.VITE_APP_API_STAGE === "prod" + ? "production-url" + : "http://localhost:3000", // Make sure to use the exact URL + responseType: "token", + }, + }, + API: { + endpoints: [ + { + name: "api", + endpoint: import.meta.env.VITE_APP_API_URL, + region: import.meta.env.VITE_APP_REGION, + }, + ], + }, +}); + +ReactDOM.render( + + + , + document.getElementById("root") +); +``` + +## Adding login UI + +{%change%} Replace `frontend/src/App.jsx` with below code. + +{% raw %} + +```jsx +import { Auth, API } from "aws-amplify"; +import React, { useState, useEffect } from "react"; + +const App = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // Get the current logged in user info + const getUser = async () => { + const user = await Auth.currentUserInfo(); + if (user) setUser(user); + setLoading(false); + }; + + // Trigger Google login + const signIn = async () => + await Auth.federatedSignIn({ + provider: "Google", + }); + + // Logout the authenticated user + const signOut = async () => await Auth.signOut(); + + // Send an API call to the /public endpoint + const publicRequest = async () => { + const response = await API.get("api", "/public"); + alert(JSON.stringify(response)); + }; + + // Send an API call to the /private endpoint with authentication details. + const privateRequest = async () => { + try { + const response = await API.get("api", "/private", { + headers: { + Authorization: `Bearer ${(await Auth.currentSession()) + .getAccessToken() + .getJwtToken()}`, + }, + }); + alert(JSON.stringify(response)); + } catch (error) { + alert(error); + } + }; + + // Check if there's any user on mount + useEffect(() => { + getUser(); + }, []); + + if (loading) return
Loading...
; + + return ( +
+

SST + Cognito + Google OAuth + React

+ {user ? ( +
+

Welcome {user.attributes.given_name}!

+ +

{user.attributes.email}

+ +
+ ) : ( +
+

Not signed in

+ +
+ )} +
+ + +
+
+ ); +}; + +export default App; +``` + +{% endraw %} + +{%change%} Replace `frontend/src/index.css` with the below styles. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 120px; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +.api-section { + width: 100%; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + +.api-section > button { + background-color: darkorange; +} +``` + +Let's start our frontend in development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +npm run dev +``` + +Open up your browser and go to `http://localhost:3000`. + +![Browser view of localhost](/assets/examples/api-oauth-google/browser-view-of-localhost.png) + +There are 2 buttons that invokes the endpoints we created above. + +The **call /public** button invokes **GET /public** route using the `publicRequest` method we created in our frontend. + +Similarly, the **call /private** button invokes **GET /private** route using the `privateRequest` method. + +When you're not logged in and try to click the buttons, you'll see responses like below. + +![public button click without login](/assets/examples/api-oauth-google/public-button-click-without-login.png) + +![private button click without login](/assets/examples/api-oauth-google/private-button-click-without-login.png) + +Once you click on login, you're asked to login through your Google account. + +![login button click google login screen](/assets/examples/api-oauth-google/login-button-click-google-login-screen.png) + +Once it's done you can check your info. + +![current logged in user info](/assets/examples/api-oauth-google/current-logged-in-user-info.png) + +Now that you've authenticated repeat the same steps as you did before, you'll see responses like below. + +![public button click with login](/assets/examples/api-oauth-google/public-button-click-with-login.png) + +![private button click with login](/assets/examples/api-oauth-google/private-button-click-with-login.png) + +As you can see the private route is only working while we are logged in. + +## Deploying your API + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-api-oauth-google-ExampleStack + + +Stack prod-api-oauth-google-ExampleStack + Status: deployed + Outputs: + api_url: https://ck198mfop1.execute-api.us-east-1.amazonaws.com + auth_client_id: 875t1t5o6jjur88jd4t891eat5 + auth_domain: prod-demo-auth-domain + site_url: https://c1767f41smqkh7.cloudfront.net +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Google. A local development environment, to test. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-google-login-to-your-sst-apps.md b/_examples/how-to-add-google-login-to-your-sst-apps.md new file mode 100644 index 0000000000..b827c6b15f --- /dev/null +++ b/_examples/how-to-add-google-login-to-your-sst-apps.md @@ -0,0 +1,899 @@ +--- +layout: example +title: How to Add Google Login to Your Serverless App with SST Auth +short_title: Google Auth +date: 2022-10-10 00:00:00 +lang: en +index: 1 +type: sst-auth +description: In this example we will look at how to add Google Login to your serverless app using SST Auth. We'll be using the Api, Auth, Table, and StaticSite constructs to create a full-stack app with Google authentication. +short_desc: Adding Google auth to a full-stack serverless app. +repo: api-sst-auth-google +ref: how-to-add-google-login-to-your-sst-app-with-sst-auth +comments_id: how-to-add-google-login-to-your-sst-app-with-sst-auth/2643 +--- + +In this example, we will look at how to add Google Login to your serverless app using [SST Auth]({{ site.v2_url }}/auth). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-sst-auth-google +$ cd api-sst-auth-google +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-sst-auth-google", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of three parts. + +1. `stacks/` — Infrastructure code + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — Application code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Auth flow + +Before we start let's first take a look at the auth flow at a high level. + +![Google Auth Flow](/assets/examples/api-sst-auth-google/auth-flow.png) + +1. The user clicks on "Sign in with Google" in the frontend, and gets redirected to an **Authorize URL** to initiate the auth flow. + +2. This will redirect the user to Google and they login to their Google account. + +3. Google redirects the user back to a **Callback URL** with the user's details. + +4. In the **Callback URL** we: + + Store the user data in our database, create a session token for the user, and redirect to the frontend with the session token. + + The session token is then stored in the cookie or local storage. At this point, the user is authenticated. + +5. From now on, when the user makes API requests, the session token gets passed in through the request header. + +6. The backend API verifies the session token, decodes the user id from it, and looks up the user in the database. + +In this tutorial, we will be implementing each of the above steps. + +## Create a Google project + +Before we start, make sure you have a Google Project with OAuth client credentials. You can follow the steps below to create a new project and a new OAuth client. + +Head over to the [Google Cloud console](https://console.cloud.google.com), select the navigation menu on the top left, then **APIs & Services**, and then **Credentials**. + +![GCP Console Select Credentials](/assets/examples/api-sst-auth-google/gcp-console-select-credentials.png) + +If you don't have an existing Google project, click **CREATE PROJECT**. + +![GCP Console Select Create Project](/assets/examples/api-sst-auth-google/gcp-console-select-create-project.png) + +Enter a project name, then click **CREATE**. + +![GCP Console Create Project](/assets/examples/api-sst-auth-google/gcp-console-create-project.png) + +After the project is created, select **CREATE CREDENTIALS**, then **OAuth client ID**. + +![GCP Console Create Credentials](/assets/examples/api-sst-auth-google/gcp-console-select-create-credentials.png) + +Select **CONFIGURE CONSENT SCREEN**. + +![GCP Console Configure Consent Screen](/assets/examples/api-sst-auth-google/gcp-console-select-configure-consent-screen.png) + +Select **External**, and hit **CREATE**. + +![GCP Console Configure Consent Screen User Type](/assets/examples/api-sst-auth-google/gcp-console-configure-consent-screen-user-type.png) + +Enter the following details: + +- **App name**: the name of your app, here we picked `SST Auth` +- **User support email**: select your email address in the drop-down +- **Developer contact information**: enter your email address again + +![GCP Console Configure Consent Screen Form](/assets/examples/api-sst-auth-google/gcp-console-configure-consent-screen-form.png) + +Click **SAVE AND CONTINUE** for the rest of the steps. And on the last step hit **BACK TO DASHBOARD**. + +![GCP Console Select Back To Dashboard](/assets/examples/api-sst-auth-google/gcp-console-back-to-dashboard.png) + +Select **Credentials** on the left. Then select **CREATE CREDENTIALS**, then **OAuth client ID**. + +![GCP Console Create Credentials](/assets/examples/api-sst-auth-google/gcp-console-select-create-credentials-again.png) + +Select **Web application** type, then hit **CREATE**. + +![GCP Console Create Client](/assets/examples/api-sst-auth-google/gcp-console-create-client.png) + +Make a note of the **Client ID**. We will need it in the following steps. + +![GCP Console Copy Client Credentials](/assets/examples/api-sst-auth-google/gcp-console-copy-client-credentials.png) + +## Add the authorize URL + +Next, we need to create an **Authorize URL** to initiate the auth flow. + +#### Configure the construct + +We are going to use the [`Auth`]({{ site.v2_url }}/constructs/Auth) construct. It will help us create both the **Authorize URL** and the **Callback URL**. + +{%change%} Add the following below the `Api` construct in `stacks/ExampleStack.ts`. + +```typescript +const auth = new Auth(stack, "auth", { + authenticator: { + handler: "packages/functions/src/auth.handler", + }, +}); +auth.attach(stack, { + api, + prefix: "/auth", +}); +``` + +Behind the scenes, the `Auth` construct creates a `/auth/*` catch-all route. Both the Authorize and Callback URLs will fall under this route. + +{%change%} Also remember to import the `Auth` construct up top. + +```diff +- import { StackContext, Api } from "sst/constructs"; ++ import { StackContext, Api, Auth } from "sst/constructs"; +``` + +#### Add the auth handler + +Now let's implement the `authenticator` function. + +{%change%} Add a file in `packages/functions/src/auth.ts` with the following. + +```typescript +import { AuthHandler, GoogleAdapter } from "sst/node/auth"; + +const GOOGLE_CLIENT_ID = + "1051197502784-vjtbj1rnckpagefmcoqnaon0cbglsdac.apps.googleusercontent.com"; + +export const handler = AuthHandler({ + providers: { + google: GoogleAdapter({ + mode: "oidc", + clientID: GOOGLE_CLIENT_ID, + onSuccess: async (tokenset) => { + return { + statusCode: 200, + body: JSON.stringify(tokenset.claims(), null, 4), + }; + }, + }), + }, +}); +``` + +Make sure to replace `GOOGLE_CLIENT_ID` with the OAuth Client ID created in the previous section. + +The `sst/node` package provides helper libraries used in Lambda functions. In the snippet above, we are using the package to create an `AuthHandler` with a `GoogleAdapter` named `google`. This creates two routes behind the scenes: + +- **Authorize URL** at `/auth/google/authorize` +- **Callback URL** at `/auth/google/callback` + +When the Authorize URL is invoked, it will initialize the auth flow and redirects the user to Google. + +## Set up our React app + +Next, we are going to add a **Sign in with Google** button to our frontend. And on click, we will redirect the user to the **Authorize URL**. + +To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Add the following above the `Auth` construct in `stacks/ExampleStack.ts`. + +```typescript +const site = new StaticSite(stack, "Site", { + path: "web", + buildCommand: "npm run build", + buildOutput: "dist", + environment: { + VITE_APP_API_URL: api.url, + }, +}); +``` + +{%change%} And add the site URL to `stack.addOutputs`. + +```diff + stack.addOutputs({ + ApiEndpoint: api.url, ++ SiteURL: site.url, + }); +``` + +The construct is pointing to the directory where we are going to add our React.js app. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +{%change%} Also remember to import the `StaticSite` construct up top. + +```diff +- import { StackContext, Api, Auth } from "sst/constructs"; ++ import { StackContext, Api, Auth, StaticSite } from "sst/constructs"; +``` + +## Create the frontend + +{%change%} Run the below commands in our project root to create a basic react project. + +```bash +$ npx create-vite@latest web --template react +$ cd web +$ npm install +``` +For a react `TypeScript` project + +```bash +npx create-vite@latest web --template react-ts +``` + +This sets up our React app in the `web/` directory. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `web/package.json`. + +```diff +-"dev": "vite" ++"dev": "sst bind vite" +``` + +## Start our dev environment + +SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +{%change%} Run in the root. + +```bash +$ npm run dev +``` + +The first time you run this command it'll prompt you to enter a stage name. + +```txt +Look like you’re running sst for the first time in this directory. +Please enter a stage name you’d like to use locally. Or hit enter +to use the one based on your AWS credentials (frank): +``` + +You can press enter to use the default stage, or manually enter a stage name. SST uses the stage to namespace all the resources in your application. + +The first time `sst dev` runs, it can take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +After `sst dev` starts up, you will see the following output in your terminal. + +``` +Stack frank-api-sst-auth-google-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://2wk0bl6b7i.execute-api.us-east-1.amazonaws.com + SiteURL: https://d54gkw8ds19md.cloudfront.net + + +========================== + Starting Live Lambda Dev +========================== + +SST Console: https://console.sst.dev/api-sst-auth-google/frank/local +Debug session started. Listening for requests... +``` + +## Update the Google redirect URI + +The `ApiEndpoint` is the API we just created. That means our: + +- **Authorize URL** is `https://2wk0bl6b7i.execute-api.us-east-1.amazonaws.com/auth/google/authorize` +- **Callback URL** is `https://2wk0bl6b7i.execute-api.us-east-1.amazonaws.com/auth/google/callback` + +And the `SiteURL` is where our React app will be hosted. While in development, it's just a placeholder website. + +Add our **Callback URL** to the **Authorized redirect URIs** in our Google project's GCP Console. + +![GCP Console Authorize Redirect URI](/assets/examples/api-sst-auth-google/gcp-console-add-authorized-redirect-uri.png) + +## Add the login UI + +{%change%} Replace `web/src/App.jsx` with below code. + +```jsx +const App = () => { + return ( +
+

SST Auth Example

+ +
+ ); +}; + +export default App; +``` + +Let's start our frontend in the development environment. + +{%change%} In the `web/` directory run. + +```bash +$ npm run dev +``` + +Open up your browser and go to the URL it shows. In our case it is: `http://127.0.0.1:5173` + +![Web app not signed in unstyled](/assets/examples/api-sst-auth-google/react-site-not-signed-in-unstyled.png) + +Click on `Sign in with Google`, and you will be redirected to Google to sign in. + +![Google Sign in screen](/assets/examples/api-sst-auth-google/google-sign-in-screen.png) + +Once you are signed in, you will be redirected to the **Callback URL** we created earlier with the user's details. Recall in our `authenticator` handler function, that we are simply printing the user's claims in the `onSuccess` callback. + +![Google Sign in callback screen](/assets/examples/api-sst-auth-google/google-sign-in-callback.png) + +🎉 Sweet! We have just completed steps 1, 2, and 3 of our [Auth Flow](#auth-flow). + +## Create a session token + +Now, let's implement step 4. In the `onSuccess` callback, we will create a session token and pass that back to the frontend. + +#### Define a session type + +First, to make creating and retrieving session typesafe, we'll start by defining our session types. + +{%change%} Add the following above the `AuthHandler` in `packages/functions/src/auth.ts`. + +```typescript +declare module "sst/node/auth" { + export interface SessionTypes { + user: { + userID: string; + }; + } +} +``` + +We are going to keep it simple and create a `user` session type. And it contains a `userId` property. Note that if you have a multi-tenant app, you might want to add something like the `tenantID` as well. + +Also note that as your app grows, you can define multiple session types like an `api_key` session type that represents any server-to-server requests. + +#### Create a session + +Now let's create the session object. + +{%change%} Make the following changes to the `onSuccess` callback. + +```diff + export const handler = AuthHandler({ + providers: { + google: GoogleAdapter({ + mode: "oidc", + clientID: GOOGLE_CLIENT_ID, + onSuccess: async (tokenset) => { +- return { +- statusCode: 200, +- body: JSON.stringify(tokenset.claims(), null, 4), +- }; ++ const claims = tokenset.claims(); ++ return Session.parameter({ ++ redirect: "http://127.0.0.1:5173", ++ type: "user", ++ properties: { ++ userID: claims.sub, ++ }, ++ }); + }, + }), + }, + }); +``` + +Remember to replace the `redirect` URL with the URL of your local React app. + +The `Session.parameter` call encrypts the given session object to generate a token. It'll then redirect to the given `redirect` URL with `?token=xxxx` as the query string parameter. + +{%change%} Also import the `Session` up top. + +```diff +- import { AuthHandler, GoogleAdapter } from "sst/node/auth"; ++ import { AuthHandler, GoogleAdapter, Session } from "sst/node/auth"; +``` + +## Use the session + +Now let's use the session token in the frontend. + +#### Store the session token + +Then in the frontend, we will check if the URL contains the `token` query string when the page loads. If it is passed in, we will store it in the local storage, and then redirect the user to the root domain. + +{%change%} Add the following above the `return` in `web/src/App.jsx`. + +```typescript +useEffect(() => { + const search = window.location.search; + const params = new URLSearchParams(search); + const token = params.get("token"); + if (token) { + localStorage.setItem("session", token); + window.location.replace(window.location.origin); + } +}, []); +``` + +#### Load the session + +On page load, we will also check if the session token exists in the local storage. If it does, we want to display the user that is signed in, and have a button for the user to sign out. + +{%change%} Add this above the `useEffect` we just added. + +```typescript +const [session, setSession] = useState(null); + +const getSession = async () => { + const token = localStorage.getItem("session"); + if (token) { + setSession(token); + } +}; + +useEffect(() => { + getSession(); +}, []); +``` + +{%change%} Replace the `return` to conditionally render the page based on `session`. + +```diff + return ( +
+

SST Auth Example

++ {session ? ( ++
++

Yeah! You are signed in.

++ ++
++ ) : ( + ++ )} +
+ ); +``` + +#### Clear the session on logout + +And finally, when the user clicks on `Sign out`, we need to clear the session token from the local storage. + +{%change%} Add the following above the `return`. + +```typescript +const signOut = async () => { + localStorage.removeItem("session"); + setSession(null); +}; +``` + +{%change%} Also, remember to add the imports up top. + +```typescript +import { useEffect, useState } from "react"; +``` + +Let's go back to our browser. Click on **Sign in with Google** again. After you authenticate with Google, you will be redirected back to the same page with the _"Yeah! You are signed in."_ message. + +![Web app signed in unstyled](/assets/examples/api-sst-auth-google/react-site-signed-in-unstyled.png) + +Try refreshing the page, you will remain signed in. This is because the session token is stored in the browser's local storage. + +Let's sign out before continuing with the next section. Click on **Sign out**. + +🎉 Awesome! We have now completed step 4 of our [Auth Flow](#auth-flow). + +Let's move on to steps 5 and 6. We will create a session API that will return the user data given the session token. + +## Store the user data + +So far we haven't been storing the data Google's been returning through the **Callback URL**. Let's create a database table to store this. + +#### Create a DynamoDB table + +We'll be using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. + +{%change%} Add the following above the `Api` construct in `stacks/ExampleStack.ts`. + +```typescript +const table = new Table(stack, "users", { + fields: { + userId: "string", + }, + primaryIndex: { partitionKey: "userId" }, +}); +``` + +{%change%} Then let's bind the `table` to the `api`. Make the following changes to the `Api` construct. + +```diff + const api = new Api(stack, "api", { ++ defaults: { ++ function: { ++ bind: [table], ++ }, ++ }, + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); +``` + +{%change%} Import the `Table` construct up top. + +```diff +- import { StackContext, Api, Auth, StaticSite } from "sst/constructs"; ++ import { StackContext, Api, Auth, StaticSite, Table } from "sst/constructs"; +``` + +#### Store the claims + +Now let's update our `authenticator` function to store the user data in the `onSuccess` callback. + +{%change%} Update the `onSuccess` callback. + +```diff + export const handler = AuthHandler({ + providers: { + google: GoogleAdapter({ + mode: "oidc", + clientID: GOOGLE_CLIENT_ID, + onSuccess: async (tokenset) => { + const claims = tokenset.claims(); + ++ const ddb = new DynamoDBClient({}); ++ await ddb.send(new PutItemCommand({ ++ TableName: Table.users.tableName, ++ Item: marshall({ ++ userId: claims.sub, ++ email: claims.email, ++ picture: claims.picture, ++ name: claims.given_name, ++ }), ++ })); + + return Session.parameter({ + redirect: "http://127.0.0.1:5173", + type: "user", + properties: { + userID: claims.sub, + }, + }); + }, + }), + }, + }); +``` + +This is saving the `claims` we get from Google in our DynamoDB table. + +{%change%} Also add these imports up top. + +```typescript +import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import { Table } from "sst/node/table"; +``` + +{%change%} And finally install these packages inside the `packages/functions` directory. + +```bash +npm install --save @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb +``` + +## Fetch the user data + +Now that the user data is stored in the database; let's create an API endpoint that returns the user details given a session token. + +#### Create a session API + +{%change%} Add a `/session` route in the `Api` construct's routes definition in `stacks/ExampleStacks.ts`. + +```diff + routes: { + "GET /": "packages/functions/src/lambda.handler", ++ "GET /session": "packages/functions/src/session.handler", + }, +``` + +{%change%} Add a file at `packages/functions/src/session.ts`. + +```typescript +import { Table } from "sst/node/table"; +import { ApiHandler } from "sst/node/api"; +import { useSession } from "sst/node/auth"; +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; + +export const handler = ApiHandler(async () => { + const session = useSession(); + + // Check user is authenticated + if (session.type !== "user") { + throw new Error("Not authenticated"); + } + + const ddb = new DynamoDBClient({}); + const data = await ddb.send( + new GetItemCommand({ + TableName: Table.users.tableName, + Key: marshall({ + userId: session.properties.userID, + }), + }) + ); + + return { + statusCode: 200, + body: JSON.stringify(unmarshall(data.Item!)), + }; +}); +``` + +The handler calls a [`useSession()`]({{ site.v2_url }}/clients/auth#usesession) hook to decode the session token and retrieve the user's `userID` from the session data. Note that, `useSession` can be called anywhere in your Lambda handler. This works because we are using the [`ApiHandler`]({{ site.v2_url }}/clients/api#apihandler) to wrap our Lambda function. + +We then fetch the user's data from our database table with `userID` being the key. + +Save the changes. And then open up the `sst dev` terminal window. You will be prompted with: + +```bash +Stacks: There are new infrastructure changes. Press ENTER to redeploy. +``` + +Press **ENTER** to deploy the infrastructure changes. + +As we wait, let's update our frontend to make a request to the `/session` API to fetch the user data. + +#### Call the session API + +{%change%} Add the following above the `signOut` function in `web/src/App.jsx`. + +```typescript +const getUserInfo = async (session) => { + try { + const response = await fetch( + `${import.meta.env.VITE_APP_API_URL}/session`, + { + method: "GET", + headers: { + Authorization: `Bearer ${session}`, + }, + } + ); + return response.json(); + } catch (error) { + alert(error); + } +}; +``` + +{%change%} Update the `getSession` function to fetch from the new session API. + +```diff + const getSession = async () => { + const token = localStorage.getItem("session"); + if (token) { +- setSession(token); ++ const user = await getUserInfo(token); ++ if (user) setSession(user); + } ++ setLoading(false); + }; +``` + +And finally, add a loading state to indicate the API is being called. + +{%change%} Add the following below the session `useState` hook. + +```diff + const [session, setSession] = useState(null); ++ const [loading, setLoading] = useState(true); +``` + +## Render the user data + +Let's render the user info. + +{%change%} Update our `return` statement with. + +{% raw %} + +```diff +-
+-

Yeah! You are signed in.

+- +-
++
++

Welcome {session.name}!

++ ++

{session.email}

++ ++
+``` + +{% endraw %} + +Also, let's display a loading sign while waiting for the `/session` API to return. + +{%change%} Add the following above the `return`. + +```diff ++ if (loading) return
Loading...
; + + return ( +
+ ... +``` + +Finally, let's add some basic styles to the page. + +{%change%} Replace `web/src/index.css` with the following. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 100%; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +``` + +Let's go back to our browser. Make sure you are signed out. + +Click on **Sign in with Google** again. After you authenticate with Google, you will be redirected back to the same page with your details. + +![Web app signed in styled](/assets/examples/api-sst-auth-google/react-site-signed-in-styled.png) + +🎉 Congratulations! We have completed the entire [Auth Flow](#auth-flow). + +## Deploy your API + +When deploying to prod, we need to change our `authenticator` to redirect to the deployed frontend URL instead of `127.0.0.1`. + +{%change%} In `stacks/ExampleStack.ts`, make this change to the `Auth` construct. + +```diff + const auth = new Auth(stack, "auth", { + authenticator: { + handler: "packages/functions/src/auth.handler", ++ bind: [site], + }, + }); +``` + +{%change%} In `packages/functions/src/auth.ts`, change `redirect` to: + +```diff +-redirect: "http://127.0.0.1:5173", ++redirect: process.env.IS_LOCAL ? "http://127.0.0.1:5173" : StaticSite.site.url, +``` + +Note that when we are developing locally via `sst dev`, the `IS_LOCAL` environment variable is set. We will conditionally redirect to `127.0.0.1` or the site's URL depending on `IS_LOCAL`. + +{%change%} Also remember to import the `StaticSite` construct up top. + +```typescript +import { StaticSite } from "sst/node/site"; +``` + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so that when we are developing locally, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash +Stack prod-api-sst-auth-google-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://jd8jpfjue6.execute-api.us-east-1.amazonaws.com + SiteURL: https://d36g0g26jff9tr.cloudfront.net +``` + +#### Add the prod redirect URI + +Like we did when we ran `sst dev`; add the `prod` **Callback URL** to the **Authorized redirect URIs** in the GCP Console. In our case this is — `https://jd8jpfjue6.execute-api.us-east-1.amazonaws.com/auth/google/callback` + +![GCP Console Authorize Redirect URI For Prod](/assets/examples/api-sst-auth-google/gcp-console-add-authorized-redirect-uri-for-prod.png) + +## Conclusion + +And that's it! You've got a brand new serverless full-stack app that supports _Sign in with Google_. With a local development environment, to test. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! + +## Clean up + +Optionally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` diff --git a/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md b/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md new file mode 100644 index 0000000000..c6fc0ac899 --- /dev/null +++ b/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md @@ -0,0 +1,580 @@ +--- +layout: example +title: How to add JWT authorization with Auth0 to a serverless API +short_title: Auth0 JWT +date: 2021-03-02 00:00:00 +lang: en +index: 2 +type: jwt-auth +description: In this example we will look at how to add JWT authorization with Auth0 to a serverless API using SST. We'll be using the Api and Auth constructs to create an authenticated API. +short_desc: Adding JWT authentication with Auth0. +repo: api-auth-jwt-auth0 +ref: how-to-add-jwt-authorization-with-auth0-to-a-serverless-api +comments_id: how-to-add-jwt-authorization-with-auth0-to-a-serverless-api/2337 +--- + +In this example we will look at how to add JWT authorization with [Auth0](https://auth0.com) to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- An [Auth0 account](https://auth0.com) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-jwt-auth0 +$ cd api-auth-jwt-auth0 +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-auth-jwt-auth0", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up Auth0 + +Go to the applications page in your Auth0 dashboard and click on **Create Application** button. + +![Auth0 create application](/assets/examples/api-auth-jwt-auth0/auth0-create-application.png) + +For this example we are going to use React for the frontend so on the next screen select single page application. + +![Auth0 choose single page application](/assets/examples/api-auth-jwt-auth0/auth0-spa.png) + +Go to the settings tab in your application dashboard and copy the **Domain** and **Client ID** values and add them into a `.env.local` file in the root. + +![Auth0 applications page](/assets/examples/api-auth-jwt-auth0/auth0-applications-page.png) + +``` +AUTH0_DOMAIN= +AUTH0_CLIENT_ID= +``` + +Scroll down to **Application URIs** section and add `http://localhost:3000` in Callback, Logout and Web Origins to give access to our React client. + +Note, after deployment you need to replace these values with the deployed URL. + +![Auth0 URLs setup](/assets/examples/api-auth-jwt-auth0/auth0-urls-setup.png) + +## Setting the Environment Variables + +Edit (or create) a file at the root of your project named `.env` and add the following to it: + +``` +AUTH0_DOMAIN= +AUTH0_CLIENT_ID= +``` + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +Note that, the `issuer` option **ends with a trailing slash** (`/`). + +```typescript +import { StackContext, Api } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create Api + const api = new Api(stack, "Api", { + authorizers: { + auth0: { + type: "jwt", + jwt: { + issuer: `https://${process.env.AUTH0_DOMAIN}/`, + audience: [ + `${process.env.AUTH0_CLIENT_ID}`, + `https://${process.env.AUTH0_DOMAIN}/api/v2/`, + ], + }, + }, + }, + defaults: { + authorizer: "auth0", + }, + routes: { + "GET /private": "functions/private.main", + "GET /public": { + function: "functions/public.main", + authorizer: "none", + }, + }, + }); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +To secure our APIs we are adding the authorization type `JWT` and a JWT authorizer. This means the caller of the API needs to pass in a valid JWT token. In this case, it relies on Auth0 to authenticate users. The first route is a private endpoint. The second is a public endpoint and its authorization type is overridden to `NONE`. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +import { APIGatewayProxyHandlerV2WithJWTAuthorizer } from "aws-lambda"; + +export const main: APIGatewayProxyHandlerV2WithJWTAuthorizer = async ( + event +) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.jwt.claims.sub}!`, + }; +}; +``` + +## Setting up our React app + +To deploy a React.js app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Replace the following in `stacks/ExampleStack.ts`: + +```typescript +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +{%change%} With: + +```typescript +const site = new StaticSite(stack, "Site", { + path: "packages/frontend", + buildOutput: "dist", + buildCommand: "npm run build", + environment: { + VITE_APP_AUTH0_DOMAIN: process.env.AUTH0_DOMAIN, + VITE_APP_AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, + VITE_APP_API_URL: api.url, + VITE_APP_REGION: app.region, + }, +}); + +// Show the API endpoint and other info in the output +stack.addOutputs({ + ApiEndpoint: api.url, + SiteUrl: site.url, +}); +``` + +The construct is pointing to where our React.js app is located. We haven't created our app yet but for now we'll point to the `packages/frontend` directory. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +We are going to print out the resources that we created for reference. + +Make sure to import the `StaticSite` construct by adding below line + +```typescript +import { StaticSite } from "sst/constructs"; +``` + +## Creating the frontend + +Run the below commands in the `packages/` directory to create a basic react project. + +```bash +$ npx create-vite@latest frontend --template react +$ cd frontend +$ npm install +``` + +This sets up our React app in the `packages/frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `frontend/package.json`. + +```bash +"dev": "vite" +``` + +{%change%} With the following: + +```bash +"dev": "sst bind vite" +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-jwt-auth0-ExampleStack: deploying... + + ✅ dev-api-auth-jwt-auth0-ExampleStack + + +Stack dev-api-auth-jwt-auth0-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://9ero2xj9cl.execute-api.us-east-1.amazonaws.com + SiteUrl: https://d3uxpgrgqdfnl5.cloudfront.net +``` + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button of the `GET /public` to send a `GET` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/api-oauth-google/api-explorer-invocation-response.png) + +You should see a `Hello, stranger!` in the response body. + +And if you try for `GET /private`, you will see `{"message":"Unauthorized"}`. + +## Adding AWS Amplify + +To use our AWS resources on the frontend we are going to use [AWS Amplify](https://aws.amazon.com/amplify/). + +Note, to know more about configuring Amplify with SST check [this chapter]({% link _chapters/configure-aws-amplify.md %}). + +Run the below command to install AWS Amplify and the Auth0 React SDK in the `packages/frontend/` directory. + +```bash +npm install aws-amplify @auth0/auth0-react +``` + +{%change%} Replace `frontend/src/main.jsx` with below code. + +```jsx +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; +import { Auth0Provider } from "@auth0/auth0-react"; +import Amplify from "aws-amplify"; + +Amplify.configure({ + API: { + endpoints: [ + { + name: "api", + endpoint: import.meta.env.VITE_APP_API_URL, + region: import.meta.env.VITE_APP_REGION, + }, + ], + }, +}); + +ReactDOM.createRoot(document.getElementById("root")).render( + + + + + +); +``` + +## Adding login UI + +{%change%} Replace `frontend/src/App.jsx` with below code. + +{% raw %} + +```jsx +import { API } from "aws-amplify"; +import React from "react"; +import { useAuth0 } from "@auth0/auth0-react"; + +const App = () => { + const { + loginWithRedirect, + logout, + user, + isAuthenticated, + isLoading, + getAccessTokenSilently, + } = useAuth0(); + + const publicRequest = async () => { + const response = await API.get("api", "/public"); + alert(JSON.stringify(response)); + }; + + const privateRequest = async () => { + try { + const accessToken = await getAccessTokenSilently({ + audience: `https://${import.meta.env.VITE_APP_AUTH0_DOMAIN}/api/v2/`, + scope: "read:current_user", + }); + const response = await API.get("api", "/private", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + alert(JSON.stringify(response)); + } catch (error) { + alert(error); + } + }; + + if (isLoading) return
Loading...
; + + return ( +
+

SST + Auth0 + React

+ {isAuthenticated ? ( +
+

Welcome!

+

{user.email}

+ +
+ ) : ( +
+

Not signed in

+ +
+ )} +
+ + +
+
+ ); +}; + +export default App; +``` + +{% endraw %} + +{%change%} Replace `frontend/src/index.css` with the below styles. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 120px; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +.api-section { + width: 100%; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + +.api-section > button { + background-color: darkorange; +} +``` + +Let's start our frontend in development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +npm run dev +``` + +Open up your browser and go to `http://localhost:3000`. + +![Browser view of localhost](/assets/examples/api-auth-jwt-auth0/browser-view-of-localhost.png) + +Note, if you get a blank page add this ` +``` + +There are 2 buttons that invokes the endpoints we created above. + +The **call /public** button invokes **GET /public** route using the `publicRequest` method we created in our frontend. + +Similarly, the **call /private** button invokes **GET /private** route using the `privateRequest` method. + +When you're not logged in and try to click the buttons, you'll see responses like below. + +![public button click without login](/assets/examples/api-oauth-google/public-button-click-without-login.png) + +![private button click without login](/assets/examples/api-oauth-google/private-button-click-without-login.png) + +Once you click on login, you're asked to login through your Auth0 account. + +![login button click auth0 login screen](/assets/examples/api-auth-jwt-auth0/auth0-login-screen.png) + +Once it's done you can check your info. + +![current logged in user info](/assets/examples/api-auth-jwt-auth0/current-logged-in-user-info.png) + +Now that you've authenticated repeat the same steps as you did before, you'll see responses like below. + +![public button click with login](/assets/examples/api-auth-jwt-auth0/public-button-click-with-login.png) + +![private button click with login](/assets/examples/api-auth-jwt-auth0/private-button-click-with-login.png) + +As you can see the private route is only working while we are logged in. + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +Note, if you get any error like `'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js` replace `vite.config.js` with below code. + +```typescript +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + ... + resolve: { + alias: { + "./runtimeConfig": "./runtimeConfig.browser", + }, + }, + ... +}); +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API with a JWT authorizer using Auth0. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md b/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md new file mode 100644 index 0000000000..c2a0cb45fa --- /dev/null +++ b/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md @@ -0,0 +1,694 @@ +--- +layout: example +title: How to add JWT authorization with Cognito User Pool to a serverless API +short_title: Cognito JWT +date: 2021-03-02 00:00:00 +lang: en +index: 1 +type: jwt-auth +description: In this example we will look at how to add JWT authorization with Cognito User Pool to a serverless API using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Adding JWT authentication with Cognito. +repo: api-auth-jwt-cognito-user-pool +ref: how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api +comments_id: how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api/2338 +--- + +In this example we will look at how to add JWT authorization with [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-jwt-cognito-user-pool +$ cd api-auth-jwt-cognito-user-pool +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "api-auth-jwt-cognito-user-pool", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, Cognito, StackContext, StaticSite } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create User Pool + const auth = new Cognito(stack, "Auth", { + login: ["email"], + }); + + // Create Api + const api = new Api(stack, "Api", { + authorizers: { + jwt: { + type: "user_pool", + userPool: { + id: auth.userPoolId, + clientIds: [auth.userPoolClientId], + }, + }, + }, + defaults: { + authorizer: "jwt", + }, + routes: { + "GET /private": "functions/private.main", + "GET /public": { + function: "functions/public.main", + authorizer: "none", + }, + }, + }); + + // allowing authenticated users to access API + auth.attachPermissionsForAuthUsers(stack, [api]); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + UserPoolId: auth.userPoolId, + UserPoolClientId: auth.userPoolClientId, + }); +} +``` + +This creates a [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html); a user directory that manages user sign up and login. We've configured the User Pool to allow users to login with their email and password. + +We are also creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +By default, all routes have the authorization type `JWT`. This means the caller of the API needs to pass in a valid JWT token. The first is a private endpoint. The second is a public endpoint and its authorization type is overridden to `NONE`. + +## Adding function code + +Let's create two functions, one for the public route, and one for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +import { APIGatewayProxyHandlerV2WithJWTAuthorizer } from "aws-lambda"; + +export const main: APIGatewayProxyHandlerV2WithJWTAuthorizer = async ( + event +) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.jwt.claims.sub}!`, + }; +}; +``` + +## Setting up our React app + +To deploy a React.js app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Replace the following in `stacks/ExampleStack.ts`: + +```typescript +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, + UserPoolId: auth.userPoolId, + UserPoolClientId: auth.userPoolClientId, +}); +``` + +{%change%} With: + +```typescript +const site = new StaticSite(stack, "Site", { + path: "frontend", + environment: { + VITE_APP_API_URL: api.url, + VITE_APP_REGION: app.region, + VITE_APP_USER_POOL_ID: auth.userPoolId, + VITE_APP_USER_POOL_CLIENT_ID: auth.userPoolClientId, + }, +}); + +// Show the API endpoint and other info in the output +stack.addOutputs({ + ApiEndpoint: api.url, + UserPoolId: auth.userPoolId, + UserPoolClientId: auth.userPoolClientId, + SiteUrl: site.url, +}); +``` + +The construct is pointing to where our React.js app is located. We haven't created our app yet but for now we'll point to the `frontend` directory. + +We are also setting up [build time React environment variables](https://vitejs.dev/guide/env-and-mode.html) with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +We are going to print out the resources that we created for reference. + +Make sure to import the `StaticSite` construct by adding below line + +```typescript +import { StaticSite } from "sst/constructs"; +``` + +## Creating the frontend + +Run the below commands in the root to create a basic react project. + +```bash +$ npx create-vite@latest frontend --template react +$ cd frontend +$ npm install +``` + +This sets up our React app in the `frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `dev` script in your `frontend/package.json`. + +```bash +"dev": "vite" +``` + +{%change%} With the following: + +```bash +"dev": "sst bind vite" +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-jwt-cognito-user-pool-ExampleStack: deploying... + + ✅ dev-api-auth-jwt-cognito-user-pool-ExampleStack + + +Stack dev-api-auth-jwt-cognito-user-pool-ExampleStack + Status: deployed + Outputs: + UserPoolClientId: t4gepqqbmbg90dh61pam8rg9r + UserPoolId: us-east-1_QLBISRQwA + ApiEndpoint: https://4foju6nhne.execute-api.us-east-1.amazonaws.com + SiteUrl: https://d3uxpgrgqdfnl5.cloudfront.net +``` + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button of the `GET /public` to send a `GET` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/api-oauth-google/api-explorer-invocation-response.png) + +You should see a `Hello, stranger!` in the response body. + +And if you try for `GET /private`, you will see `{"message":"Unauthorized"}`. + +## Adding AWS Amplify + +To use our AWS resources on the frontend we are going to use [AWS Amplify](https://aws.amazon.com/amplify/). + +Note, to know more about configuring Amplify with SST check [this chapter]({% link _chapters/configure-aws-amplify.md %}). + +Run the below command to install AWS Amplify in the `frontend/` directory. + +```bash +npm install aws-amplify +``` + +{%change%} Replace `frontend/src/main.jsx` with below code. + +```jsx +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; +import { Amplify } from "aws-amplify"; + +Amplify.configure({ + Auth: { + region: import.meta.env.VITE_APP_REGION, + userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, + userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, + }, + API: { + endpoints: [ + { + name: "api", + endpoint: import.meta.env.VITE_APP_API_URL, + region: import.meta.env.VITE_APP_REGION, + }, + ], + }, +}); + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); +``` + +## Adding Signup component + +{%change%} Add a `frontend/src/components/Signup.jsx` with below code. + +```jsx +import { useState } from "react"; +import { Auth } from "aws-amplify"; + +export default function Signup({ setScreen }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [code, setCode] = useState(""); + const [verifying, setVerifying] = useState(false); + + return ( +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + {verifying && ( + setCode(e.target.value)} + /> + )} + + setScreen("login")}> + Already have an account? Login + +
+ ); +} +``` + +## Adding Signin component + +{%change%} Add a `frontend/src/components/Signin.jsx` with below code. + +```jsx +import { useState } from "react"; +import { Auth } from "aws-amplify"; + +export default function Login({ setScreen, setUser }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + return ( +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + setScreen("signup")}> + Don't have an account? Sign up + +
+ ); +} +``` + +## Adding Home Page + +{%change%} Replace `frontend/src/App.jsx` with below code. + +{% raw %} + +```jsx +import { Auth, API } from "aws-amplify"; +import React, { useState, useEffect } from "react"; +import Login from "./components/Login"; +import Signup from "./components/Signup"; + +const App = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [screen, setScreen] = useState("signup"); + + // Get the current logged in user info + const getUser = async () => { + const user = await Auth.currentUserInfo(); + if (user) setUser(user); + setLoading(false); + }; + + // Logout the authenticated user + const signOut = async () => { + await Auth.signOut(); + setUser(null); + }; + + // Send an API call to the /public endpoint + const publicRequest = async () => { + const response = await API.get("api", "/public"); + alert(JSON.stringify(response)); + }; + + // Send an API call to the /private endpoint with authentication details. + const privateRequest = async () => { + try { + const response = await API.get("api", "/private", { + headers: { + Authorization: `Bearer ${(await Auth.currentSession()) + .getAccessToken() + .getJwtToken()}`, + }, + }); + alert(JSON.stringify(response)); + } catch (error) { + alert(error); + } + }; + + // Check if there's any user on mount + useEffect(() => { + getUser(); + }, []); + + if (loading) return
Loading...
; + + return ( +
+

SST + Cognito + React

+ {user ? ( +
+

Welcome {user.attributes.given_name}!

+

{user.attributes.email}

+ +
+ ) : ( +
+ {screen === "signup" ? ( + + ) : ( + + )} +
+ )} +
+ + +
+
+ ); +}; + +export default App; +``` + +{% endraw %} + +{%change%} Replace `frontend/src/index.css` with the below styles. + +```css +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +button { + width: 120px; + padding: 10px; + border: none; + border-radius: 4px; + background-color: #000; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.profile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 4px; +} +.api-section { + width: 100%; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + +.api-section > button { + background-color: darkorange; +} + +input { + width: 100%; + padding: 10px; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; +} + +.signup, +.login { + display: flex; + flex-direction: column; + gap: 20px; + align-items: center; +} +``` + +Let's start our frontend in development environment. + +{%change%} In the `frontend/` directory run. + +```bash +npm run dev +``` + +Open up your browser and go to `http://localhost:3000`. + +![Browser view of localhost](/assets/examples/api-auth-jwt-cogntio/browser-view-of-localhost.png) + +Note, if you get a blank page add this ` +``` + +There are 2 buttons that invokes the endpoints we created above. + +The **call /public** button invokes **GET /public** route using the `publicRequest` method we created in our frontend. + +Similarly, the **call /private** button invokes **GET /private** route using the `privateRequest` method. + +When you're not logged in and try to click the buttons, you'll see responses like below. + +![public button click without login](/assets/examples/api-oauth-google/public-button-click-without-login.png) + +![private button click without login](/assets/examples/api-oauth-google/private-button-click-without-login.png) + +Once you enter your details and click on **Signup**, you're asked to verify your account by entering a code that's sent to your mail. + +![Cognito signup verification](/assets/examples/api-auth-jwt-cogntio/cognito-signup-verification.png) + +After you enter the code, click on **Verify** to signin. + +![Cognito code verification](/assets/examples/api-auth-jwt-cogntio/cognito-code-verification.png) + +Once it's done you can check your info. + +![current logged in user info](/assets/examples/api-auth-jwt-cogntio/current-logged-in-user-info.png) + +Now that you've authenticated repeat the same steps as you did before, you'll see responses like below. + +![public button click with login](/assets/examples/api-auth-jwt-auth0/public-button-click-with-login.png) + +![private button click with login](/assets/examples/api-auth-jwt-auth0/private-button-click-with-login.png) + +As you can see the private route is only working while we are logged in. + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `sst.config.ts`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +Note, if you get any error like `'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js` replace `vite.config.js` with below code. + +```typescript +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + ... + resolve: { + alias: { + "./runtimeConfig": "./runtimeConfig.browser", + }, + }, + ... +}); +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API with a JWT authorizer using Cognito User Pool. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md b/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md new file mode 100644 index 0000000000..157d6b1f2d --- /dev/null +++ b/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md @@ -0,0 +1,375 @@ +--- +layout: example +title: How to add Twitter authentication to a serverless API +short_title: Twitter Auth +date: 2021-02-08 00:00:00 +lang: en +index: 4 +type: iam-auth +description: In this example we will look at how to add Twitter authentication to a serverless API using SST. We'll be using the Api and Cognito constructs to create an authenticated API. +short_desc: Authenticating a serverless API with Twitter. +repo: api-auth-twitter +ref: how-to-add-twitter-authentication-to-a-serverless-api +comments_id: how-to-add-twitter-authentication-to-a-serverless-api/2319 +--- + +In this example we will look at how to add Twitter authentication to a serverless API using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Twitter app](https://developer.twitter.com/en/portal/dashboard) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example api-auth-twitter +$ cd api-auth-twitter +$ npm install +``` + +By default, our app will be deployed to an environment (or stage) called `dev` and the `us-east-1` AWS region. This can be changed in the `json` in your project root. + +```js +import { SSTConfig } from "sst"; +import { Api } from "sst/constructs"; + +export default { + config(_input) { + return { + name: "api-auth-twitter", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up the API + +Let's start by setting up an API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, Cognito, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Api + const api = new Api(stack, "Api", { + defaults: { + authorizer: "iam", + }, + routes: { + "GET /private": "packages/functions/src/private.main", + "GET /public": { + function: "packages/functions/src/public.main", + authorizer: "iam", + }, + }, + }); + + // Show the API endpoint and other info in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding two routes to it. + +``` +GET /private +GET /public +``` + +To secure our APIs we are adding the authorization type `AWS_IAM`. This means the caller of the API needs to have the right permissions. The first route is a private endpoint. The second is a public endpoint and its authorization type is overriden to `NONE`. + +## Setting up authentication + +Now let's add authentication for our serverless app. + +{%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `consumerKey` and `consumerSecret` with that of your Twitter app. + +```typescript +// Create auth provider +const auth = new Cognito(stack, "Auth", { + identityPoolFederation: { + twitter: { + consumerKey: "gyMbPOiwefr6x63SjIW8NN0d1", + consumerSecret: "qxld8zic5c2eyahqK3gjGLGQaOTogGfAgHh17MYOIcOUR9l2Nz", + }, + }, +}); + +// Allow authenticated users invoke API +auth.attachPermissionsForAuthUsers(stack, [api]); +``` + +This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html) which relies on Google to authenticate users. And we use the [`attachPermissionsForAuthUsers`]({{ site.v2_url }}/constructs/Auth#attachpermissionsforauthusers) method to allow our logged in users to access our API. + +{%change%} Replace the `stack.addOutputs` call with the following. + +```typescript +stack.addOutputs({ + ApiEndpoint: api.url, + IdentityPoolId: auth.cognitoIdentityPoolId, +}); +``` + +We are going to print out the resources that we created for reference. + +## Adding function code + +Let's create two functions, one handling the public route, and the other for the private route. + +{%change%} Add a `packages/functions/src/public.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello stranger!", + }; +} +``` + +{%change%} Add a `packages/functions/src/private.ts`. + +```typescript +export async function main() { + return { + statusCode: 200, + body: "Hello user!", + }; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-api-auth-twitter-ExampleStack: deploying... + + ✅ dev-api-auth-twitter-ExampleStack + + +Stack dev-api-auth-twitter-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://b3njix6irk.execute-api.us-east-1.amazonaws.com + IdentityPoolId: us-east-1:abc36c64-36d5-4298-891c-7aa9ea318f1d +``` + +The `ApiEndpoint` is the API we just created. Make a note of the `IdentityPoolId`, we'll need that later. + +Now let's try out our public route. Head over to the following in your browser. Make sure to replace the URL with your API. + +``` +https://b3njix6irk.execute-api.us-east-1.amazonaws.com/public +``` + +You should see the greeting `Hello stranger!`. + +And if you try to visit the private route, you will see `{"message":"Forbidden"}`. + +``` +https://b3njix6irk.execute-api.us-east-1.amazonaws.com/private +``` + +## Login with Twitter + +We are going to use the [**twurl**](https://github.com/twitter/twurl) tool to test logging in with Twitter. Follow the project README to install twurl. + +Once installed, we'll need to set our app credentials. Run the following and replace it with those from your Twitter app. + +```bash +$ twurl authorize --consumer-key gyMbPOiwefr6x63SjIW8NN0d1 \ + --consumer-secret qxld8zic5c2eyahqK3gjGLGQaOTogGfAgHh17MYOIcOUR9l2Nz +``` + +This will return an authentication URL. + +``` +https://api.twitter.com/oauth/authorize?oauth_consumer_key=gyMbPOiwefr6x63SjIW8NN0d1&oauth_nonce=ELNkf9FaDqzNhLkxeuxFlnlDjwvQ17WBLlabN1Sg&oauth_signature=i%252By%252BuupyXcYAENs1XbL3zjb6CBY%253D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1612769097&oauth_token=PAXt3wAAAAABMhofAAABd4CHeJY&oauth_version=1.0 and paste in the supplied PIN +``` + +Open the URL in your browser. Authenticate to Twitter, and then enter the PIN back into the terminal. If you've authenticated successfully, you should get the message. + +``` +Authorization successful +``` + +Twurl stores your access token information in the `~/.twurlrc` file. Note the **token** and **secret** in your profile. + +``` +--- +profiles: + fanjiewang: + gyMbPOiwefr6x63SjIW8NN0d1: + username: fanjiewang + consumer_key: gyMbPOiwefr6x63SjIW8NN0d1 + consumer_secret: qxld8zic5c2eyahqK3gjGLGQaOTogGfAgHh17MYOIcOUR9l2Nz + token: 29528254-ULNl2qISn2wEtmHUj1VJ4ZhQrNezi2SH2MP4b8lSV + secret: v769kfAoC3UJG28DXBDE8N1bMjx6ZRuKUUTtkaek1m8qq +configuration: + default_profile: + - fanjiewang + - gyMbPOiwefr6x63SjIW8NN0d1 +``` + +Next, we need to get the user's Cognito Identity id. Replace `--identity-pool-id` with the `IdentityPoolId` from the `sst dev` log output; and replace the `--logins` with the **TOKEN** and **SECRET** from the previous step. + +```bash +$ aws cognito-identity get-id \ + --identity-pool-id us-east-1:abc36c64-36d5-4298-891c-7aa9ea318f1d \ + --logins api.twitter.com="TOKEN:SECRET" +``` + +You should get an identity id for the Twitter user. + +```json +{ + "IdentityId": "us-east-1:0a6b1bb0-614c-4e00-9028-146854eaee4a" +} +``` + +Now we'll need to get the IAM credentials for the identity user. + +```bash +$ aws cognito-identity get-credentials-for-identity \ + --identity-id us-east-1:0a6b1bb0-614c-4e00-9028-146854eaee4a \ + --logins graph.facebook.com="EAAF9u0npLFUBAGv7SlHXIMigP0nZBF2LxZA5ZCe3NqZB6Wc6xbWxwHqn64T5QLEsjOZAFhZCLJj1yIsDLPCc9L3TRWZC3SvKf2D1vEZC3FISPWENQ9S5BZA94zxtn6HWQFD8QLMvjt83qOGHeQKZAAtJRgHeuzmd2oGn3jbZBmfYl2rhg3dpEnFhkAmK3lC7BZAEyc0ZD" +``` + +This should give you a set of temporary IAM credentials. + +```json +{ + "IdentityId": "us-east-1:0a6b1bb0-614c-4e00-9028-146854eaee4a", + "Credentials": { + "AccessKeyId": "ASIARUIS6Q2MF3FI5XCV", + "SecretKey": "3znZKINfXmEv9y3UC1MASUkJkhw/AVV+9Ny88qua", + "SessionToken": "IQoJb3JpZ2luX2VjEHgaCXVzLWVhc3QtMSJHMEUCIQDNFismZshPFba10yJTB7hiX1S0qzPRGM0yb09hL+lOHgIgHuX2SBa75YbF/iyRoPEUyP+3gpkKAek6Mv/d35nuFBEqmAQIcRACGgwxMTIyNDU3Njk4ODAiDOHVJcoQ5KUmpbzzgSr1A7xg1HzbehJ/rejQkuaoWyiyVSecrPgBkLN01/jxief7/zoViKUdaocZrUNOcFXm9PtKKgEbEg16gUfTtaid6nhVE6MthX82f8oBArIancEgi5uj5KW9H2HOUjlmAmpcaooDeyDmTjtTwlKPpsWjz2B5NCDfQCrBVBQlv5st/sPSA88jkG1PYuQSmsueqiWeqVViDjaPaxNcVuuHgQofbPhSI0fUduXM9ePDP5O5rGNMo/g0oOLeyhgzJX/Xzf1qYx1BURILfKH10cx4PaCO5Zr49NggdfXAdooqZPqlAYAvDOA8FafiE7k2aG0pEC84yhiWl4BzHkAUGiMYjJD2eua7QMvfWHu1o/DIFH4jFzPjqKWV00CVCjyI8aFbmkarvdVkK+jqCfWYXYdD5HJSTwsjvmPhdF/3B7WWYTqb5eQPWVcPCbzj1WPGpKX0zbytKg4Z+Klb+Wp0yHG2QZ8blMHir6WgNoDJ/PisO6HbbpxqkWe+1GMkxi29IhjRZ18tAtpCwZRarIeEYfgPiHtt+QVAKg5T84Qprcslr6T6wkyNB8dqlVf4ozLekF/RbfAGq/BVbQy8iM62hU30SCoJrqyC6dq3xhcpiSv+kyKi0Q0NaT6rZ9w/oeQ+0olkrJRec1eVDeCmvw2O0eKXPsTEQoiFEGpIeTCc2YOBBjqEAqxGdKjVZWrMzG2yBIK55A9yqluEAyp3nnXPbnpU6VaKnCeVt16TR4sbD8/Y4HFZCW/zGo0K3ymQI+lfzpquaR9NdGnjjuiTcGqJRcaj94N/Aop/jGDXoBUnjqTelj7lkZtPO0sQ7Xf+NlVhzbPulMbnkIBe9f9FXnC2xpxGyfcWQOwoenqXRuXLzMQYGzVH9+ApEkzSVH0bqPZFqXm4cOFTw648Y41MrIE6EuXAorrJd/CfyvmWVd6WzMSplIG8UfNIDO1mS81D2Xg/O1urH8Bu0h6LsPjg1d3KLo9Cdd48+kNNcqqXkg2d+lSJA68Cxq3ne8N3jNnsxWDfBSj27hm9D2r0", + "Expiration": "2021-02-08T03:47:40-05:00" + } +} +``` + +Let's make a call to the private route using the credentials. The API request needs to be [signed with AWS SigV4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). We are going to use [Insomnia](https://insomnia.rest) to help us sign and make this request. + +Make sure to replace the **Access Key Id**, **Secret Access Key**, **Region**, and **Session Token** below. In our case the region is `us-east-1`. You can see this in the API URL. + +``` +https://b3njix6irk.execute-api.us-east-1.amazonaws.com +``` + +![Invoke Twitter authenticated API Gateway route](/assets/examples/api-auth-twitter/invoke-twitter-authenticated-api-gateway-route.png) + +You should now see. + +``` +Hello user! +``` + +The above process might seem fairly tedious. But once we integrate it into our frontend app, we'll be able to use something like [AWS Amplify]({% link _chapters/configure-aws-amplify.md %}) to handle these steps for us. + +## Making changes + +Let's make a quick change to our private route and print out the caller's user id. + +{%change%} Replace `packages/functions/src/private.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + body: `Hello ${event.requestContext.authorizer.iam.cognitoIdentity.identityId}!`, + }; +}; +``` + +We are getting the user id from the event object. + +If you head back to Insomnia and hit the `/private` endpoint again. + +![Get caller identity id in Twitter authenticated route](/assets/examples/api-auth-twitter/get-caller-identity-id-in-twitter-authenticated-route.png) + +You should see the user id. Note, this matches the identity id that was generated from the step where we generated a set of IAM credentials. + +``` +Hello us-east-1:46625265-9c97-420f-a826-15dbc812a008! +``` + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a `dev` environment, the one specified in your `json`. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API authenticated with Twitter. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-automatically-resize-images-with-serverless.md b/_examples/how-to-automatically-resize-images-with-serverless.md new file mode 100644 index 0000000000..9cd6582626 --- /dev/null +++ b/_examples/how-to-automatically-resize-images-with-serverless.md @@ -0,0 +1,345 @@ +--- +layout: example +title: How to automatically resize images with serverless +short_title: Resize Images +date: 2021-02-08 00:00:00 +lang: en +index: 4 +type: async +description: In this example we will look at how to automatically resize images that are uploaded to your S3 bucket using SST. We'll be using the Bucket construct and a Lambda layer to set this up. +short_desc: Automatically resize images uploaded to S3. +repo: bucket-image-resize +ref: how-to-automatically-resize-images-with-serverless +comments_id: how-to-automatically-resize-images-with-serverless/2399 +--- + +In this example we will look at how to automatically resize images that are uploaded to your S3 bucket using [SST]({{ site.sst_github_repo }}). We'll be using the [Sharp](https://github.com/lovell/sharp) package as a [Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html). + +We'll be using SST's [Live Lambda Development]({{ site.v2_url }}/live-lambda-development). It allows you to make changes and test locally without having to redeploy. + +Here is a video of it in action. + +
+ +
+ +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example bucket-image-resize +$ cd bucket-image-resize +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "bucket-image-resize", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Creating the bucket + +Let's start by creating a bucket. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Bucket, StackContext } from "sst/constructs"; +import * as lambda from "aws-cdk-lib/aws-lambda"; + +export function ExampleStack({ stack }: StackContext) { + // Create a new bucket + const bucket = new Bucket(stack, "Bucket", { + notifications: { + resize: { + function: { + handler: "packages/functions/src/resize.main", + nodejs: { + esbuild: { + external: ["sharp"], + }, + }, + layers: [ + new lambda.LayerVersion(stack, "SharpLayer", { + code: lambda.Code.fromAsset("layers/sharp"), + }), + ], + }, + events: ["object_created"], + }, + }, + }); + + // Allow the notification functions to access the bucket + bucket.attachPermissions([bucket]); + + // Show the endpoint in the output + stack.addOutputs({ + BucketName: bucket.bucketName, + }); +} +``` + +This creates a S3 bucket using the [`Bucket`]({{ site.v2_url }}/constructs/Bucket) construct. + +We are subscribing to the `OBJECT_CREATED` notification with a [`Function`]({{ site.v2_url }}/constructs/Function). The image resizing library that we are using, [Sharp](https://github.com/lovell/sharp), needs to be compiled specifically for the target runtime. So we are going to use a [Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) to upload it. Locally, the `sharp` package is not compatible with how our functions are bundled. So we are marking it in the `external`. + +Finally, we are allowing our functions to access the bucket by calling `attachPermissions`. We are also outputting the name of the bucket that we are creating. + +## Using Sharp as a Layer + +Next let's set up Sharp as a Layer. + +{%change%} First create a new directory in your project root. + +```bash +$ mkdir -p layers/sharp +``` + +Then head over to this repo and download the latest `sharp-lambda-layer.zip` from the releases — [https://github.com/Umkus/lambda-layer-sharp/releases](https://github.com/Umkus/lambda-layer-sharp/releases) + +Unzip that into the `layers/sharp` directory that we just created. Make sure that the path looks something like `layers/sharp/nodejs/node_modules`. + +## Adding function code + +Now in our function, we'll be handling resizing an image once it's uploaded. + +{%change%} Add a new file at `packages/functions/src/resize.ts` with the following. + +```ts +import sharp from "sharp"; +import stream from "stream"; + +import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { Upload } from "@aws-sdk/lib-storage"; + +const width = 400; +const prefix = `${width}w`; + +const S3 = new S3Client({}); + +// Read stream for downloading from S3 +async function readStreamFromS3({ + Bucket, + Key, +}: { + Bucket: string; + Key: string; +}) { + const commandPullObject = new GetObjectCommand({ + Bucket, + Key, + }); + const response = await S3.send(commandPullObject); + + return response; +} + +// Write stream for uploading to S3 +function writeStreamToS3({ Bucket, Key }: { Bucket: string; Key: string }) { + const pass = new stream.PassThrough(); + const upload = new Upload({ + client: S3, + params: { + Bucket, + Key, + Body: pass, + }, + }); + + return { + writeStream: pass, + upload, + }; +} + +// Sharp resize stream +function streamToSharp(width: number) { + return sharp().resize(width); +} + +import { S3Handler } from "aws-lambda"; + +export const main: S3Handler = async (event) => { + const s3Record = event.Records[0].s3; + + // Grab the filename and bucket name + const Key = s3Record.object.key; + const Bucket = s3Record.bucket.name; + + // Check if the file has already been resized + if (Key.startsWith(prefix)) { + return; + } + + // Create the new filename with the dimensions + const newKey = `${prefix}-${Key}`; + + // Stream to read the file from the bucket + const readStream = await readStreamFromS3({ Key, Bucket }); + // Stream to resize the image + const resizeStream = streamToSharp(width); + // Stream to upload to the bucket + const { writeStream, upload } = writeStreamToS3({ + Bucket, + Key: newKey, + }); + + // Trigger the streams + (readStream?.Body as NodeJS.ReadableStream) + .pipe(resizeStream) + .pipe(writeStream); + + try { + // Wait for the file to upload + await upload.done(); + } catch (err) { + console.log(err); + } +}; +``` + +We are doing a few things here. Let's go over them in detail. + +1. In the `main` function, we start by grabbing the `Key` or filename of the file that's been uploaded. We also get the `Bucket` or name of the bucket that it was uploaded to. +2. Check if the file has already been resized, by looking at the filename and if it starts with the dimensions. If it has, then we quit the function. +3. Generate the new filename with the dimensions. +4. Create a stream to read the file from S3, another to resize the image, and finally upload it back to S3. We use streams because really large files might hit the limit for what can be downloaded on to the Lambda function. +5. Finally, we start the streams and wait for the upload to complete. + +Now let's install the npm packages we are using here. + +{%change%} Run this command in the `packages/functions/` folder. + +```bash +$ npm install sharp @aws-sdk/client-s3 @aws-sdk/lib-storage +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-bucket-image-resize-ExampleStack: deploying... + + ✅ dev-bucket-image-resize-ExampleStack + + +Stack dev-bucket-image-resize-ExampleStack + Status: deployed + Outputs: + BucketName: dev-bucket-image-resize-ExampleStack-bucketd7feb781-k3myfpcm6qp1 +``` + +## Uploading files + +Now head over to the **Buckets** tab in [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Note, The Buckets explorer allows you to manage the S3 Buckets created with the **Bucket** constructs in your app. It allows you upload, delete, and download files. You can also create and delete folders. + +![S3 bucket created with SST](/assets/examples/bucket-image-resize/s3-bucket-created-with-sst.png) + +Here you can click **Upload** and select an image to upload it. After uploading you'll notice the resized image shows up. + +![Drag and drop file to upload to S3](/assets/examples/bucket-image-resize/file-upload-to-s3.png) + +Now refresh your console to see the resized image. + +![SST resized image in S3 bucket](/assets/examples/bucket-image-resize/sst-resized-image-in-s3-bucket.png) + +## Making changes + +Let's try making a quick change. + +{%change%} Change the `width` in your `packages/functions/src/resize.ts`. + +```bash +const width = 100; +``` + +Now if you go back to SST console and upload that same image again, you should see the new resized image show up in your Buckets explorer. + +![Updated SST resized image in S3 bucket](/assets/examples/bucket-image-resize/updated-sst-resized-image-in-s3-bucket.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +Note that, by default resources like the S3 bucket are not removed automatically. To do so, you'll need to explicitly set it. + +```typescript +import * as cdk from "aws-cdk-lib"; + +const bucket = new Bucket(stack, "Bucket", { + cdk: { + bucket: { + autoDeleteObjects: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }, + }, + ... +} + +``` + +## Conclusion + +And that's it! We've got a completely serverless image resizer that automatically resizes any images uploaded to our S3 bucket. And we can test our changes locally before deploying to AWS! Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md b/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md new file mode 100644 index 0000000000..10ee68e25e --- /dev/null +++ b/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md @@ -0,0 +1,471 @@ +--- +layout: example +title: How to create a CRUD API with serverless using DynamoDB +short_title: CRUD DynamoDB +date: 2021-02-04 00:00:00 +lang: en +index: 4 +type: database +description: In this example we will look at how to create a CRUD API with serverless using DynamoDB. We'll be using the Api and Table constructs from SST. +short_desc: Building a CRUD API with DynamoDB. +repo: crud-api-dynamodb +ref: how-to-create-a-crud-api-with-serverless-using-dynamodb +comments_id: how-to-create-a-crud-api-with-serverless-using-dynamodb/2309 +--- + +In this example we will look at how to create a CRUD API with serverless using [DynamoDB](https://amazon.com/dynamodb/). We'll be using [SST]({{ site.sst_github_repo }}). Our API will be creating, reading, updating, and deleting notes. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example crud-api-dynamodb +$ cd crud-api-dynamodb +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "crud-api-dynamodb", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding DynamoDB + +[Amazon DynamoDB](https://amazon.com/dynamodb/) is a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext, Table } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Notes", { + fields: { + userId: "string", + noteId: "string", + }, + primaryIndex: { partitionKey: "userId", sortKey: "noteId" }, + }); +} +``` + +This creates a serverless DynamoDB table using [`Table`]({{ site.v2_url }}/constructs/Table). Our table is going to look something like this: + +| userId | noteId | content | createdAt | +| ------ | ------ | ------- | --------- | +| 123 | 1 | Hi! | Feb 5 | + +## Setting up our routes + +Now let's add the API. + +{%change%} Add this after the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "GET /notes": "packages/functions/src/list.main", + "POST /notes": "packages/functions/src/create.main", + "GET /notes/{id}": "packages/functions/src/get.main", + "PUT /notes/{id}": "packages/functions/src/update.main", + "DELETE /notes/{id}": "packages/functions/src/delete.main", + }, +}); + +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding five routes to it. + +``` +GET /notes +POST /notes +GET /notes/{id} +PUT /notes/{id} +DELETE /notes/{id} +``` + +These will be getting a list of notes, creating a note, getting, updating, and deleting a specific note respectively. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +## Create a note + +Let's turn towards the functions that'll be powering our API. Starting with the one that creates our note. + +{%change%} Add the following to `packages/functions/src/create.ts`. + +```typescript +import * as uuid from "uuid"; +import { DynamoDB } from "aws-sdk"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + const data = JSON.parse(event.body); + + const params = { + // Get the table name from the environment variable + TableName: Table.Notes.tableName, + Item: { + userId: "123", + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + createdAt: Date.now(), + }, + }; + await dynamoDb.put(params).promise(); + + return { + statusCode: 200, + body: JSON.stringify(params.Item), + }; +}; +``` + +Here we are creating a new row in our DynamoDB table. First we JSON parse the request body. That gives us the content of the note. Then we are hard coding the `userId` to `123` for now. Our API will not be tied to a user. We'll tackle that in a later example. We are also using a `uuid` package to generate a unique `noteId`. + +{%change%} Let's install both the packages we are using here. + +Run the below command in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk uuid +``` + +## Read the list of notes + +Next, let's write the function that'll fetch all our notes. + +{%change%} Add the following to `packages/functions/src/list.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function main() { + const params = { + // Get the table name from the environment variable + TableName: Table.Notes.tableName, + // Get all the rows where the userId is our hardcoded user id + KeyConditionExpression: "userId = :userId", + ExpressionAttributeValues: { + ":userId": "123", + }, + }; + const results = await dynamoDb.query(params).promise(); + + return { + statusCode: 200, + body: JSON.stringify(results.Items), + }; +} +``` + +Here we are getting all the notes for our hard coded `userId`, `123`. + +## Read a specific note + +We'll do something similar for the function that gets a single note. + +{%change%} Create a `packages/functions/src/get.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + const params = { + // Get the table name from the environment variable + TableName: Table.Notes.tableName, + // Get the row where the noteId is the one in the path + Key: { + userId: "123", + noteId: event.pathParameters.id, + }, + }; + const results = await dynamoDb.get(params).promise(); + + return { + statusCode: 200, + body: JSON.stringify(results.Item), + }; +}; +``` + +We are getting the note with the id that's passed in through the API endpoint path. The `event.pathParameters.id` corresponds to the id in `/notes/{id}`. + +## Update a note + +Now let's update our notes. + +{%change%} Add a `packages/functions/src/update.ts` with: + +```typescript +import { DynamoDB } from "aws-sdk"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + const data = JSON.parse(event.body); + + const params = { + // Get the table name from the environment variable + TableName: Table.Notes.tableName, + // Get the row where the noteId is the one in the path + Key: { + userId: "123", + noteId: event.pathParameters.id, + }, + // Update the "content" column with the one passed in + UpdateExpression: "SET content = :content", + ExpressionAttributeValues: { + ":content": data.content || null, + }, + ReturnValues: "ALL_NEW", + }; + + const results = await dynamoDb.update(params).promise(); + + return { + statusCode: 200, + body: JSON.stringify(results.Attributes), + }; +}; +``` + +We are first JSON parsing the request body. We use the content we get from it, to update the note. The `ALL_NEW` property means that this update call will return the updated row. + +## Delete a note + +To complete the CRUD operations, let's delete the note. + +{%change%} Add this to `packages/functions/src/delete.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandlerV2 = async (event) => { + const params = { + // Get the table name from the environment variable + TableName: Table.Notes.tableName, + // Get the row where the noteId is the one in the path + Key: { + userId: "123", + noteId: event.pathParameters.id, + }, + }; + await dynamoDb.delete(params).promise(); + + return { + statusCode: 200, + body: JSON.stringify({ status: true }), + }; +}; +``` + +Now let's test what we've created so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-dynamodb-ExampleStack: deploying... + + ✅ dev-rest-api-dynamodb-ExampleStack + + +Stack dev-rest-api-dynamodb-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://t34witddz7.execute-api.us-east-1.amazoncom +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Let's create our first note, go to the **API** explorer and click on the `POST /notes` route. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +In the **Headers** tab enter `Content-type` in **Header 1** input and `application/json` in **Value 1** input. Go to the **Body** tab and paste the below json. + +```json +{ "content": "Hello World" } +``` + +Now, hit the **Send** button to send the request. + +![API explorer create a note response](/assets/examples/crud-rest-api-dynamodb/api-explorer-create-a-note-response.png) + +This should create a new note. + +To retrieve the created note, go to `GET /notes/{id}` route and in the **URL** tab enter the **id** of the note we created in the **id** field and click the **Send** button to get that note. + +![API explorer get a note response](/assets/examples/crud-rest-api-dynamodb/api-explorer-get-a-note-response.png) + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been created in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of table](/assets/examples/crud-rest-api-dynamodb/dynamo-table-view-of-table.png) + +Now to update our note, we need to make a `PUT` request, go to `PUT /notes/{id}` route. + +In the **URL** tab, enter the **id** of the note we created and in the **body** tab and enter the below json value and hit **Send**. + +```json +{ "content": "Updating the note" } +``` + +![API explorer update a note response](/assets/examples/crud-rest-api-dynamodb/api-explorer-update-a-note-response.png) + +This should respond with the updated note. + +Click the **Send** button of the `GET /notes` route to get a list of notes. + +![API explorer get notes response](/assets/examples/crud-rest-api-dynamodb/api-explorer-get-notes-response.png) + +You should see the list of notes. + +To delete a note, go to the `DELETE /notes/{id}` and enter the **id** of the note to delete in the **URL** tab and hot **Send**. + +![API explorer delete note response](/assets/examples/crud-rest-api-dynamodb/api-explorer-delete-note-response.png) + +## Making changes + +Let's make a quick change to test our Live Lambda Development environment. We want our `get` function to return an error if it cannot find the note. + +{%change%} Replace the `return` statement in `packages/functions/src/gets.ts` with: + +```typescript +return results.Item + ? { + statusCode: 200, + body: JSON.stringify(results.Item), + } + : { + statusCode: 404, + body: JSON.stringify({ error: true }), + }; +``` + +Now let's send an invalid request by entering a random note id which is not present in the table. + +![API explorer invalid note response](/assets/examples/crud-rest-api-dynamodb/api-explorer-invalid-note-response.png) + +You should see an error being printed out. + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-rest-api-dynamodb-ExampleStack + + +Stack prod-rest-api-dynamodb-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazoncom +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** explorer and click **Send** button of the `GET /notes` route, to send a `GET` request. + +![Prod API explorer get notes response](/assets/examples/crud-rest-api-dynamodb/prod-api-explorer-get-notes-response.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless CRUD API. In another example, we'll add authentication to our API, so we can fetch the notes for a given user. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-flutter-app-with-serverless.md b/_examples/how-to-create-a-flutter-app-with-serverless.md new file mode 100644 index 0000000000..714be1491e --- /dev/null +++ b/_examples/how-to-create-a-flutter-app-with-serverless.md @@ -0,0 +1,448 @@ +--- +layout: example +title: How to create a Flutter app with serverless +short_title: Flutter +date: 2021-10-25 00:00:00 +lang: en +index: 2 +type: mobileapp +description: In this example we will look at how to use Flutter with a serverless API to create a simple click counter app. We'll be using SST. +short_desc: Native app with Flutter and a serverless API. +repo: flutter-app +ref: how-to-create-a-flutter-app-with-serverless +comments_id: how-to-create-an-flutter-app-with-serverless/2516 +--- + +In this example we will look at how to use [Flutter](https://flutter.dev) with a [serverless]({% link _chapters/what-is-serverless.md %}) API to create a simple click counter app. We'll be using the [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- Flutter installed +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example flutter-app +$ cd flutter-app +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "flutter-app", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +3. `packages/frontend/` — Flutter App + + The code for our frontend Flutter app. + +## Create our infrastructure + +Our app is made up of a simple API and a Flutter app. The API will be talking to a database to store the number of clicks. We'll start by creating the database. + +### Adding the table + +We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Table, Api } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Counter", { + fields: { + counter: "string", + }, + primaryIndex: { partitionKey: "counter" }, + }); +} +``` + +This creates a serverless DynamoDB table using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. It has a primary key called `counter`. Our table is going to look something like this: + +| counter | tally | +| ------- | ----- | +| clicks | 123 | + +### Creating our API + +Now let's add the API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.main", + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +### Reading from our table + +Our API is powered by a Lambda function. In the function we'll read from our DynamoDB table. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function main() { + const getParams = { + // Get the table name from the environment variable + TableName: Table.Counter.tableName, + // Get the row where the counter is called "clicks" + Key: { + counter: "clicks", + }, + }; + const results = await dynamoDb.get(getParams).promise(); + + // If there is a row, then get the value of the + // column called "tally" + let count = results.Item ? results.Item.tally : 0; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We make a `get` call to our DynamoDB table and get the value of a row where the `counter` column has the value `clicks`. Since we haven't written to this column yet, we are going to just return `0`. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-flutter-app-ExampleStack: deploying... + + ✅ dev-flutter-app-ExampleStack + + +Stack dev-flutter-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://sez1p3dsia.execute-api.ap-south-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Setting up our Flutter app + +We are now ready to use the API we just created. Let's use [Flutter CLI](https://flutter.dev/docs/get-started/install) to setup our Flutter app. + +If you don't have the Flutter CLI installed on your machine, [head over here to install it](https://flutter.dev/docs/get-started/install). + +{%change%} Run the following in the `packages/` directory. + +```bash +$ flutter create frontend +$ cd frontend +``` + +This sets up our Flutter app in the `packages/frontend/` directory. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`flutter_dotenv`](https://pub.dev/packages/flutter_dotenv) package. + +{%change%} Install the `flutter_dotenv` package by running the following in the `packages/frontend/` directory. + +```bash +$ flutter pub add flutter_dotenv +``` + +{%change%} Create a `.env` file inside `packages/frontend/` and create two variables to hold the development and production API endpoints. Replace the `DEV_API_URL` with the one from the steps above. + +``` +DEV_API_URL=https://sez1p3dsia.execute-api.us-east-1.amazonaws.com +PROD_API_URL=OUTPUT_FROM_SST_DEPLOY +``` + +We'll add the `PROD_API_URL` later in this example. + +{%change%} Add the `.env` file to your assets bundle in `pubspec.yaml` by uncommenting the `assets` section under `flutter`. + +```yaml +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - .env +``` + +Ensure that the path corresponds to the location of the `.env` file! + +We also need the `http` package to call the endpoint. + +{%change%} In the `packages/frontend/` directory run. + +```bash +$ flutter pub add http +``` + +Let's start our Flutter development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +$ flutter run +``` + +This will open up an emulator and load the app. + +### Add the click button + +We are now ready to add the UI for our app and connect it to our serverless API. + +{%change%} Replace `packages/frontend/lib/main.dart` with. + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; + +Future main() async { + await dotenv.load(fileName: ".env"); + runApp(MyApp()); +} + +class MyApp extends StatefulWidget { + MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + update() async { + Uri uri = kReleaseMode ? Uri.parse(dotenv.env['PROD_API_URL']!) : Uri.parse(dotenv.env['DEV_API_URL']!); + var result = await http.post(uri); + setState(() { + counter = int.parse(result.body); + }); + } + + int counter = 0; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: "Counter App", + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: Scaffold( + appBar: AppBar( + title: Text("Counter App"), + ), + body: Container( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("This button is pressed $counter times"), + MaterialButton( + onPressed: () { + setState(() { + update(); + }); + }, + child: Text( + "Click Me", + style: TextStyle(color: Colors.white), + ), + color: Colors.blue.shade500, + ), + ], + ), + ), + ), + ), + ); + } +} +``` + +Here we are adding a simple button that when clicked, makes a request to our API. We are getting the API endpoint from the environment variable depending on the build mode. + +The response from our API is then stored in our app's state. We use that to display the count of the number of times the button has been clicked. + +Now if you head over to your emulator, your Flutter app should look something like this. + +![Click counter UI in Flutter app](/assets/examples/flutter-app/click-counter-ui-in-flutter-app.png){: width="432" } + +Of course if you click on the button multiple times, the count doesn't change. That's because we are not updating the count in our API. We'll do that next. + +## Making changes + +Let's update our table with the clicks. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +const putParams = { + TableName: Table.Connections.tableName, + Key: { + counter: "clicks", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your emulator and click the button again, you should see the count increase! + +![Click counter updating in Flutter app](/assets/examples/flutter-app/click-counter-updating-in-flutter-app.png){: width="432" } + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working locally it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-flutter-app-ExampleStack + + +Stack prod-flutter-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com +``` + +{%change%} Add the above endpoint to the `.env` file in `packages/frontend/.env` as a production API endpoint. + +``` +DEV_API_URL=https://sez1p3dsia.execute-api.us-east-1.amazonaws.com +PROD_API_URL=https://k40qchmtvf.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +Now we are ready to ship our app! + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless click counter app in Flutter. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md b/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md new file mode 100644 index 0000000000..c839fbcac6 --- /dev/null +++ b/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md @@ -0,0 +1,436 @@ +--- +layout: example +title: How to create a Gatsby app with serverless +short_title: Gatsby +date: 2021-10-15 00:00:00 +lang: en +index: 5 +type: webapp +description: In this example we will look at how to use Gatsby with a serverless API to create a simple click counter app. We'll be using SST and the SST StaticSite construct to deploy our app to AWS S3 and CloudFront. +short_desc: Full-stack Gatsby app with a serverless API. +repo: gatsby-app +ref: how-to-create-a-gatsby-app-with-serverless +comments_id: how-to-create-a-gatsby-app-with-serverless/2595 +--- + +In this example we will look at how to use [Gatsby](https://www.gatsbyjs.com/) with a [serverless]({% link _chapters/what-is-serverless.md %}) API to create a simple click counter app. We'll be using the [SST]({{ site.sst_github_repo }}) and the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite#creating-a-gatsby-site) construct to deploy our app to AWS. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example gatsby-app +$ cd gatsby-app +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "gatsby-app", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +3. `packages/frontend/` — Gatsby App + + The code for our frontend Gatsby app. + +## Create our infrastructure + +Our app is made up of a simple API and a Gatsby app. The API will be talking to a database to store the number of clicks. We'll start by creating the database. + +### Adding the table + +We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StaticSite, StackContext, Table } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Counter", { + fields: { + counter: "string", + }, + primaryIndex: { partitionKey: "counter" }, + }); +} +``` + +This creates a serverless DynamoDB table using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. It has a primary key called `counter`. Our table is going to look something like this: + +| counter | tally | +| ------- | ----- | +| clicks | 123 | + +### Creating our API + +Now let's add the API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.main", + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +### Setting up our Gatsby app + +To deploy a Gatsby app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite#creating-a-gatsby-site) construct. + +{%change%} Replace the following in `stacks/ExampleStack.ts`: + +```typescript +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +{%change%} With: + +```typescript +const site = new StaticSite(stack, "GatsbySite", { + path: "frontend", + buildOutput: "public", + buildCommand: "npm run build", + errorPage: "redirect_to_index_page", + environment: { + // Pass in the API endpoint to our app + GATSBY_APP_API_URL: api.url, + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + SiteUrl: site.url, + ApiEndpoint: api.url, +}); +``` + +The construct is pointing to where our Gatsby app is located. We haven't created our app yet but for now we'll point to the `frontend` directory. + +We are also setting up a [build time Gatsby environment variable](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/) `GATSBY_APP_API_URL` with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite#creating-a-gatsby-site) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +You can also optionally configure a custom domain. + +```typescript +// Deploy our Gatsby app +const site = new StaticSite(stack, "GatsbySite", { + // ... + customDomain: "www.my-gatsby-app.com", +}); +``` + +But we'll skip this for now. + +### Reading from our table + +Our API is powered by a Lambda function. In the function we'll read from our DynamoDB table. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function main() { + const getParams = { + // Get the table name from the environment variable + TableName: Table.Counter.tableName, + // Get the row where the counter is called "clicks" + Key: { + counter: "clicks", + }, + }; + const results = await dynamoDb.get(getParams).promise(); + + // If there is a row, then get the value of the + // column called "tally" + let count = results.Item ? results.Item.tally : 0; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We make a `get` call to our DynamoDB table and get the value of a row where the `counter` column has the value `clicks`. Since we haven't written to this column yet, we are going to just return `0`. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-gatsby-app-ExampleStack: deploying... + + ✅ dev-gatsby-app-ExampleStack + + +Stack dev-gatsby-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://sez1p3dsia.execute-api.ap-south-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Setting up our Gatsby app + +We are now ready to use the API we just created. Let's use [Gatsby Quick Start](https://www.gatsbyjs.com/docs/quick-start/) to setup our Gatsby app. + +{%change%} Run the following in the `packages/` directory. + +```bash +$ npx create-gatsby@latest +``` + +Follow the prompts to choose your preferred CMS, styling tools and additional features. Name the app as `frontend`, For this tutorial we're going to use the default options as shown below. + +![Create Gatsby app](/assets/examples/gatsby-app/create-gatsby-app.png) + +This sets up our Gatsby app in the `frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +Navigate into the folder we created. + +```bash +$ cd frontend +``` + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `develop` script in your `frontend/package.json`. + +```bash +"develop": "gatsby develop", +``` + +{%change%} With the following: + +```bash +"develop": "sst bind gatsby develop", +``` + +Let's start our Gatsby development environment. + +{%change%} In the `frontend/` directory run. + +```bash +$ npm run develop +``` + +Open up your browser and go to `http://localhost:8000`. + +### Add the click button + +We are now ready to add the UI for our app and connect it to our serverless API. + +{%change%} Replace `packages/frontend/src/pages/index.js` with. + +{% raw %} + +```jsx +import React, { useState } from "react"; + +export default function App() { + const [count, setCount] = useState(0); + + function onClick() { + fetch(process.env.GATSBY_APP_API_URL, { + method: "POST", + }) + .then((response) => response.text()) + .then(setCount); + } + + return ( +
+
+

You clicked me {count} times.

+ +
+
+ ); +} +``` + +{% endraw %} + +Here we are adding a simple button that when clicked, makes a request to our API. We are getting the API endpoint from the environment variable, `process.env.GATSBY_APP_API_URL`. + +The response from our API is then stored in our app's state. We use that to display the count of the number of times the button has been clicked. + +Now if you head over to your browser, your Gatsby app should look something like this. + +![Click counter UI in Gatsby app](/assets/examples/react-app/click-counter-ui-in-react-app.png) + +Of course if you click on the button multiple times, the count doesn't change. That's because we are not updating the count in our API. We'll do that next. + +## Making changes + +Let's update our table with the clicks. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +const putParams = { + TableName: Table.Counter.tableName, + Key: { + counter: "clicks", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your browser and click the button again, you should see the count increase! + +![Click counter updating in Gatsby app](/assets/examples/react-app/click-counter-updating-in-react-app.png) + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-gatsby-app-ExampleStack + + +Stack prod-gatsby-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com + SiteUrl: https://d1wuzrecqjflrh.cloudfront.net +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +If you head over to the `SiteUrl` in your browser, you should see your new Gatsby app in action! + +![Gatsby app deployed to AWS](/assets/examples/react-app/react-app-deployed-to-aws.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless click counter in Gatsby. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-nextjs-app-with-serverless.md b/_examples/how-to-create-a-nextjs-app-with-serverless.md new file mode 100644 index 0000000000..d8e96e1f12 --- /dev/null +++ b/_examples/how-to-create-a-nextjs-app-with-serverless.md @@ -0,0 +1,251 @@ +--- +layout: example +title: How to create a Next.js app with serverless +short_title: Next.js +date: 2021-09-17 00:00:00 +lang: en +index: 2 +type: webapp +description: In this example we will look at how to deploy a full-stack Next.js app to your AWS account with SST and OpenNext. We'll also compare the various deployment options for Next.js. +short_desc: Full-stack Next.js app with DynamoDB. +repo: quickstart-nextjs +ref: how-to-create-a-nextjs-app-with-serverless +comments_id: how-to-create-a-next-js-app-with-serverless/2486 +--- + +In this example we will look at how to deploy a full-stack [Next.js](https://nextjs.org) app to your AWS account with [OpenNext](https://open-next.js.org) and the [`NextjsSite`]({{ site.v2_url }}/constructs/NextjsSite) construct. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create a Next.js app + +{%change%} Let's start by creating a Next.js app. We'll just go with the defaults. + +```bash +$ npx create-next-app@latest +``` + +## Initialize SST in your app + +{%change%} Initialize SST in your Next.js app by running this in the root. + +```bash +$ npx create-sst@latest +$ npm install +``` + +This will detect that you are trying to configure a Next.js app. It'll add a `sst.config.ts` and a couple of packages to your `package.json`. + +```js +import { SSTConfig } from "sst"; +import { NextjsSite } from "sst/constructs"; + +export default { + config(_input) { + return { + name: "quickstart-nextjs", + region: "us-east-1", + }; + }, + stacks(app) { + app.stack(function Site({ stack }) { + const site = new NextjsSite(stack, "site"); + + stack.addOutputs({ + SiteUrl: site.url, + }); + }); + }, +} satisfies SSTConfig; +``` + +The `stacks` code describes the infrastructure of your serverless app. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}). + +You are **ready to deploy** your Next.js app at this point! But for the purpose of this example, we'll go a bit further and add a file uploads feature to our app. + +## Start the dev environment + +{%change%} Let's start our SST dev environment. + +```bash +$ npx sst dev +``` + +SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. This will ask you to start your Next.js dev environment as well. + +{%change%} Start Next.js locally in a separate terminal. + +```bash +$ npm run dev +``` + +This will run `sst bind next dev`. More on bind later. + +## Create our infrastructure + +To support file uploads in our app, we need an S3 bucket. Let's add that. + +### Add the table + +{%change%} Add the following above our `NextjsSite` definition in the `sst.config.ts`. + +```typescript +const bucket = new Bucket(stack, "public"); +``` + +Here we are using the [`Bucket`]({{ site.v2_url }}/constructs/Bucket) construct to create an S3 bucket. + +{%change%} Add it to the imports. + +```diff +- import { NextjsSite } from "sst/constructs"; ++ import { Bucket, NextjsSite } from "sst/constructs"; +``` + +### Bind it to our app + +We want our Next.js app to be able to access our bucket. + +{%change%} Add this to our Next.js definition in the `sst.config.ts`. + +```diff +- const site = new NextjsSite(stack, "site"); ++ const site = new NextjsSite(stack, "site", { ++ bind: [bucket], ++ }); +``` + +We'll see what bind does below. + +## Support file uploads + +Now to let our users upload files in our Next.js app we need to start by generating a presigned URL. This is a temporary URL that our frontend can make a request to upload files. + +### Generate a presigned URL + +{%change%} Add this to `pages/index.ts` above the `Home` component. + +```typescript +export async function getServerSideProps() { + const command = new PutObjectCommand({ + ACL: "public-read", + Key: crypto.randomUUID(), + Bucket: Bucket.public.bucketName, + }); + const url = await getSignedUrl(new S3Client({}), command); + + return { props: { url } }; +} +``` + +This generates a presigned URL when our app loads. Note how we can access our S3 bucket in a typesafe way — `Bucket.public.bucketName`. [You can learn more about Resource Binding over on our docs]({{ site.v2_url }}/resource-binding). + +{%change%} We need to install a couple of packages. + +```bash +$ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner +``` + +{%change%} And add these to the imports. + +```typescript +import crypto from "crypto"; +import { Bucket } from "sst/node/bucket"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +``` + +### Add an upload form + +Now let's add the form. + +{%change%} Replace the `Home` component in `pages/index.tsx` with. + +```tsx +export default function Home({ url }: { url: string }) { + return ( +
+
{ + e.preventDefault(); + + const file = (e.target as HTMLFormElement).file.files?.[0]!; + + const image = await fetch(url, { + body: file, + method: "PUT", + headers: { + "Content-Type": file.type, + "Content-Disposition": `attachment; filename="${file.name}"`, + }, + }); + + window.location.href = image.url.split("?")[0]; + }} + > + + +
+
+ ); +} +``` + +## Test your app + +Now if you flip over to your browser, you should be able to upload an image and it'll redirect to it! + +![Upload a file to S3 in Next.js app](/assets/examples/nextjs-app/upload-a-file-to-s3-in-next-js-app.png) + +## Deploy to AWS + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in our local environment, it doesn't break the app for our users. You can stop the `npm run dev` command that we had previously run. + +Once deployed, you should see your app's URL. + +```bash +SiteUrl: https://dq1n2yr6krqwr.cloudfront.net +``` + +If you head over to the `URL` in your browser, you should see your new Next.js app in action! + +![Deployed Next.js app with SST](/assets/examples/nextjs-app/deployed-next-js-app-with-sst.png) + +We can [add a custom domain]({{ site.v2_url }}/custom-domains) to our app but we'll leave that as an exercise for later. + +### Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +--- + +## Comparisons + +In this example we looked at how to use SST to deploy a Next.js app to AWS. But there are a few different ways to deploy Next.js apps. Let's look at how they all compare. + +- [Vercel](https://vercel.com) is the most popular way to deploy Next.js apps. It's the most expensive and isn't open source. + +- [Amplify](https://docs.amplify.aws/guides/hosting/nextjs/q/platform/js/) in many ways is AWS's version of Vercel. It's cheaper and deploys to your AWS account. But their implementation is incomplete and not on par with Vercel. And because they are not open source, you'll need to file a support ticket to get your issues fixed. + +- [Serverless Next.js (sls-next) Component](https://github.com/serverless-nextjs/serverless-next.js) is open source and deploys to your AWS account. But this project is not being maintained anymore. + +- [SST]({{ site.sst_github_repo }}) is completely open source and deploys directly to your AWS account. It uses [OpenNext](https://open-next.js.org) — an open-source serverless adapter for Next.js. The OpenNext project is a community effort to reverse engineer Vercel's implementation and make it available to everybody. + +We hope this example has helped you deploy your Next.js apps to AWS. And given you an overview of all the deployment options out there. diff --git a/_examples/how-to-create-a-reactjs-app-with-serverless.md b/_examples/how-to-create-a-reactjs-app-with-serverless.md new file mode 100644 index 0000000000..27a92df69c --- /dev/null +++ b/_examples/how-to-create-a-reactjs-app-with-serverless.md @@ -0,0 +1,453 @@ +--- +layout: example +title: How to create a React.js app with serverless +short_title: React.js +date: 2021-06-17 00:00:00 +lang: en +index: 1 +type: webapp +description: In this example we will look at how to use React.js with a serverless API to create a simple click counter app. We'll be using SST and the StaticSite construct to deploy our app to AWS S3 and CloudFront. +short_desc: Full-stack React app with a serverless API. +repo: react-app +ref: how-to-create-a-reactjs-app-with-serverless +comments_id: how-to-create-a-react-js-app-with-serverless/2413 +--- + +In this example we will look at how to use [React.js](https://reactjs.org) with a [serverless]({% link _chapters/what-is-serverless.md %}) API to create a simple click counter app. We'll be using the [SST]({{ site.sst_github_repo }}) and the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct to deploy our app to AWS. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example react-app +$ cd react-app +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "react-app", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +3. `packages/frontend/` — React App + + The code for our frontend React.js app. + +## Create our infrastructure + +Our app is made up of a simple API and a React.js app. The API will be talking to a database to store the number of clicks. We'll start by creating the database. + +### Adding the table + +We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StaticSite, StackContext, Table } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Counter", { + fields: { + counter: "string", + }, + primaryIndex: { partitionKey: "counter" }, + }); +} +``` + +This creates a serverless DynamoDB table using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. It has a primary key called `counter`. Our table is going to look something like this: + +| counter | tally | +| ------- | ----- | +| clicks | 123 | + +### Creating our API + +Now let's add the API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.handler", + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +### Setting up our React app + +To deploy a React.js app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) construct. + +{%change%} Replace the following in `stacks/ExampleStack.ts`: + +```typescript +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +{%change%} With: + +```typescript +// Deploy our React app +const site = new StaticSite(stack, "ReactSite", { + path: "packages/frontend", + buildCommand: "npm run build", + buildOutput: "build", + environment: { + REACT_APP_API_URL: api.url, + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + SiteUrl: site.url, + ApiEndpoint: api.url, +}); +``` + +The construct is pointing to where our React.js app is located. We haven't created our app yet but for now we'll point to the `packages/frontend` directory. + +We are also setting up a [build time React environment variable](https://create-react-app.dev/docs/adding-custom-environment-variables/) `REACT_APP_API_URL` with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. You can read more about this over in our chapter on, [Setting serverless environments variables in a React app]({% link _archives/setting-serverless-environments-variables-in-a-react-app.md %}). + +You can also optionally configure a custom domain. + +```typescript +// Deploy our React app +const site = new StaticSite(stack, "ReactSite", { + // ... + customDomain: "www.my-react-app.com", +}); +``` + +But we'll skip this for now. + +### Reading from our table + +Our API is powered by a Lambda function. In the function we'll read from our DynamoDB table. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function handler() { + const getParams = { + // Get the table name from the environment variable + TableName: Table.Counter.tableName, + // Get the row where the counter is called "clicks" + Key: { + counter: "clicks", + }, + }; + const results = await dynamoDb.get(getParams).promise(); + + // If there is a row, then get the value of the + // column called "tally" + let count = results.Item ? results.Item.tally : 0; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We make a `get` call to our DynamoDB table and get the value of a row where the `counter` column has the value `clicks`. Since we haven't written to this column yet, we are going to just return `0`. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-react-app-ExampleStack: deploying... + + ✅ dev-react-app-ExampleStack + + +Stack dev-react-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://51q98mf39e.execute-api.us-east-1.amazonaws.com + SiteUrl: http://localhost:5173 +``` + +The `ApiEndpoint` is the API we just created. While the `SiteUrl` our React app will run locally once we start it. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Setting up our React app + +We are now ready to use the API we just created. Let's use [Create React App](https://github.com/facebook/create-react-app) to setup our React.js app. + +{%change%} Run the following in the project root. + +```bash +$ npx create-react-app packages/frontend --use-npm +$ cd frontend +``` + +This sets up our React app in the `packages/frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +Create React App will throw a warning if it is installed inside a repo that uses Jest. To disable this, we'll need to set an environment variable. + +{%change%} Add the following to `frontend/.env`. + +```bash +SKIP_PREFLIGHT_CHECK=true +``` + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Replace the `start` script in your `frontend/package.json`. + +```bash +"start": "react-scripts start", +``` + +{%change%} With the following: + +```bash +"start": "sst bind react-scripts start", +``` + +Let's start our React development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +$ npm run start +``` + +This should open up our React.js app in your browser. + +### Add the click button + +We are now ready to add the UI for our app and connect it to our serverless API. + +{%change%} Replace `packages/frontend/src/App.js` with. + +```jsx +import { useState } from "react"; +import "./App.css"; + +export default function App() { + const [count, setCount] = useState(null); + + function onClick() { + fetch(process.env.REACT_APP_API_URL, { + method: "POST", + }) + .then((response) => response.text()) + .then(setCount); + } + + return ( +
+ {count &&

You clicked me {count} times.

} + +
+ ); +} +``` + +Here we are adding a simple button that when clicked, makes a request to our API. We are getting the API endpoint from the environment variable, `process.env.REACT_APP_API_URL`. + +The response from our API is then stored in our app's state. We use that to display the count of the number of times the button has been clicked. + +Let's add some styles. + +{%change%} Replace `packages/frontend/src/App.css` with. + +```css +body, +html { + height: 100%; + display: grid; +} +#root { + margin: auto; +} +.App { + text-align: center; +} +p { + margin-top: 0; + font-size: 20px; +} +button { + font-size: 48px; +} +``` + +Now if you head over to your browser, your React app should look something like this. + +![Click counter UI in React app](/assets/examples/react-app/click-counter-ui-in-react-app.png) + +Of course if you click on the button multiple times, the count doesn't change. That's because we are not updating the count in our API. We'll do that next. + +## Making changes + +Let's update our table with the clicks. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +const putParams = { + TableName: Table.Counter.tableName, + Key: { + counter: "clicks", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your browser and click the button again, you should see the count increase! + +![Click counter updating in React app](/assets/examples/react-app/click-counter-updating-in-react-app.png) + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-react-app-ExampleStack + + +Stack prod-react-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazonaws.com + SiteUrl: https://d1wuzrecqjflrh.cloudfront.net +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +If you head over to the `SiteUrl` in your browser, you should see your new React app in action! + +![React app deployed to AWS](/assets/examples/react-app/react-app-deployed-to-aws.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless click counter in React.js. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md b/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md new file mode 100644 index 0000000000..1f9f8d949a --- /dev/null +++ b/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md @@ -0,0 +1,361 @@ +--- +layout: example +title: How to create a REST API in Golang with serverless +short_title: Go REST API +date: 2021-04-04 00:00:00 +lang: en +index: 4 +type: api +description: In this example we will look at how to create a serverless REST API on AWS with Golang using SST. We'll be using the Api construct to define the routes of our API. +short_desc: Building a REST API with Golang. +repo: rest-api-go +ref: how-to-create-a-rest-api-in-golang-with-serverless +comments_id: how-to-create-a-rest-api-in-golang-with-serverless/2367 +--- + +In this example we'll look at how to create a serverless REST API with Golang on AWS using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later for our CDK code +- Golang 1.16 or similar for our Lambda code +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=other/go rest-api-go +$ cd rest-api-go +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api-go", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Setting up our routes + +Let's start by setting up the routes for our API. + +{%change%} Add the following below the `config` function in the `sst.config.ts`. + +```typescript +stacks(app) { + app.setDefaultFunctionProps({ + runtime: "go1.x", + }); + app.stack(function Stack({ stack }) { + const api = new Api(stack, "api", { + routes: { + "GET /notes": "functions/lambda/list.go", + "GET /notes/{id}": "functions/lambda/get.go", + "PUT /notes/{id}": "functions/lambda/update.go", + }, + }); + stack.addOutputs({ + ApiEndpoint: api.url, + }); + }); +}, +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding three routes to it. + +``` +GET /notes +GET /notes/{id} +PUT /notes/{id} +``` + +The first is getting a list of notes. The second is getting a specific note given an id. And the third is updating a note. + +## Adding function code + +For this example, we are not using a database. We'll look at that in detail in another example. So internally we are just going to get the list of notes from a file. + +{%change%} Let's add a file that contains our notes in `db/notes.go`. + +```go +package db + +import ( + "strconv" + "time" +) + +func Notes() map[string]map[string]string { + return map[string]map[string]string{ + "id1": { + "noteId": "id1", + "userId": "user1", + "content": "Hello World!", + "createdAt": strconv.FormatInt(time.Now().Unix(), 10), + }, + "id2": { + "noteId": "id2", + "userId": "user2", + "content": "Hello Old World!", + "createdAt": strconv.FormatInt(time.Now().Unix()-1000, 10), + }, + } +} +``` + +Now add the code for our first endpoint. + +### Getting a list of notes + +{%change%} Add a `functions/lambda/list.go`. + +```go +package main + +import ( + "encoding/json" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/sst/examples/rest-api-go/db" +) + +func Handler(request events.APIGatewayV2HTTPRequest) (events.APIGatewayProxyResponse, error) { + response, _ := json.Marshal(db.Notes()) + + return events.APIGatewayProxyResponse{ + Body: string(response), + StatusCode: 200, + }, nil +} + +func main() { + lambda.Start(Handler) +} +``` + +Here we are simply converting a list of notes to string, and responding with that in the request body. + +### Getting a specific note + +{%change%} Add the following to `functions/lambda/get.go`. + +```go +package main + +import ( + "encoding/json" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/sst/examples/rest-api-go/db" +) + +func Handler(request events.APIGatewayV2HTTPRequest) (events.APIGatewayProxyResponse, error) { + var notes = db.Notes() + var note = notes[request.PathParameters["id"]] + + if note == nil { + return events.APIGatewayProxyResponse{ + Body: `{"error":true}`, + StatusCode: 404, + }, nil + } + + response, _ := json.Marshal(note) + + return events.APIGatewayProxyResponse{ + Body: string(response), + StatusCode: 200, + }, nil +} + +func main() { + lambda.Start(Handler) +} +``` + +Here we are checking if we have the requested note. If we do, we respond with it. If we don't, then we respond with a 404 error. + +### Updating a note + +{%change%} Add the following to `functions/lambda/update.go`. + +```go +package main + +import ( + "encoding/json" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/sst/examples/rest-api-go/db" +) + +func Handler(request events.APIGatewayV2HTTPRequest) (events.APIGatewayProxyResponse, error) { + var notes = db.Notes() + var note = notes[request.PathParameters["id"]] + + if note == nil { + return events.APIGatewayProxyResponse{ + Body: `{"error":true}`, + StatusCode: 404, + }, nil + } + + var body map[string]string + _ = json.Unmarshal([]byte(request.Body), &body) + + note["content"] = body["content"] + + response, _ := json.Marshal(note) + + return events.APIGatewayProxyResponse{ + Body: string(response), + StatusCode: 200, + }, nil +} + +func main() { + lambda.Start(Handler) +} +``` + +We first check if the note with the requested id exists. And then we update the content of the note and return it. Of course, we aren't really saving our changes because we don't have a database! + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-go-ExampleStack: deploying... + + ✅ dev-rest-api-go-ExampleStack + + +Stack dev-rest-api-go-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://rxk5buowgi.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** explorer and click the **Send** button of the `GET /notes` route to get a list of notes. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API tab get notes response](/assets/examples/rest-api/api-tab-get-notes-response.png) + +You should see the list of notes as a JSON string. + +To retrieve a specific note, Go to `GET /notes/{id}` route and in the **URL** tab enter the **id** of the note you want to get in the **id** field and click the **Send** button to get that note. + +![API tab get specific note response](/assets/examples/rest-api/api-tab-get-specific-note-response.png) + +Now to update our note, we need to make a `PUT` request, go to `PUT /notes/{id}` route. + +In the **URL** tab, enter the **id** of the note you want to update and in the **body** tab and enter the below json value and hit **Send**. + +```json +{ "content": "Updating my note" } +``` + +![API tab update note response](/assets/examples/rest-api/api-tab-update-note-response.png) + +This should respond with the updated note. + +## Making changes + +Let's make a quick change to our API. It would be good if the JSON strings are pretty printed to make them more readable. + +{%change%} Replace `Handler` function in `functions/lambda/list.go` with the following. + +```go +func Handler(request events.APIGatewayV2HTTPRequest) (events.APIGatewayProxyResponse, error) { + response, _ := json.MarshalIndent(db.Notes(), "", " ") + + return events.APIGatewayProxyResponse{ + Body: string(response), + StatusCode: 200, + }, nil +} +``` + +Here we are just adding some spaces to pretty print the JSON. + +If you head back to the `GET /notes` route and hit **Send** again. + +![API tab get notes response with spaces](/assets/examples/rest-api/api-tab-get-notes-response-with-spaces.png) +You should see your list of notes in a more readable format. + +## Deploying your API + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-rest-api-go-ExampleStack + + +Stack prod-rest-api-go-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** explorer and click **Send** button of the `GET /notes` route, to send a `GET` request. + +![Prod API explorer get notes response with spaces](/assets/examples/rest-api/prod-api-tab-get-notes-response-with-spaces.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md b/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md new file mode 100644 index 0000000000..b71fabfcd9 --- /dev/null +++ b/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md @@ -0,0 +1,346 @@ +--- +layout: example +title: How to create a REST API in TypeScript with serverless +short_title: TypeScript REST API +date: 2021-02-04 00:00:00 +lang: en +index: 3 +# type: api +description: In this example we will look at how to create a serverless REST API on AWS with TypeScript using SST. We'll be using the Api construct to define the routes of our API. +short_desc: Building a REST API with TypeScript. +repo: rest-api +ref: how-to-create-a-rest-api-in-typescript-with-serverless +comments_id: how-to-create-a-rest-api-in-typescript-with-serverless/2306 +--- + +In this example we'll look at how to create a serverless REST API with TypeScript on AWS using [SST]({{ site.sst_github_repo }}). We also have a [a JavaScript version of this example]({% link _examples/how-to-create-a-rest-api-in-typescript-with-serverless.md %}) as well. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example rest-api-ts +$ cd rest-api-ts +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api-ts", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our routes + +Let's start by setting up the routes for our API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { api, stackcontext } from "sst/constructs"; + +export function examplestack({ stack }: stackcontext) { + // create the http api + const api = new api(stack, "api", { + routes: { + "get /notes": "packages/functions/src/list.main", + "get /notes/{id}": "packages/functions/src/get.main", + "put /notes/{id}": "packages/functions/src/update.main", + }, + }); + + // show the api endpoint in the output + stack.addoutputs({ + apiendpoint: api.url, + }); +} +``` + +we are creating an api here using the [`api`]({{ site.v2_url }}/constructs/api) construct. and we are adding three routes to it. + +``` +GET /notes +GET /notes/{id} +PUT /notes/{id} +``` + +The first is getting a list of notes. The second is getting a specific note given an id. And the third is updating a note. + +## Adding function code + +For this example, we are not using a database. We'll look at that in detail in another example. So internally we are just going to get the list of notes from a file. + +{%change%} Let's add a file that contains our notes in `src/notes.ts`. + +```typescript +interface Note { + noteId: string; + userId: string; + createdAt: number; + content: string; +} + +const notes: { [key: string]: Note } = { + id1: { + noteId: "id1", + userId: "user1", + createdAt: Date.now(), + content: "Hello World!", + }, + id2: { + noteId: "id2", + userId: "user2", + createdAt: Date.now() - 10000, + content: "Hello Old World! Old note.", + }, +}; + +export default notes; +``` + +Now add the code for our first endpoint. + +### Getting a list of notes + +{%change%} Add a `src/list.ts`. + +```typescript +import { APIGatewayProxyResult } from "aws-lambda"; +import notes from "./notes"; + +export async function main(): Promise { + return { + statusCode: 200, + body: JSON.stringify(notes), + }; +} +``` + +Here we are simply converting a list of notes to string, and responding with that in the request body. + +Note that this function need to be `async` to be invoked by AWS Lambda. Even though, in this case we are doing everything synchronously. + +### Getting a specific note + +{%change%} Add the following to `src/get.ts`. + +```typescript +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import notes from "./notes"; + +export async function main( + event: APIGatewayProxyEvent +): Promise { + const note = + event.pathParameters && event.pathParameters.id + ? notes[event.pathParameters.id] + : null; + return note + ? { + statusCode: 200, + body: JSON.stringify(note), + } + : { + statusCode: 404, + body: JSON.stringify({ error: true }), + }; +} +``` + +Here we are checking if we have the requested note. If we do, we respond with it. If we don't, then we respond with a 404 error. + +### Updating a note + +{%change%} Add the following to `src/update.ts`. + +```typescript +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import notes from "./notes"; + +export async function main( + event: APIGatewayProxyEvent +): Promise { + const note = + event.pathParameters && event.pathParameters.id + ? notes[event.pathParameters.id] + : null; + + if (!note) { + return { + statusCode: 404, + body: JSON.stringify({ error: true }), + }; + } + + if (event.body) { + const data = JSON.parse(event.body); + note.content = data.content || note.content; + } + + return { + statusCode: 200, + body: JSON.stringify(note), + }; +} +``` + +We first check if the note with the requested id exists. And then we update the content of the note and return it. Of course, we aren't really saving our changes because we don't have a database! + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-ts-ExampleStack: deploying... + + ✅ dev-rest-api-ts-ExampleStack + + +Stack dev-rest-api-ts-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://rxk5buowgi.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** explorer and click the **Send** button of the `GET /notes` route to get a list of notes. + +Note, the [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API tab get notes response](/assets/examples/rest-api/api-tab-get-notes-response.png) + +You should see the list of notes as a JSON string. + +To retrieve a specific note, Go to `GET /notes/{id}` route and in the **URL** tab enter the **id** of the note you want to get in the **id** field and click the **Send** button to get that note. + +![API tab get specific note response](/assets/examples/rest-api/api-tab-get-specific-note-response.png) + +Now to update our note, we need to make a `PUT` request, go to `PUT /notes/{id}` route. + +In the **URL** tab, enter the **id** of the note you want to update and in the **body** tab and enter the below json value and hit **Send**. + +```json +{ "content": "Updating my note" } +``` + +![API tab update note response](/assets/examples/rest-api/api-tab-update-note-response.png) + +This should respond with the updated note. + +## Making changes + +Let's make a quick change to our API. It would be good if the JSON strings are pretty printed to make them more readable. + +{%change%} Replace `src/list.ts` with the following. + +```typescript +import { APIGatewayProxyResult } from "aws-lambda"; +import notes from "./notes"; + +export async function main(): Promise { + return { + statusCode: 200, + body: JSON.stringify(notes, null, " "), + }; +} +``` + +Here we are just [adding some spaces](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) to pretty print the JSON. + +If you head back to the `GET /notes` route and hit **Send** again. + +![API tab get notes response with spaces](/assets/examples/rest-api/api-tab-get-notes-response-with-spaces.png) + +You should see your list of notes in a more readable format. + +## Deploying your API + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-rest-api-ts-ExampleStack + + +Stack prod-rest-api-ts-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** explorer and click **Send** button of the `GET /notes` route, to send a `GET` request. + +![Prod API explorer get notes response with spaces](/assets/examples/rest-api/prod-api-tab-get-notes-response-with-spaces.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-rest-api-with-serverless.md b/_examples/how-to-create-a-rest-api-with-serverless.md new file mode 100644 index 0000000000..3d35c88389 --- /dev/null +++ b/_examples/how-to-create-a-rest-api-with-serverless.md @@ -0,0 +1,336 @@ +--- +layout: example +title: How to create a REST API with serverless +short_title: REST API +date: 2021-01-27 00:00:00 +lang: en +index: 1 +type: api +description: In this example we will look at how to create a serverless REST API on AWS using SST. We'll be using the Api construct to define the routes of our API. +short_desc: Building a simple REST API. +repo: rest-api +ref: how-to-create-a-rest-api-with-serverless +comments_id: how-to-create-a-rest-api-with-serverless/2305 +--- + +In this example we will look at how to create a serverless REST API on AWS using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example rest-api +$ cd rest-api +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our routes + +Let's start by setting up the routes for our API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /notes": "packages/functions/src/list.handler", + "GET /notes/{id}": "packages/functions/src/get.handler", + "PUT /notes/{id}": "packages/functions/src/update.handler", + }, + }); + + // Show the API endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an API here using the [`Api`]({{ site.v2_url }}/constructs/api) construct. And we are adding three routes to it. + +``` +GET /notes +GET /notes/{id} +PUT /notes/{id} +``` + +The first is getting a list of notes. The second is getting a specific note given an id. And the third is updating a note. + +## Adding function code + +For this example, we are not using a database. We'll look at that in detail in another example. So internally we are just going to get the list of notes from a file. + +{%change%} Let's add a file that contains our notes in `packages/core/src/notes.ts`. + +```typescript +export default { + id1: { + noteId: "id1", + userId: "user1", + createdAt: Date.now(), + content: "Hello World!", + }, + id2: { + noteId: "id2", + userId: "user2", + createdAt: Date.now() - 10000, + content: "Hello Old World! Old note.", + }, +}; +``` + +Now add the code for our first endpoint. + +### Getting a list of notes + +{%change%} Add a `packages/functions/src/list.ts`. + +```typescript +import notes from "@rest-api/core/notes"; + +export async function handler() { + return { + statusCode: 200, + body: JSON.stringify(notes), + }; +} +``` + +Here we are simply converting a list of notes to string, and responding with that in the request body. + +Note that this function need to be `async` to be invoked by AWS Lambda. Even though, in this case we are doing everything synchronously. + +### Getting a specific note + +{%change%} Add the following to `packages/functions/src/get.ts`. + +```typescript +import notes from "@rest-api/core/notes"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const note = notes[event.pathParameters.id]; + return note + ? { + statusCode: 200, + body: JSON.stringify(note), + } + : { + statusCode: 404, + body: JSON.stringify({ error: true }), + }; +}; +``` + +Here we are checking if we have the requested note. If we do, we respond with it. If we don't, then we respond with a 404 error. + +### Updating a note + +{%change%} Add the following to `packages/functions/src/update.ts`. + +```typescript +import notes from "@rest-api/core/notes"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const note = notes[event.pathParameters.id]; + + if (!note) { + return { + statusCode: 404, + body: JSON.stringify({ error: true }), + }; + } + + const data = JSON.parse(event.body); + + note.content = data.content; + + return { + statusCode: 200, + body: JSON.stringify(note), + }; +}; +``` + +We first check if the note with the requested id exists. And then we update the content of the note and return it. Of course, we aren't really saving our changes because we don't have a database! + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-ExampleStack: deploying... + + ✅ dev-rest-api-ExampleStack + + +Stack dev-rest-api-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://2q0mwp6r8d.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** explorer and click the **Send** button of the `GET /notes` route to get a list of notes. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API tab get notes response](/assets/examples/rest-api/api-tab-get-notes-response.png) + +You should see the list of notes as a JSON string. + +To retrieve a specific note, Go to `GET /notes/{id}` route and in the **URL** tab enter the **id** of the note you want to get in the **id** field and click the **Send** button to get that note. + +![API tab get specific note response](/assets/examples/rest-api/api-tab-get-specific-note-response.png) + +Now to update our note, we need to make a `PUT` request, go to `PUT /notes/{id}` route. + +In the **URL** tab, enter the **id** of the note you want to update and in the **body** tab and enter the below json value and hit **Send**. + +```json +{ "content": "Updating my note" } +``` + +![API tab update note response](/assets/examples/rest-api/api-tab-update-note-response.png) + +This should respond with the updated note. + +## Making changes + +Let's make a quick change to our API. It would be good if the JSON strings are pretty printed to make them more readable. + +{%change%} Replace `packages/functions/src/list.ts` with the following. + +```typescript +import notes from "@rest-api/core/notes"; + +export async function handler() { + return { + statusCode: 200, + body: JSON.stringify(notes, null, " "), + }; +} +``` + +Here we are just [adding some spaces](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) to pretty print the JSON. + +If you head back to the `GET /notes` route and hit **Send** again. + +![API tab get notes response with spaces](/assets/examples/rest-api/api-tab-get-notes-response-with-spaces.png) + +You should see your list of notes in a more readable format. + +## Deploying your API + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-rest-api-ExampleStack + + +Stack prod-rest-api-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** explorer and click **Send** button of the `GET /notes` route, to send a `GET` request. + +![Prod API explorer get notes response with spaces](/assets/examples/rest-api/prod-api-tab-get-notes-response-with-spaces.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md b/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md new file mode 100644 index 0000000000..3bedd214c1 --- /dev/null +++ b/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md @@ -0,0 +1,523 @@ +--- +layout: example +title: How to create a serverless GraphQL API with AWS AppSync +short_title: AppSync +date: 2021-03-27 00:00:00 +lang: en +index: 2 +type: graphql +description: In this example we'll look at how to use SST to test AppSync locally. We'll create a GraphQL API on AWS using the AppSyncApi construct. +short_desc: Building a serverless GraphQL API with AppSync. +repo: graphql-appsync +ref: how-to-create-a-serverless-graphql-api-with-aws-appsync +comments_id: how-to-create-a-serverless-graphql-api-with-aws-appsync/2362 +--- + +In this example we'll look at how to create an [AppSync GraphQL API]({% link _archives/what-is-aws-appsync.md %}) on AWS using [SST]({{ site.sst_github_repo }}). We'll be allowing our users to get, create, update, delete, and list notes. + +We'll be using SST's [Live Lambda Development]({{ site.v2_url }}/live-lambda-development). It allows you to make changes and test AppSync locally without having to redeploy. + +Here is a video of it in action. + +
+ +
+ +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example graphql-appsync +$ cd graphql-appsync +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "graphql-appsync", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our infrastructure + +Let's start by defining our AppSync API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Table, AppSyncApi } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create a notes table + const notesTable = new Table(stack, "Notes", { + fields: { + id: "string", + }, + primaryIndex: { partitionKey: "id" }, + }); + + // Create the AppSync GraphQL API + const api = new AppSyncApi(stack, "AppSyncApi", { + schema: "packages/functions/src/graphql/schema.graphql", + defaults: { + function: { + // Bind the table name to the function + bind: [notesTable], + }, + }, + dataSources: { + notes: "packages/functions/src/main.handler", + }, + resolvers: { + "Query listNotes": "notes", + "Query getNoteById": "notes", + "Mutation createNote": "notes", + "Mutation updateNote": "notes", + "Mutation deleteNote": "notes", + }, + }); + + // Show the AppSync API Id and API Key in the output + stack.addOutputs({ + ApiId: api.apiId, + APiUrl: api.url, + ApiKey: api.cdk.graphqlApi.apiKey || "", + }); +} +``` + +We are creating an AppSync GraphQL API here using the [`AppSyncApi`]({{ site.v2_url }}/constructs/AppSyncApi) construct. We are also creating a DynamoDB table using the [`Table`]({{ site.v2_url }}/constructs/Table) construct. It'll store the notes we'll be creating with our GraphQL API. + +Finally, we bind our table to our API. + +## Define the GraphQL schema + +{%change%} Add the following to `packages/functions/src/graphql/schema.graphql`. + +```graphql +type Note { + id: ID! + content: String! +} + +input NoteInput { + id: ID! + content: String! +} + +input UpdateNoteInput { + id: ID! + content: String +} + +type Query { + listNotes: [Note] + getNoteById(noteId: String!): Note +} + +type Mutation { + createNote(note: NoteInput!): Note + deleteNote(noteId: String!): String + updateNote(note: UpdateNoteInput!): Note +} +``` + +Let's also add a type for our note object. + +{%change%} Add the following to a new file in `packages/functions/src/graphql/Note.ts`. + +```typescript +type Note = { + id: string; + content: string; +}; + +export default Note; +``` + +## Adding the function handler + +To start with, let's create the Lambda function that'll be our AppSync data source. + +{%change%} Replace `packages/functions/src/main.ts` with the following. + +```typescript +import Note from "./graphql/Note"; +import listNotes from "./graphql/listNotes"; +import createNote from "./graphql/createNote"; +import updateNote from "./graphql/updateNote"; +import deleteNote from "./graphql/deleteNote"; +import getNoteById from "./graphql/getNoteById"; + +type AppSyncEvent = { + info: { + fieldName: string; + }; + arguments: { + note: Note; + noteId: string; + }; +}; + +export async function handler( + event: AppSyncEvent +): Promise[] | Note | string | null | undefined> { + switch (event.info.fieldName) { + case "listNotes": + return await listNotes(); + case "createNote": + return await createNote(event.arguments.note); + case "updateNote": + return await updateNote(event.arguments.note); + case "deleteNote": + return await deleteNote(event.arguments.noteId); + case "getNoteById": + return await getNoteById(event.arguments.noteId); + default: + return null; + } +} +``` + +Now let's implement our resolvers. + +## Create a note + +Starting with the one that'll create a note. + +{%change%} Add a file to `packages/functions/src/graphql/createNote.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; +import Note from "./Note"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export default async function createNote(note: Note): Promise { + const params = { + Item: note as Record, + TableName: Table.Notes.tableName, + }; + + await dynamoDb.put(params).promise(); + + return note; +} +``` + +Here, we are storing the given note in our DynamoDB table. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder package that we are using. + +```bash +$ npm install aws-sdk +``` + +## Read the list of notes + +Next, let's write the function that'll fetch all our notes. + +{%change%} Add the following to `packages/functions/src/graphql/listNotes.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export default async function listNotes(): Promise< + Record[] | undefined +> { + const params = { + TableName: Table.Notes.tableName, + }; + + const data = await dynamoDb.scan(params).promise(); + + return data.Items; +} +``` + +Here we are getting all the notes from our table. + +## Read a specific note + +We'll do something similar for the function that gets a single note. + +{%change%} Create a `packages/functions/src/graphql/getNoteById.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; +import Note from "./Note"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export default async function getNoteById( + noteId: string +): Promise { + const params = { + Key: { id: noteId }, + TableName: Table.Notes.tableName, + }; + + const { Item } = await dynamoDb.get(params).promise(); + + return Item as Note; +} +``` + +We are getting the note with the id that's passed in. + +## Update a note + +Now let's update our notes. + +{%change%} Add a `packages/functions/src/graphql/updateNote.ts` with: + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; +import Note from "./Note"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export default async function updateNote(note: Note): Promise { + const params = { + Key: { id: note.id }, + ReturnValues: "UPDATED_NEW", + UpdateExpression: "SET content = :content", + TableName: Table.Notes.tableName, + ExpressionAttributeValues: { ":content": note.content }, + }; + + await dynamoDb.update(params).promise(); + + return note as Note; +} +``` + +We are using the id and the content of the note that's passed in to update a note. + +## Delete a note + +To complete all the operations, let's delete the note. + +{%change%} Add this to `packages/functions/src/graphql/deleteNote.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export default async function deleteNote(noteId: string): Promise { + const params = { + Key: { id: noteId }, + TableName: Table.Notes.tableName, + }; + + // await dynamoDb.delete(params).promise(); + + return noteId; +} +``` + +Note that, we are purposely disabling the delete query for now. We'll come back to this later. + +Let's test what we've created so far! + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-graphql-appsync-ExampleStack: deploying... + + ✅ dev-graphql-appsync-ExampleStack + + +Stack dev-graphql-appsync-ExampleStack + Status: deployed + Outputs: + ApiId: lk2fgfxsizdstfb24c4y4dnad4 + ApiKey: da2-3oknz5th4nbj5oobjz4jwid62q + ApiUrl: https://2ngraxbyo5cwdpsk47wgn3oafu.appsync-api.us-east-1.amazonaws.com/graphql +``` + +The `ApiId` is the Id of the AppSync API we just created, the `ApiKey` is the API key of our AppSync API and `ApiUrl` is the AppSync API URL. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **GraphQL** tab and you should see the GraphQL Playground in action. + +Note, The GraphQL explorer lets you query GraphQL endpoints created with the GraphQLApi and AppSyncApi constructs in your app. + +Let's start by creating a note. Paste the below mutation in the left part of the playground. + +```graphql +mutation createNote { + createNote(note: { id: "001", content: "My note" }) { + id + content + } +} +``` + +![GraphQL console create note](/assets/examples/graphql-appsync/graphql-console-create-note.png) + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been created in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB explorer create note](/assets/examples/graphql-appsync/dynamodb-explorer-create-note.png) + +And let's get the note we just created by running this query instead. + +```graphql +query getNoteById { + getNoteById(noteId: "001") { + id + content + } +} +``` + +![GraphQL console get note](/assets/examples/graphql-appsync/graphql-console-get-note.png) + +Let's test our update mutation by running: + +```graphql +mutation updateNote { + updateNote(note: { id: "001", content: "My updated note" }) { + id + content + } +} +``` + +![GraphQL console update note](/assets/examples/graphql-appsync/graphql-console-update-note.png) + +Now let's try deleting our note. + +```graphql +mutation deleteNote { + deleteNote(noteId: "001") +} +``` + +![GraphQL console delete note](/assets/examples/graphql-appsync/graphql-console-delete-note.png) + +Let's test if the delete worked by getting all the notes. + +```graphql +query listNotes { + listNotes { + id + content + } +} +``` + +![GraphQL console list notes](/assets/examples/graphql-appsync/graphql-console-list-notes.png) + +You'll notice a couple of things. Firstly, the note we created is still there. This is because our `deleteNote` method isn't actually running our query. Secondly, our note should have the updated content from our previous query. + +## Making changes + +{%change%} Let's fix our `packages/functions/src/graphql/deleteNote.ts` by un-commenting the query. + +```typescript +await dynamoDb.delete(params).promise(); +``` + +If you head back to the query editor and run the delete mutation again. + +```graphql +mutation deleteNote { + deleteNote(noteId: "001") +} +``` + +![GraphQL console delete note after change](/assets/examples/graphql-appsync/graphql-console-delete-note.png) + +And running the list query should now show that the note has been removed! + +```graphql +query listNotes { + listNotes { + id + content + } +} +``` + +![GraphQL console list notes after change](/assets/examples/graphql-appsync/graphql-console-list-notes-after-change.png) + +Notice we didn't need to redeploy our app to see the change. + +## Deploying your API + +Now that our API is tested, let's deploy it to production. You'll recall that we were using a `dev` environment, the one specified in our `sst.config.ts`. However, we are going to deploy it to a different environment. This ensures that the next time we are developing locally, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless GraphQL API built with AppSync. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-svelte-app-with-serverless.md b/_examples/how-to-create-a-svelte-app-with-serverless.md new file mode 100644 index 0000000000..8cfc1dd789 --- /dev/null +++ b/_examples/how-to-create-a-svelte-app-with-serverless.md @@ -0,0 +1,248 @@ +--- +layout: example +title: How to create a Svelte app with serverless +short_title: Svelte +date: 2021-10-15 00:00:00 +lang: en +index: 4 +type: webapp +description: In this example we will look at how to create and deploy a full-stack SvelteKit app to AWS with SST. +short_desc: Full-stack SvelteKit app with a serverless API. +repo: quickstart-sveltekit +ref: how-to-create-a-svelte-app-with-serverless +comments_id: how-to-create-a-svelte-app-with-serverless/2522 +--- + +In this example we will look at how to create and deploy [SvelteKit](https://kit.svelte.dev) to AWS with SST. We'll be using the [`SvelteKitSite`]({{ site.v2_url }}/constructs/SvelteKitSite) construct. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create a SvelteKit app + +{%change%} Let's start by creating a SvelteKit app. We'll use TypeScript and just go with the defaults. + +```bash +$ npx create-svelte@latest +``` + +## Initialize SST in your app + +{%change%} Initialize SST in your Svelte app by running this in the root. + +```bash +$ npx create-sst@latest +$ npm install +``` + +This will detect that you are trying to configure a Svelte app. It'll add a `sst.config.ts` and a couple of packages to your `package.json`. + +```typescript +import type { SSTConfig } from "sst"; +import { Cron, Bucket, SvelteKitSite } from "sst/constructs"; + +export default { + config(_input) { + return { + name: "quickstart-sveltekit", + region: "us-east-1", + }; + }, + stacks(app) { + app.stack(function Site({ stack }) { + const site = new SvelteKitSite(stack, "site"); + + stack.addOutputs({ + url: site.url, + }); + }); + }, +} satisfies SSTConfig; +``` + +The `stacks` code describes the infrastructure of your serverless app. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}). + +You are **ready to deploy** your Svelte app at this point! But for the purpose of this example, we'll go a bit further and add a file uploads feature to our app. + +## Start the dev environment + +{%change%} Let's start our SST dev environment. + +```bash +$ npx sst dev +``` + +SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. This will ask you to start your Svelte dev environment as well. + +{%change%} Start Svelte locally in a separate terminal. + +```bash +$ npm run dev +``` + +This will run `sst bind next dev`. More on bind later. + +## Create our infrastructure + +To support file uploads in our app, we need an S3 bucket. Let's add that. + +### Add the table + +{%change%} Add the following above our `SvelteKitSite` definition in the `sst.config.ts`. + +```typescript +const bucket = new Bucket(stack, "public"); +``` + +Here we are using the [`Bucket`]({{ site.v2_url }}/constructs/Bucket) construct to create an S3 bucket. + +{%change%} Add it to the imports. + +```diff +- import { SvelteKitSite } from "sst/constructs"; ++ import { Bucket, SvelteKitSite } from "sst/constructs"; +``` + +### Bind it to our app + +We want our Svelte app to be able to access our bucket. + +{%change%} Add this to our Svelte definition in the `sst.config.ts`. + +```diff +- const site = new SvelteKitSite(stack, "site"); ++ const site = new SvelteKitSite(stack, "site", { ++ bind: [bucket], ++ }); +``` + +We'll see what bind does below. + +## Support file uploads + +Now to let our users upload files in our Svelte app we need to start by generating a presigned URL. This is a temporary URL that our frontend can make a request to upload files. + +### Generate a presigned URL + +{%change%} Create a `src/routes/+page.server.ts` with this. + +```typescript +export const load = (async () => { + const command = new PutObjectCommand({ + ACL: "public-read", + Key: crypto.randomUUID(), + Bucket: Bucket.public.bucketName, + }); + const url = await getSignedUrl(new S3Client({}), command); + + return { url }; +}) satisfies PageServerLoad; +``` + +This generates a presigned URL when our app loads. Note how we can access our S3 bucket in a typesafe way — `Bucket.public.bucketName`. [You can learn more about Resource Binding over on our docs]({{ site.v2_url }}/resource-binding). + +{%change%} We need to install a couple of packages. + +```bash +$ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner +``` + +{%change%} And add these to the imports. + +```typescript +import crypto from "crypto"; +import { Bucket } from "sst/node/bucket"; +import type { PageServerLoad } from "./$types"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +``` + +### Add an upload form + +Now let's add the form. + +{%change%} Replace the HTML in `src/routes/+page.svelte` with. + +```coffee +
+
+ + +
+
+``` + +{%change%} And add the upload handler as ` +``` + +Here we are adding a simple button that when clicked, makes a request to our API. We are getting the API endpoint from the environment variable, `import.meta.env.VITE_APP_API_URL`. + +The response from our API is then stored in our app's state. We use that to display the count of the number of times the button has been clicked. + +Let's add some styles. + +{%change%} Add a style tag with the below styles in `App.vue` file. + +```vue + +``` + +Now if you head over to your browser, your Vue app should look something like this. + +![Click counter UI in Vue app](/assets/examples/react-app/click-counter-ui-in-react-app.png) + +Of course if you click on the button multiple times, the count doesn't change. That's because we are not updating the count in our API. We'll do that next. + +## Making changes + +Let's update our table with the clicks. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +const putParams = { + TableName: Table.Counter.tableName, + Key: { + counter: "clicks", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your browser and click the button again, you should see the count increase! + +![Click counter updating in Vue app](/assets/examples/react-app/click-counter-updating-in-react-app.png) + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-vue-app-ExampleStack + + +Stack prod-vue-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com + SiteUrl: https://d8zfb4waxo6ct.cloudfront.net +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +If you head over to the `SiteUrl` in your browser, you should see your new Vue app in action! + +![Vue app deployed to AWS](/assets/examples/react-app/react-app-deployed-to-aws.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless click counter in Vue.js. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-a-websocket-api-with-serverless.md b/_examples/how-to-create-a-websocket-api-with-serverless.md new file mode 100644 index 0000000000..2a42209500 --- /dev/null +++ b/_examples/how-to-create-a-websocket-api-with-serverless.md @@ -0,0 +1,342 @@ +--- +layout: example +title: How to create a WebSocket API with serverless +short_title: WebSocket API +date: 2021-02-04 00:00:00 +lang: en +index: 2 +type: api +description: In this example we will look at how to create a serverless WebSocket API on AWS using SST. We'll be using the WebSocketApi construct to define the routes of our API. +short_desc: Building a simple WebSocket API. +repo: websocket +ref: how-to-create-a-websocket-api-with-serverless +comments_id: how-to-create-a-websocket-api-with-serverless/2397 +--- + +In this example we will look at how to create a serverless WebSocket API on AWS using [SST]({{ site.sst_github_repo }}). You'll be able to connect to the WebSocket API and send messages to all the connected clients in real time. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example websocket +$ cd websocket +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "websocket", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Storing connections + +We are going to use [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) to store the connection ids from all the clients connected to our WebSocket API. DynamoDB is a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Table, WebSocketApi } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Connections", { + fields: { + id: "string", + }, + primaryIndex: { partitionKey: "id" }, + }); +} +``` + +This creates a serverless DynamoDB table using [`Table`]({{ site.v2_url }}/constructs/Table). It has a primary key called `id`. Our table is going to look something like this: + +| id | +| -------- | +| abcd1234 | + +Where the `id` is the connection id as a string. + +## Setting up the WebSocket API + +Now let's add the WebSocket API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the WebSocket API +const api = new WebSocketApi(stack, "Api", { + defaults: { + function: { + bind: [table], + }, + }, + routes: { + $connect: "packages/functions/src/connect.main", + $disconnect: "packages/functions/src/disconnect.main", + sendmessage: "packages/functions/src/sendMessage.main", + }, +}); + +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are creating a WebSocket API using the [`WebSocketApi`]({{ site.v2_url }}/constructs/WebSocketApi) construct. It has a couple of routes; the `$connect` and `$disconnect` handles the requests when a client connects or disconnects from our WebSocket API. The `sendmessage` route handles the request when a client wants to send a message to all the connected clients. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +## Connecting clients + +Now in our functions, let's first handle the case when a client connects to our WebSocket API. + +{%change%} Add the following to `packages/functions/src/connect.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { APIGatewayProxyHandler } from "aws-lambda"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandler = async (event) => { + const params = { + TableName: Table.Connections.tableName, + Item: { + id: event.requestContext.connectionId, + }, + }; + + await dynamoDb.put(params).promise(); + + return { statusCode: 200, body: "Connected" }; +}; +``` + +Here when a new client connects, we grab the connection id from `event.requestContext.connectionId` and store it in our table. + +{%change%} We are using the `aws-sdk`, so let's install it in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +## Disconnecting clients + +Similarly, we'll remove the connection id from the table when a client disconnects. + +{%change%} Add the following to `packages/functions/src/disconnect.ts`. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { APIGatewayProxyHandler } from "aws-lambda"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandler = async (event) => { + const params = { + TableName: Table.Connections.tableName, + Key: { + id: event.requestContext.connectionId, + }, + }; + + await dynamoDb.delete(params).promise(); + + return { statusCode: 200, body: "Disconnected" }; +}; +``` + +Now before handling the `sendmessage` route, let's do a quick test. We'll leave a placeholder function there for now. + +{%change%} Add this to `packages/functions/src/sendMessage.ts`. + +```typescript +import { APIGatewayProxyHandler } from "aws-lambda"; + +export const main: APIGatewayProxyHandler = async (event) => { + return { statusCode: 200, body: "Message sent" }; +}; +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-websocket-ExampleStack: deploying... + + ✅ dev-websocket-ExampleStack + + +Stack dev-websocket-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: wss://oivzpnqnb6.execute-api.us-east-1.amazonaws.com/dev +``` + +The `ApiEndpoint` is the WebSocket API we just created. Let's test our endpoint. + +Head over to [**WebSocket Echo Test**](https://www.piesocket.com/websocket-tester) to create a WebSocket client that'll connect to our API. + +Enter the `ApiEndpoint` from above as the **url** field and hit **Connect**. + +![Connect to serverless WebSocket API](/assets/examples/websocket/connect-to-serverless-websocket-api.png) + +You should see `CONNECTED` being printed out in the **Log**. + +![Serverless WebSocket API response](/assets/examples/websocket/serverless-websocket-api-response.png) + +Whenever a new client is connected to the API, we will store the connection ID in the DynamoDB **Connections** table. + +Let's go to the **DynamoDB** tab in the SST Console and check that the value has been created in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of connections table](/assets/examples/websocket/dynamo-table-view-of-connections-table.png) + +You should see a random connection ID created in the table. + +## Sending messages + +Now let's update our function to send messages. + +{%change%} Replace your `packages/functions/src/sendMessage.ts` with: + +```typescript +import { DynamoDB, ApiGatewayManagementApi } from "aws-sdk"; +import { Table } from "sst/node/table"; +import { APIGatewayProxyHandler } from "aws-lambda"; + +const TableName = Table.Connections.tableName; +const dynamoDb = new DynamoDB.DocumentClient(); + +export const main: APIGatewayProxyHandler = async (event) => { + const messageData = JSON.parse(event.body).data; + const { stage, domainName } = event.requestContext; + + // Get all the connections + const connections = await dynamoDb + .scan({ TableName, ProjectionExpression: "id" }) + .promise(); + + const apiG = new ApiGatewayManagementApi({ + endpoint: `${domainName}/${stage}`, + }); + + const postToConnection = async function ({ id }) { + try { + // Send the message to the given client + await apiG + .postToConnection({ ConnectionId: id, Data: messageData }) + .promise(); + } catch (e) { + if (e.statusCode === 410) { + // Remove stale connections + await dynamoDb.delete({ TableName, Key: { id } }).promise(); + } + } + }; + + // Iterate through all the connections + await Promise.all(connections.Items.map(postToConnection)); + + return { statusCode: 200, body: "Message sent" }; +}; +``` + +We are doing a couple of things here: + +1. We first JSON decode our message body. +2. Then we grab all the connection ids from our table. +3. We iterate through all the ids and use the `postToConnection` method of the [`AWS.ApiGatewayManagementApi`](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayManagementApi.html) class to send out our message. +4. If it fails to send the message because the connection has gone stale, we delete the connection id from our table. + +Now let's do a complete test! + +Create another client by opening the [**WebSocket Echo Test**](https://www.piesocket.com/websocket-tester) page in **a different browser window**. Just like before, paste the `ApiEndpoint` as the **url** and hit **Connect**. + +![Connect to serverless WebSocket API again](/assets/examples/websocket/connect-to-serverless-websocket-api-again.png) + +Once connected, paste the following into the **Message** field and hit **Send**. + +``` +{"action":"sendmessage", "data":"Hello World"} +``` + +![Send message to serverless WebSocket API](/assets/examples/websocket/send-message-to-serverless-websocket-api.png) + +You'll notice in the **Log** that it sends the message (`SENT:`) and receives it as well (`RECEIVED:`). + +Also, if you flip back to our original WebSocket client window, you'll notice that the message was received there as well! + +![Receive message from serverless WebSocket API](/assets/examples/websocket/receive-message-from-serverless-websocket-api.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless WebSocket API. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-an-angular-app-with-serverless.md b/_examples/how-to-create-an-angular-app-with-serverless.md new file mode 100644 index 0000000000..b72320d90a --- /dev/null +++ b/_examples/how-to-create-an-angular-app-with-serverless.md @@ -0,0 +1,543 @@ +--- +layout: example +title: How to create an Angular app with serverless +short_title: Angular +date: 2021-11-17 00:00:00 +lang: en +index: 6 +type: webapp +description: In this example we will look at how to use Angular with a serverless API to create a simple click counter app. We'll be using SST, the StaticSite construct, and the SST Console to deploy our app to AWS S3 and CloudFront. +short_desc: Full-stack Angular app with a serverless API. +repo: angular-app +ref: how-to-create-an-angular-app-with-serverless +comments_id: how-to-create-an-angular-app-with-serverless/2599 +--- + +In this example we will look at how to use [Angular](https://angular.io) with a [serverless]({% link _chapters/what-is-serverless.md %}) API to create a simple click counter app. We'll be using the [SST]({{ site.sst_github_repo }}) and the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite#creating-an-Angular-site) construct to deploy our app to AWS. + +## Requirements + +- Node.js 16 or later +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example angular-app +$ cd angular-app +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "angular-app", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +3. `packages/frontend/` — Angular app + + The code for our frontend Angular app. + +## Create our infrastructure + +Our app is made up of a simple API and an Angular app. The API will be talking to a database to store the number of clicks. We'll start by creating the database. + +### Adding the table + +We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { + Api, + StaticSite, + StackContext, + Table, + StaticSiteErrorOptions, +} from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Counter", { + fields: { + counter: "string", + }, + primaryIndex: { partitionKey: "counter" }, + }); +} +``` + +This creates a serverless DynamoDB table using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. It has a primary key called `counter`. Our table is going to look something like this: + +| counter | tally | +| ------- | ----- | +| clicks | 123 | + +### Creating our API + +Now let's add the API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.main", + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +### Setting up our Angular app + +To deploy an Angular app to AWS, we'll be using the SST [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite#creating-an-angular-site) construct. + +{%change%} Replace the following in `stacks/ExampleStack.ts`: + +```typescript +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +{%change%} With: + +```typescript +const site = new StaticSite(stack, "AngularSite", { + path: "frontend", + buildOutput: "dist", + buildCommand: "ng build --output-path dist", + errorPage: StaticSiteErrorOptions.REDIRECT_TO_INDEX_PAGE, + // To load the API URL from the environment in development mode (environment.ts) + environment: { + DEV_API_URL: api.url, + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + SiteUrl: site.url, + ApiEndpoint: api.url, +}); +``` + +The construct is pointing to where our Angular app is located. We haven't created our app yet but for now we'll point to the `frontend` directory. + +We are also setting up an [Angular environment variable](https://Angular.io/guide/build) `DEV_API_URL` with the endpoint of our API. The [`StaticSite`]({{ site.v2_url }}/constructs/StaticSite#creating-an-Angular-site) allows us to set environment variables automatically from our backend, without having to hard code them in our frontend. + +You can also optionally configure a custom domain. + +```typescript +// Deploy our Angular app +const site = new StaticSite(stack, "AngularSite", { + // ... + customDomain: "www.my-angular-app.com", +}); +``` + +But we'll skip this for now. + +### Reading from our table + +Our API is powered by a Lambda function. In the function we'll read from our DynamoDB table. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function main() { + const getParams = { + // Get the table name from the environment variable + TableName: Table.Counter.tableName, + // Get the row where the counter is called "clicks" + Key: { + counter: "clicks", + }, + }; + const results = await dynamoDb.get(getParams).promise(); + + // If there is a row, then get the value of the + // column called "tally" + let count = results.Item ? results.Item.tally : 0; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We make a `get` call to our DynamoDB table and get the value of a row where the `counter` column has the value `clicks`. Since we haven't written to this column yet, we are going to just return `0`. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-angular-app-ExampleStack: deploying... + + ✅ dev-angular-app-ExampleStack + + +Stack dev-angular-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://sez1p3dsia.execute-api.ap-south-1.amazonaws.com + SiteUrl: https://d2uyljrh4twuwq.cloudfront.net +``` + +The `ApiEndpoint` is the API we just created. While the `SiteUrl` is where our Angular app will be hosted. For now it's just a placeholder website. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Setting up our Angular app + +We are now ready to use the API we just created. Let's use the [Angular CLI](https://angular.io/cli) to setup our Angular app. + +{%change%} Run the following in the project root. + +```bash +$ npm install -g @angular/cli +$ ng new packages/frontend +$ cd packages/frontend +``` + +This sets up our Angular app in the `packages/frontend/` directory. Recall that, earlier in the guide we were pointing the `StaticSite` construct to this path. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +In Angular, we have our `environment.ts` and `environment.prod.ts` files defined in the `src/environments` folder. The `environment.ts` file is where we usually keep our environment variables by convention, as the Angular compiler looks for these files before the build process. But we don't want to hard code these. We want them automatically set from our backend. To do this we'll use a script that generates env variables at build time. + +{%change%} Create a `setenv.ts` file inside `frontend/scripts` folder and add the below code + +```typescript +/* eslint-disable @typescript-eslint/no-var-requires */ +const { writeFile } = require("fs"); + +const targetPath = `./src/environments/environment.ts`; + +const environmentFileContent = ` +export const environment = { + production: ${false}, + API_URL: "${process.env["DEV_API_URL"]}", +}; +`; +// write the content to the respective file +writeFile(targetPath, environmentFileContent, function (err: unknown) { + if (err) { + console.log(err); + } + console.log(`Wrote variables to ${targetPath}`); +}); +``` + +The above script creates the environment file, `environment.ts` for dev and populates it with the variables from your `.env` file (available in `process.env`) with our `API_URL`. + +We need to update our scripts to use this and the [`sst bind`](https://docs.sst.dev/packages/sst#sst-bind) command. + +{%change%} Update the `package.json` in the `packages/frontend/` directory. + +```typescript +{ + // ... + "scripts": { + // ... + "config": "ts-node ./scripts/setenv.ts", + "start": "sst bind npm run config && ng serve", + // ... + }, + // ... +} +``` + +{%change%} Install `ts-node`. + +```bash +$ npm install ts-node --save-dev +``` + +Let's start our Angular development environment. + +{%change%} In the `packages/frontend/` directory run. + +```bash +$ npm run start +``` + +Open up your browser and go to `http://localhost:4200`. + +### Add the click button + +We are now ready to add the UI for our app and connect it to our serverless API. + +{%change%} Replace `packages/frontend/src/app/app.component.html` with. + +```html +
+
+

You clicked me {{ response }} times.

+ +
+
+``` + +{%change%} Replace `packages/frontend/src/app/app.component.ts` with. + +```typescript +import { environment } from "./../environments/environment"; +import { Component } from "@Angular/core"; +import { HttpClient } from "@Angular/common/http"; + +@Component({ + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.css"], +}) +export class AppComponent { + response = "0"; + constructor(private http: HttpClient) {} + + onClick() { + this.http.post(environment.API_URL, {}).subscribe((data: any) => { + this.response = data; + }); + } +} +``` + +Here we are adding a simple button that when clicked, makes a request to our API. We are getting the API endpoint from the environment. + +The response from our API is then stored in our app's state. We use that to display the count of the number of times the button has been clicked. + +We need `HttpClientModule` to make API calls with our API, To make `HttpClientModule` available everywhere in the app, replace code in `app.module.ts` with below. + +{%change%} Replace `packages/frontend/src/app/app.module.ts` with. + +```typescript +import { HttpClientModule } from "@Angular/common/http"; +import { NgModule } from "@Angular/core"; +import { BrowserModule } from "@Angular/platform-browser"; + +import { AppComponent } from "./app.component"; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, HttpClientModule], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +Let's add some styles. + +{%change%} Replace `packages/frontend/src/app/app.component.css` with. + +```css +.App { + display: grid; + height: 100vh; + place-content: center; +} +p { + margin-top: 0; + font-size: 20px; +} +button { + font-size: 48px; +} +``` + +Now if you head over to your browser, your Angular app should look something like this. + +![Click counter UI in Angular app](/assets/examples/react-app/click-counter-ui-in-react-app.png) + +Of course if you click on the button multiple times, the count doesn't change. That's because we are not updating the count in our API. We'll do that next. + +## Making changes + +Let's update our table with the clicks. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +const putParams = { + TableName: Table.Counter.tableName, + Key: { + counter: "clicks", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your browser and click the button again, you should see the count increase! + +![Click counter updating in Angular app](/assets/examples/react-app/click-counter-updating-in-react-app.png) + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +To wrap things up we'll deploy our app to prod. + +However the current way of loading environment variables only works in dev, as we can't use `sst bind` in prod. To load the environment variables from `process.env` in production we need to make a couple of changes. + +We'll replace placeholder env values in `environment.prod.ts` in our app with the [deployed values]({{ site.v2_url }}/constructs/StaticSite#replace-deployed-values). + +{%change%} Replace `packages/frontend/src/environments/environment.prod.ts` with. + +```typescript +export const environment = { + production: true, + API_URL: "{{ PROD_API_URL }}", +}; +``` + +{%change%} In `stacks/ExampleStack.ts` add the following, right below the `environment` key. + +```typescript +// To load the API URL from the environment in production mode (environment.prod.ts) +replaceValues: [ + { + files: "**/*.js", + search: "{{ PROD_API_URL }}", + replace: api.url, + }, +], +``` + +{%raw%} +This replaces `{{ PROD_API_URL }}` with the deployed API endpoint in all the `.js` files in your compiled Angular app. +{%endraw%} + +{%change%} That's it, now run the deploy command. + +```bash +$ npx sst deploy --stage prod +``` + +The `--stage` option allows us to separate our environments, so when we are working in locally, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-angular-app-ExampleStack + + +Stack prod-angular-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com + SiteUrl: https://d1wuzrecqjflrh.cloudfront.net +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +If you head over to the `SiteUrl` in your browser, you should see your new Angular app in action! + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless click counter app built with Angular. A local development environment, to test and make changes. A web based dashboard to manage your app. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-an-apollo-graphql-api-with-serverless.md b/_examples/how-to-create-an-apollo-graphql-api-with-serverless.md new file mode 100644 index 0000000000..48c63408d6 --- /dev/null +++ b/_examples/how-to-create-an-apollo-graphql-api-with-serverless.md @@ -0,0 +1,226 @@ +--- +layout: example +title: How to create an Apollo GraphQL API with serverless +short_title: Apollo +date: 2021-03-27 00:00:00 +lang: en +index: 1 +type: graphql +description: In this example we will look at how to create an Apollo GraphQL API on AWS using SST. We'll be using the GraphQLApi construct to define the Apollo Lambda server. +short_desc: Building a serverless GraphQL API with Apollo. +repo: graphql-apollo +ref: how-to-create-an-apollo-graphql-api-with-serverless +comments_id: how-to-create-an-apollo-graphql-api-with-serverless/2361 +--- + +In this example we'll look at how to create an [Apollo GraphQL API](https://www.apollographql.com) on AWS using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example graphql-apollo +$ cd graphql-apollo +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "graphql-apollo", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our infrastructure + +Let's start by setting up our GraphQL API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { GraphQLApi, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the GraphQL API + const api = new GraphQLApi(stack, "ApolloApi", { + server: { + handler: "packages/functions/src/lambda.handler", + bundle: { + format: "cjs", + }, + }, + }); + + // Show the API endpoint in output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are creating an Apollo GraphQL API here using the [`GraphQLApi`]({{ site.v2_url }}/constructs/GraphQLApi) construct. Our Apollo Server is powered by the Lambda function in `packages/functions/src/lambda.ts`. + +## Adding function code + +For this example, we are not using a database. We'll look at that in detail in another example. So we'll just be printing out a simple string. + +{%change%} Let's add a file that contains our notes in `packages/functions/src/lambda.ts`. + +```typescript +import { gql, ApolloServer } from "apollo-server-lambda"; + +const typeDefs = gql` + type Query { + hello: String + } +`; + +const resolvers = { + Query: { + hello: () => "Hello, World!", + }, +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, + introspection: !!process.env.IS_LOCAL, +}); + +export const handler = server.createHandler(); +``` + +Here we are creating an Apollo Server. We are also enabling introspection if we are running our Lambda function locally. SST sets the `process.env.IS_LOCAL` when run locally. + +{%change%} Let's install `apollo-server-lambda` in the `packages/functions/` folder. + +```bash +$ npm install apollo-server-lambda +``` + +We also need to quickly update our `tsconfig.json` to work with the Apollo Server package. + +{%change%} Add the following to the `compilerOptions` block in the `tsconfig.json`. + +```json +"esModuleInterop": true +``` + +Now let's test our new Apollo GraphQL API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-graphql-apollo-ExampleStack: deploying... + + ✅ dev-graphql-apollo-ExampleStack + + +Stack dev-graphql-apollo-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://keocx594ue.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **GraphQL** tab and you should see the GraphQL Playground in action. + +Note, The GraphQL explorer lets you query GraphQL endpoints created with the GraphQLApi and AppSyncApi constructs in your app. + +Now let's run our query. Paste the following on the left and hit the run button. + +```graphql +query { + hello +} +``` + +![Apollo GraphQL Playground Hello World](/assets/examples/graphql-apollo/apollo-graphql-playground-hello-world.png) +You should see `Hello, World!`. + +## Making changes + +Let's make a quick change to our API. + +{%change%} In `packages/functions/src/lambda.ts` replace `Hello, World!` with `Hello, New World!`. + +```typescript +const resolvers = { + Query: { + hello: () => "Hello, New World!", + }, +}; +``` + +If you head back to the GraphQL playground in SST console and run the query again, you should see the change! + +![Apollo GraphQL Playground Hello New World](/assets/examples/graphql-apollo/apollo-graphql-playground-hello-new-world.png) + +## Deploying your API + +Now that our API is tested, let's deploy it to production. You'll recall that we were using a `dev` environment, the one specified in our `sst.config.ts`. However, we are going to deploy it to a different environment. This ensures that the next time we are developing locally, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless Apollo GraphQL API. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-create-an-expo-app-with-serverless.md b/_examples/how-to-create-an-expo-app-with-serverless.md new file mode 100644 index 0000000000..b5fae6ef06 --- /dev/null +++ b/_examples/how-to-create-an-expo-app-with-serverless.md @@ -0,0 +1,428 @@ +--- +layout: example +title: How to create an Expo app with serverless +short_title: Expo +date: 2021-10-23 00:00:00 +lang: en +index: 1 +type: mobileapp +description: In this example we will look at how to use Expo with a serverless API to create a simple click counter app. We'll be using SST. +short_desc: Native app with Expo and a serverless API. +repo: expo-app +ref: how-to-create-an-expo-app-with-serverless +comments_id: how-to-create-an-expo-app-with-serverless/2515 +--- + +In this example we will look at how to use [Expo](https://expo.dev) with a [serverless]({% link _chapters/what-is-serverless.md %}) API to create a simple click counter app. We'll be using the [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example expo-app +$ cd expo-app +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "expo-app", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +3. `packages/frontend/` — Expo App + + The code for our frontend Expo app. + +## Create our infrastructure + +Our app is made up of a simple API and a Expo app. The API will be talking to a database to store the number of clicks. We'll start by creating the database. + +### Adding the table + +We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Table, Api } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Counter", { + fields: { + counter: "string", + }, + primaryIndex: { partitionKey: "counter" }, + }); +} +``` + +This creates a serverless DynamoDB table using the SST [`Table`]({{ site.v2_url }}/constructs/Table) construct. It has a primary key called `counter`. Our table is going to look something like this: + +| counter | tally | +| ------- | ----- | +| clicks | 123 | + +### Creating our API + +Now let's add the API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.main", + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +### Reading from our table + +Our API is powered by a Lambda function. In the function we'll read from our DynamoDB table. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function main() { + const getParams = { + // Get the table name from the environment variable + TableName: Table.Counter.tableName, + // Get the row where the counter is called "clicks" + Key: { + counter: "clicks", + }, + }; + const results = await dynamoDb.get(getParams).promise(); + + // If there is a row, then get the value of the + // column called "tally" + let count = results.Item ? results.Item.tally : 0; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We make a `get` call to our DynamoDB table and get the value of a row where the `counter` column has the value `clicks`. Since we haven't written to this column yet, we are going to just return `0`. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-expo-app-ExampleStack: deploying... + + ✅ dev-expo-app-ExampleStack + + +Stack dev-expo-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://sez1p3dsia.execute-api.ap-south-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Setting up our Expo app + +We are now ready to use the API we just created. Let's use [Expo CLI](https://docs.expo.dev/workflow/expo-cli/) to setup our Expo app. + +{%change%} Run the following in the project root and create a **blank** project. + +```bash +$ npm install -g expo-cli +$ expo init packages/frontend +$ cd packages/frontend +``` + +![Blank Expo app](/assets/examples/expo-app/expo-setup.png) + +This sets up our Expo app in the `packages/frontend/` directory. + +We also need to load the environment variables from our SST app. To do this, we'll be using the [`babel-plugin-inline-dotenv`](https://github.com/brysgo/babel-plugin-inline-dotenv) package. + +{%change%} Install the `babel-plugin-inline-dotenv` package by running the following in the `packages/frontend/` directory. + +```bash +$ npm install babel-plugin-inline-dotenv +``` + +We need to update our script to use this package in `babel.config.js`. + +{%change%} Update your `babel.config.js` like below. + +```js +module.exports = function (api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + plugins: ["inline-dotenv"], + }; +}; +``` + +{%change%} Create a `.env` file inside `packages/frontend/` and create two variables to hold dev and prod API endpoints and replace `DEV_API_URL` with the deployed URL from the steps above. + +``` +DEV_API_URL=https://sez1p3dsia.execute-api.us-east-1.amazonaws.com +PROD_API_URL= +``` + +We'll add the `PROD_API_URL` later in this example. + +Let's start our Expo development environment. + +{%change%} In the `packages/frontend/` directory run the following for the iOS emulator. + +```bash +$ expo start --ios +``` + +{%change%} Or run this for the Android emulator. + +```bash +$ expo start --android +``` + +This will open up an emulator and load your app. + +### Add the click button + +We are now ready to add the UI for our app and connect it to our serverless API. + +{%change%} Replace `packages/frontend/App.js` with. + +```jsx +/* eslint-disable no-undef */ +import { StatusBar } from "expo-status-bar"; +import React, { useState } from "react"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; + +export default function App() { + const [count, setCount] = useState(0); + + const API_URL = __DEV__ ? process.env.DEV_API_URL : process.env.PROD_API_URL; + + function onClick() { + fetch(API_URL, { + method: "POST", + }) + .then((response) => response.text()) + .then(setCount); + } + + return ( + + + You clicked me {count} times. + + Click me! + + + ); +} +``` + +Here we are adding a simple button that when clicked, makes a request to our API. We are getting the API endpoint from the environment variable. + +The response from our API is then stored in our app's state. We use it to display the count of the number of times the button has been clicked. + +Let's add some styles. + +{%change%} Add a `StyleSheet` in your `App.js`. + +```jsx +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#fff", + alignItems: "center", + justifyContent: "center", + }, + btn: { + backgroundColor: "lightblue", + padding: 10, + margin: 10, + borderRadius: 5, + }, +}); +``` + +Now if you head over to your emulator, your Expo app should look something like this. + +![Click counter UI in Expo app](/assets/examples/expo-app/click-counter-ui-in-expo-app.png){: width="432" } + +Of course if you click on the button multiple times, the count doesn't change. That's because we are not updating the count in our API. We'll do that next. + +## Making changes + +Let's update our table with the clicks. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.js`. + +```typescript +const putParams = { + TableName: Table.Counter.tableName, + Key: { + counter: "clicks", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your emulator and click the button again, you should see the count increase! + +![Click counter updating in Expo app](/assets/examples/expo-app/click-counter-updating-in-expo-app.png){: width="432" } + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working locally it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-expo-app-ExampleStack + + +Stack prod-expo-app-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com +``` + +{%change%} Add the above endpoint to the `.env` file in `frontend/.env` as the production API endpoint + +``` +DEV_API_URL=https://hfv2gyuwdh.execute-api.us-east-1.amazonaws.com +PROD_API_URL=https://k40qchmtvf.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +Now we are ready to ship our app! + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless click counter Expo app. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-debug-lambda-functions-with-intellij-idea.md b/_examples/how-to-debug-lambda-functions-with-intellij-idea.md new file mode 100644 index 0000000000..a49812b475 --- /dev/null +++ b/_examples/how-to-debug-lambda-functions-with-intellij-idea.md @@ -0,0 +1,247 @@ +--- +layout: example +title: How to debug Lambda functions with IntelliJ IDEA +short_title: Debug With IntelliJ +date: 2021-11-13 00:00:00 +lang: en +index: 3 +type: editor +description: In this example we will look at how to debug AWS Lambda functions with IntelliJ IDEA using SST. +short_desc: Using IntelliJ IDEA to debug serverless apps. +repo: intellij-idea +ref: how-to-debug-lambda-functions-with-intellij-idea +comments_id: how-to-debug-lambda-functions-with-intellij-idea/2530 +--- + +In this example we will look at how to debug AWS Lambda functions with [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/) using [SST]({{ site.sst_github_repo }}). + +SST allows you to build and test Lambda functions locally using [Live Lambda Development]({{ site.v2_url }}/live-lambda-development). This means that you can attach breakpoints and inspect your Lambda functions locally, even if they are invoked remotely. + +Here is a video of it in action. + +
+ +
+ +Let's look at how. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example intellij-idea +$ cd intellij-idea +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "intellij-idea", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our API + +For this example we'll be testing using a simple API endpoint. + +Our API is defined in the `stacks/ExampleStack.ts`. + +```typescript +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +## Adding function code + +Our functions are stored in the `packages/functions/` directory. In this case, we have a simple Lambda function that's printing out the time the request was made. + +{%change%} Replace your `packages/functions/src/lambda.ts` with. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const message = `The time in Lambda is ${event.requestContext.time}.`; + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! ${message}`, + }; +}; +``` + +## Adding IntelliJ IDEA Debug Configuration + +To allow IntelliJ IDEA to set breakpoints and debug our Lambda functions we'll add it to our [Debug Configurations](https://www.jetbrains.com/help/idea/run-debug-configuration.html). + +Select the `package.json` from the left panel, click on the `▶️` icon next to the `dev` script, and then select **Modify Run Configuration**. + +![Select run icon beside start script in IntelliJ](/assets/examples/intellij-idea/select-run-icon-beside-start-script-in-intellij.png) + +It will open up a dialog where you need to configure the settings as per the project, IntelliJ IDEA does it automatically for us. Make sure your settings looks like below. + +![Create run configuration in IntelliJ](/assets/examples/intellij-idea/create-run-configuration-in-intellij.png) + +## Extending Lambda function timeouts + +Since we are going to set breakpoints in our Lambda functions, it makes sense to increase the timeouts. + +SST has an [`--increase-timeout`]({{ site.v2_url }}/packages/cli#options) option that increases the function timeouts in your app to the maximum 15 minutes. + +{%change%} Add `--increase-timeout` to the arguments to increase the timeout. + +![Set increase timeout in run configuration in IntelliJ](/assets/examples/intellij-idea/set-increase-timeout-in-run-configuration-in-intellij.png) + +Note that, this doesn't increase the timeout of an API. Since the API Gateway timeout cannot be increased for more than 30 seconds. But you can continue debugging the Lambda function, even after the API request times out. + +## Starting your dev environment + +Now if you navigate to `packages/functions/src/lambda.ts`, you can set a breakpoint. + +Click on **Debug** button to start the debugging + +![Set Lambda function breakpoint in IntelliJ](/assets/examples/intellij-idea/set-lambda-function-breakpoint-in-intellij.png) + +The first time you start the [Live Lambda Development environment]({{ site.v2_url }}/live-lambda-development), you will be prompted to enter a stage name to use locally. If you are working within a team, it is recommended that you use a stage that's specific to you. This ensures that you and your teammate can share an AWS account and still have standalone environments. [Read more about this over on our docs]({{ site.v2_url }}/working-with-your-team). + +Note that the prompt will be shown under the **Process Console** tab. + +![Enter stage name in Process Console](/assets/examples/intellij-idea/enter-stage-name-in-process-console.png) + +It'll then take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-intellij-idea-ExampleStack: deploying... + + ✅ dev-intellij-idea-ExampleStack + + +Stack dev-intellij-idea-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://siyp617yh1.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. Now if you head over to that endpoint in your browser, you'll notice that you'll hit the breakpoint. + +![Hitting a breakpoint in a Lambda function in IntelliJ](/assets/examples/intellij-idea/hitting-a-breakpoint-in-a-lambda-function-in-intellij.png) + +## Making changes + +An advantage of using the Live Lambda Development environment is that you can make changes without having to redeploy them. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}; +``` + +Now if you head back to the endpoint. + +``` +https://siyp617yh1.execute-api.us-east-1.amazonaws.com +``` + +You should see the new message being printed out. + +## Making improvements + +If you're running into any memory issues while debugging, you can disable the unused plugins and exclude the folders that are not included in the source code. Right click on the folder and mark it as **Excluded**. In this case we are marking the `node_modules/` directory as _Excluded_. + +![Exclude folders in IntelliJ](/assets/examples/intellij-idea/excluded-folders-in-intellij.png) + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a dev environment. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in our local environment, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API. A local development environment, to test and make changes. And you can use IntelliJ IDEA to debug and set breakpoints in your Lambda functions. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-debug-lambda-functions-with-visual-studio-code.md b/_examples/how-to-debug-lambda-functions-with-visual-studio-code.md new file mode 100644 index 0000000000..41851a61a7 --- /dev/null +++ b/_examples/how-to-debug-lambda-functions-with-visual-studio-code.md @@ -0,0 +1,250 @@ +--- +layout: example +title: How to debug Lambda functions with Visual Studio Code +short_title: Debug With VS Code +date: 2021-04-26 00:00:00 +lang: en +index: 1 +type: editor +description: In this example we will look at how to debug AWS Lambda functions with Visual Studio Code using SST. +short_desc: Using VS Code to debug serverless apps. +repo: vscode +ref: how-to-debug-lambda-functions-with-visual-studio-code +comments_id: how-to-debug-lambda-functions-with-visual-studio-code/2388 +--- + +In this example we will look at how to debug AWS Lambda functions with [Visual Studio Code (VS Code)](https://code.visualstudio.com) using [SST]({{ site.sst_github_repo }}). + +SST allows you to build and test Lambda functions locally using [Live Lambda Development]({{ site.v2_url }}/live-lambda-development). This means that you can attach breakpoints and inspect your Lambda functions locally, even if they are invoked remotely. + +Here is a video of it in action. + +
+ +
+ +Let's look at how. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript in this example but you can use regular JavaScript as well +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example vscode +$ cd vscode +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "vscode", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our API + +For this example we'll be testing using a simple API endpoint. + +{%change%} Replace your `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the API endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +## Adding function code + +Our functions are stored in the `packages/functions/` directory. In this case, we'll have a simple Lambda function that's printing out the time the request was made. + +{%change%} Create a `packages/functions/src/lambda.ts` with. + +```typescript +import { APIGatewayProxyEventV2, APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async ( + event: APIGatewayProxyEventV2 +) => { + const message = `The time in Lambda is ${event.requestContext.time}.`; + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! ${message}`, + }; +}; +``` + +## Adding VS Code Launch Configurations + +To allow VS Code to set breakpoints and debug our Lambda functions we'll add it to our [Launch Configurations](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations). The starter template should already come with this but you can also add it manually by doing the following. + +{%change%} Create a `.vscode/launch.json` file with. + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug SST Dev", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["start", "--increase-timeout"], + "console": "integratedTerminal", + "skipFiles": ["/**"] + } + ] +} +``` + +## Extending Lambda function timeouts + +Since we are going to set breakpoints in our Lambda functions, it makes sense to increase the timeouts. + +SST has an [`--increase-timeout`]({{ site.v2_url }}/packages/sst#options) option that increases the function timeouts in your app to the maximum 15 minutes. We are using this option in our `launch.json`. + +```typescript +"runtimeArgs": ["start", "--increase-timeout"], +``` + +Note that, this doesn't increase the timeout of an API. Since those cannot be increased for more than 30 seconds. But you can continue debugging the Lambda function, even after the API request times out. + +## Starting your dev environment + +Now if you open up your project in VS Code, you can set a breakpoint in your `packages/functions/src/lambda.ts`. + +Next, head over to the **Run And Debug** tab > select the above configured **Debug SST Dev**, and hit **Play**. + +![Set Lambda function breakpoint in VS Code](/assets/examples/vscode/set-lambda-function-breakpoint-in-vs-code.png) + +The first time you start the Live Lambda Development environment, it'll take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-vscode-ExampleStack: deploying... + + ✅ dev-vscode-ExampleStack + + +Stack dev-vscode-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://siyp617yh1.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. Now if you head over to that endpoint in your browser, you'll notice that you'll hit the breakpoint. + +![Hitting a breakpoint in a Lambda function in VS Code](/assets/examples/vscode/set-lambda-hitting-a-breakpoint-in-a-lambda-function-in-vs-code.png) + +## Making changes + +An advantage of using the Live Lambda Development environment is that you can make changes without having to redeploy them. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { APIGatewayProxyEventV2, APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async ( + event: APIGatewayProxyEventV2 +) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}; +``` + +Now if you head back to the endpoint. + +``` +https://siyp617yh1.execute-api.us-east-1.amazonaws.com/ +``` + +You should see the new message being printed out. + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in our local stage, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API. A local development environment, to test and make changes. And you can use Visual Studio Code to debug and set breakpoints in your Lambda functions. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-debug-lambda-functions-with-webstorm.md b/_examples/how-to-debug-lambda-functions-with-webstorm.md new file mode 100644 index 0000000000..673d17a4c8 --- /dev/null +++ b/_examples/how-to-debug-lambda-functions-with-webstorm.md @@ -0,0 +1,249 @@ +--- +layout: example +title: How to debug Lambda functions with WebStorm +short_title: Debug With WebStorm +date: 2021-11-13 00:00:00 +lang: en +index: 2 +type: editor +description: In this example we will look at how to debug AWS Lambda functions with WebStorm using SST. +short_desc: Using WebStorm to debug serverless apps. +repo: webstorm +ref: how-to-debug-lambda-functions-with-webstorm +comments_id: how-to-debug-lambda-functions-with-webstorm/2529 +--- + +In this example we will look at how to debug AWS Lambda functions with [WebStorm](https://www.jetbrains.com/webstorm/) using [SST]({{ site.sst_github_repo }}). + +SST allows you to build and test Lambda functions locally using [Live Lambda Development]({{ site.v2_url }}/live-lambda-development). This means that you can attach breakpoints and inspect your Lambda functions locally, even if they are invoked remotely. + +Here is a video of it in action. + +
+ +
+ +Let's look at how. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example webstorm +$ cd webstorm +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "webstorm", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Setting up our API + +For this example we'll be testing using a simple API endpoint. + +Our API is defined in the `stacks/ExampleStack.ts`. + +```typescript +import { StackContext, Api } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +## Adding function code + +Our functions are stored in the `packages/functions/` directory. In this case, we have a simple Lambda function that's printing out the time the request was made. + +{%change%} Replace your `packages/functions/src/lambda.ts` with. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const message = `The time in Lambda is ${event.requestContext.time}.`; + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! ${message}`, + }; +}; +``` + +## Adding WebStorm Debug Configuration + +To allow WebStorm to set breakpoints and debug our Lambda functions we'll add it to our [Debug Configurations](https://www.jetbrains.com/help/webstorm/running-and-debugging-node-js.html#running). + +Select the `package.json` from the left panel, click on the `▶️` icon next to the `dev` script, and then select **Modify Run Configuration**. + +![Select run icon beside start script in WebStorm](/assets/examples/webstorm/select-run-icon-beside-start-script-in-webstorm.png) + +It will open up a dialog where you need to configure the settings as per the project, WebStorm does it automatically for us. Make sure your settings look like below. + +![Create run configuration in WebStorm](/assets/examples/webstorm/create-run-configuration-in-webstorm.png) + +In some versions of WebStorm you might need to disable stepping through library scripts. You can do this by heading to **Preferences** > **Build, Execution, Deployment** > **Debugger** > **Stepping** > unchecking **Do not step into library scripts**. + +## Extending Lambda function timeouts + +Since we are going to set breakpoints in our Lambda functions, it makes sense to increase the timeouts. + +SST has an [`--increase-timeout`]({{ site.v2_url }}/packages/cli#options) option that increases the function timeouts in your app to the maximum 15 minutes. + +{%change%} Add `--increase-timeout` to the arguments to increase the timeout. + +![Set increase timeout in run configuration in WebStorm](/assets/examples/webstorm/set-increase-timeout-in-run-configuration-in-webstorm.png) + +Note that, this doesn't increase the timeout of an API. Since the API Gateway timeout cannot be increased for more than 30 seconds. But you can continue debugging the Lambda function, even after the API request times out. + +## Starting your dev environment + +Now if you navigate to `packages/functions/src/lambda.ts`, you can set a breakpoint. + +Click on **Debug** icon to start the debugging + +![Set Lambda function breakpoint in WebStorm](/assets/examples/webstorm/set-lambda-function-breakpoint-in-webstorm.png) + +The first time you start the [Live Lambda Development environment]({{ site.v2_url }}/live-lambda-development), you will be prompted to enter a stage name to use locally. If you are working within a team, it is recommended that you use a stage that's specific to you. This ensures that you and your teammate can share an AWS account and still have standalone environments. [Read more about this over on our docs]({{ site.v2_url }}/working-with-your-team). + +Note that the prompt will be shown under the **Process Console** tab. + +![Enter stage name in Process Console](/assets/examples/webstorm/enter-stage-name-in-process-console.png) + +It'll then take a couple of minutes to do the following: + +1. It'll bootstrap your AWS environment to use CDK. +2. Deploy a debug stack to power the Live Lambda Development environment. +3. Deploy your app, but replace the functions in the `packages/functions/` directory with ones that connect to your local client. +4. Start up a local client. + +Once complete, you should see something like this. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +frank-webstorm-ExampleStack: deploying... + + ✅ frank-webstorm-ExampleStack + + +Stack frank-webstorm-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://siyp617yh1.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. Now if you head over to that endpoint in your browser, you'll notice that you'll hit the breakpoint. + +![Hitting a breakpoint in a Lambda function in WebStorm](/assets/examples/webstorm/hitting-a-breakpoint-in-a-lambda-function-in-webstorm.png) + +## Making changes + +An advantage of using the Live Lambda Development environment is that you can make changes without having to redeploy them. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}; +``` + +Now if you head back to the endpoint. + +``` +https://siyp617yh1.execute-api.us-east-1.amazonaws.com/ +``` + +You should see the new message being printed out. + +### Making improvements + +If you're running into any memory issues while debugging, you can disable the unused plugins and exclude the folders that are not included in the source code. Right click on the folder and mark it as **Excluded**. In this case we are marking the `node_modules/` directory as _Excluded_. + +![Exclude folders in WebStorm](/assets/examples/webstorm/excluded-folders-in-webstorm.png) + +## Deploying your API + +Now that our API is tested and ready to go. Let's go ahead and deploy it for our users. You'll recall that we were using a dev environment. + +However, we are going to deploy your API again. But to a different environment, called `prod`. This allows us to separate our environments, so when we are working in our local environment, it doesn't break the API for our users. + +{%change%} Run the following in your terminal. + +```bash +$ npx sst deploy --stage prod +``` + +A note on these environments. SST is simply deploying the same app twice using two different `stage` names. It prefixes the resources with the stage names to ensure that they don't thrash. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following command. + +```bash +$ npx sst remove +``` + +And to remove the prod environment. + +```bash +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! You've got a brand new serverless API. A local development environment, to test and make changes. And you can use WebStorm to debug and set breakpoints in your Lambda functions. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-cron-jobs-in-your-serverless-app.md b/_examples/how-to-use-cron-jobs-in-your-serverless-app.md new file mode 100644 index 0000000000..0ceec89db3 --- /dev/null +++ b/_examples/how-to-use-cron-jobs-in-your-serverless-app.md @@ -0,0 +1,188 @@ +--- +layout: example +title: How to use cron jobs in your serverless app +short_title: Cron +date: 2021-02-08 00:00:00 +lang: en +index: 1 +type: async +description: In this example we will look at how to create a cron job in your serverless app on AWS using SST. We'll be using the Cron to create a simple weather tracking app that checks the weather forecast every minute. +short_desc: A simple serverless Cron job. +repo: cron-job +ref: how-to-use-cron-jobs-in-your-serverless-app +comments_id: how-to-use-cron-jobs-in-your-serverless-app/2313 +--- + +In this example we will look at how to create a cron job in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple task that runs every minute and prints the weather forecast. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example cron-job +$ cd cron-job +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "cron-job", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Creating Cron Job + +Let's start by creating a cron job. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Cron, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + new Cron(stack, "Cron", { + schedule: "rate(1 minute)", + job: "packages/functions/src/lambda.main", + }); +} +``` + +This creates a serverless cron job using [`Cron`]({{ site.v2_url }}/constructs/Cron). We've configured the cron job to run every minute. + +## Adding function code + +Now in our function, we'll print out a message every time the function is run. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +export async function main() { + console.log("Hi!"); + return {}; +} +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-cron-job-ExampleStack: deploying... + + ✅ dev-cron-job-ExampleStack + + +Stack dev-cron-job-ExampleStack + Status: deployed +``` + +Let's test our cron job using the integrated [SST Console](https://console.sst.dev). + +Note, the SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **Local** tab in the console. + +Note, The **Local** tab display real-time logs from your Live Lambda Dev environment + +Wait for a couple of minutes and you should see `Hi!` gets printed out every minute in your invocations. + +![local tab invocations](/assets/examples/cron-job/local-tab-invocations.png) + +## Checking weather forecast + +Now let's make a call to [MetaWeather](https://www.metaweather.com)'s API and print out the weather in San Francisco. + +{%change%} Let's install the `node-fetch` in the `packages/functions/` folder. + +```bash +$ npm install node-fetch +``` + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import fetch from "node-fetch"; + +export async function main() { + const weather = await checkSFWeather(); + console.log(weather.consolidated_weather[0]); + return {}; +} + +function checkSFWeather() { + return fetch("https://www.metaweather.com/api/location/2487956/").then( + (res) => res.json() + ); +} +``` + +Now if you head over to your console and wait for the function to get invoked in the next minute, you'll notice the weather data is printed out in the invocations! + +![local tab weather data invocation](/assets/examples/cron-job/local-tab-weather-data-invocation.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless cron job that checks the weather every minute. You can change this to run a job that you want. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-datadog-to-monitor-your-serverless-app.md b/_examples/how-to-use-datadog-to-monitor-your-serverless-app.md new file mode 100644 index 0000000000..93f61b4e31 --- /dev/null +++ b/_examples/how-to-use-datadog-to-monitor-your-serverless-app.md @@ -0,0 +1,210 @@ +--- +layout: example +title: How to use Datadog to monitor your serverless app +short_title: Datadog +date: 2021-11-01 00:00:00 +lang: en +index: 1 +type: monitoring +description: In this example we will look at how to use Datadog with a serverless API to create and monitor a simple click counter app. We'll be using SST. +short_desc: Using Datadog to monitor a serverless app. +repo: datadog +ref: how-to-use-datadog-to-monitor-your-serverless-app +comments_id: how-to-use-datadog-to-monitor-your-serverless-app/2520 +--- + +In this example we will look at how to use [Datadog](https://www.datadoghq.com/) to monitor the Lambda functions in your [SST serverless application]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Datadog account](https://app.datadoghq.com/signup) and that's [configured with your AWS account](https://docs.datadoghq.com/integrations/amazon_web_packages/?tab=roledelegation#setup) + +## What is Datadog + +When a serverless app is deployed to production, it's useful to be able to monitor your Lambda functions. There are a few different services that you can use for this. One of them is [Datadog](https://www.datadoghq.com). Datadog offers an End-to-end Serverless Monitoring solution that works with Lambda functions. + +Let's look at how to set this up. + +## Create an SST app + +{%change%} Start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example datadog +$ cd datadog +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "datadog", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Create our infrastructure + +Our app is going to be a simple API that returns a _Hello World_ response. + +### Creating our API + +Let's add the API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Api } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint at the root. When we make a `GET` request to this endpoint the function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +{%change%} Your `packages/functions/src/lambda.ts` should look something like this. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}; +``` + +## Setting up our app with Datadog + +Now let's setup [Datadog](https://www.datadoghq.com/) to monitor our API. Make sure [Datadog](https://docs.datadoghq.com/integrations/amazon_web_packages/?tab=roledelegation#setup) has been configured with your AWS account. + +{%change%} Run the following in our project root. + +```bash +$ npm install --W datadog-cdk-constructs-v2 +``` + +Next, go to the [**API keys**](https://app.datadoghq.com/organization-settings/api-keys) page of your Datadog dashboard and copy the API key. + +![Copy Datadog API key from dashboard](/assets/examples/datadog/copy-datadog-api-key-from-dashboard.png) + +{%change%} Create a `.env.local` file with the API key in your project root. + +```bash +DATADOG_API_KEY= +``` + +Note that, this file should not be committed to Git. If you are deploying the app through a CI service, configure the `DATADOG_API_KEY` as an environment variable in the CI provider. If you are deploying through Seed, you can [configure this in your stage settings](https://seed.run/docs/storing-secrets.html). + +Next, you'll need to import it into the stack and pass in the functions you want monitored. + +{%change%} Add the following above the `stack.addOutputs` line in `stacks/ExampleStack.ts`. + +```typescript +// Configure Datadog only in prod +if (!app.local) { + // Configure Datadog + const datadog = new Datadog(stack, "Datadog", { + nodeLayerVersion: 65, + extensionLayerVersion: 13, + apiKey: process.env.DATADOG_API_KEY, + }); + + // Monitor all functions in the stack + datadog.addLambdaFunctions(stack.getAllFunctions()); +} +``` + +{%change%} Also make sure to include the Datadog construct. + +```typescript +import { Datadog } from "datadog-cdk-constructs-v2"; +``` + +Note that [`getAllFunctions`]({{ site.v2_url }}/constructs/Stack#getallfunctions) gives you an array of all the Lambda functions created in this stack. If you want to monitor all the functions in your stack, make sure to call it at the end of your stack definition. + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-datadog-ExampleStack + + +Stack prod-datadog-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `GET` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/datadog/api-explorer-invocation-response.png) + +You should see the _Hello World_ message. + +Now head over to your Datadog dashboard to start exploring key performance metrics; invocations, errors, and duration from your function. The [Serverless view](https://app.datadoghq.com/functions) aggregates data from all of the serverless functions running in your environment, enabling you to monitor their performance in one place. You can search and filter by name, AWS account, region, runtime, or any tag. Or click on a specific function to inspect its key performance metrics, distributed traces, and logs. + +![Datadog functions dashboard](/assets/examples/datadog/datadog-functions-dashboard.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a serverless API monitored with Datadog. We also have a local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-dynamodb-in-your-serverless-app.md b/_examples/how-to-use-dynamodb-in-your-serverless-app.md new file mode 100644 index 0000000000..6b47e4f7f3 --- /dev/null +++ b/_examples/how-to-use-dynamodb-in-your-serverless-app.md @@ -0,0 +1,256 @@ +--- +layout: example +title: How to use DynamoDB in your serverless app +short_title: DynamoDB +date: 2021-02-04 00:00:00 +lang: en +index: 1 +type: database +description: In this example we will look at how to use DynamoDB in your serverless app on AWS using SST. We'll be using the Api and Table constructs to create a simple hit counter. +short_desc: Using DynamoDB in a serverless API. +repo: rest-api-dynamodb +ref: how-to-use-dynamodb-in-your-serverless-app +comments_id: how-to-use-dynamodb-in-your-serverless-app/2307 +--- + +In this example we will look at how to use DynamoDB in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple hit counter. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example rest-api-dynamodb +$ cd rest-api-dynamodb +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api-dynamodb", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding DynamoDB + +[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) is a reliable and highly-performant NoSQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, ReactStaticSite, StackContext, Table } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create the table + const table = new Table(stack, "Counter", { + fields: { + counter: "string", + }, + primaryIndex: { partitionKey: "counter" }, + }); +} +``` + +This creates a serverless DynamoDB table using [`Table`]({{ site.v2_url }}/constructs/Table). It has a primary key called `counter`. Our table is going to look something like this: + +| counter | tally | +| ------- | ----- | +| hits | 123 | + +## Setting up the API + +Now let's add the API. + +{%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [table], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.main", + }, +}); + +// Show the URLs in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +Our [API]({{ site.v2_url }}/constructs/api) simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our table to our API. It allows our API to access (read and write) the table we just created. + +## Reading from our table + +Now in our function, we'll start by reading from our DynamoDB table. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { DynamoDB } from "aws-sdk"; +import { Table } from "sst/node/table"; + +const dynamoDb = new DynamoDB.DocumentClient(); + +export async function main() { + const getParams = { + // Get the table name from the environment variable + TableName: Table.Counter.tableName, + // Get the row where the counter is called "hits" + Key: { + counter: "hits", + }, + }; + const results = await dynamoDb.get(getParams).promise(); + + // If there is a row, then get the value of the + // column called "tally" + let count = results.Item ? results.Item.tally : 0; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We make a `get` call to our DynamoDB table and get the value of a row where the `counter` column has the value `hits`. Since, we haven't written to this column yet, we are going to just return `0`. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-dynamodb-ExampleStack: deploying... + + ✅ dev-rest-api-dynamodb-ExampleStack + + +Stack dev-rest-api-dynamodb-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://u3nnmgdigh.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Writing to our table + +Now let's update our table with the hits. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +const putParams = { + TableName: Table.Counter.tableName, + Key: { + counter: "hits", + }, + // Update the "tally" column + UpdateExpression: "SET tally = :count", + ExpressionAttributeValues: { + // Increase the count + ":count": ++count, + }, +}; +await dynamoDb.update(putParams).promise(); +``` + +Here we are updating the `clicks` row's `tally` column with the increased count. + +And if you head over to your API explorer and hit the **Send** button again, you should see the count increase! + +Also let's go to the **DynamoDB** tab in the SST Console and check that the value has been updated in the table. + +Note, The [DynamoDB explorer]({{ site.v2_url }}/console#dynamodb) allows you to query the DynamoDB tables in the [`Table`]({{ site.v2_url }}/constructs/Table) constructs in your app. You can scan the table, query specific keys, create and edit items. + +![DynamoDB table view of counter table](/assets/examples/angular-app/dynamo-table-view-of-counter-table.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless hit counter. In another example, [we'll expand on this to create a CRUD API]({% link _examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md %}). Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-event-bus-in-your-serverless-app.md b/_examples/how-to-use-event-bus-in-your-serverless-app.md new file mode 100644 index 0000000000..0000dfdc9f --- /dev/null +++ b/_examples/how-to-use-event-bus-in-your-serverless-app.md @@ -0,0 +1,280 @@ +--- +layout: example +title: How to use event bus in your serverless app +short_title: EventBus +date: 2021-12-18 00:00:00 +lang: en +index: 6 +type: async +description: In this example we will look at how to use event bus in your serverless app on AWS using SST. We'll be using the Api and EventBus constructs to create a simple checkout system. +short_desc: A simple EventBridge system with EventBus. +repo: eventbus +ref: how-to-use-event-bus-in-your-serverless-app +comments_id: how-to-use-event-bus-in-your-serverless-app/2607 +--- + +In this example we will look at how to use EventBus to create [an EventBridge system](https://aws.amazon.com/eventbridge/) in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple checkout flow. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example eventbus +$ cd eventbus +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "eventbus", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding EventBridge EventBus + +[Amazon EventBridge](https://aws.amazon.com/eventbridge/) is a serverless event bus that makes it easier to build event-driven applications at scale using events generated from your applications, integrated Software-as-a-Service (SaaS) applications, and AWS services. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, EventBus, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + const bus = new EventBus(stack, "Ordered", { + rules: { + rule1: { + pattern: { + source: ["myevent"], + detailType: ["Order"], + }, + targets: { + receipt: "packages/functions/src/receipt.handler", + shipping: "packages/functions/src/shipping.handler", + }, + }, + }, + }); +} +``` + +This creates an EventBridge EventBus using [`EventBus`]({{ site.v2_url }}/constructs/EventBus) and it has two targets. Meaning when the event is published, both the functions will get run. + +## Setting up the API + +Now let's add the API. + +{%change%} Add this below the `EventBus` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create a HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + bind: [bus], + }, + }, + routes: { + "POST /order": "packages/functions/src/order.handler", + }, +}); + +// Show the endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +Our [API]({{ site.v2_url }}/constructs/api) simply has one endpoint (`/order`). When we make a `POST` request to this endpoint the Lambda function called `handler` in `packages/functions/src/order.ts` will get invoked. + +We'll also bind our event bus to our API. + +## Adding function code + +We will create three functions, one handling the `/order` API request, and two for the EventBus targets. + +{%change%} Add a `packages/functions/src/order.ts`. + +```typescript +export async function handler() { + console.log("Order confirmed!"); + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +{%change%} Add a `packages/functions/src/receipt.ts`. + +```typescript +export async function handler() { + console.log("Receipt sent!"); + return {}; +} +``` + +{%change%} Add a `packages/functions/src/shipping.ts`. + +```typescript +export async function handler() { + console.log("Item shipped!"); + return {}; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-eventbus-ExampleStack: deploying... + + ✅ dev-eventbus-ExampleStack + + +Stack dev-eventbus-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://gevkgi575a.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **Functions** tab and click the **Invoke** button of the `POST /order` function to send a `POST` request. + +![Functions tab invoke button](/assets/examples/eventbus/functions_tab_invoke_button.png) + +After you see a success status in the logs, go to the Local tab in the console to see all function invocations. Local tab displays real-time logs from your Live Lambda Dev environment. + +![Local tab response without event](/assets/examples/eventbus/Local_tab_response_without_events.png) + +You should see `Order confirmed!` logged in the console. + +## Publishing to our EventBus + +Now let's publish a event to our EventBus. + +{%change%} Replace the `packages/functions/src/order.ts` with the following. + +```typescript +import AWS from "aws-sdk"; +import { EventBus } from "sst/node/event-bus"; + +const client = new AWS.EventBridge(); + +export async function handler() { + client + .putEvents({ + Entries: [ + { + EventBusName: EventBus.Ordered.eventBusName, + Source: "myevent", + DetailType: "Order", + Detail: JSON.stringify({ + id: "123", + name: "My order", + items: [ + { + id: "1", + name: "My item", + price: 10, + }, + ], + }), + }, + ], + }) + .promise() + .catch((e) => { + console.log(e); + }); + + console.log("Order confirmed!"); + + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +Here we are getting the EventBus name from the environment variable, and then publishing an event to it. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And now if you head over to your console and invoke the function again, You'll notice in the **Local** tab that our EventBus targets are called. And you should see `Receipt sent!` and `Item shipped!` printed out. + +![Local tab response with event](/assets/examples/eventbus/Local_tab_response_with_events.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless checkout system, powered by EventBus. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-kinesis-data-streams-in-your-serverless-app.md b/_examples/how-to-use-kinesis-data-streams-in-your-serverless-app.md new file mode 100644 index 0000000000..603f3aeca1 --- /dev/null +++ b/_examples/how-to-use-kinesis-data-streams-in-your-serverless-app.md @@ -0,0 +1,256 @@ +--- +layout: example +title: How to use Kinesis data streams in your serverless app +short_title: Kinesis data streams +date: 2021-02-08 00:00:00 +lang: en +index: 5 +type: async +description: In this example we will look at how to use Kinesis Data Streams in your serverless app on AWS using SST. We'll be using the API and KinesisStream constructs to create it. +short_desc: A simple Kinesis Data Stream system. +repo: kinesisstream +ref: how-to-use-kinesis-data-streams-in-your-serverless-app +comments_id: how-to-use-kinesis-data-streams-in-your-serverless-app/2600 +--- + +In this example we will look at how to create a Kinesis Data Stream in our serverless app using [SST]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example kinesisstream +$ cd kinesisstream +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "kinesisstream", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding a Kinesis Data Stream + +[Amazon Kinesis Data Streams](https://aws.amazon.com/kinesis/data-streams/) is a serverless streaming data service that makes it easy to capture, process, and store data streams at any scale. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, KinesisStream, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // create a kinesis stream + const stream = new KinesisStream(stack, "Stream", { + consumers: { + consumer1: "packages/functions/src/consumer1.handler", + consumer2: "packages/functions/src/consumer2.handler", + }, + }); +} +``` + +This creates an Kinesis Data Stream using [`KinesisStream`]({{ site.v2_url }}/constructs/KinesisStream) and it has a consumer that polls for messages from the Kinesis Data Stream. The consumer function will run when it has polled 1 or more messages. + +## Setting up the API + +Now let's add the API. + +{%change%} Add this below the `KinesisStream` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create a HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + bind: [stream], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.handler", + }, +}); + +// Show the endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +Our [API]({{ site.v2_url }}/constructs/api) simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our stream to our API. + +## Adding function code + +We will create three functions, one for handling the API request, and the other two for the consumers. + +{%change%} Replace the `packages/functions/src/lambda.ts` with the following. + +```typescript +export async function handler() { + console.log("Message queued!"); + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +{%change%} Add a `packages/functions/src/consumer1.ts`. + +```typescript +export async function handler() { + console.log("Message 1 processed!"); + return {}; +} +``` + +{%change%} Add a `packages/functions/src/consumer2.ts`. + +```typescript +export async function handler() { + console.log("Message 2 processed!"); + return {}; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-kinesisstream-ExampleStack: deploying... + + ✅ dev-kinesisstream-ExampleStack + + +Stack dev-kinesisstream-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://i8ia1epqnh.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **Functions** tab and click the **Invoke** button of the `POST /` function to send a `POST` request. + +![Functions tab invoke button](/assets/examples/kinesisstream/functions_tab_invoke_button.png) + +After you see a success status in the logs, go to the **Local** tab in the console to see all function invocations. Local tab displays **real-time logs** from your Live Lambda Dev environment. + +![Local tab response without kinesis](/assets/examples/kinesisstream/Local_tab_response_without_kinesis.png) + +You should see `Message queued!` logged in the console. + +## Sending messages to our Kinesis Data Stream + +Now let's send a message to our Kinesis Data Stream. + +{%change%} Replace the `packages/functions/src/lambda.ts` with the following. + +```typescript +import AWS from "aws-sdk"; +import { KinesisStream } from "sst/node/kinesis-stream"; + +const stream = new AWS.Kinesis(); + +export async function handler() { + await stream + .putRecord({ + Data: JSON.stringify({ + message: "Hello from Lambda!", + }), + PartitionKey: "key", + StreamName: KinesisStream.Stream.streamName, + }) + .promise(); + + console.log("Message queued!"); + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +Here we are getting the Kinesis Data Stream name from the environment variable, and then sending a message to it. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +And now if you head over to your console and invoke the function again. You'll notice in the **Local** tab that our consumers are called. You should see `Message 1 processed!` and `Message 2 processed!` being printed out. + +![Local tab response with kinesis](/assets/examples/kinesisstream/Local_tab_response_with_kinesis.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in dev, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless Kinesis Data Stream system. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-lambda-layers-in-your-serverless-app.md b/_examples/how-to-use-lambda-layers-in-your-serverless-app.md new file mode 100644 index 0000000000..dc02a3af38 --- /dev/null +++ b/_examples/how-to-use-lambda-layers-in-your-serverless-app.md @@ -0,0 +1,319 @@ +--- +layout: example +title: How to use Lambda Layers in your serverless app +short_title: Lambda Layers +date: 2021-05-26 00:00:00 +lang: en +index: 2 +type: misc +description: In this example we'll look at how to use a Lambda Layer in a serverless app using SST. We'll be using the @sparticuz/chromium Layer to take a screenshot of a webpage and return the image in our API. +short_desc: Using the @sparticuz/chromium layer to take screenshots. +repo: layer-chrome-aws-lambda +ref: how-to-use-lambda-layers-in-your-serverless-app +comments_id: how-to-use-lambda-layers-in-your-serverless-app/2405 +--- + +In this example we will look at how to use [Layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) in your serverless app with [SST]({{ site.sst_github_repo }}). We'll be using the [@sparticuz/chromium](https://github.com/Sparticuz/chromium) Layer to take a screenshot of a webpage and return the image in our API. + +We'll be using SST's [Live Lambda Development]({{ site.v2_url }}/live-lambda-development). It allows you to make changes and test locally without having to redeploy. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example layer-chrome-aws-lambda +$ cd layer-chrome-aws-lambda +$ npm install +``` + +By default, our app will be deployed to an environment (or stage) called `dev` and the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; +import { Api } from "sst/constructs"; + +export default { + config(_input) { + return { + name: "layer-chrome-aws-lambda", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +3. `layers/` — Lambda Layers + + The Lambda layer that contains Chromium. + +## Creating the API + +Let's start by creating our API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```ts +import * as lambda from "aws-cdk-lib/aws-lambda"; +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + const layerChromium = new lambda.LayerVersion(stack, "chromiumLayers", { + code: lambda.Code.fromAsset("layers/chromium"), + }); + + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": { + function: { + handler: "packages/functions/src/lambda.handler", + // Use 18.x here because in 14, 16 layers have some issue with using NODE_PATH + runtime: "nodejs18.x", + // Increase the timeout for generating screenshots + timeout: 15, + // Increase the memory + memorySize: "2 GB", + // Load Chrome in a Layer + layers: [layerChromium], + // Exclude bundling it in the Lambda function + nodejs: { + esbuild: { + external: ["@sparticuz/chromium"], + }, + }, + }, + }, + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We then use the [`Api`]({{ site.v2_url }}/constructs/Api) construct and add a single route (`GET /`). For the function that'll be handling the route, we increase the timeout, since generating a screenshot can take a little bit of time. + +We create a layer based on what's in the `layers/chromium` directory, we'll download this below. We also exclude the Lambda function from bundling the [@sparticuz/chromium](https://github.com/Sparticuz/chromium) npm package. + +Finally, we output the endpoint of our newly created API. + +## Install Chromium + +We need to install Chromium so we can run it locally and we need to install it so we can package it up as a Lambda layer. + +### Installing locally + +{%change%} Download Chromium locally, then you will have `YOUR_LOCAL_CHROMIUM_PATH`. You will need it in Lambda function to run Chromium locally. + +```bash +$ npx @puppeteer/browsers install chromium@latest --path /tmp/localChromium +``` + +### Download Layer + +{%change%} Create a `layers/chromium` directory. + +```bash +$ mkdir -p layers/chromium +``` + +{%change%} Download the asset that looks like `chromium-v121.0.0-layer.zip` from the [@sparticuz/chromium](https://github.com/Sparticuz/chromium/releases/) GitHub releases. + +{%change%} Unzip and copy the `node_modules/` directory to the `layers/chromium/` directory. + +## Adding function code + +Now in our function, we'll be handling taking a screenshot of a given webpage. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```ts +import puppeteer from "puppeteer-core"; +import chromium from "@sparticuz/chromium"; + +// chrome-aws-lambda handles loading locally vs from the Layer + +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +// This is the path to the local Chromium binary +const YOUR_LOCAL_CHROMIUM_PATH = "/tmp/localChromium/chromium/mac-1165945/chrome-mac/Chromium.app/Contents/MacOS/Chromium"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + // Get the url and dimensions from the query string + const { url, width, height } = event.queryStringParameters!; + + if (!url) { + return { + statusCode: 400, + body: "Please provide a url", + }; + } + + const browser = await puppeteer.launch({ + args: chromium.args, + defaultViewport: chromium.defaultViewport, + executablePath: process.env.IS_LOCAL + ? YOUR_LOCAL_CHROMIUM_PATH + : await chromium.executablePath(), + headless: chromium.headless, + }); + + const page = await browser.newPage(); + + if (width && height) { + await page.setViewport({ + width: Number(width), + height: Number(height), + }); + } + + // Navigate to the url + await page.goto(url!); + + // Take the screenshot + const screenshot = (await page.screenshot({ encoding: "base64" })) as string; + + const pages = await browser.pages(); + for (let i = 0; i < pages.length; i++) { + await pages[i].close(); + } + + await browser.close(); + + return { + headers: { "Content-Type": "text/plain" }, + body: "Screenshot taken", + }; +}; + +``` + +First, we grab the webpage URL and dimensions for the screenshot from the query string. We then launch the browser and navigate to that URL, with those dimensions and take the screenshot. + +Now let's install the npm packages we need. You need to check [Puppeteer's Chromium Support page](https://pptr.dev/chromium-support) and install the **correct version of Chromium**. At the moment writing this tutorial, `puppeteer-core@20` is compatible with `Chromium@113` is most stable. + +{%change%} Run the below command in the `packages/functions/` folder. + +```bash +$ npm install puppeteer-core@20.1.2 @sparticuz/chromium@113.0.1 +``` + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-layer-chrome-aws-lambda-ExampleStack: deploying... + + ✅ dev-layer-chrome-aws-lambda-ExampleStack + + +Stack dev-layer-chrome-aws-lambda-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://d9rxpfhft0.execute-api.us-east-1.amazonaws.com +``` + +Now if you head over to your API endpoint and add the URL and dimensions to the query string: + +``` +?url=https://sst.dev/examples&width=390&height=640 +``` + +You should see `Screenshot taken` being printed out. + +## Returning an image + +Now let's make a change to our function so that we return the screenshot directly as an image. + +{%change%} Replace the following lines in `packages/functions/src/lambda.ts`. + +```typescript +// Take the screenshot +await page.screenshot(); + +return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: "Screenshot taken", +}; +``` + +with: + +```typescript +// Take the screenshot +const screenshot = (await page.screenshot({ encoding: "base64" })) as string; + +return { + statusCode: 200, + // Return as binary data + isBase64Encoded: true, + headers: { "Content-Type": "image/png" }, + body: screenshot, +}; +``` + +Here we are returning the screenshot image as binary data in the body. We are also setting the `isBase64Encoded` option to `true`. + +Now if you go back and load the same link in your browser, you should see the screenshot! + +![Chrome screenshot in Lambda function](/assets/examples/layer-chrome-aws-lambda/chrome-screenshot-in-lambda-function.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless screenshot taking API that automatically returns an image of any webpage we want. And we can test our changes locally before deploying to AWS! Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-lumigo-to-monitor-your-serverless-app.md b/_examples/how-to-use-lumigo-to-monitor-your-serverless-app.md new file mode 100644 index 0000000000..2022579188 --- /dev/null +++ b/_examples/how-to-use-lumigo-to-monitor-your-serverless-app.md @@ -0,0 +1,192 @@ +--- +layout: example +title: How to use Lumigo to monitor your serverless app +short_title: Lumigo +date: 2021-11-26 00:00:00 +lang: en +index: 4 +type: monitoring +description: In this example we will look at how to use Lumigo with a serverless API to create and monitor a simple click counter app. We'll be using SST. +short_desc: Using Lumigo to monitor a serverless app. +repo: lumigo +ref: how-to-use-lumigo-to-monitor-your-serverless-app +comments_id: how-to-use-lumigo-to-monitor-your-serverless-app/2615 +--- + +In this example we will look at how to use [Lumigo](https://lumigo.io/) to monitor the Lambda functions in your [SST serverless application]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Lumigo account](https://platform.lumigo.io/signup) and that's [configured with your AWS account](https://platform.lumigo.io/wizard) + +## What is Lumigo + +When a serverless app is deployed to production, it's useful to be able to monitor your Lambda functions. There are a few different services that you can use for this. One of them is [Lumigo](https://lumigo.io/). Lumigo offers an End-to-end Serverless Monitoring solution that works with Lambda functions. + +Let's look at how to set this up. + +## Create an SST app + +{%change%} Start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example lumigo +$ cd lumigo +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "lumigo", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Create our infrastructure + +Our app is going to be a simple API that returns a _Hello World_ response. + +### Creating our API + +Let's add the API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext } from "sst/constructs"; +import * as cdk from "aws-cdk-lib"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint at the root. When we make a `GET` request to this endpoint the function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +{%change%} Your `packages/functions/src/lambda.ts` should look something like this. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}; +``` + +## Setting up our app with Lumigo + +Now let's setup [Lumigo](https://lumigo.io/) to monitor our API. Make sure [Lumigo has been configured with your AWS account](https://platform.lumigo.io/wizard) . + +To enable Lambda monitoring for a function, add a `lumigo:auto-trace` tag and set it to `true`. + +{%change%} Add the following above `stack.addOutputs` in `stacks/ExampleStack.ts`. + +```typescript +// Enable auto trace only in prod +if (!app.local) + cdk.Tags.of(api.getFunction("GET /")).add("lumigo:auto-trace", "true"); +``` + +To monitor all the functions in a stack, you can use the [Stack]({{ site.v2_url }}/constructs/Stack) construct's `getAllFunctions` method and do the following at the bottom of your stack definition like below + +```typescript +stack + .getAllFunctions() + .forEach((fn) => cdk.Tags.of(fn).add("lumigo:auto-trace", "true")); +``` + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-lumigo-ExampleStack + + +Stack prod-lumigo-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Run the below command to start SST console in **prod** stage. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click the **Send** button. + +Note, The API explorer lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API tab invoke button](/assets/examples/lumigo/api_tab_invoke_button.png) + +You will see the response of your function. + +Now head over to your Lumigo dashboard to start exploring key performance metrics; invocations, errors, and duration from your function. The Dashboard aggregates data from all of the serverless functions running in your environment, enabling you to monitor their performance in one place. + +![Lumigo-functions-page](/assets/examples/lumigo/lumigo-functions-page.png) + +![Lumigo-functions-stats](/assets/examples/lumigo/lumigo-function-stats.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a serverless API monitored with Lumigo. We also have a local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-middy-to-validate-your-serverless-api-requests.md b/_examples/how-to-use-middy-to-validate-your-serverless-api-requests.md new file mode 100644 index 0000000000..e2370d4ba4 --- /dev/null +++ b/_examples/how-to-use-middy-to-validate-your-serverless-api-requests.md @@ -0,0 +1,383 @@ +--- +layout: example +title: How to use Middy to validate your serverless API requests +short_title: Middy Validator +date: 2021-06-17 00:00:00 +lang: en +index: 3 +type: misc +description: In this example we will look at how to use the Middy validator middleware in a serverless API to validate request and response schemas. +short_desc: Use Middy to validate API request and responses. +repo: middy-validator +ref: how-to-use-middy-to-validate-your-serverless-api-requests +comments_id: how-to-use-middy-to-validate-your-serverless-api-requests/2525 +--- + +In this example we will look at how to use the [Middy validator](https://middy.js.org/packages/validator/) middleware with a [serverless]({% link _chapters/what-is-serverless.md %}) API to validate request and response schemas. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## What is Middy + +[Middy](https://middy.js.org) is a very simple middleware engine that allows you to simplify your AWS Lambda code when using Node.js. It allows you to focus on the strict business logic of your Lambda function and then attach additional common elements like authentication, authorization, validation, serialization, etc. in a modular and reusable way by decorating the main business logic. + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example middy-validator +$ cd middy-validator +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "middy-validator", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Create our infrastructure + +Our app is made up of a simple API. The API will read two variables from the request and return a greeting message as a response. + +### Creating our API + +Let's start by adding the API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "POST /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +{%change%} Replace the code in `packages/functions/src/lambda.ts` with: + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const { fname, lname } = JSON.parse(event.body); + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, ${fname}-${lname}.`, + }; +}; +``` + +We are reading two variables `fname` and `lname` from the event body and returning a simple greeting message. + +Let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-middy-validator-ExampleStack: deploying... + + ✅ dev-middy-validator-ExampleStack + + +Stack dev-middy-validator-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://bqoc5prkna.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** explorer and click on the `POST /` route. In the **Headers** tab set the content-type as **JSON** `Content-type: application/json`. + +![Console with headers](/assets/examples/middy-validator/console-with-header.png) + +In the **Body** tab, enter the below JSON and click **Send** to send a POST request. + +```json +{ + "fname": "mani", + "lname": "teja" +} +``` + +![Request with correct schema](/assets/examples/middy-validator/request-with-correct-schema.png) + +As you can see the endpoint is working as expected. We sent `mani` and `teja` as our `fname` and `lname` respectively and got `Hello, mani-teja` as the response. + +Now let's remove `lname` from the body and see what happens. + +![Request with incorrect schema](/assets/examples/middy-validator/request-with-incorrect-schema.png) + +You'll notice that the endpoint is working fine but it returned `undefined` for `lname`. Since we'd only sent the `fname` in the request, so it returned `undefined` in the place of `lname`. + +In a production app it can be difficult to catch these issues. We'd like to explicitly throw an error when there is a missing parameter in the request body. + +## Setting up our Middy middleware + +To fix this let's use the [Middy validator](https://middy.js.org/packages/validator/) middleware to validate our API. + +{%change%} Run the following in the `packages/functions/` directory. + +```bash +$ npm install --save @middy/core @middy/http-json-body-parser @middy/http-error-handler @middy/validator +``` + +Let's understand what the above packages are. + +| package | explanation | +| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`@middy/core`](https://www.npmjs.com/package/@middy/core) | The core package of the Middy framework. | +| [`@middy/http-json-body-parser`](https://www.npmjs.com/package/@middy/http-json-body-parser) | A middleware that parses HTTP requests with a JSON body and converts the body into an object. | +| [`@middy/http-error-handler`](https://www.npmjs.com/package/@middy/http-error-handler) | A middleware that handles uncaught errors that contain the properties `statusCode` (number) and `message` (string) and creates a proper HTTP response for them (using the message and the status code provided by the error object). | +| [`@middy/validator`](https://www.npmjs.com/package/@middy/validator) | A middleware that validates incoming events and outgoing responses against custom schemas defined with the [JSON schema syntax](https://json-schema.org). | + +### Adding request validation + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import middy from "@middy/core"; +import validator from "@middy/validator"; +import httpErrorHandler from "@middy/http-error-handler"; +import jsonBodyParser from "@middy/http-json-body-parser"; + +const baseHandler = (event) => { + // You don't need JSON.parse since we are using the jsonBodyParser middleware + const { fname, lname } = event.body; + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, ${fname}-${lname}.`, + }; +}; + +const inputSchema = { + type: "object", + properties: { + body: { + type: "object", + properties: { + fname: { type: "string" }, + lname: { type: "string" }, + }, + required: ["fname", "lname"], + }, + }, +}; + +const handler = middy(baseHandler) + .use(jsonBodyParser()) + .use( + validator({ + inputSchema, + }) + ) + .use(httpErrorHandler()); + +export { handler }; +``` + +Here we are creating an `inputSchema`. We are explicitly setting that `fname` and `lname` are required. + +**Important:** Compiling schemas on the fly will cause a 50-100ms performance hit during cold start for simple JSON Schemas. Precompiling is highly recommended. [Read more about this](https://github.com/willfarrell/middy-ajv). + +Now go back to console and click the **Send** button again to send a new request. + +![Request with Middy schema request validation](/assets/examples/middy-validator/request-with-middy-schema-validation.png) + +Great! The server throws a `Bad request` error to let us know that something is wrong. + +### Adding response validation + +While we are here, let's add response validation as well. + +{%change%} Replace `packages/functions/src/lambda.ts` with this: + +```typescript +import middy from "@middy/core"; +import validator from "@middy/validator"; +import httpErrorHandler from "@middy/http-error-handler"; +import jsonBodyParser from "@middy/http-json-body-parser"; + +const baseHandler = (event) => { + const { fname, lname } = event.body; + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, ${fname}-${lname}.`, + }; +}; + +const inputSchema = { + type: "object", + properties: { + body: { + type: "object", + properties: { + fname: { type: "string" }, + lname: { type: "string" }, + }, + required: ["fname", "lname"], + }, + }, +}; + +const outputSchema = { + type: "object", + required: ["body", "statusCode"], + properties: { + body: { + type: "string", + }, + statusCode: { + type: "number", + }, + headers: { + type: "object", + }, + }, +}; + +const handler = middy(baseHandler) + .use(jsonBodyParser()) + .use( + validator({ + inputSchema, + outputSchema, + }) + ) + .use(httpErrorHandler()); + +export { handler }; +``` + +We added a new `outputSchema` and added it to the Middy validator. + +Let's test our API again with correct schema and Middy validator. + +Add **lname** parameter again in the **Body** tab and click **Send**. + +![Request with correct schema](/assets/examples/middy-validator/request-with-correct-schema.png) + +Now the API is back working as expected. + +If the schema violates the schema we set in our middleware, the API will throw an error. Let's quickly try out the case of returning an invalid response. + +Instead of returning a number for status code, we'll return a string instead. + +{%change%} Replace this line: + +```typescript +statusCode: 200, +``` + +With this: + +```typescript +statusCode: "success", +``` + +Let's test the API again. + +![Request with Middy schema response validation](/assets/examples/middy-validator/request-with-middy-schema-response-validation.png) + +Great! The server now throws a `500 Internal Server Error` to let us know that something is wrong. + +{%change%} Let's change the status code back. + +```typescript +statusCode: 200, +``` + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-middy-validator-ExampleStack + + +Stack prod-middy-validator-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://bqoc5prkna.execute-api.us-east-1.amazonaws.com +``` + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely validated serverless API. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-mongodb-atlas-in-your-serverless-app.md b/_examples/how-to-use-mongodb-atlas-in-your-serverless-app.md new file mode 100644 index 0000000000..5c225d837e --- /dev/null +++ b/_examples/how-to-use-mongodb-atlas-in-your-serverless-app.md @@ -0,0 +1,342 @@ +--- +layout: example +title: How to use MongoDB Atlas in your serverless app +short_title: MongoDB Atlas +date: 2021-06-04 00:00:00 +lang: en +index: 2 +type: database +description: In this example we will look at how to use MongoDB Atlas in your serverless app on AWS using SST. We'll be using the Api construct to create a simple API that gets a list of users. +short_desc: Using MongoDB Atlas in a serverless API. +repo: rest-api-mongodb +ref: how-to-use-mongodb-in-your-serverless-app +redirect_from: /examples/how-to-use-mongodb-in-your-serverless-app.html +comments_id: how-to-use-mongodb-in-your-serverless-app/2406 +--- + +In this example we will look at how to use [MongoDB Atlas](https://www.mongodb.com/atlas/database?utm_campaign=serverless_stack&utm_source=serverlessstack&utm_medium=website&utm_term=partner) in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple API that returns a list of users. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example rest-api-mongodb +$ cd rest-api-mongodb +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api-mongodb", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding the API + +First let's create the API endpoint and connect it to a Lambda function. We'll be using this to query our MongoDB database. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + defaults: { + function: { + environment: { + MONGODB_URI: process.env.MONGODB_URI, + }, + }, + }, + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are doing a couple of things here. + +- We are creating an endpoint at `GET /` and connecting it to a Lambda function. +- We are passing in the MongoDB connection string as an environment variable (`MONGODB_URI`). We'll be loading this from a `.env` file that we'll be soon. +- Finally, we are printing out the API endpoint in our outputs. + +## What is MongoDB Atlas + +[MongoDB Atlas](https://www.mongodb.com/atlas/database?utm_campaign=serverless_stack&utm_source=serverlessstack&utm_medium=website&utm_term=partner) is the most advanced cloud database service on the market, with unmatched data distribution and mobility across AWS, Azure, and Google Cloud. It also has built-in automation for resource and workload optimization. + +MongoDB’s JSON-like document data model maps to the objects in your application code, providing the flexibility to model for a wide variety of use cases while also enabling you to easily evolve your data structures. + +## Setting up MongoDB + +Let's create our MongoDB database. Start by heading over to [MongoDB.com](https://www.mongodb.com/cloud/atlas/register?utm_campaign=serverless_stack&utm_source=serverlessstack&utm_medium=website&utm_term=partner) to create an Atlas account. + +MongoDB Atlas can deploy two types of cloud databases: **serverless instances** and **clusters**. + +- **Serverless Instances** require minimal configuration. Atlas automatically scales the storage capacity, storage throughput, and computing power for a serverless instance seamlessly to meet your workload requirements. They always run the latest MongoDB version, and you only pay for the operations that you run. + +- **Clusters** give you more flexibility in choosing your database configuration. You can set the cluster tier, use advanced capabilities such as sharding and Continuous Cloud Backups, distribute your data to multiple regions and cloud providers, and scale your cluster on-demand. You can also enable autoscaling, but it requires preconfiguration. MongoDB bills clusters based on the deployment configuration and cluster tier. + +To learn more about the deployment types [head over to the MongoDB docs](https://docs.atlas.mongodb.com/choose-database-deployment-type/). + +Note that serverless instances are in a preview release and currently do not support some Atlas features. You can [read more about the supported capabilities for serverless instance](https://docs.atlas.mongodb.com/reference/serverless-instance-limitations/). + +To **create a new database**, we are using the new **Serverless Instance** option. Make sure to **select AWS** as the cloud provider and **pick a region** where you are deploying your SST app. In this example, we are using `us-east-1`. + +Once our database is created, click **Add New Database User**. + +![New MongoDB cluster created](/assets/examples/rest-api-mongodb/new-mongodb-cluster-created.png) + +For now we are using **Password** as our **Authentication Method**. In this case we are using `mongodb` as our username and using the **Autogenerate Secure Password** option. Make sure to save the password because we are going to need it later. + +![Add new MongoDB database user](/assets/examples/rest-api-mongodb/add-new-mongodb-database-user.png) + +Now while those changes are being deployed, let's configure the **Network Access**. Click **Add IP Address**. + +![Add IP address network access](/assets/examples/rest-api-mongodb/add-ip-address-network-access.png) + +For now we'll use the **Allow Access From Anywhere** option. + +![Allow access to the database from anywhere](/assets/examples/rest-api-mongodb/allow-access-to-the-database-from-anywhere.png) + +Once those changes have been deployed, click the **Browse collections** button and add some test data. We are creating a database named **demo** and inside the database, adding a collection called **users** and with two sample documents. + +![sample dataset in database](/assets/examples/rest-api-mongodb/sample-data-in-database.png) + +Finally, let's get the connection string to connect to our new MongoDB database. + +Click **Connect** and use the **Connect your application** option. + +![Choose a connection method to the database](/assets/examples/rest-api-mongodb/choose-a-connection-method-to-the-database.png) + +Now **copy** the connection string. + +![Copy connection string to the database](/assets/examples/rest-api-mongodb/copy-connection-string-to-the-database.png) + +{%change%} Create a new `.env.local` file in your project root and add your connection string. + +```bash +MONGODB_URI=mongodb+srv://mongodb:@serverlessinstance0.j9n6s.mongodb.net/demo?retryWrites=true&w=majority +``` + +Make sure to replace `` with the password that we had copied while creating a database user above. + +We also want to make sure that this file is not committed to Git. + +{%change%} So add it to the `.gitignore` in your project root. + +``` +.env.local +``` + +## Query our MongoDB database + +We are now ready to add the function code to query our newly created MongoDB database. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import * as mongodb from "mongodb"; + +const MongoClient = mongodb.MongoClient; + +// Once we connect to the database once, we'll store that connection +// and reuse it so that we don't have to connect to the database on every request. +let cachedDb = null; + +async function connectToDatabase() { + if (cachedDb) { + return cachedDb; + } + + // Connect to our MongoDB database hosted on MongoDB Atlas + const client = await MongoClient.connect(process.env.MONGODB_URI); + + // Specify which database we want to use + cachedDb = await client.db("demo"); + + return cachedDb; +} + +export async function handler(event, context) { + // By default, the callback waits until the runtime event loop is empty + // before freezing the process and returning the results to the caller. + // Setting this property to false requests that AWS Lambda freeze the + // process soon after the callback is invoked, even if there are events + // in the event loop. + context.callbackWaitsForEmptyEventLoop = false; + + // Get an instance of our database + const db = await connectToDatabase(); + + // Make a MongoDB MQL Query + const users = await db.collection("users").find({}).toArray(); + + return { + statusCode: 200, + body: JSON.stringify(users, null, 2), + }; +} +``` + +The first thing to note here is the `connectToDatabase` method. We use the connection string from the environment and connect to our sample database. But we save a reference to it. This allows us to reuse the connection as long as this Lambda function container is being used. + +The `handler` function should be pretty straightforward here. We connect to our database and query the `users` collection in our database. And return 2 items. We then JSON stringify it and pretty print it. + +The line of note is: + +```typescript +context.callbackWaitsForEmptyEventLoop = false; +``` + +As the comment explains, we are telling AWS to not wait for the Node.js event loop to empty before freezing the Lambda function container. We need this because the connection to our MongoDB database is still around after our function returns. + +Let's install our MongoDB client, run the below command in the `packages/functions/` folder. + +```bash +$ npm install mongodb +``` + +We are now ready to test our API! + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-mongodb-ExampleStack: deploying... + + ✅ dev-rest-api-mongodb-ExampleStack + + +Stack dev-rest-api-mongodb-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://4gqqjg6ima.execute-api.us-east-1.amazonaws.com/ +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** explorer and click the **Send** button of the `GET /` route to get a list of users. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer list of users response](/assets/examples/rest-api-mongodb/api-explorer-list-of-users-response.png) + +You should see the list of users as a JSON string. + +## Making changes + +Now let's make a quick change to our database query. + +{%change%} Replace the following line in `packages/functions/src/lambda.ts`. + +```typescript +const users = await db.collection("users").find({}).toArray(); +``` + +{%change%} With: + +```typescript +const users = await db.collection("users").find({}).limit(1).toArray(); +``` + +This will limit the number of users to 1. + +![API explorer list of users limit 1 response](/assets/examples/rest-api-mongodb/api-explorer-list-of-users-limit-1-response.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-rest-api-mongodb-ExampleStack + + +Stack prod-rest-api-mongodb-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** explorer and click **Send** button of the `GET /notes` route, to send a `GET` request. + +![Prod API explorer list users response](/assets/examples/rest-api-mongodb/prod-api-explorer-list-of-users-response.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a serverless API connected to a [MongoDB serverless database](https://www.mongodb.com/cloud/atlas/serverless?utm_campaign=serverless_stack&utm_source=serverlessstack&utm_medium=website&utm_term=partner). We also have a local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-planetscale-in-your-serverless-app.md b/_examples/how-to-use-planetscale-in-your-serverless-app.md new file mode 100644 index 0000000000..3270fa2114 --- /dev/null +++ b/_examples/how-to-use-planetscale-in-your-serverless-app.md @@ -0,0 +1,330 @@ +--- +layout: example +title: How to use PlanetScale in your serverless app +short_title: PlanetScale +date: 2021-12-12 00:00:00 +lang: en +index: 5 +type: database +description: In this example we will look at how to use PlanetScale in your serverless app on AWS using SST. We'll be using the Api construct to create a simple hit counter. +short_desc: Using PlanetScale in a serverless API. +repo: planetscale +ref: how-to-use-planetscale-in-your-serverless-app +comments_id: how-to-use-planetscale-in-your-serverless-app/2594 +--- + +In this example we will look at how to use PlanetScale in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple hit counter. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [PlanetScale account](https://auth.planetscale.com/sign-up) with the [pscale CLI configured locally](https://docs.planetscale.com/reference/planetscale-environment-setup) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example planetscale +$ cd planetscale +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "planetscale", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## What is PlanetScale? + +PlanetScale is a MySQL-compatible, serverless database platform powered by Vitess, which is a database clustering system for horizontal scaling of MySQL (as well as Percona and MariaDB). Vitess also powers Slack, Square, GitHub, and YouTube, among others. The Slack deployment of Vitess has about 6,000 servers; the current largest Vitess deployment has about 70,000 servers. + +## Adding PlanetScale + +[PlanetScale](https://planetscale.com/) is a reliable and highly-performant MySQL database that can be configured as a true serverless database. Meaning that it'll scale up and down automatically. And you won't get charged if you are not using it. + +Note, Make sure you have [pscale CLI](https://docs.planetscale.com/reference/planetscale-environment-setup) installed locally + +Sign in to your account + +```bash +pscale auth login +``` + +You'll be taken to a screen in the browser where you'll be asked to confirm the code displayed in your terminal. If the confirmation codes match, click the "Confirm code" button in your browser. + +You should receive the message "Successfully logged in" in your terminal. You can now close the confirmation page in the browser and proceed in the terminal. + +### Creating a database + +Now that you have signed into PlanetScale using the CLI, you can create a database. + +Run the following command to create a database named `demo`: + +```bash +pscale database create demo +``` + +### Add a schema to your database + +To add a schema to your database, you will need to connect to MySQL using the pscale shell. + +Run the following command: + +```bash +pscale shell demo main +``` + +You are now connected to your main branch and can run MySQL queries against it. + +### Create a table by running the following command: + +```sql +CREATE TABLE IF NOT EXISTS counter (counter VARCHAR(255) PRIMARY KEY, tally INT); +``` + +You can confirm that the table has been added by running the following command: + +```sql +DESCRIBE counter; +``` + +### Insert data into your database + +Now that you have created a counter table, you can insert data into that table. + +Run the following command to add a hits counter to your table. + +```sql +INSERT INTO counter (counter, tally) VALUES ('hits', 0) ON DUPLICATE KEY UPDATE tally = 0; +``` + +![terminal output](/assets/examples/planetscale/terminal-output.png) + +We will be using the PlanetScale API to provision a TLS certificate, and then connect to the database. It uses Service Tokens for authentication, so you'll need to create one for the app: + +```bash +$ pscale service-token create + NAME TOKEN + -------------- ------------------------------------------ + nyprhd2z6bd3 [REDACTED] + +$ pscale service-token add-access nyprhd2z6bd3 connect_branch --database demo + DATABASE ACCESSES + ---------- --------------------------- + demo connect_branch +``` + +Add the above values in the `.env.local` file in the root + +``` +PLANETSCALE_TOKEN='' +PLANETSCALE_TOKEN_NAME='nyprhd2z6bd3' +PLANETSCALE_ORG='' +PLANETSCALE_DB='demo' +``` + +Note, The names of the env variables should not be changed + +## Setting up the API + +Now let's add the API. + +{%change%} Replace the code in `stacks/ExampleStack.ts` with below. + +```typescript +import { StackContext, Api } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + defaults: { + function: { + environment: { + PLANETSCALE_TOKEN: process.env.PLANETSCALE_TOKEN, + PLANETSCALE_TOKEN_NAME: process.env.PLANETSCALE_TOKEN_NAME, + PLANETSCALE_ORG: process.env.PLANETSCALE_ORG, + PLANETSCALE_DB: process.env.PLANETSCALE_DB, + }, + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +Our [API]({{ site.v2_url }}/constructs/api) simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +We also pass in the credentials we created to our API through environment variables. + +## Reading from our table + +Now in our function, we'll start by reading from our PlanetScale table. + +To access PlanetScale database we'll be using a package `planetscale-node`, install it the by running below command in the `packages/functions/` folder. + +```bash +npm install planetscale-node +``` + +{%change%} Now replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { PSDB } from "planetscale-node"; +// connect to main branch +const db = new PSDB("main"); + +export async function handler() { + // get tally from counter table + const [rows] = await db.query( + "SELECT tally FROM counter WHERE counter = 'hits'" + ); + + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: rows[0].tally, + }; +} +``` + +We make a `POST` call to our PlanetScale table and get the value of a row where the `counter` column has the value `hits`. Since, we haven't written to this column yet, we are going to just return `0`. + +And let's test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-planetscale-ExampleStack: deploying... + + ✅ dev-planetscale-ExampleStack + + +Stack dev-planetscale-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://u3nnmgdigh.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/angular-app/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Writing to our table + +Now let's update our table with the hits. + +{%change%} Add this above the query statement in `packages/functions/src/lambda.ts`. + +```typescript +// increment tally by 1 +await db.query("UPDATE counter SET tally = tally + 1 WHERE counter = 'hits'"); +``` + +Here we are updating the `hits` row's `tally` column with the increased count. + +And now if you head over to your console and click the **Send** button again you'll notice the count increase! + +![api-explorer-invocation-response-after-update](/assets/examples/rest-api-postgresql/api-explorer-invocation-response-after-update.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +Note, `env.local` is not committed to the git and remember to set the environment variables in your CI pipeline. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-planetscale-ExampleStack + + +Stack prod-planetscale-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://ck198mfop1.execute-api.us-east-1.amazonaws.com +``` + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** explorer and click **Send** button of the `POST /` route, to send a `POST` request. + +![API explorer prod invocation response](/assets/examples/angular-app/api-explorer-prod-invocation-response.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless hit counter. A local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-postgresql-in-your-serverless-app.md b/_examples/how-to-use-postgresql-in-your-serverless-app.md new file mode 100644 index 0000000000..1b8b2237af --- /dev/null +++ b/_examples/how-to-use-postgresql-in-your-serverless-app.md @@ -0,0 +1,336 @@ +--- +layout: example +title: How to use PostgreSQL in your serverless app +short_title: PostgreSQL +date: 2021-02-04 00:00:00 +lang: en +index: 3 +type: database +description: In this example we will look at how to use PostgreSQL in your serverless app on AWS using SST. We'll be using the Api construct and Amazon Aurora Serverless to create a simple hit counter. +short_desc: Using PostgreSQL and Aurora in a serverless API. +repo: rest-api-postgresql +ref: how-to-use-postgresql-in-your-serverless-app +comments_id: how-to-use-postgresql-in-your-serverless-app/2409 +--- + +In this example we will look at how to use PostgreSQL in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple hit counter using [Amazon Aurora Serverless](https://aws.amazon.com/rds/aurora/serverless/). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example rest-api-postgresql +$ cd rest-api-postgresql +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "rest-api-postgresql", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding PostgreSQL + +[Amazon Aurora Serverless](https://aws.amazon.com/rds/aurora/serverless/) is an auto-scaling managed relational database that supports PostgreSQL. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, RDS, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + const DATABASE = "CounterDB"; + + // Create the Aurora DB cluster + const cluster = new RDS(stack, "Cluster", { + engine: "postgresql10.14", + defaultDatabaseName: DATABASE, + migrations: "services/migrations", + }); +} +``` + +This creates an [RDS Serverless cluster]({{ site.v2_url }}/constructs/RDS). We also set the database engine to PostgreSQL. The database in the cluster that we'll be using is called `CounterDB` (as set in the `defaultDatabaseName` variable). + +The `migrations` prop should point to the folder where your migration files are. The `RDS` construct uses [Kysely](https://koskimas.github.io/kysely/) to run and manage schema migrations. You can [read more about migrations here]({{ site.v2_url }}/constructs/RDS#configuring-migrations). + +## Setting up the Database + +Let's create a migration file that creates a table called `tblcounter`. + +Create a `migrations` folder inside the `services/` folder. + +Let's write our first migration file, create a new file called `first.mjs` inside the newly created `services/migrations` folder and paste the below code. + +```js +import { Kysely } from "kysely"; + +/** + * @param db {Kysely} + */ +export async function up(db) { + await db.schema + .createTable("tblcounter") + .addColumn("counter", "text", (col) => col.primaryKey()) + .addColumn("tally", "integer") + .execute(); + + await db + .insertInto("tblcounter") + .values({ + counter: "hits", + tally: 0, + }) + .execute(); +} + +/** + * @param db {Kysely} + */ +export async function down(db) { + await db.schema.dropTable("tblcounter").execute(); +} +``` + +## Setting up the API + +Now let's add the API. + +{%change%} Add this below the `cluster` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create a HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + bind: [cluster], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.handler", + }, +}); + +// Show the resource info in the output +stack.addOutputs({ + ApiEndpoint: api.url, + SecretArn: cluster.secretArn, + ClusterIdentifier: cluster.clusterIdentifier, +}); +``` + +Our [API]({{ site.v2_url }}/constructs/Api) simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our database cluster to our API. + +## Reading from our database + +Now in our function, we'll start by reading from our PostgreSQL database. + +{%change%} Replace `packages/functions/src/lambda.ts` with the following. + +```typescript +import { Kysely } from "kysely"; +import { DataApiDialect } from "kysely-data-api"; +import { RDSData } from "@aws-sdk/client-rds-data"; +import { RDS } from "sst/node/rds"; + +interface Database { + tblcounter: { + counter: string; + tally: number; + }; +} + +const db = new Kysely({ + dialect: new DataApiDialect({ + mode: "postgres", + driver: { + database: RDS.Cluster.defaultDatabaseName, + secretArn: RDS.Cluster.secretArn, + resourceArn: RDS.Cluster.clusterArn, + client: new RDSData({}), + }, + }), +}); + +export async function handler() { + const record = await db + .selectFrom("tblcounter") + .select("tally") + .where("counter", "=", "hits") + .executeTakeFirstOrThrow(); + + let count = record.tally; + + return { + statusCode: 200, + body: count, + }; +} +``` + +We are using the [Data API](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html). It allows us to connect to our database over HTTP using the [kysely-data-api](https://github.com/sst/kysely-data-api). + +For now we'll get the number of hits from a table called `tblcounter` and return it. + +{%change%} Let's install the new packages in the `packages/functions/` folder. + + ```bash + $ npm install kysely kysely-data-api @aws-sdk/client-rds-data + ``` + + And test what we have so far. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-rest-api-postgresql-ExampleStack: deploying... + + ✅ dev-rest-api-postgresql-ExampleStack + + +Stack dev-rest-api-postgresql-ExampleStack + Status: deployed + Outputs: + SecretArn: arn:aws:secretsmanager:us-east-1:087220554750:secret:CounterDBClusterSecret247C4-MhR0f3WMmWBB-dnCizN + ApiEndpoint: https://u3nnmgdigh.execute-api.us-east-1.amazonaws.com + ClusterIdentifier: dev-rest-api-postgresql-counterdbcluster09367634-1wjmlf5ijd4be +``` + +The `ApiEndpoint` is the API we just created. While the `SecretArn` is what we need to login to our database securely. The `ClusterIdentifier` is the id of our database cluster. + +Before we can test our endpoint let's create the `tblcounter` table in our database. + +## Running migrations + +You can run migrations from the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **RDS** tab and click the **Migrations** button on the top right corner. + +It will list out all the migration files in the specified folder. + +Now to apply the migration that we created, click on the **Apply** button beside to the migration name. + +![list-of-migrations-in-the-stack](/assets/examples/rest-api-postgresql/list-of-migrations-in-the-stack.png) + +To confirm if the migration is successful, let's display the `tblcounter` table by running the below query. + +```sql +SELECT * FROM tblcounter +``` + +![successful-migration-output](/assets/examples/rest-api-postgresql/successful-migration-output.png) + +You should see the table with 1 row . + +## Test our API + +Now that our table is created, let's test our endpoint with the [SST Console](https://console.sst.dev). + +Go to the **API** tab and click **Send** button to send a `POST` request. + +Note, The [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/rest-api-postgresql/api-explorer-invocation-response.png) + +You should see a `0` in the response body. + +## Writing to our table + +So let's update our table with the hits. + +{%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. + +```typescript +await db + .updateTable("tblcounter") + .set({ + tally: ++count, + }) + .execute(); +``` + +Here we are updating the `hits` row's `tally` column with the increased count. + +And now if you head over to your console and make a request to our API. You'll notice the count increase! + +![api-explorer-invocation-response-after-update](/assets/examples/rest-api-postgresql/api-explorer-invocation-response-after-update.png) + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +Run the below command to open the SST Console in **prod** stage to test the production endpoint. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click **Send** button to send a `POST` request. + +![api-explorer-invocation-response-prod](/assets/examples/rest-api-postgresql/api-explorer-invocation-response-prod.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless hit counter. And we can test our changes locally before deploying to AWS! Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-pub-sub-in-your-serverless-app.md b/_examples/how-to-use-pub-sub-in-your-serverless-app.md new file mode 100644 index 0000000000..6b57deade1 --- /dev/null +++ b/_examples/how-to-use-pub-sub-in-your-serverless-app.md @@ -0,0 +1,257 @@ +--- +layout: example +title: How to use Pub/Sub in your serverless app +short_title: Pub/Sub +date: 2021-02-08 00:00:00 +lang: en +index: 3 +type: async +description: In this example we will look at how to use SNS in your serverless app on AWS using SST. We'll be using the Api and Topic constructs to create a simple checkout system. +short_desc: A simple pub/sub system with SNS. +repo: pub-sub +ref: how-to-use-pub-sub-in-your-serverless-app +comments_id: how-to-use-pub-sub-in-your-serverless-app/2315 +--- + +In this example we will look at how to use SNS to create [a pub/sub system](https://en.wikipedia.org/wiki/Publish–subscribe_pattern) in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple checkout flow. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example pub-sub +$ cd pub-sub +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "pub-sub", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding SNS Topic + +[Amazon SNS](https://aws.amazon.com/sns/) is a reliable and high-throughput messaging service. You are charged based on the number of API requests made to SNS. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { Api, StackContext, Topic } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Topic + const topic = new Topic(stack, "Ordered", { + subscribers: { + receipt: "packages/functions/src/receipt.main", + shipping: "packages/functions/src/shipping.main", + }, + }); +} +``` + +This creates an SNS topic using [`Topic`]({{ site.v2_url }}/constructs/Topic). And it has two subscribers. Meaning when the topic is published, both the functions will get run. + +## Setting up the API + +Now let's add the API. + +{%change%} Add this below the `Topic` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the SNS topic name to our API + bind: [topic], + }, + }, + routes: { + "POST /order": "packages/functions/src/order.main", + }, +}); + +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +Our [API]({{ site.v2_url }}/constructs/api) simply has one endpoint (`/order`). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/order.ts` will get invoked. + +We'll also bind our topic to our API. + +## Adding function code + +We will create three functions, one handling the `/order` API request, and two for the topic subscribers. + +{%change%} Add a `packages/functions/src/order.ts`. + +```typescript +export async function main() { + console.log("Order confirmed!"); + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +{%change%} Add a `packages/functions/src/receipt.ts`. + +```typescript +import { SNSEvent } from "aws-lambda"; + +export async function main(event: SNSEvent) { + const records: any[] = event.Records; + console.log(`Receipt sent: "${records[0].Sns.Message}"`); + + return {}; +} +``` + +{%change%} Add a `packages/functions/src/shipping.ts`. + +```typescript +import { SNSEvent } from "aws-lambda"; + +export async function main(event: SNSEvent) { + const records: any[] = event.Records; + console.log(`Item shipped: "${records[0].Sns.Message}"`); + + return {}; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +Deployed: +ExampleStack +ApiEndpoint: https://gevkgi575a.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint. Run the following in a new terminal. + +```bash +$ curl -X POST https://gevkgi575a.execute-api.us-east-1.amazonaws.com/order +``` + +This makes a POST request to our API. You should see `Order confirmed!` in the `sst dev` terminal. + +## Publishing to our topic + +Now let's publish a message to our topic. + +{%change%} Replace the `packages/functions/src/order.ts` with the following. + +```typescript +import AWS from "aws-sdk"; +import { Topic } from "sst/node/topic"; + +const sns = new AWS.SNS(); + +export async function main() { + // Publish a message to topic + await sns + .publish({ + // Get the topic from the environment variable + TopicArn: Topic.Ordered.topicArn, + Message: JSON.stringify({ ordered: true }), + MessageStructure: "string", + }) + .promise(); + + console.log("Order confirmed!"); + + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +Here we are getting the topic arn from the environment variable, and then publishing a message to it. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +Now if you hit our API again. + +```bash +$ curl -X POST https://gevkgi575a.execute-api.us-east-1.amazonaws.com/order +``` + +You should see the following in your `sst dev` terminal. + +```txt +Item shipped: "{"ordered":true}" +Receipt sent: "{"ordered":true}" +``` + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless checkout system, powered by SNS. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-queues-in-your-serverless-app.md b/_examples/how-to-use-queues-in-your-serverless-app.md new file mode 100644 index 0000000000..44ebbee1cc --- /dev/null +++ b/_examples/how-to-use-queues-in-your-serverless-app.md @@ -0,0 +1,235 @@ +--- +layout: example +title: How to use queues in your serverless app +short_title: Queues +date: 2021-02-08 00:00:00 +lang: en +index: 2 +type: async +description: In this example we will look at how to use SQS in your serverless app on AWS using SST. We'll be using the Api and Queue constructs to create a simple queue system. +short_desc: A simple queue system with SQS. +repo: queue +ref: how-to-use-queues-in-your-serverless-app +comments_id: how-to-use-queues-in-your-serverless-app/2314 +--- + +In this example we will look at how to use SQS to create a queue in our serverless app using [SST]({{ site.sst_github_repo }}). We'll be creating a simple queue system. + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example queue +$ cd queue +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "queue", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of two parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Adding SQS Queue + +[Amazon SQS](https://aws.amazon.com/sqs/) is a reliable and high-throughput message queuing service. You are charged based on the number of API requests made to SQS. And you won't get charged if you are not using it. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Queue, Api } from "sst/constructs"; + +export function ExampleStack({ stack }: StackContext) { + // Create Queue + const queue = new Queue(stack, "Queue", { + consumer: "packages/functions/src/consumer.main", + }); +} +``` + +This creates an SQS queue using [`Queue`]({{ site.v2_url }}/constructs/Queue). And it has a consumer that polls for messages from the queue. The consumer function will run when it has polled 1 or more messages. + +## Setting up the API + +Now let's add the API. + +{%change%} Add this below the `Queue` definition in `stacks/ExampleStack.ts`. + +```typescript +// Create the HTTP API +const api = new Api(stack, "Api", { + defaults: { + function: { + // Bind the table name to our API + bind: [queue], + }, + }, + routes: { + "POST /": "packages/functions/src/lambda.main", + }, +}); + +// Show the API endpoint in the output +stack.addOutputs({ + ApiEndpoint: api.url, +}); +``` + +Our [API]({{ site.v2_url }}/constructs/api) simply has one endpoint (the root). When we make a `POST` request to this endpoint the Lambda function called `main` in `packages/functions/src/lambda.ts` will get invoked. + +We'll also bind our queue to our API. + +## Adding function code + +We will create two functions, one for handling the API request, and one for the consumer. + +{%change%} Replace the `packages/functions/src/lambda.ts` with the following. + +```typescript +export async function main() { + console.log("Message queued!"); + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +{%change%} Add a `packages/functions/src/consumer.ts`. + +```typescript +import { SQSEvent } from "aws-lambda"; + +export async function main(event: SQSEvent) { + const records: any[] = event.Records; + console.log(`Message processed: "${records[0].body}"`); + + return {}; +} +``` + +Now let's test our new API. + +## Starting your dev environment + +{%change%} SST features a [Live Lambda Development]({{ site.v2_url }}/live-lambda-development) environment that allows you to work on your serverless apps live. + +```bash +$ npm run dev +``` + +The first time you run this command it'll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment. + +``` +Deployed: +ExampleStack +ApiEndpoint: https://3vi820odbc.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint. Run the following in a new terminal. + +```bash +$ curl -X POST https://3vi820odbc.execute-api.us-east-1.amazonaws.com +``` + +This makes a POST request to our API. You should see `Message queued!` in the `sst dev` terminal. + +## Sending message to our queue + +Now let's send a message to our queue. + +{%change%} Replace the `packages/functions/src/lambda.ts` with the following. + +```typescript +import AWS from "aws-sdk"; +import { Queue } from "sst/node/queue"; + +const sqs = new AWS.SQS(); + +export async function main() { + // Send a message to queue + await sqs + .sendMessage({ + // Get the queue url from the environment variable + QueueUrl: Queue.Queue.queueUrl, + MessageBody: JSON.stringify({ ordered: true }), + }) + .promise(); + + console.log("Message queued!"); + + return { + statusCode: 200, + body: JSON.stringify({ status: "successful" }), + }; +} +``` + +Here we are getting the queue url from the environment variable, and then sending a message to it. + +{%change%} Let's install the `aws-sdk` package in the `packages/functions/` folder. + +```bash +$ npm install aws-sdk +``` + +Now if you hit our API again. + +```bash +$ curl -X POST https://3vi820odbc.execute-api.us-east-1.amazonaws.com +``` + +You should see `Message processed: "{"ordered":true}"` printed out in the `sst dev` terminal. + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the API for our users. + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a completely serverless queue system. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-sentry-to-monitor-your-serverless-app.md b/_examples/how-to-use-sentry-to-monitor-your-serverless-app.md new file mode 100644 index 0000000000..c170fe4402 --- /dev/null +++ b/_examples/how-to-use-sentry-to-monitor-your-serverless-app.md @@ -0,0 +1,237 @@ +--- +layout: example +title: How to use Sentry to monitor your serverless app +short_title: Sentry +date: 2021-11-01 00:00:00 +lang: en +index: 2 +type: monitoring +description: In this example we will look at how to use Sentry with a serverless API to create and monitor a simple click counter app. We'll be using SST. +short_desc: Using Sentry to monitor a serverless app. +repo: sentry +ref: how-to-use-sentry-to-monitor-your-serverless-app +comments_id: how-to-use-sentry-to-monitor-your-serverless-app/2521 +--- + +In this example we will look at how to use [Sentry](https://www.sentry.io) to monitor the Lambda functions in your [SST serverless application]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Sentry account](https://sentry.io/signup/) + +## What is Sentry + +When a serverless app is deployed to production, it's useful to be able to monitor your Lambda functions. There are a few different services that you can use for this. One of them is [Sentry](https://sentry.io/signup/). Sentry offers [Serverless Error and Performance Monitoring](https://sentry.io/for/serverless/) for your Lambda functions. + +## Create an SST app + +{%change%} Let's start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example sentry +$ cd sentry +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "sentry", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Create our infrastructure + +Our app is going to be a simple API that returns a _Hello World_ response. + +### Creating our API + +Let's add the API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { LayerVersion } from "aws-cdk-lib/aws-lambda"; +import { Api, StackContext } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint at the root. When we make a `GET` request to this endpoint the function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +{%change%} Your `packages/functions/src/lambda.ts` should look something like this. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}; +``` + +## Setting up our app with Sentry + +We are now ready to use [Sentry](https://www.sentry.io/) to monitor our API. Sentry offers [Serverless Error and Performance Monitoring](https://sentry.io/for/serverless/) for your Lambda functions. Integration is done through a Lambda Layer. + +Go to the **Settings** > **Projects**. Select the project. Then scroll down to **SDK SETUP** and select **Client Keys (DSN)**. And **copy the DSN**. + +![Copy Sentry DSN from settings](/assets/examples/sentry/copy-sentry-dsn-from-settings.png) + +{%change%} Create a `.env.local` file with the `SENTRY_DSN` in your project root. + +```bash +SENTRY_DSN=https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@xxxxxxxx.ingest.sentry.io/xxxxxxx +``` + +Note that, this file should not be committed to Git. If you are deploying the app through a CI service, configure the `SENTRY_DSN` as an environment variable in the CI provider. If you are deploying through Seed, you can [configure this in your stage settings](https://seed.run/docs/storing-secrets.html). + +Next, you'll need to add the Sentry Lambda layer in your app. + +[Head over to the Sentry docs](https://docs.sentry.io/platforms/node/guides/aws-lambda/layer/) and get the layer they provide. **Select your region** and **copy the layer ARN**. + +![Copy Sentry Lambda Layer ARN](/assets/examples/sentry/copy-sentry-lambda-layer-arn.png) + +You can then set the layer for all the functions in your stack using the [`addDefaultFunctionLayers`]({{ site.v2_url }}/constructs/Stack#adddefaultfunctionlayers) and [`addDefaultFunctionEnv`]({{ site.v2_url }}/constructs/Stack#adddefaultfunctionenv). Note we only want to enable this when the function is deployed, and not when using [Live Lambda Dev]({{ site.v2_url }}/live-lambda-development). + +{%change%} Add the following to the top of the `ExampleStack` function in `stacks/ExampleStack.ts`. + +```typescript +// Configure Sentry +if (!app.local) { + const sentry = LayerVersion.fromLayerVersionArn( + stack, + "SentryLayer", + `arn:aws:lambda:${app.region}:943013980633:layer:SentryNodeServerlessSDK:35` + ); + + stack.addDefaultFunctionLayers([sentry]); + stack.addDefaultFunctionEnv({ + SENTRY_DSN: process.env.SENTRY_DSN, + SENTRY_TRACES_SAMPLE_RATE: "1.0", + NODE_OPTIONS: "-r @sentry/serverless/dist/awslambda-auto", + }); +} +``` + +Note that `addDefaultFunctionLayers` and `addDefaultFunctionEnv` only affects the functions added after it's been called. So make sure to call it at the beginning of your stack definition if you want to monitor all the Lambda functions in your stack. + +Also, replace the layer ARN with the one that we copied above. + +## Wrapping our Lambda handler + +Next, we'll instrument our Lambda functions by wrapping them with the Sentry handler. + +{%change%} Replace the code in `packages/functions/src/lambda.ts` with this. + +```typescript +import * as Sentry from "@sentry/serverless"; + +export const handler = Sentry.AWSLambda.wrapHandler(async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received at ${event.requestContext.time}.`, + }; +}); +``` + +Let's test what we have so far. + +## Deploy your app + +We need to deploy the API in order to track any errors. + +{%change%} Run the following. + +```bash +$ npx sst deploy +``` + +The first time you run this command it'll take a couple of minutes to deploy your app from scratch. + +``` +=============== + Deploying app +=============== + +Preparing your SST app +Transpiling source +Linting source +Deploying stacks +dev-sentry-ExampleStack: deploying... + + ✅ dev-sentry-ExampleStack + + +Stack dev-sentry-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://753gre9wkh.execute-api.us-east-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint with the [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps. [Learn more about it in our docs]({{ site.v2_url }}/console). + +Go to the **API** tab and click **Send** button to send a `GET` request. + +Note, the [API explorer]({{ site.v2_url }}/console#api) lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API explorer invocation response](/assets/examples/datadog/api-explorer-invocation-response.png) + +You should see the _Hello World_ message. + +Now head over to your Sentry dashboard to start exploring key metrics like the execution duration, failure rates, and transactions per minute. You can also click through to inspect specific errors. + +![View Sentry serverless dashboard](/assets/examples/sentry/view-sentry-serverless-dashboard.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following. + +```bash +$ npx sst remove +``` + +## Conclusion + +And that's it! We've got a serverless API monitored with Sentry. It's deployed to production, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/how-to-use-thundra-to-monitor-your-serverless-app.md b/_examples/how-to-use-thundra-to-monitor-your-serverless-app.md new file mode 100644 index 0000000000..2ffa35f5d3 --- /dev/null +++ b/_examples/how-to-use-thundra-to-monitor-your-serverless-app.md @@ -0,0 +1,291 @@ +--- +layout: example +title: How to use Thundra APM to monitor your serverless app +short_title: Thundra +date: 2021-11-01 00:00:00 +lang: en +index: 3 +type: monitoring +description: In this example we will look at how to use Thundra APM with a serverless API to create and monitor a simple Hello world app. We'll be using SST. +short_desc: Using Thundra APM to monitor a serverless app. +repo: thundra +ref: how-to-use-thundra-apm-to-monitor-your-serverless-app +comments_id: how-to-use-thundra-apm-to-monitor-your-serverless-app/2614 +--- + +In this example we will look at how to use [Thundra APM](https://www.thundra.io/apm) to monitor the Lambda functions in your [SST serverless application]({{ site.sst_github_repo }}). + +## Requirements + +- Node.js 16 or later +- We'll be using TypeScript +- An [AWS account]({% link _chapters/create-an-aws-account.md %}) with the [AWS CLI configured locally]({% link _chapters/configure-the-aws-cli.md %}) +- A [Thundra account](https://www.thundra.io) and that's it. + +## What is Thundra + +When a serverless app is deployed to production, it's useful to be able to monitor your Lambda functions. There are a few different services that you can use for this. One of them is [Thundra](https://www.thundra.io). Thundra offers an End-to-end Serverless Monitoring solution called Thundra APM that works with Lambda functions. + +Let's look at how to set this up. + +## Create an SST app + +{%change%} Start by creating an SST app. + +```bash +$ npx create-sst@latest --template=base/example thundra +$ cd thundra +$ npm install +``` + +By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. + +```js +import { SSTConfig } from "sst"; + +export default { + config(_input) { + return { + name: "thundra", + region: "us-east-1", + }; + }, +} satisfies SSTConfig; +``` + +## Project layout + +An SST app is made up of a couple of parts. + +1. `stacks/` — App Infrastructure + + The code that describes the infrastructure of your serverless app is placed in the `stacks/` directory of your project. SST uses [AWS CDK]({% link _archives/what-is-aws-cdk.md %}), to create the infrastructure. + +2. `packages/functions/` — App Code + + The code that's run when your API is invoked is placed in the `packages/functions/` directory of your project. + +## Create our infrastructure + +Our app is going to be a simple API that returns a _Hello World_ response. + +### Creating our API + +Let's add the API. + +{%change%} Replace the `stacks/ExampleStack.ts` with the following. + +```typescript +import { StackContext, Api } from "sst/constructs"; + +export function ExampleStack({ stack, app }: StackContext) { + // Create a HTTP API + const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, + }); + + // Show the endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); +} +``` + +We are using the SST [`Api`]({{ site.v2_url }}/constructs/Api) construct to create our API. It simply has one endpoint at the root. When we make a `GET` request to this endpoint the function called `handler` in `packages/functions/src/lambda.ts` will get invoked. + +{%change%} Your `packages/functions/src/lambda.ts` should look something like this. + +```typescript +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, World! Your request was received`, + }; +}; +``` + +## Setting up our app with Thundra + +[Thundra](https://thundra.io/) offers [Thundra APM - Application Performance Monitoring for Serverless and Containers](https://thundra.io/apm). + +To get started, [sign up for an account](https://console.thundra.io/landing/) in Thundra. + +Next, go to the [**Projects**](https://apm.thundra.io/projects) page of your Thundra dashboard and copy the API key. + +![Copy Thundra API key from dashboard](/assets/examples/thundra/thundra-api-key-page.png) + +{%change%} Create a `.env.local` file with the API key in your project root. + +```bash +THUNDRA_APIKEY= +``` + +Note that, this file should not be committed to Git. If you are deploying the app through a CI service, configure the `THUNDRA_APIKEY` as an environment variable in the CI provider. If you are deploying through Seed, you can [configure this in your stage settings](https://seed.run/docs/storing-secrets.html). + +You can connect to Thundra in two ways, + +1. You can connect your AWS account and add our CloudFormation stack into your AWS. +2. If you don’t want to give access to your AWS account, you can always manually instrument your application to monitor with Thundra. Thundra supports different types of languages, platforms, web frameworks, and applications. You can learn more about connecting Thundra [here](https://apm.docs.thundra.io/getting-started/quick-start-guide/connect-thundra). + +For this tutorial let's follow the second way. + +You can then set the layer for all the functions in your stack using the [`addDefaultFunctionLayers`]({{ site.v2_url }}/constructs/Stack#adddefaultfunctionlayers) and [`addDefaultFunctionEnv`]({{ site.v2_url }}/constructs/Stack#adddefaultfunctionenv). Note we only want to enable this when the function is deployed, and not when using [Live Lambda Dev]({{ site.v2_url }}/live-lambda-development). + +{%change%} Add the following above the `api` definiton line in `stacks/ExampleStack.ts`. + +```typescript +// Configure thundra to only prod +if (!app.local) { + const thundraAWSAccountNo = 269863060030; + const thundraNodeLayerVersion = 107; // Latest version at time of writing + const thundraLayer = LayerVersion.fromLayerVersionArn( + this, + "ThundraLayer", + `arn:aws:lambda:${app.region}:${thundraAWSAccountNo}:layer:thundra-lambda-node-layer:${thundraNodeLayerVersion}` + ); + stack.addDefaultFunctionLayers([thundraLayer]); + + stack.addDefaultFunctionEnv({ + THUNDRA_APIKEY: process.env.THUNDRA_APIKEY, + NODE_OPTIONS: "-r @thundra/core/dist/bootstrap/lambda", + }); +} +``` + +Note, to figure out the layer ARN for the latest version, [check the badge here](https://apm.docs.thundra.io/Node.js/nodejs-integration-options). + +Note that `addDefaultFunctionLayers` and `addDefaultFunctionEnv` only affects the functions added after it's been called. So make sure to call it at the beginning of your stack definition if you want to monitor all the Lambda functions in your stack. + +## Deploying to prod + +{%change%} To wrap things up we'll deploy our app to prod. + +```bash +$ npx sst deploy --stage prod +``` + +This allows us to separate our environments, so when we are working in `dev`, it doesn't break the app for our users. + +Once deployed, you should see something like this. + +```bash + ✅ prod-thundra-ExampleStack + + +Stack prod-thundra-ExampleStack + Status: deployed + Outputs: + ApiEndpoint: https://k40qchmtvf.execute-api.ap-south-1.amazonaws.com +``` + +The `ApiEndpoint` is the API we just created. + +Let's test our endpoint using the integrated [SST Console](https://console.sst.dev). The SST Console is a web based dashboard to manage your SST apps [Learn more about it in our docs]({{ site.v2_url }}/console). + +Run the below command to start SST console in **prod** stage. + +```bash +npx sst console --stage prod +``` + +Go to the **API** tab and click the **Send** button. + +Note, The API explorer lets you make HTTP requests to any of the routes in your `Api` construct. Set the headers, query params, request body, and view the function logs with the response. + +![API tab invoke button](/assets/examples/thundra/api_tab_invoke_button.png) + +You will see the response of your function. + +Now let's go to Thundra dashboard to check if we are able to monitor the invocation. + +The [Functions view](https://apm.thundra.io/functions) aggregates data from all of the serverless functions running in your environment, enabling you to monitor their performance in one place. + +![Thundra functions dashboard](/assets/examples/thundra/thundra-initial-page-after-start.png) + +Note, you may need to wait for 5-10 minutes before you can see the metrics of your function. + +![Thundra functions metric page](/assets/examples/thundra/thundra-metrics-page.png) + +### Time Travel Debugging + +Thudra also offers a feature called [Time Travel Debugging (TTD)](https://apm.docs.thundra.io/debugging/offline-debugging) that makes it possible to travel back in time to previous states of your application by getting a snapshot of when each line is executed. You can step over each line of the code and track the values of the variables captured during execution. + +To enable TTD in your SST app, follow the below steps. + +If you use SST and your code is bundled, you can use `thundra-esbuild-plugin` to activate TTD (Time-Travel Debugging). + +Install the package by running below command in the `packages/functions/` folder. + +```bash +npm install --save-dev @thundra/esbuild-plugin +``` + +Create a new file called `esbuild.js` inside `config` folder in root and add the below code. + +```typescript +// config/esbuild.js + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { ThundraEsbuildPlugin } = require("@thundra/esbuild-plugin"); + +module.exports = [ + ThundraEsbuildPlugin({ + traceableConfigs: [ + "src.*.*[traceLineByLine=true]", // activate line by line tracing for all files/methods under src folder + ], + }), +]; +``` + +And then in `stacks/ExampleStack.ts` add the below code under the `defaults` in `Api` construct. + +{%change%} Replace the following in `stacks/ExampleStack.ts`: + +```typescript +const api = new Api(stack, "Api", { + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, +}); +``` + +{%change%} With: + +```typescript +const api = new Api(stack, "Api", { + defaults: { + function: { + bundle: { + esbuildConfig: { + plugins: "config/esbuild.js", + }, + }, + }, + }, + routes: { + "GET /": "packages/functions/src/lambda.handler", + }, +}); +``` + +Now in the Trace chart of the invocation you can see the code that is executed. + +![time travel debugging demo](/assets/examples/thundra/time_travel_debugging_demo.png) + +## Cleaning up + +Finally, you can remove the resources created in this example using the following commands. + +```bash +$ npx sst remove +$ npx sst remove --stage prod +``` + +## Conclusion + +And that's it! We've got a serverless API monitored with Thundra. We also have a local development environment, to test and make changes. And it's deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions! diff --git a/_examples/index.md b/_examples/index.md new file mode 100644 index 0000000000..594bb0808e --- /dev/null +++ b/_examples/index.md @@ -0,0 +1,15 @@ +--- +layout: example +title: Serverless Examples +date: 2021-01-27 00:00:00 +lang: en +description: A collection of example serverless apps built with SST. +ref: examples-index +repo: true +comments_id: example-apps-created-with-sst/2304 +--- + +{:.page-desc} +A collection of example serverless apps built with [SST]({{ site.sst_github_repo }}). The [source for these examples are available on GitHub]({{ site.sst_github_repo }}{{ site.sst_github_examples_prefix }}). + +{% include examples-list.html %} diff --git a/_includes/email-octopus-form.html b/_includes/email-octopus-form.html new file mode 100644 index 0000000000..ed2fff7ee1 --- /dev/null +++ b/_includes/email-octopus-form.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/_includes/examples-list.html b/_includes/examples-list.html new file mode 100644 index 0000000000..8244d27c72 --- /dev/null +++ b/_includes/examples-list.html @@ -0,0 +1,83 @@ +{% assign groups = site.examples | group_by: "type" %} +
+ +
+ +
+ {% assign apis_group = groups | find: "name", "api" %} + {% assign apis = apis_group.items | sort: "index" %} + {% include toc-examples.html items=apis title="APIs" id="apis" %} + +
+ +
+ {% assign webapps_group = groups | find: "name", "webapp" %} + {% assign webapps = webapps_group.items | sort: "index" %} + {% include toc-examples.html items=webapps title="Web Apps" id="webapps" %} + +
+ +
+ {% assign mobileapps_group = groups | find: "name", "mobileapp" %} + {% assign mobileapps = mobileapps_group.items | sort: "index" %} + {% include toc-examples.html items=mobileapps title="Mobile Apps" id="mobileapps" %} + +
+ +
+ {% assign graphql_group = groups | find: "name", "graphql" %} + {% assign graphql = graphql_group.items | sort: "index" %} + {% include toc-examples.html items=graphql title="GraphQL" id="graphql" %} + +
+ +
+ {% assign databases_group = groups | find: "name", "database" %} + {% assign databases = databases_group.items | sort: "index" %} + {% include toc-examples.html items=databases title="Databases" id="databases" %} + +
+ +
+
+

Authentication

+ {% assign sst_auth_group = groups | find: "name", "sst-auth" %} + {% assign sst_auth = sst_auth_group.items | sort: "index" %} + {% include toc-examples.html items=sst_auth title="Using SST Auth" id="sst-auth" %} + {% assign iam_auth_group = groups | find: "name", "iam-auth" %} + {% assign iam_auth = iam_auth_group.items | sort: "index" %} + {% include toc-examples.html items=iam_auth title="Using Cognito Identity Pools" id="iam-auth" %} + {% assign jwt_auth_group = groups | find: "name", "jwt-auth" %} + {% assign jwt_auth = jwt_auth_group.items | sort: "index" %} + {% include toc-examples.html items=jwt_auth title="Using Cognito User Pools" id="jwt-auth" %} +
+ +
+ +
+ {% assign async_group = groups | find: "name", "async" %} + {% assign async = async_group.items | sort: "index" %} + {% include toc-examples.html items=async title="Async Tasks" id="async" %} + +
+ +
+ {% assign editors_group = groups | find: "name", "editor" %} + {% assign editors = editors_group.items | sort: "index" %} + {% include toc-examples.html items=editors title="Editors" id="editors" %} + +
+ +
+ {% assign monitoring_group = groups | find: "name", "monitoring" %} + {% assign monitoring = monitoring_group.items | sort: "index" %} + {% include toc-examples.html items=monitoring title="Monitoring" id="monitoring" %} + +
+ +
+ {% assign misc_group = groups | find: "name", "misc" %} + {% assign misc = misc_group.items | sort: "index" %} + {% include toc-examples.html items=misc title="Miscellaneous" id="misc" %} + +
diff --git a/_includes/footer.html b/_includes/footer.html index a6ba24bf52..54adfc6624 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -4,24 +4,45 @@ diff --git a/_includes/lander-examples/preview-deploys.html b/_includes/lander-examples/preview-deploys.html new file mode 100644 index 0000000000..fb9eb655a9 --- /dev/null +++ b/_includes/lander-examples/preview-deploys.html @@ -0,0 +1,56 @@ +
    +
  • +
    Deployed to pr#14
    +
    + +
    +

    Updating products page

    +
    + + + products-page + + + db65c0a +
    +
    +
    Deployed
    +
    +
  • +
  • +
    Deployed to branch-profiles
    +
    + +
    +

    Adding new user profiles page

    +
    + + + branch-profiles + + + f0bd4af +
    +
    +
    Deployed
    +
    +
  • +
  • +
    Deployed to pr#13
    +
    + +
    +

    Fixing bug in settings page

    +
    + + + settings-page + + + bc32207 +
    +
    +
    Deployed
    +
    +
  • +
\ No newline at end of file diff --git a/_includes/lander-header.html b/_includes/lander-header.html deleted file mode 100644 index dfa834ac7e..0000000000 --- a/_includes/lander-header.html +++ /dev/null @@ -1,33 +0,0 @@ - diff --git a/_includes/lander.html b/_includes/lander.html deleted file mode 100644 index 908f9d84bd..0000000000 --- a/_includes/lander.html +++ /dev/null @@ -1,106 +0,0 @@ -
- -

- Our readers are from some of the biggest companies in the world -

- -
-
    -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
-
- -
- -
-

Brought to you by

- A fully-configured CI/CD pipeline for Serverless -

Check out our supporters

-
- -
- -

- Our comprehensive tutorials include -

- -
    -
  • Step-by-step instructions with screenshots for setting up your AWS services
  • -
  • Build a REST API backend with our Serverless tutorial
  • -
  • Easy to understand explanations for creating a single-page application with our React.js tutorial
  • -
  • Over 100 chapters that take you all the way from creating your AWS account to deploying your app on your own domain
  • -
  • Complete code samples hosted on GitHub for both the backend and frontend
  • -
  • Up to date info thanks to contributions from the community
  • -
  • Forums on Discourse to answer your questions
  • -
  • And it’s all free!
  • -
- -
- -

- Recent updates to Serverless Stack -

- -
-
- -
-
-
{{ site.data.changelog.current.title }}
-

{{ site.data.changelog.current.desc }}

- View all the past updates -
-
- -
- -

- Here is what folks are saying about us -

- -
    -
  • -

    I just completed your tutorial on the Serverless stack, and it was fantastic. It was carefully thought out, well-written, and incredibly thorough. You guys rock!

    -

    ‒ Chris Deery, Architect at QuickBase

    -
  • -
  • -

    This is the best and most comprehensive fullstack serverless tutorial available. Take the time to go through every step, you will learn a ton!

    -

    ‒ Marcus Molchany, Engineer at Fullscreen

    -
  • -
  • -

    Rarely do tutorials neatly break things down into bite-sized steps. Even rarer do they clearly explain what each step is for and why it is necessary. The Serverless-Stack is the Holy Grail that does all three.

    -

    ‒ Allison Yaeger, Independent UX Architect

    -
  • -
  • -

    I think this is the best tech tutorial I've read in the last 20 years. It's detailed, well-written, covers a multitude of the latest technologies, and the layout of each chapter makes the subject matter easy to digest. Cheers!

    -

    ‒ Kevin, Engineer

    -
  • -
-
diff --git a/_includes/newsletter-form-sidebar.html b/_includes/newsletter-form-sidebar.html new file mode 100644 index 0000000000..a90ad13e1c --- /dev/null +++ b/_includes/newsletter-form-sidebar.html @@ -0,0 +1,21 @@ +
+
+ + {% if include.type == "guide" %} +

Part of the

+
SST Guide
+

Learn to build full-stack apps with serverless and React.

+ {% else %} +

Get the

+
SST Guide
+

The most popular resource for building serverless apps.

+ {% endif %} +
+ +
+ diff --git a/_includes/newsletter-form.html b/_includes/newsletter-form.html index aa514bd8a3..018882197c 100644 --- a/_includes/newsletter-form.html +++ b/_includes/newsletter-form.html @@ -1,33 +1,28 @@ -

Download this guide as a 640 page PDF!
And get notified when we publish updates.

-