├── .circleci └── config.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── images ├── c9-conf.png ├── c9-create-name.png ├── c9-create.png ├── c9-deploy.png ├── c9-env.png ├── c9-region.png ├── dynamodb-create.png ├── dynamodb-policy.png ├── s3-create-1.png ├── s3-create-btn.png ├── security-group.png ├── security-group2.png ├── serverless-framework-1.png ├── static-front.png ├── static-web1.png ├── static-web2.png └── static-web3.png ├── sample-app ├── .gitignore ├── handler.js └── serverless.yml ├── serverless-api ├── README.md ├── app.js ├── bin │ └── www ├── handler.js ├── package-lock.json ├── package.json ├── routes │ └── todo.js ├── serverless.yml ├── spec │ └── todo.spec.js ├── swagger.yml ├── template.yaml └── test │ └── handler.js ├── serverless-ts-api ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── serverless.yml ├── sls-build.sh ├── sls-run.sh ├── src │ ├── app.ts │ ├── handler.ts │ ├── middlewares │ │ └── ErrorHandler.ts │ ├── routes │ │ └── TodoRoute.ts │ └── www.ts └── tsconfig.json ├── sls-ts-static-web-front ├── .babelrc ├── README.md ├── deploy.sh ├── package-lock.json ├── package.json └── src │ ├── components │ ├── App.js │ └── Top.js │ ├── index.css │ ├── index.html │ └── index.js ├── static-web-front ├── .babelrc ├── README.md ├── package.json ├── src │ ├── components │ │ ├── App.js │ │ └── Top.js │ ├── index.css │ ├── index.html │ └── index.js └── yarn.lock └── ws-static-web-front ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html └── src ├── App.vue ├── assets └── logo.jpeg ├── components └── HelloWorld.vue ├── main.js └── store.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | test: 4 | docker: 5 | - image: lambci/lambda:build-nodejs8.10 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - install-{{ .Branch }}-{{ checksum "serverless-api/package.json" }} 11 | - run: 12 | working_directory: serverless-api 13 | command: npm i 14 | - run: echo "Hello world" 15 | - save_cache: 16 | paths: 17 | - serverless-api/node_modules 18 | key: install-{{ .Branch }}-{{ checksum "serverless-api/package.json" }} 19 | - run: 20 | working_directory: serverless-api 21 | command: npm test 22 | # build: 23 | # docker: 24 | # - image: lambci/lambda:build-nodejs8.10 25 | # steps: 26 | # - restore_cache: 27 | # keys: 28 | # - install-{{ checksum "serverless-api/package-lock.json" }} 29 | # - checkout 30 | # - run: 31 | # working_directory: serverless-api 32 | # command: npm i 33 | # - save_cache: 34 | # paths: 35 | # - serverless-api/node_modules 36 | # key: install-{{ checksum "serverless-api/package-lock.json" }} 37 | # - run: 38 | # working_directory: serverless-api 39 | # command: npm run build 40 | # - save_cache: 41 | # key: build-{{ .Branch }}-{{ checksum "serverless-api/package-lock.json" }} 42 | deploy: 43 | docker: 44 | - image: lambci/lambda:build-nodejs8.10 45 | steps: 46 | - checkout 47 | - restore_cache: 48 | keys: 49 | - install-{{ .Branch }}-{{ checksum "serverless-api/package.json" }} 50 | - run: 51 | working_directory: serverless-api 52 | command: npm i 53 | - save_cache: 54 | paths: 55 | - serverless-api/node_modules 56 | key: install-{{ .Branch }}-{{ checksum "serverless-api/package.json" }} 57 | - run: 58 | working_directory: serverless-api 59 | command: 'echo -e "STAGE: $STAGE \nAWS_REGION: $AWS_REGION \nDEPLOYMENT_BUCKET: $DEPLOYMENT_BUCKET" > config.yml' 60 | - run: 61 | working_directory: serverless-api 62 | command: npm run deploy 63 | workflows: 64 | version: 2 65 | serverless-api: 66 | jobs: 67 | - test: 68 | filters: 69 | branches: 70 | only: 71 | - master 72 | - alpha 73 | # - build: 74 | # requires: 75 | # - test 76 | # filters: 77 | # branches: 78 | # only: 79 | # - master 80 | # - alpha 81 | - hold: 82 | type: approval 83 | requires: 84 | - test 85 | filters: 86 | branches: 87 | only: 88 | - master 89 | - deploy: 90 | requires: 91 | - hold 92 | filters: 93 | branches: 94 | only: 95 | - master -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .serverless 61 | 62 | # parcel bundler 63 | .cache/ 64 | dist/ 65 | 66 | serverless-*/config.yml 67 | .DS_Store 68 | 69 | build/ 70 | .envrc -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 khbyun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Group First Hands-on Part 1 2 | 3 | AWSKRUG Serverless Group의 첫번째 핸즈온 Part.1 웹어플리케이션 만들기입니다.😁 4 | 5 | #### [DEMO](http://khbyun-serverless-static-web.s3-website.ap-northeast-2.amazonaws.com/) 6 | 7 | ## Objective 8 | 9 | Amazon Web Service 를 활용하여 Serverless architecture로 구성된 API를 배포합니다. 10 | 결과는 S3에 static-web-site로 배포된 React Web app을 통해 확인합니다. 11 | 12 | ## AWS Resources 13 | 14 | AWS에서 사용하는 리소스는 다음과 같습니다. 15 | 16 | - Cloud9: 코드 작성, 실행 및 디버깅을 위한 클라우드 기반 IDE. 17 | - EC2: 클라우드에서 확장식 컴퓨팅을 제공. 여기서는 Cloud9을 동작하기 위해 사용. 18 | - API Gateway : API를 생성, 게시, 유지 관리, 모니터링 및 보호할 수 있게 해주는 서비스. 19 | - Lambda: 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있게 해주는 컴퓨팅 서비스. 서버리스 아키텍쳐의 핵심 서비스. 20 | - DynamoDB: 완벽하게 관리되는 NoSQL 데이터베이스 서비스로, 원활한 확장성과 함께 빠르고 예측 가능한 성능을 제공. 21 | - S3: 어디서나 원하는 양의 데이터를 저장하고 검색할 수 있도록 구축된 객체 스토리지. 소스코드의 저장소로 활용할 예정. 22 | 23 | ## Cloud 9 시작하기 24 | 25 | Cloud9 은 하나의 IDE입니다. 그렇지만 이전의 설치형 IDE와는 다릅니다. 설치형 IDE는 로컬 PC에 프로그램을 설치하던가 26 | 실행하는 방식이었다면, Cloud9은 브라우저가 실행가능한 모든 OS에서 사용이 가능합니다. 27 | 28 | 맨 처음 Cloud9은 AWS 내에서가 아닌 별도의 서비스로 제공되었습니다. AWS에 인수된 이후 Cloud9은 AWS의 Managed Service형태로 바뀌었고, 29 | AWS의 서비스와 결합하여 사용이 가능해졌습니다. 코드 편집과 명령줄 지원 등의 평범한 IDE 기능을 지니고 있던 반면에, 현재는 AWS 서비스와 30 | 결합되어 직접 Lambda 코드를 배포하던가, 실제로 Cloud9이 실행되고 있는 EC2의 컴퓨팅 성능을 향상시켜서 31 | 로컬 PC의 사양에 종속되지 않은 개발을 할 수가 있습니다. 32 | 33 | 그러면 Cloud9 환경을 시작해봅시다. 34 | 35 | [Cloud 9 Console](https://ap-northeast-2.console.aws.amazon.com/cloud9/home?region=ap-northeast-2#)에 접속합니다. 36 | 37 | 아래와 같은 화면에서 [Create Environment](https://ap-northeast-1.console.aws.amazon.com/cloud9/home/create) 버튼을 누릅니다. 38 | 39 | ![c9-create](/images/c9-create.png) 40 | 41 | Name과 Description을 다음과 같이 입력합니다. 42 | 43 | - Name: ServerlessHandsOn 44 | - Description: Serverless hands-on in AWSKRUG Serverless Group 45 | 46 | ![c9-create-name](/images/c9-create-name.png) 47 | 48 | Configure Setting은 다음과 같이 합니다. 49 | 50 | - Environment Type: EC2 51 | - Instance Type: T2.micro 52 | - Cost Save Setting: After 30 minutes 53 | - Network Settings: Default 54 | 55 | ![c9-conf](/images/c9-conf.png) 56 | 57 | 모든 설정을 마쳤다면 Cloud9 Environment를 생성하고 Open IDE를 통해 개발 환경에 접속합니다. 58 | 59 | 접속하면 다음과 같은 화면을 볼 수 있습니다. 60 | 61 | 1. 현재 Environment name 62 | 2. EC2에서 명령어를 입력할 수 있는 Terminal 63 | 3. Lambda Functions 64 | - Local Functions: 배포되지 않은 편집중인 Functions 65 | - Remote Functions: 현재 설정해놓은 Region에 배포된 Lambda Functions 66 | 4. Preferences 67 | 68 | ![c9-env](/images/c9-env.png) 69 | 70 | 85 | 86 | Cloud9 설정을 완료하였습니다. 87 | 88 | ## [Serverless Framework](https://serverless.com/) 89 | 90 | ![serverless framework main](/images/serverless-framework-1.png) 91 | 92 | Serverless Framework 메인에 나와있는 소개문구는 다음과 같습니다. 93 | 94 | Serverless is your toolkit for deploying and operating serverless architectures. 95 | Focus on your application, not your infrastructure. 96 | 97 | 위 내용을 번역한 내용은 "Serverless는 서버 없는 아키텍처를 배치하고 운영하기 위한 툴킷입니다. 인프라가 아닌 애플리케이션에 집중합니다." 입니다. 98 | 이처럼 Serverless framework는 Serverless architecture를 운영하기 위한 툴이라고 생각하면 됩니다. 99 | 100 | 그러면 serverless framework를 사용하기 위한 환경은 어떻게 될까요? 101 | 102 | node.js가 설치되어 있는 환경에서 사용할 수 있습니다. 103 | 104 | open source로 기여하고 싶다면 [https://github.com/serverless/serverless](https://github.com/serverless/serverless)에서 issue와 pull request를 등록해주세요. 105 | 106 | ### Serverless Framework 살펴보기 107 | 108 | Serverless Framework를 사용하기 위해서 명령어들을 살펴봅시다. 109 | 110 | ```sh 111 | $ npm i -g serverless 112 | 113 | # 명령어들을 확인해봅니다. 114 | $ serverless --help 115 | 116 | Commands 117 | * You can run commands with "serverless" or the shortcut "sls" 118 | * Pass "--verbose" to this command to get in-depth plugin info 119 | * Pass "--no-color" to disable CLI colors 120 | * Pass "--help" after any for contextual help 121 | 122 | Framework 123 | * Documentation: https://serverless.com/framework/docs/ 124 | 125 | config ........................ Configure Serverless 126 | config credentials ............ Configures a new provider profile for the Serverless Framework 127 | create ........................ Create new Serverless service 128 | deploy ........................ Deploy a Serverless service 129 | deploy function ............... Deploy a single function from the service 130 | deploy list ................... List deployed version of your Serverless Service 131 | deploy list functions ......... List all the deployed functions and their versions 132 | info .......................... Display information about the service 133 | install ....................... Install a Serverless service from GitHub or a plugin from the Serverless registry 134 | invoke ........................ Invoke a deployed function 135 | invoke local .................. Invoke function locally 136 | logs .......................... Output the logs of a deployed function 137 | metrics ....................... Show metrics for a specific function 138 | package ....................... Packages a Serverless service 139 | plugin ........................ Plugin management for Serverless 140 | plugin install ................ Install and add a plugin to your service 141 | plugin uninstall .............. Uninstall and remove a plugin from your service 142 | plugin list ................... Lists all available plugins 143 | plugin search ................. Search for plugins 144 | print ......................... Print your compiled and resolved config file 145 | remove ........................ Remove Serverless service and all resources 146 | rollback ...................... Rollback the Serverless service to a specific deployment 147 | rollback function ............. Rollback the function to the previous version 148 | slstats ....................... Enable or disable stats 149 | 150 | Platform (Beta) 151 | * The Serverless Platform is currently in experimental beta. Follow the docs below to get started. 152 | * Documentation: https://serverless.com/platform/docs/ 153 | 154 | emit .......................... Emits an event to a running Event Gateway 155 | login ......................... Login or sign up for the Serverless Platform 156 | logout ........................ Logout from the Serverless Platform 157 | run ........................... Runs the Event Gateway and the Emulator 158 | 159 | Plugins 160 | AwsConfigCredentials, Config, Create, Deploy, Emit, Info, Install, Invoke, Login, Logout, Logs, Metrics, Package, Plugin, PluginInstall, PluginList, PluginSearch, PluginUninstall, Print, Remove, Rollback, Run, SlStats 161 | ``` 162 | 163 | 여기서 자주 사용하게 될 명령어는 다음과 같습니다. 164 | 165 | - create: 프로젝트 생성시 사용 166 | - deploy: 배포할 때 사용 167 | - package: 배포될 패키지의 구조를 보고싶을 때 사용 168 | - invoke: 특정 handler를 동작시킬 때 사용 169 | - remove: 배포된 리소스를 제거할 때 사용 170 | 171 | 간단하게 로컬에서 serverless 명령어를 테스트해봅니다. deploy 명령어는 추후에 사용하겠습니다. 172 | 173 | ```sh 174 | 175 | # serverless service 생성 힌트 받기 176 | $ serverless create --help 177 | Plugin: Create 178 | create ........................ Create new Serverless service 179 | --template / -t .................... Template for the service. Available templates: "aws-nodejs", "aws-nodejs-typescript", "aws-nodejs-ecma-script", "aws-python", "aws-python3", "aws-groovy-gradle", "aws-java-maven", "aws-java-gradle", "aws-kotlin-jvm-maven", "aws-kotlin-jvm-gradle", "aws-kotlin-nodejs-gradle", "aws-scala-sbt", "aws-csharp", "aws-fsharp", "aws-go", "aws-go-dep", "azure-nodejs", "fn-nodejs", "fn-go", "google-nodejs", "kubeless-python", "kubeless-nodejs", "openwhisk-java-maven", "openwhisk-nodejs", "openwhisk-php", "openwhisk-python", "openwhisk-swift", "spotinst-nodejs", "spotinst-python", "spotinst-ruby", "spotinst-java8", "webtasks-nodejs", "plugin" and "hello-world" 180 | --template-url / -u ................ Template URL for the service. Supports: GitHub, BitBucket 181 | --template-path .................... Template local path for the service. 182 | --path / -p ........................ The path where the service should be created (e.g. --path my-service) 183 | --name / -n ........................ Name for the service. Overwrites the default name of the created service. ## " 184 | 185 | # node를 사용하므로 템플릿을 "aws-nodejs" 로 "sample-app" 생성하기 186 | $ serverless create -t "aws-nodejs" -p sample-app 187 | 188 | # sample-app에서 명령어 연습하기 189 | $ cd sample-app 190 | ~/sample-app $ serverless package 191 | Serverless: Packaging service... 192 | Serverless: Excluding development dependencies... 193 | 194 | # 여기까지 진행했다면 .serverless 디렉터리를 확인할 수 있습니다. 195 | ~/sample-app $ cd .serverless 196 | 197 | # 생성된 파일을 보면 다음과 같음을 알 수 있습니다. 198 | ~/sample-app/.serverless $ ls 199 | cloudformation-template-create-stack.json 200 | cloudformation-template-update-stack.json 201 | sample-app.zip 202 | serverless-state.json 203 | ``` 204 | 205 | 위에 생성된 파일이 어떻게 동작하는지는 파일명만으로도 유추할 수 있습니다. 206 | 207 | 현재 cloudformation에 stack이 존재하지 않을 경우 스택을 생성한 다음, 208 | 업데이트를 하여 원하는 코드가 Lambda에 배포되도록 하는 것입니다. 209 | 210 | serverless-state.json파일은 해당 버전의 serverless application에 대한 211 | 정보가 담겨 있습니다. 212 | 213 | ```sh 214 | # 다시 앱의 루트디렉터리로 돌와와서 invoke를 해보겠습니다. 215 | ~/sample-app/.serverless $ cd .. 216 | ~/sample-app $ serverless invoke local --function hello 217 | { 218 | "statusCode": 200, 219 | "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":\"\"}" 220 | } 221 | ``` 222 | 223 | ## S3 Bucket 생성하기 224 | 225 | S3는 Object Storage로 쉽게 설명하자면 하나의 저장소입니다. 파일들을 업로드 / 다운로드 할 수 있으며 AWS에서 핵심적인 서비스 중 하나입니다. 226 | 여러 방면으로 활용할 수 있지만 여기서는 소스코드의 저장소 역할을 합니다. 227 | 228 | S3의 메인으로 가서 버킷 생성하기 버튼을 클릭합니다. 229 | 230 | ![s3-create-btn.png](/images/s3-create-btn.png) 231 | 232 | 아래와 같이 입력하고 생성버튼을 클릭합니다. 233 | 234 | - 버킷 이름(Bucket name): USERNAME-serverless-hands-on-1 // 여기서 USERNAME을 수정합니다. ex) khbyun-serverless-hands-on-1 235 | - 리전(Region): 아시아 태평양(서울) 236 | 237 | ![s3-create-btn.png](/images/s3-create-1.png) 238 | 239 | 240 | ## Node Express api server 만들어보기 241 | 242 | 파일 트리는 다음과 같습니다. 243 | 244 | ```txt 245 | environment 246 | ├── serverless-api : API server 247 | │ ├── bin 248 | │ │ └── www : app.js를 로컬에서 실행하기 위한 파일 249 | │ ├── routes 250 | │ │ └── todo.js : /todo 로 라우팅하는 파일 251 | │ ├── spec 252 | │ │ └── todo.spec.js : /todo 를 테스트 하는 spec 파일 253 | │ ├── app.js : express 서버 254 | │ ├── handler.js : express를 wrapping하기 위한 handler 255 | │ ├── config.yml : serverless.yml에서 사용하기 위한 변수 256 | │ ├── package.json 257 | │ └── serverless.yml : Serverless Framework config file 258 | └── static-web-front : SPA 방식의 Web Front 259 | ``` 260 | 261 | 먼저 serverless-api 디렉터리를 생성하고 npm 초기화를 시켜줍니다. 262 | 263 | ```sh 264 | $ mkdir serverless-api 265 | $ cd serverless-api 266 | $ npm init -y 267 | ``` 268 | 269 | 필요한 npm module들을 install합니다. 270 | 여기서 aws-sdk는 개발을 위해 설치합니다. 271 | Lambda는 aws-sdk를 기본적으로 포함하고 있기 때문에 실제로 배포할 때는 포함시키지 않아야합니다. 272 | dev-dependency로 넣어두면 배포할 때 제외됩니다. 273 | 274 | - Dependencies 275 | - express : Web Application Framework 276 | - body-parser : Request Body를 parsing하기 위한 미들웨어 277 | - aws-serverless-express : Express를 Lambda에서 사용할 수 있도록 Wrapping하는 패키지 278 | - dynamoose : DynamoDB를 사용하기 쉽도록 Modeling하는 도구 279 | - dotenv : 환경 변수를 손쉽게 관리할 수 있게 하는 패키지 280 | - cors : 손쉽게 cors를 허용하는 미들웨어 281 | - Dev-dependencies 282 | - mocha : 개발 도구 283 | - supertest : HTTP 테스트를 하기 위한 모듈 284 | - should: BDD(Behaviour-Driven Development)를 지원하기 위한 모듈 285 | - serverless: Serverless Framework 286 | - aws-sdk : AWS 리소스를 사용하기 위한 SDK 287 | - serverless-apigw-binary: Binary Media Type을 지원하기 위한 플러그인 288 | 289 | ```sh 290 | $ npm i -S express aws-serverless-express body-parser dynamoose dotenv cors 291 | $ npm i -D mocha should supertest serverless aws-sdk serverless-apigw-binary 292 | ``` 293 | 294 | 각 파일을 편집합니다. 295 | 296 | ### serverless-api/config.yml 297 | 298 | ```yml 299 | AWS_REGION: ap-northeast-2 300 | STAGE: dev 301 | DEPLOYMENT_BUCKET: USERNAME-serverless-hands-on-1 # USERNAME 수정 필요! 302 | ``` 303 | 304 | ### serverless-api/app.js 305 | 306 | ```js 307 | const express = require("express"); 308 | const bodyParser = require("body-parser"); 309 | const cors = require("cors"); 310 | const app = express(); 311 | 312 | require("aws-sdk").config.region = "ap-northeast-2" 313 | 314 | app.use(cors()); 315 | app.use(bodyParser.json()); 316 | app.use(bodyParser.urlencoded({ extended: false })); 317 | 318 | // 실제로 사용한다고 가정하면 유저정보를 실어주어야함. 319 | app.use((req, res, next) => { 320 | res.locals.userId = "1"; 321 | next(); 322 | }); 323 | 324 | app.get("/", (req, res, next) => { 325 | res.send("hello world!\n"); 326 | }); 327 | 328 | app.use("/todo", require("./routes/todo")); 329 | 330 | app.use((req, res, next) => { 331 | res.status(404).send("Not Found"); 332 | }); 333 | 334 | app.use((err, req, res, next) => { 335 | console.error(err); 336 | res.status(500).send(err); 337 | }); 338 | 339 | module.exports = app; 340 | ``` 341 | 342 | ### serverless-api/bin/www 343 | 344 | ```js 345 | const app = require("../app"); 346 | const http = require("http"); 347 | const port = process.env.PORT || 3000; 348 | const server = http.createServer(app); 349 | 350 | server.on("error", (err) => console.error(err)); 351 | 352 | server.listen(port, () => console.log(`Server is running on ${port}`)); 353 | ``` 354 | 355 | ### serverless-api/handler.js 356 | 357 | ```js 358 | // lambda.js 359 | 'use strict' 360 | const awsServerlessExpress = require('aws-serverless-express') 361 | const app = require('./app') 362 | const binaryMimeTypes = [ 363 | 'application/javascript', 364 | 'application/json', 365 | 'application/octet-stream', 366 | 'application/x-font-ttf', 367 | 'application/xml', 368 | 'font/eot', 369 | 'font/opentype', 370 | 'font/otf', 371 | 'font/woff', 372 | 'font/woff2', 373 | 'image/jpeg', 374 | 'image/png', 375 | 'image/svg+xml', 376 | 'text/comma-separated-values', 377 | 'text/css', 378 | 'text/html', 379 | 'text/javascript', 380 | 'text/plain', 381 | 'text/text', 382 | 'text/xml' 383 | ] 384 | 385 | const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes) 386 | 387 | module.exports.api = (event, context) => awsServerlessExpress.proxy(server, event, context) 388 | ``` 389 | 390 | ### serverless-api/routes/todo.js 391 | 392 | ```js 393 | const router = require("express").Router(); 394 | const dynamoose = require('dynamoose'); 395 | const _ = require('lodash'); 396 | 397 | dynamoose.AWS.config.region = process.env.AWS_REGION; 398 | const Todo = dynamoose.model('Todo', { 399 | userId: { 400 | type: String, 401 | hashKey: true 402 | }, 403 | createdAt: { 404 | type: String, 405 | rangeKey: true 406 | }, 407 | updatedAt: String, 408 | title: String, 409 | content: String 410 | }, { 411 | create: false, // Create a table if not exist, 412 | }); 413 | 414 | router.get("/", (req, res, next) => { 415 | const userId = res.locals.userId; 416 | let lastKey = req.query.lastKey; 417 | 418 | return Todo.query('userId').eq(userId).startAt(lastKey).limit(1000).descending().exec((err, result) => { 419 | if(err) return next(err, req, res, next); 420 | 421 | res.status(200).json(result); 422 | }) 423 | }); 424 | 425 | router.get("/:createdAt", (req, res, next) => { 426 | const userId = res.locals.userId; 427 | const createdAt = String(req.params.createdAt); 428 | 429 | return Todo.get({userId, createdAt}, function (err, result) { 430 | if(err) return next(err, req, res, next); 431 | 432 | res.status(200).json(result); 433 | }); 434 | }); 435 | 436 | router.post("/", (req, res, next) => { 437 | const userId = res.locals.userId; 438 | const body = req.body; 439 | 440 | body.createdAt = new Date().toISOString(); 441 | body.updatedAt = new Date().toISOString(); 442 | body.userId = userId; 443 | 444 | return new Todo(body).save((err, result) => { 445 | if(err) return next(err, req, res, next); 446 | 447 | res.status(201).json(result); 448 | }); 449 | }); 450 | 451 | router.put("/:createdAt", (req, res, next) => { 452 | const userId = res.locals.userId; 453 | const createdAt = req.params.createdAt; 454 | const body = req.body; 455 | 456 | if(body.createdAt) delete body.createdAt; 457 | 458 | body.updatedAt = new Date().toISOString(); 459 | 460 | return new Todo(_.assign(body, { 461 | userId, 462 | createdAt 463 | })).save((err, result) => { 464 | if(err) return next(err, req, res, next); 465 | 466 | res.status(200).json(result); 467 | }); 468 | }); 469 | 470 | router.delete("/:createdAt", (req, res, next) => { 471 | const createdAt = req.params.createdAt; 472 | const userId = res.locals.userId; 473 | 474 | if(!createdAt) return res.status(400).send("Bad request. createdAt is undefined"); 475 | 476 | return Todo.delete({ 477 | userId, 478 | createdAt 479 | }, (err) => { 480 | if(err) return next(err, req, res, next); 481 | 482 | res.status(204).json(); 483 | }); 484 | }); 485 | 486 | module.exports = router; 487 | ``` 488 | 489 | ### serverless-api/spec/todo.spec.js 490 | 491 | ```js 492 | const request = require('supertest'); 493 | const _ = require('lodash'); 494 | const app = require('../app'); 495 | const data = { 496 | title: "hello", 497 | content: "world" 498 | } 499 | let createdData = null; 500 | 501 | describe("POST /todo", () => { 502 | it('Should return 201 status code', (done) => { 503 | request(app).post('/todo').send(data).expect(201, (err, res) => { 504 | if(err) return done(err); 505 | 506 | createdData = res.body; 507 | done(); 508 | }); 509 | }); 510 | }); 511 | 512 | describe("PUT /todo/:id", () => { 513 | it('Should return 200 status code', (done) => { 514 | request(app).put(`/todo/${createdData.createdAt}`).send(_.assign(data, { 515 | content: "world. Successfully modified!" 516 | })).expect(200, done); 517 | }); 518 | }); 519 | 520 | describe("GET /todo", () => { 521 | it('Should return 200 status code', (done) => { 522 | request(app).get('/todo').expect(200).end((err, res) => { 523 | if(err) return done(err); 524 | 525 | console.log(res.body); 526 | done(); 527 | }); 528 | }); 529 | }); 530 | 531 | describe("GET /todo/:createdAt", () => { 532 | it('Should return 200 status code', (done) => { 533 | request(app).get(`/todo/${createdData.createdAt}`).expect(200).end((err, res) => { 534 | if(err) return done(err); 535 | 536 | console.log(res.body); 537 | done(); 538 | }); 539 | }); 540 | }); 541 | 542 | describe("DELETE /todo/:id", () => { 543 | it('Should return 204 status code', (done) => { 544 | request(app).delete(`/todo/${createdData.createdAt}`).send(data).expect(204, done); 545 | }); 546 | }); 547 | ``` 548 | 549 | ### serverless-api/package.json 550 | 551 | npm script 내용을 추가해주어야 합니다. 552 | 553 | ```json 554 | { 555 | "name": "serverless-api", 556 | .... 557 | //// 이 스크립트 영역을 복사해서 붙여넣어줍니다. 558 | "scripts": { 559 | "test": "mocha spec/*.spec.js --timeout 10000", 560 | "start": "AWS_REGION=ap-northeast-2 node bin/www", 561 | "deploy": "serverless deploy" 562 | }, 563 | //// 564 | ... 565 | "keywords": [], 566 | "author": "", 567 | ... 568 | } 569 | ``` 570 | 571 | 572 | 마지막으로 serverless.yml을 생성합니다. 이것은 Serverless Framework를 통해 AWS에 손쉽게 serverless 환경을 배포할 수 있게 도와줍니다. 573 | 내부적으로는 CloudFormation Template을 생성하여 배포합니다. 배포된 Artifact는 S3에서 확인해볼 수 있습니다. 574 | 575 | ### serverless-api/serverless.yml 576 | 577 | ```yml 578 | service: ServerlessHandsOnPart1 579 | 580 | provider: 581 | name: aws 582 | runtime: nodejs12.x 583 | memorySize: 128 584 | stage: ${file(./config.yml):STAGE} 585 | region: ${file(./config.yml):AWS_REGION} 586 | deploymentBucket: ${file(./config.yml):DEPLOYMENT_BUCKET} 587 | environment: 588 | NODE_ENV: production 589 | iamRoleStatements: 590 | - Effect: Allow 591 | Action: 592 | - dynamodb:DescribeTable 593 | - dynamodb:Query 594 | - dynamodb:Scan 595 | - dynamodb:GetItem 596 | - dynamodb:PutItem 597 | - dynamodb:UpdateItem 598 | - dynamodb:DeleteItem 599 | Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:*" 600 | 601 | plugins: 602 | - serverless-apigw-binary 603 | custom: 604 | apigwBinary: 605 | types: 606 | - 'application/json' 607 | - 'text/html' 608 | 609 | functions: 610 | webapp: 611 | handler: handler.api 612 | events: 613 | - http: 614 | path: /{proxy+} 615 | method: ANY 616 | cors: true 617 | - http: 618 | path: /{proxy+} 619 | method: OPTIONS 620 | cors: true 621 | ``` 622 | 623 | ** 위에서 살펴보면 app.js와 serverless.yml에 cors관련 옵션이 있습니다. 보안 상의 이유로, 브라우저들은 스크립트 내에서 초기화되는 624 | cross-origin HTTP 요청을 제한하기 때문에 별도로 API Gateway에서 허용을 해주고, 실제로 동작하는 Lambda에서도 서버처럼 동작하기 때문에 625 | 옵션을 추가해야됩니다. 이에 대한 자세한 내용은 626 | [HTTP 접근 제어 (CORS)](https://developer.mozilla.org/ko/docs/Web/HTTP/Access_control_CORS)에서 627 | 확인할 수 있습니다. 628 | 629 | --- 630 | 631 | 632 | 모든 파일을 편집하였다면 서버를 가동해봅니다. 633 | 634 | ```sh 635 | ec2-user:~/environment/serverless-api $ npm start 636 | > serverless-api@1.0.0 start /home/ec2-user/environment/serverless-api 637 | > node bin/www 638 | 639 | Server is running on 8080 640 | ``` 641 | 642 | 서버가 제대로 응답하는지 확인하기 위해 새로운 터미널을 열어 get 요청을 해봅니다. 643 | 644 | ```sh 645 | ec2-user:~/environment/serverless-api $ curl localhost:8080 646 | hello world! 647 | ec2-user:~/environment/serverless-api $ curl localhost:8080/todo 648 | 응답없음 649 | ``` 650 | 651 | 서버를 가동하였지만 API가 사용가능한 상태는 아닙니다. 652 | DynamoDB의 테이블을 생성하지 않았기 때문입니다. 653 | 654 | ## DynamoDB 테이블 생성하기 655 | 656 | DynamoDB를 설계할 시 주의해야할 점은 [FAQ](https://aws.amazon.com/ko/dynamodb/faqs/)를 참고하시길 바랍니다. 657 | 658 | 이제 DynamoDB에 Todo table을 생성할 것입니다. 파티션 키와 정렬 키는 다음과 같이 설정합니다. 659 | 660 | - 파티션키(Partition Key): userId 661 | - 정렬키(Sort Key): createdAt 662 | 663 | 소스코드 상에서는 userId를 "1"로 고정시켜두었습니다. 일반적으로 유저의 키값을 partition key로 사용하기 때문입니다. 664 | 또한 레코드의 생성 시간을 정렬키로 사용합니다. 665 | 666 | 그럼 [DynamoDB Console](https://ap-northeast-2.console.aws.amazon.com/dynamodb/home?region=ap-northeast-2#)로 이동합니다. 667 | 테이블 만들기를 클릭하여 아래와 같이 테이블을 생성합니다. 668 | 669 | ![dynamodb-create](/images/dynamodb-create.png) 670 | 671 | 그런 다음에 다시 Cloud9으로 돌아가서 테스트 코드를 돌려봅니다. 672 | 673 | ```sh 674 | ec2-user:~/environment/serverless-api $ npm test 675 | 676 | > serverless-api@1.0.0 test /home/ec2-user/environment/serverless-api 677 | > mocha spec/*.spec.js --timeout 10000 678 | 679 | 680 | 681 | POST /todo 682 | ✓ Should return 201 status code (912ms) 683 | 684 | PUT /todo/:id 685 | ✓ Should return 200 status code (225ms) 686 | 687 | GET /todo 688 | [ { content: 'world. Successfully modified!', 689 | createdAt: '2018-04-01T13:56:34.808Z', 690 | userId: '1', 691 | updatedAt: '2018-04-01T13:56:35.687Z', 692 | title: 'hello' } ] 693 | ✓ Should return 200 status code (224ms) 694 | 695 | GET /todo/:createdAt?user_id= 696 | 2018-04-01T13:56:34.808Z 697 | 1 698 | { content: 'world. Successfully modified!', 699 | createdAt: '2018-04-01T13:56:34.808Z', 700 | userId: '1', 701 | updatedAt: '2018-04-01T13:56:35.687Z', 702 | title: 'hello' } 703 | ✓ Should return 200 status code (215ms) 704 | 705 | DELETE /todo/:id 706 | ✓ Should return 204 status code (219ms) 707 | 708 | 709 | 5 passing (2s) 710 | ``` 711 | 712 | DynamoDB에서 간단하게 CRUD작업하는 것을 확인할 수 있습니다. 713 | 714 | ## Cloud9에서 배포하기 715 | 716 | 723 | Node가 8.x버전이 설치되어 있으면 dev-dependency에 설치된 serverless 명령어를 바로 사용할 수 있습니다. 724 | 만일 node 6.x버전이라면 Global로 serverless를 설치하여 줍니다. 현재는 8.x의 버전을 사용하기 때문에 다음 명령어는 넘어가겠습니다. 725 | 726 | 설치가 완료되었으면 배포를 합니다. package.json에 script에 serverless deploy를 넣어 두었기 때문에 727 | 다음과 같이 배포를 합니다. 728 | 729 | ```sh 730 | ec2-user:~/environment/serverless-todo-demo/serverless-api (master) $ npm run deploy 731 | Serverless: Packaging service... 732 | Serverless: Excluding development dependencies... 733 | Serverless: Uploading CloudFormation file to S3... 734 | Serverless: Uploading artifacts... 735 | Serverless: Uploading service .zip file to S3 (8.02 MB)... 736 | Serverless: Validating template... 737 | Serverless: Updating Stack... 738 | Serverless: Checking Stack update progress... 739 | .............. 740 | Serverless: Stack update finished... 741 | Service Information 742 | service: ServerlessHandsOn 743 | stage: dev 744 | region: ap-northeast-2 745 | stack: ServerlessHandsOn-dev 746 | api keys: 747 | None 748 | endpoints: 749 | ANY - https://YOUR_CLOUD_FRONT_URL/dev/{proxy+} 750 | functions: 751 | serverlessHandsOn: ServerlessHandsOn-dev-serverlessHandsOn 752 | Serverless: 'Too many requests' received, sleeping 5 seconds 753 | Serverless: 'Too many requests' received, sleeping 5 seconds 754 | ``` 755 | 756 | 위와같이 배포되었으면 URL에 접속하여 실제 동작하는지 확인합니다. 757 | 758 | ```sh 759 | ec2-user:~/environment/serverless-todo-demo/serverless-api (master) $ curl https://YOUR_CLOUD_FRONT_URL/dev/{proxy+} 760 | [] 761 | ``` 762 | 763 | ## Static Web Site에서 API 호출해보기 764 | 765 | 지금까지 API를 구성해보았습니다. API만드로도 서비스가 가능할까요? 766 | 이를 호출할 클라이언트가 없다면 서비스가 될 수 없을 겁니다. 767 | 작성한 node server에서 Web site를 뿌려주는 Server Side Rendering방식을 택할 수도있습니다. 768 | 그렇지만 이번에는 Static Web Site를 하나의 앱이라고 생각하고 769 | 데이터만 서버에 요청하여 UI에 반영하려고 합니다. 770 | 작업한 내용이 어떻게 표현되는지 확인하고, 771 | CloudFront + S3로 Static Web Site를 호스팅해봅시다. 772 | 773 | 첫 번째로, [Git repository](https://github.com/novemberde/serverless-todo-demo.git)를 가져옵니다. 774 | 775 | ```sh 776 | # Work directory로 이동 777 | ec2-user:~/environment $ cd ~/environment 778 | 779 | # !! 여기서는 yarn으로 패키지를 설치. npm으로 설치하게 되면 Parcel bundler가 제대로 동작하지 않습니다. 780 | ec2-user:~/environment $ npm i -g yarn 781 | 782 | # !! 여기서 사용하는 패키지 중에 하나가 node 9.x 이하로 지원됩니다. 783 | ec2-user:~/environment $ nvm install 9 784 | ec2-user:~/environment $ nvm use 9 785 | 786 | # Git repository clone하기 787 | ec2-user:~/environment $ git clone https://github.com/novemberde/serverless-todo-demo.git 788 | 789 | # Static Web Site를 구성한 directory로 이동 790 | ec2-user:~/environment $ cd serverless-todo-demo/static-web-front 791 | 792 | # npm으로 package 설치 793 | ec2-user:~/environment/serverless-todo-demo/static-web-front $ yarn install 794 | 795 | # Static Web Site 시작하기 796 | ec2-user:~/environment/serverless-todo-demo/static-web-front $ npm start 797 | 798 | > serverless-todo-demo-app@1.0.0 start /Users/kyuhyunbyun/WorkSpace/workshop/serverless-todo-demo/static-web-front 799 | > npx parcel src/index.html 800 | 801 | Server running at http://localhost:1234 802 | ✨ Built in 3.99s. 803 | ``` 804 | 805 | 웹페이지는 출력되지만 현재 웹이 호출하는 API의 주소를 수정해주어야 현재 올린 API를 사용할 수 있습니다. 806 | 아래와 같은 파일을 열어 baseUrl값을 수정합니다. 이 값은 api를 배포하였을 때 복사해둔 CloudFront 주소입니다. 807 | 808 | 복사하지 않으셨다면 다음을 다시 참고해주시길 바랍니다. 809 | Cloud9에서 배포하기 810 | 811 | ### static-web-front/src/components/App.js 812 | 813 | ```js 814 | import 'setimmediate' 815 | import React from 'react' 816 | import styled from 'styled-components' 817 | import axios from 'axios' 818 | import MaterialUiThemeProvider from 'material-ui/styles/MuiThemeProvider' 819 | import { List, ListItem } from 'material-ui/List' 820 | import { TextField, RaisedButton } from 'material-ui' 821 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 822 | import Top from './Top' 823 | 824 | const baseURL = 'CLOUD_FRONT_URL'; // Insert your CloudFront url. 825 | ... 826 | ``` 827 | 828 | 829 | 830 | 정상적으로 동작하는지 확인하고 싶다면 새로운 터미널을 열고(맥은 option+t, 윈도우는 alt+t) 다음과 같이 확인합니다. 831 | 832 | ```sh 833 | ec2-user:~/environment $ curl localhost:1234 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 |
842 | 843 | 844 | 845 | ``` 846 | 847 | 정상적으로 출력이 되는 것을 확인하였습니다. 하지만 기본적으로 EC2는 네트워크를 통제하고 있습니다. 848 | Cloud9은 EC2를 생성하여 사용하는데, 보안그룹(Security Group)에서 포트를 열어주어야 외부에서 849 | 접근이 가능합니다. 850 | 851 | [Security Group Setting](https://ap-southeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-southeast-1#SecurityGroups:sort=groupId)으로 852 | 들어가서 그룹이름(Group name)이 aws-cloud9-serverless-hands-on로 시작하는 것을 선택합니다. 853 | 854 | ![security-group](/images/security-group.png) 855 | 856 | 편집(Edit)을 눌러서 "TCP / 1234 / 위치 무관(Anywhere)"으로 추가합니다. 857 | 858 | ![security-group](/images/security-group2.png) 859 | 860 | 861 | 이제 브라우저를 열고 http://{CLOUD9_PUBLIC_DNS}:1234 에 접속하시면 다음과 같은 화면을 볼 수 있습니다. 862 | 863 | CLOUD9_PUBLIC_DNS 확인하는 것은 EC2 console에서 할 수 있습니다. 864 | 그렇지만 이게 귀찮다면 다음과 같이 terminal에 입력합니다. 간단히 public ip를 얻을 수 있습니다. 865 | 그럼 CLOUD9_PUBLIC_DNS 대신 ip를 넣어서 접속해봅니다. 866 | 867 | ```sh 868 | ec2-user:~/environment $ curl http://checkip.amazonaws.com/ 869 | xxx.xxx.xxx.66 870 | ``` 871 | 872 | ![static-front](/images/static-front.png) 873 | 874 | 이제 마음껏 추가 삭제 수정을 해보세요! 😀 875 | 876 | ## S3를 통해 Static Web Site를 호스팅하기 877 | 878 | Amazon S3는 파일을 저장하는 저장소 역할을 합니다. 파일을 저장하고 URL을 통해서 파일에 접근합니다. 879 | 그렇다면 URL로 접근하는 파일이 HTML, CSS, JAVASCRIPT로 작성되어 있다면 브라우저에서 사용이 가능하겠죠? 880 | 881 | 그래서 S3는 정적인 웹사이트 호스팅을 지원합니다. 882 | 883 | 이전과 똑같이 [S3 Console](https://console.aws.amazon.com/s3/home?region=ap-northeast-2)에 접속하여 버킷을 생성합니다. 884 | 885 | - 버킷이름(Bucket name): USERNAME-serverless-static-web 886 | - 리전(Region): 아시아 태평양(서울) 887 | 888 | ![static-web1](/images/static-web1.png) 889 | 890 | 속성 설정은 Default로 두고, 권한설정에서 "이 버킷에 퍼블릭 읽기 액세스 권한을 부여함"을 선택하고 생성합니다. 891 | 892 | ![static-web3](/images/static-web3.png) 893 | 894 | 895 | 그 다음에 생성한 버킷 > 속성 메뉴에 들어가서 [정적 웹사이트 호스팅](Static Website Hosting)을 클릭하고 다음과 같이 입력합니다. 896 | 897 | - 인덱스 문서(Index document): index.html 898 | - 오류 문서(Error document): index.html 899 | 900 | ![static-web2](/images/static-web2.png) 901 | 902 | 설정을 완료하였습니다. 그럼 빌드된 html 문서를 S3에 업로드하면 됩니다. 903 | 904 | 다시 Cloud9으로 돌아와서 다음과 같이 입력합니다. 905 | 906 | ```sh 907 | $ cd ~/environment/serverless-todo-demo/static-web-front/dist/ 908 | # USERNAME 은 수정합니다. 909 | ec2-user:~/environment/serverless-todo-demo/static-web-front/dist (master) $ aws s3 cp ./ s3://USERNAME-serverless-static-web/ --recursive --acl public-read 910 | ``` 911 | 912 | 모든 배포가 완료되었습니다. 913 | 914 | http://USERNAME-serverless-static-web.s3-website.ap-northeast-2.amazonaws.com/ 에 접속하여 나만의 Todo List를 확인해보세요! 915 | 916 | 만약 https를 적용하고 싶으시다면 CloudFront를 활용해야합니다. 917 | 918 | [https://console.aws.amazon.com/cloudfront/home](https://console.aws.amazon.com/cloudfront/home) 919 | 920 | > 여기서 s3의 website 주소를 origin으로 하여 생성해야합니다. 921 | > s3 origin을 select box에서 선택하지 않고, 직접 정적 웹사이트 호스팅 란에 나와있는 주소를 복/붙합니다. 922 | > Redirect HTTP to HTTPS 선택 923 | > Default Root Object는 index.html 로 설정합니다. 924 | 925 | 커스텀 도메인은 나중에 생기면 넣어봅니다. 926 | 927 | 928 | ## 리소스 삭제하기 929 | 930 | 서버리스 앱은 내리는 것이 어렵지 않습니다. 931 | 간단한 Command 하나면 모든 스택이 내려갑니다. 932 | Cloud9에서 새로운 터미널을 열고 다음과 같이 입력합니다. 933 | 934 | ```sh 935 | $ cd ~/environment/serverless-api 936 | $ serverless remove 937 | Serverless: Getting all objects in S3 bucket... 938 | Serverless: Removing objects in S3 bucket... 939 | Serverless: Removing Stack... 940 | Serverless: Checking Stack removal progress... 941 | ............ 942 | Serverless: Stack removal finished... 943 | ``` 944 | 945 | [DynamoDB Console](https://ap-northeast-2.console.aws.amazon.com/dynamodb/home?region=ap-northeast-2#)로 들어가서 Table을 삭제합니다. 리전은 서울입니다. 946 | 947 | [Cloud9 Console](https://ap-northeast-2.console.aws.amazon.com/cloud9/home?region=ap-northeast-2)로 들어가서 IDE를 삭제합니다. 리전은 싱가포르입니다. 948 | 949 | [S3 Console](https://s3.console.aws.amazon.com/s3/home?region=ap-northeast-2#)로 들어가서 생성된 버킷을 삭제합니다. 950 | 951 | 956 | 957 | 958 | ## References 959 | 960 | - [https://aws.amazon.com/ko/cloud9/](https://aws.amazon.com/ko/cloud9/) 961 | - [https://serverless.com/](https://serverless.com/) -------------------------------------------------------------------------------- /images/c9-conf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/c9-conf.png -------------------------------------------------------------------------------- /images/c9-create-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/c9-create-name.png -------------------------------------------------------------------------------- /images/c9-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/c9-create.png -------------------------------------------------------------------------------- /images/c9-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/c9-deploy.png -------------------------------------------------------------------------------- /images/c9-env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/c9-env.png -------------------------------------------------------------------------------- /images/c9-region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/c9-region.png -------------------------------------------------------------------------------- /images/dynamodb-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/dynamodb-create.png -------------------------------------------------------------------------------- /images/dynamodb-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/dynamodb-policy.png -------------------------------------------------------------------------------- /images/s3-create-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/s3-create-1.png -------------------------------------------------------------------------------- /images/s3-create-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/s3-create-btn.png -------------------------------------------------------------------------------- /images/security-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/security-group.png -------------------------------------------------------------------------------- /images/security-group2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/security-group2.png -------------------------------------------------------------------------------- /images/serverless-framework-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/serverless-framework-1.png -------------------------------------------------------------------------------- /images/static-front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/static-front.png -------------------------------------------------------------------------------- /images/static-web1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/static-web1.png -------------------------------------------------------------------------------- /images/static-web2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/static-web2.png -------------------------------------------------------------------------------- /images/static-web3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/images/static-web3.png -------------------------------------------------------------------------------- /sample-app/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /sample-app/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.hello = (event, context, callback) => { 4 | const response = { 5 | statusCode: 200, 6 | body: JSON.stringify({ 7 | message: 'Go Serverless v1.0! Your function executed successfully!', 8 | input: event, 9 | }), 10 | }; 11 | 12 | callback(null, response); 13 | 14 | // Use this code if you don't use the http event with the LAMBDA-PROXY integration 15 | // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); 16 | }; 17 | -------------------------------------------------------------------------------- /sample-app/serverless.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | service: sample-app 15 | 16 | # You can pin your service to only deploy with a specific Serverless version 17 | # Check out our docs for more details 18 | # frameworkVersion: "=X.X.X" 19 | 20 | provider: 21 | name: aws 22 | runtime: nodejs6.10 23 | 24 | # you can overwrite defaults here 25 | # stage: dev 26 | # region: us-east-1 27 | 28 | # you can add statements to the Lambda function's IAM Role here 29 | # iamRoleStatements: 30 | # - Effect: "Allow" 31 | # Action: 32 | # - "s3:ListBucket" 33 | # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } 34 | # - Effect: "Allow" 35 | # Action: 36 | # - "s3:PutObject" 37 | # Resource: 38 | # Fn::Join: 39 | # - "" 40 | # - - "arn:aws:s3:::" 41 | # - "Ref" : "ServerlessDeploymentBucket" 42 | # - "/*" 43 | 44 | # you can define service wide environment variables here 45 | # environment: 46 | # variable1: value1 47 | 48 | # you can add packaging information here 49 | #package: 50 | # include: 51 | # - include-me.js 52 | # - include-me-dir/** 53 | # exclude: 54 | # - exclude-me.js 55 | # - exclude-me-dir/** 56 | 57 | functions: 58 | hello: 59 | handler: handler.hello 60 | 61 | # The following are a few example events you can configure 62 | # NOTE: Please make sure to change your handler code to work with those events 63 | # Check the event documentation for details 64 | # events: 65 | # - http: 66 | # path: users/create 67 | # method: get 68 | # - s3: ${env:BUCKET} 69 | # - schedule: rate(10 minutes) 70 | # - sns: greeter-topic 71 | # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 72 | # - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx 73 | # - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx 74 | # - iot: 75 | # sql: "SELECT * FROM 'some_topic'" 76 | # - cloudwatchEvent: 77 | # event: 78 | # source: 79 | # - "aws.ec2" 80 | # detail-type: 81 | # - "EC2 Instance State-change Notification" 82 | # detail: 83 | # state: 84 | # - pending 85 | # - cloudwatchLog: '/aws/lambda/hello' 86 | # - cognitoUserPool: 87 | # pool: MyUserPool 88 | # trigger: PreSignUp 89 | 90 | # Define function environment variables here 91 | # environment: 92 | # variable2: value2 93 | 94 | # you can add CloudFormation resource templates here 95 | #resources: 96 | # Resources: 97 | # NewResource: 98 | # Type: AWS::S3::Bucket 99 | # Properties: 100 | # BucketName: my-new-bucket 101 | # Outputs: 102 | # NewOutput: 103 | # Description: "Description for the output" 104 | # Value: "Some output value" 105 | -------------------------------------------------------------------------------- /serverless-api/README.md: -------------------------------------------------------------------------------- 1 | # serverless-api -------------------------------------------------------------------------------- /serverless-api/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const bodyParser = require("body-parser"); 3 | const cors = require("cors"); 4 | const app = express(); 5 | 6 | require("aws-sdk").config.region = "ap-northeast-2" 7 | 8 | app.use(cors()); 9 | app.use(bodyParser.json()); 10 | app.use(bodyParser.urlencoded({ extended: false })); 11 | 12 | // 실제로 사용한다고 가정하면 유저정보를 실어주어야함. 13 | app.use((req, res, next) => { 14 | res.locals.userId = "1"; 15 | next(); 16 | }); 17 | 18 | app.get("/", (req, res, next) => { 19 | res.send("hello world!\n"); 20 | }); 21 | 22 | app.use("/todo", require("./routes/todo")); 23 | 24 | app.use((req, res, next) => { 25 | return res.status(404).send("Not Found"); 26 | }); 27 | 28 | app.use((err, req, res, next) => { 29 | console.error(err); 30 | return res.status(500).send(err); 31 | }); 32 | 33 | module.exports = app; -------------------------------------------------------------------------------- /serverless-api/bin/www: -------------------------------------------------------------------------------- 1 | const app = require("../app"); 2 | const http = require("http"); 3 | const port = process.env.PORT || 3000; 4 | const server = http.createServer(app); 5 | 6 | server.on("error", (err) => console.error(err)); 7 | 8 | server.listen(port, () => console.log(`Server is running on ${port}`)); -------------------------------------------------------------------------------- /serverless-api/handler.js: -------------------------------------------------------------------------------- 1 | // lambda.js 2 | 'use strict' 3 | const awsServerlessExpress = require('aws-serverless-express') 4 | const app = require('./app') 5 | const binaryMimeTypes = [ 6 | 'application/javascript', 7 | 'application/json', 8 | 'application/octet-stream', 9 | 'application/x-font-ttf', 10 | 'application/xml', 11 | 'font/eot', 12 | 'font/opentype', 13 | 'font/otf', 14 | 'font/woff', 15 | 'font/woff2', 16 | 'image/jpeg', 17 | 'image/png', 18 | 'image/svg+xml', 19 | 'text/comma-separated-values', 20 | 'text/css', 21 | 'text/html', 22 | 'text/javascript', 23 | 'text/plain', 24 | 'text/text', 25 | 'text/xml' 26 | ] 27 | // 반드시 API Gateway setting에서 Binary Media Types 에 */* 넣어줄 것! 28 | 29 | const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes) 30 | 31 | module.exports.api = (event, context) => awsServerlessExpress.proxy(server, event, context) -------------------------------------------------------------------------------- /serverless-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha spec/*.spec.js --timeout 10000", 8 | "start": "node bin/www", 9 | "deploy": "serverless deploy" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "aws-serverless-express": "^3.1.3", 16 | "body-parser": "^1.18.2", 17 | "cors": "^2.8.4", 18 | "dynamoose": "^0.8.7", 19 | "express": "^4.16.3" 20 | }, 21 | "devDependencies": { 22 | "aws-sdk": "^2.213.1", 23 | "mocha": "^5.0.5", 24 | "serverless": "^1.27.3", 25 | "serverless-apigw-binary": "^0.4.4", 26 | "should": "^13.2.1", 27 | "supertest": "^3.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /serverless-api/routes/todo.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const dynamoose = require('dynamoose'); 3 | const _ = require('lodash'); 4 | 5 | dynamoose.AWS.config.region = process.env.AWS_REGION; 6 | const Todo = dynamoose.model('Todo', { 7 | userId: { 8 | type: String, 9 | hashKey: true 10 | }, 11 | createdAt: { 12 | type: String, 13 | rangeKey: true 14 | }, 15 | updatedAt: String, 16 | title: String, 17 | content: String 18 | }, { 19 | create: false, // Create a table if not exist, 20 | }); 21 | 22 | router.get("/", (req, res, next) => { 23 | const userId = res.locals.userId; 24 | let lastKey = req.query.lastKey; 25 | 26 | return Todo.query('userId').eq(userId).startAt(lastKey).limit(1000).descending().exec((err, result) => { 27 | if(err) return next(err, req, res, next); 28 | 29 | res.status(200).json(result); 30 | }) 31 | }); 32 | 33 | router.get("/:createdAt", (req, res, next) => { 34 | const userId = res.locals.userId; 35 | const createdAt = String(req.params.createdAt); 36 | 37 | return Todo.get({userId, createdAt}, function (err, result) { 38 | if(err) return next(err, req, res, next); 39 | 40 | res.status(200).json(result); 41 | }); 42 | }); 43 | 44 | router.post("/", (req, res, next) => { 45 | const userId = res.locals.userId; 46 | const body = req.body; 47 | 48 | body.createdAt = new Date().toISOString(); 49 | body.updatedAt = new Date().toISOString(); 50 | body.userId = userId; 51 | 52 | return new Todo(body).save((err, result) => { 53 | if(err) return next(err, req, res, next); 54 | 55 | res.status(201).json(result); 56 | }); 57 | }); 58 | 59 | router.put("/:createdAt", (req, res, next) => { 60 | const userId = res.locals.userId; 61 | const createdAt = req.params.createdAt; 62 | const body = req.body; 63 | 64 | if(body.createdAt) delete body.createdAt; 65 | 66 | body.updatedAt = new Date().toISOString(); 67 | 68 | return new Todo(_.assign(body, { 69 | userId, 70 | createdAt 71 | })).save((err, result) => { 72 | if(err) return next(err, req, res, next); 73 | 74 | res.status(200).json(result); 75 | }); 76 | }); 77 | 78 | router.delete("/:createdAt", (req, res, next) => { 79 | const createdAt = req.params.createdAt; 80 | const userId = res.locals.userId; 81 | 82 | if(!createdAt) return res.status(400).send("Bad request. createdAt is undefined"); 83 | 84 | return Todo.delete({ 85 | userId, 86 | createdAt 87 | }, (err) => { 88 | if(err) return next(err, req, res, next); 89 | 90 | res.status(204).json(); 91 | }); 92 | }); 93 | 94 | module.exports = router; -------------------------------------------------------------------------------- /serverless-api/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ServerlessHandsOnPart1 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | memorySize: 128 7 | stage: ${file(./config.yml):STAGE} 8 | region: ${file(./config.yml):AWS_REGION} 9 | deploymentBucket: ${file(./config.yml):DEPLOYMENT_BUCKET} 10 | environment: 11 | NODE_ENV: production 12 | iamRoleStatements: 13 | - Effect: Allow 14 | Action: 15 | - dynamodb:DescribeTable 16 | - dynamodb:Query 17 | - dynamodb:Scan 18 | - dynamodb:GetItem 19 | - dynamodb:PutItem 20 | - dynamodb:UpdateItem 21 | - dynamodb:DeleteItem 22 | Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:*" 23 | 24 | plugins: 25 | - serverless-apigw-binary 26 | custom: 27 | apigwBinary: 28 | types: 29 | - 'application/json' 30 | - 'text/html' 31 | 32 | functions: 33 | webapp: 34 | handler: handler.api 35 | events: 36 | - http: 37 | path: /{proxy+} 38 | method: ANY 39 | cors: true 40 | - http: 41 | path: /{proxy+} 42 | method: OPTIONS 43 | cors: true -------------------------------------------------------------------------------- /serverless-api/spec/todo.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const _ = require('lodash'); 3 | const app = require('../app'); 4 | const data = { 5 | title: "hello", 6 | content: "world" 7 | } 8 | let createdData = null; 9 | 10 | describe("POST /todo", () => { 11 | it('Should return 201 status code', (done) => { 12 | request(app).post('/todo').send(data).expect(201, (err, res) => { 13 | if(err) return done(err); 14 | 15 | createdData = res.body; 16 | done(); 17 | }); 18 | }); 19 | }); 20 | 21 | describe("PUT /todo/:id", () => { 22 | it('Should return 200 status code', (done) => { 23 | request(app).put(`/todo/${createdData.createdAt}`).send(_.assign(data, { 24 | content: "world. Successfully modified!" 25 | })).expect(200, done); 26 | }); 27 | }); 28 | 29 | describe("GET /todo", () => { 30 | it('Should return 200 status code', (done) => { 31 | request(app).get('/todo').expect(200).end((err, res) => { 32 | if(err) return done(err); 33 | 34 | console.log(res.body); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | 40 | describe("GET /todo/:createdAt", () => { 41 | it('Should return 200 status code', (done) => { 42 | request(app).get(`/todo/${createdData.createdAt}`).expect(200).end((err, res) => { 43 | if(err) return done(err); 44 | 45 | console.log(res.body); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("DELETE /todo/:id", () => { 52 | it('Should return 204 status code', (done) => { 53 | request(app).delete(`/todo/${createdData.createdAt}`).send(data).expect(204, done); 54 | }); 55 | }); -------------------------------------------------------------------------------- /serverless-api/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: AWSKRUG Serverless Group Hands-on! 4 | version: v1 5 | paths: 6 | /: 7 | get: 8 | operationId: listTodos 9 | summary: List Todo 10 | produces: 11 | - application/json 12 | responses: 13 | "200": 14 | description: |- 15 | 200 response 16 | examples: 17 | application/json: |- 18 | [ 19 | { 20 | "user_id": "1", 21 | "createdAt": "2018-03-24T16:00:51.278Z", 22 | "title": "hello", 23 | "content": "world. Successfully modified!", 24 | "updatedAt": "2018-03-24T16:00:52.135Z" 25 | } 26 | ] 27 | post: 28 | operationId: createTodo 29 | summary: Create Todo 30 | produces: 31 | - application/json 32 | parameters: 33 | - 34 | name: "Todo" 35 | in: "body" 36 | description: "Todo to add" 37 | required: true 38 | schema: 39 | $ref: "#/definitions/NewTodo" 40 | responses: 41 | "201": 42 | description: |- 43 | 201 response 44 | examples: 45 | application/json: |- 46 | { 47 | "user_id": "1", 48 | "createdAt": "2018-03-24T16:00:51.278Z", 49 | "title": "hello", 50 | "content": "world. Successfully modified!", 51 | "updatedAt": "2018-03-24T16:00:52.135Z" 52 | } 53 | /:createdAt: 54 | get: 55 | operationId: getTodo 56 | summary: Get Todo 57 | produces: 58 | - application/json 59 | responses: 60 | "200": 61 | description: |- 62 | 200 response 63 | examples: 64 | application/json: |- 65 | { 66 | "user_id": "1", 67 | "createdAt": "2018-03-24T16:00:51.278Z", 68 | "title": "hello", 69 | "content": "world. Successfully modified!", 70 | "updatedAt": "2018-03-24T16:00:51.278Z" 71 | } 72 | put: 73 | operationId: updateTodo 74 | summary: Update Todo 75 | produces: 76 | - application/json 77 | parameters: 78 | - 79 | name: "Todo" 80 | in: "body" 81 | description: "Todo to update" 82 | required: true 83 | schema: 84 | $ref: "#/definitions/NewTodo" 85 | responses: 86 | "200": 87 | description: |- 88 | 200 response 89 | examples: 90 | application/json: |- 91 | { 92 | "user_id": "1", 93 | "createdAt": "2018-03-24T16:00:51.278Z", 94 | "title": "hello", 95 | "content": "world. Successfully modified!", 96 | "updatedAt": "2018-03-24T16:00:51.278Z" 97 | } 98 | delete: 99 | operatioinId: deleteTodo 100 | summary: Delete Todo 101 | produces: 102 | - application/json 103 | responses: 104 | "204": 105 | description: |- 106 | 204 response 107 | consumes: 108 | - application/json 109 | definitions: 110 | NewTodo: 111 | type: "object" 112 | required: 113 | - "name" 114 | properties: 115 | title: 116 | type: "string" 117 | content: 118 | type: "string" -------------------------------------------------------------------------------- /serverless-api/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | Description: An AWS Serverless Specification template describing your function. 4 | Resources: 5 | serverlessHandsOn: 6 | Type: 'AWS::Serverless::Function' 7 | Properties: 8 | Handler: handler.api 9 | Runtime: nodejs6.10 10 | Description: '' 11 | MemorySize: 128 12 | Timeout: 15 13 | Role: 14 | 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:role/ServerlessHandsOnRole' 15 | Events: 16 | LambdaMicroservice: 17 | Type: Api 18 | Properties: 19 | Path: '/{proxy+}' 20 | Method: ANY 21 | serverlessHandsOnPermission: 22 | Type: 'AWS::Lambda::Permission' 23 | Properties: 24 | Action: 'lambda:InvokeFunction' 25 | FunctionName: 26 | 'Fn::GetAtt': 27 | - serverlessHandsOn 28 | - Arn 29 | Principal: apigateway.amazonaws.com 30 | SourceArn: 31 | 'Fn::Sub': 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:*/*/*/*' -------------------------------------------------------------------------------- /serverless-api/test/handler.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | const getNaver = () => new Promise((resolve, reject) => { 4 | https.get('https://www.naver.com', (resp) => { 5 | let data = ''; 6 | 7 | // A chunk of data has been recieved. 8 | resp.on('data', (chunk) => { 9 | data += chunk; 10 | }); 11 | 12 | // The whole response has been received. Print out the result. 13 | resp.on('end', () => { 14 | resolve(data); 15 | }); 16 | 17 | }).on("error", (err) => { 18 | reject(err); 19 | }); 20 | }); 21 | 22 | exports.handler = async (event) => { 23 | 24 | // TODO implement 25 | return await getNaver(); 26 | }; 27 | -------------------------------------------------------------------------------- /serverless-ts-api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 khbyun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /serverless-ts-api/README.md: -------------------------------------------------------------------------------- 1 | # Pre-requirements 2 | 3 | 4 | 1. Install node-gyp globally. 5 | ```bash 6 | $ npm i -g node-gyp 7 | ``` 8 | 9 | # Steps 10 | 11 | 1. Install modules. 12 | ```bash 13 | $ npm install 14 | ``` 15 | 16 | 2. You need mecab or mecab-ko for Korean language. 17 | 18 | Build mecab using docker image "lambci/lambda:build-nodejs8.10". [reference](https://hub.docker.com/r/lambci/lambda) 19 | 20 | 21 | ```bash 22 | $ ./sls-build.sh 23 | bash: permission denied: ./sls-build.sh 24 | 25 | # Add a permission on 'sls-build.sh' file 26 | $ chmod +x sls-build.sh 27 | ``` 28 | 29 | 33 | 34 | # References 35 | 36 | - https://github.com/golbin/node-mecab-ya 37 | - https://hub.docker.com/r/lambci/lambda -------------------------------------------------------------------------------- /serverless-ts-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-ts-api", 3 | "version": "1.0.0", 4 | "description": "template", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon", 9 | "rebuild:sls": "./sls-build.sh", 10 | "build:sls": "npx tsc", 11 | "deploy:sls": "npx serverless deploy" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/novemberde/hanko_admin.git" 16 | }, 17 | "nodemonConfig": { 18 | "exec": "ts-node -r dotenv/config src/www.ts dotenv_config_path=./.env", 19 | "watch": [ 20 | "src/*" 21 | ], 22 | "delay": "500" 23 | }, 24 | "keywords": [], 25 | "author": "", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/novemberde/ts-express-orm-template/issues" 29 | }, 30 | "homepage": "https://github.com/novemberde/ts-express-orm-template#readme", 31 | "dependencies": { 32 | "aws-serverless-express": "^3.3.5", 33 | "body-parser": "^1.18.3", 34 | "cors": "^2.8.5", 35 | "dynamoose": "^1.6.2", 36 | "express": "^4.16.4", 37 | "mecab-ya": "^0.1.1", 38 | "morgan": "^1.9.1", 39 | "source-map-support": "^0.5.10" 40 | }, 41 | "devDependencies": { 42 | "@types/body-parser": "^1.17.0", 43 | "@types/express": "^4.16.0", 44 | "@types/morgan": "^1.7.35", 45 | "dotenv": "^6.2.0", 46 | "nodemon": "^1.18.8", 47 | "serverless": "^1.37.1", 48 | "serverless-apigw-binary": "^0.4.4", 49 | "ts-node": "^3.3.0", 50 | "typescript": "^3.1.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /serverless-ts-api/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ServerlessTsNativeBuild 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | memorySize: 128 7 | stage: ${file(./config.yml):STAGE} 8 | region: ${file(./config.yml):AWS_REGION} 9 | deploymentBucket: ${file(./config.yml):DEPLOYMENT_BUCKET} 10 | environment: 11 | NODE_ENV: production 12 | iamRoleStatements: 13 | - Effect: Allow 14 | Action: 15 | - dynamodb:DescribeTable 16 | - dynamodb:Query 17 | - dynamodb:Scan 18 | - dynamodb:GetItem 19 | - dynamodb:PutItem 20 | - dynamodb:UpdateItem 21 | - dynamodb:DeleteItem 22 | Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:*" 23 | 24 | plugins: 25 | - serverless-apigw-binary 26 | custom: 27 | webpack: 28 | includeModules: 29 | forceInclude: 30 | - mecab 31 | apigwBinary: 32 | types: 33 | - 'application/json' 34 | - 'text/html' 35 | - '*/*' 36 | 37 | functions: 38 | webapp: 39 | handler: build/handler.api 40 | events: 41 | - http: 42 | path: /{proxy+} 43 | method: ANY 44 | cors: true 45 | - http: 46 | path: /{proxy+} 47 | method: OPTIONS 48 | cors: true 49 | 50 | package: 51 | individually: true -------------------------------------------------------------------------------- /serverless-ts-api/sls-build.sh: -------------------------------------------------------------------------------- 1 | docker run --rm -v "$PWD":/var/task lambci/lambda:build-nodejs8.10 node_modules/mecab-ya/bin/install-mecab ko -------------------------------------------------------------------------------- /serverless-ts-api/sls-run.sh: -------------------------------------------------------------------------------- 1 | docker run --rm -e NODE_ENV=development -v $PWD:/var/task lambci/lambda:nodejs8.10 build/handler.api '{"resource": "/","path": "/test","httpMethod": "GET"}' -------------------------------------------------------------------------------- /serverless-ts-api/src/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as logger from 'morgan'; 3 | import * as bodyParser from 'body-parser'; 4 | import * as cors from 'cors'; 5 | import TodoRoute from "./routes/TodoRoute"; 6 | import ErrorHandler from "./middlewares/ErrorHandler"; 7 | 8 | const env = process.env.NODE_ENV || 'development'; 9 | const app = express(); 10 | 11 | // Set logger 12 | if (env === 'production') app.use(logger('common')); 13 | else if (env === 'development') app.use(logger('dev')); 14 | 15 | app.use(cors()); 16 | app.use((req, res: express.Response, next) => { 17 | res.locals.userId = '0'; 18 | return next(); 19 | }); 20 | app.use(bodyParser.json()); 21 | app.use(bodyParser.urlencoded({ extended: false })); 22 | app.get(['/', '/test'], (req, res, next) => { 23 | return res.send('Welcome!'); 24 | }); 25 | app.use('/todo', new TodoRoute().router); 26 | app.use(new ErrorHandler ().router); 27 | 28 | export default app; -------------------------------------------------------------------------------- /serverless-ts-api/src/handler.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | import * as awsServerlessExpress from 'aws-serverless-express'; 3 | import app from './app'; 4 | 5 | const binaryMimeTypes = [ 6 | 'application/javascript', 7 | 'application/x-www-form-urlencoded', 8 | 'application/json', 9 | 'text/plain', 10 | 'text/text', 11 | ]; 12 | const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes); 13 | 14 | export const api = (event: any, context: any) => { 15 | return awsServerlessExpress.proxy(server, event, context); 16 | } -------------------------------------------------------------------------------- /serverless-ts-api/src/middlewares/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | export default class ErrorHandler { 4 | public router: Router; 5 | 6 | constructor () { 7 | this.router = Router(); 8 | this.router.use(this.notFoundError); 9 | this.router.use(this.internalServerError); 10 | } 11 | 12 | private notFoundError (req, res, next) { 13 | res.status(404).json({message: 'Not found'}); 14 | } 15 | 16 | private internalServerError (err, req, res, next) { 17 | res.status(500).send(err); 18 | } 19 | } -------------------------------------------------------------------------------- /serverless-ts-api/src/routes/TodoRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from "express"; 2 | import * as dynamoose from 'dynamoose'; 3 | 4 | dynamoose.AWS.config.region = process.env.AWS_REGION; 5 | const TodoMecab = dynamoose.model('TodoMecab', { 6 | userId: { 7 | type: Number, 8 | hashKey: true 9 | }, 10 | createdAt: { 11 | type: Number, 12 | rangeKey: true 13 | }, 14 | updatedAt: Number, 15 | content: String, 16 | mecabResult: Object, 17 | }, { 18 | create: false, // Create a table if not exist, 19 | }); 20 | const mecab = require('mecab-ya'); 21 | 22 | export default class TodoRoute { 23 | public router: Router; 24 | 25 | constructor() { 26 | this.router = Router(); 27 | 28 | this.router.get('/', this.findAll); 29 | this.router.get('/:createdAt', this.findOne); 30 | this.router.post('/', this.middleware, this.create); 31 | this.router.put('/:createdAt', this.middleware, this.update); 32 | this.router.delete('/:createdAt', this.delete); 33 | } 34 | 35 | private getMecabResult = async (text) => { 36 | return Promise.all([ 37 | new Promise((resolve, reject) => { 38 | mecab.pos(text, function (err, result) { 39 | if(err) return reject(err); 40 | resolve(result); 41 | }) 42 | }), 43 | new Promise((resolve, reject) => { 44 | mecab.morphs(text, function (err, result) { 45 | if(err) return reject(err); 46 | resolve(result); 47 | }) 48 | }), 49 | new Promise((resolve, reject) => { 50 | mecab.nouns(text, function (err, result) { 51 | if(err) return reject(err); 52 | resolve(result); 53 | }) 54 | }) 55 | ]).then(result => { 56 | return { 57 | pos: result[0], 58 | morphs: result[1], 59 | nouns: result[2], 60 | }; 61 | }); 62 | } 63 | 64 | private middleware = (req, res, next) => { 65 | const { 66 | content, 67 | } = req.body; 68 | 69 | if(typeof content !== 'string' || content.trim().length < 2) return res.status(400).json('Invalid content'); 70 | 71 | next(); 72 | } 73 | 74 | private findAll = async (req: Request, res: Response, next) => { 75 | try { 76 | const { 77 | userId 78 | } = res.locals; 79 | let { 80 | lastKey 81 | } = req.query; 82 | 83 | const result = await TodoMecab.query('userId').eq(userId).startAt(lastKey).limit(1000).descending().exec(); 84 | 85 | return res.status(200).json(result); 86 | } catch (error) { 87 | next(error); 88 | } 89 | } 90 | 91 | private findOne = async (req: Request, res: Response, next) => { 92 | try { 93 | const { 94 | userId 95 | } = res.locals; 96 | const { 97 | createdAt 98 | } = req.params; 99 | 100 | const result = await TodoMecab.get({userId, createdAt}); 101 | 102 | return res.status(200).json(result); 103 | } catch (error) { 104 | console.error(error); 105 | next(error); 106 | } 107 | } 108 | 109 | private create = async (req: Request, res: Response, next) => { 110 | try { 111 | const { 112 | content, 113 | } = req.body; 114 | const { 115 | userId 116 | } = res.locals; 117 | 118 | const mecabResult = await this.getMecabResult(content); 119 | await new TodoMecab({ 120 | createdAt: Math.floor(new Date() / 1000), 121 | content, 122 | mecabResult, 123 | userId: String(userId), 124 | }).save(); 125 | 126 | return res.status(201).send(); 127 | } catch (error) { 128 | console.error(error); 129 | next(error); 130 | } 131 | } 132 | 133 | private update = async (req: Request, res: Response, next) => { 134 | try { 135 | const { 136 | content, 137 | } = req.body; 138 | const { 139 | createdAt 140 | } = req.params; 141 | const { 142 | userId 143 | } = res.locals; 144 | 145 | const mecabResult = await this.getMecabResult(content); 146 | await new TodoMecab({ 147 | createdAt: Number(createdAt), 148 | updatedAt: Math.floor(new Date() / 1000), 149 | content, 150 | mecabResult, 151 | userId, 152 | }).save(); 153 | 154 | return res.status(201).send(); 155 | } catch (error) { 156 | console.error(error); 157 | next(error); 158 | } 159 | } 160 | 161 | private delete = async (req: Request, res: Response, next) => { 162 | try { 163 | const { 164 | createdAt 165 | } = req.params; 166 | const { 167 | userId 168 | } = res.locals; 169 | 170 | if(!createdAt) return res.status(400).send("Bad request. createdAt is undefined"); 171 | 172 | await TodoMecab.delete({ 173 | userId, 174 | createdAt, 175 | }); 176 | 177 | return res.status(204).json({}); 178 | } catch (error) { 179 | console.error(error); 180 | next(error); 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /serverless-ts-api/src/www.ts: -------------------------------------------------------------------------------- 1 | import app from "./app"; 2 | 3 | const port = process.env.PORT || 3000; 4 | 5 | app.listen(port, () => { 6 | console.log(`Server is running at ${port}`); 7 | }).on('error', (err) => { 8 | console.error(err); 9 | }); -------------------------------------------------------------------------------- /serverless-ts-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6" 6 | ], 7 | "target": "es5", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "outDir": "./build", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "sourceMap": true 14 | } 15 | } -------------------------------------------------------------------------------- /sls-ts-static-web-front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["babel-plugin-styled-components"] 4 | } -------------------------------------------------------------------------------- /sls-ts-static-web-front/README.md: -------------------------------------------------------------------------------- 1 | # Todo Web Client 2 | Lambda와 Express.js로 만든 RESTful API에 요청을 날리고 받아오는 웹 클라이언트입니다. 3 | 4 | ## [DEMO](https://d31gn4ya6qstrb.cloudfront.net) 5 | 6 | # 할 일 목록 7 | > 2018년 3월 24일 기준 8 | 9 | ### 개발 스펙 10 | - [ ] React project scaffolding 11 | - [ ] UI Design (Responsive) 12 | - [ ] Markup 및 CSS 작성 13 | - [ ] CORS 이슈 해결 14 | - [ ] Endpoint에 Data Fetching & Binding 15 | - [ ] S3 Static Website Hosting을 통해 배포 16 | - [ ] AWS SDK를 통한 배포 Automation 17 | 18 | ### 가이드 작성 스펙 19 | - [ ] Endpoint URL 변경 20 | - [ ] `npm run build` (`yarn build`) 21 | - [ ] AWS Console을 이용해 S3에 업로드 22 | - [ ] Static Website Hosting 설정 -------------------------------------------------------------------------------- /sls-ts-static-web-front/deploy.sh: -------------------------------------------------------------------------------- 1 | # Build 2 | npm run build 3 | 4 | # Deploy 5 | aws s3 cp ./dist/ s3://sls-ts-static-web-front/ --recursive --acl public-read -------------------------------------------------------------------------------- /sls-ts-static-web-front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sls-ts-static-web-front", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Jee Hyuk Won (Tony) ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "npx parcel src/index.html" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.26.3", 12 | "babel-plugin-styled-components": "^1.5.1", 13 | "babel-preset-env": "^1.6.1", 14 | "parcel-bundler": "^1.12.3" 15 | }, 16 | "dependencies": { 17 | "@material-ui/core": "^1.2.1", 18 | "axios": "^0.18.0", 19 | "jquery": "^3.5.0", 20 | "material-ui": "^0.20.0", 21 | "open-color": "^1.6.3", 22 | "react": "^16.8.1", 23 | "react-dom": "^16.8.1", 24 | "setimmediate": "^1.0.5", 25 | "socket.io-client": "^2.2.0", 26 | "styled-components": "^3.2.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sls-ts-static-web-front/src/components/App.js: -------------------------------------------------------------------------------- 1 | import 'setimmediate' 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import axios from 'axios' 5 | import MaterialUiThemeProvider from 'material-ui/styles/MuiThemeProvider' 6 | import { List, ListItem } from 'material-ui/List' 7 | import { TextField, RaisedButton } from 'material-ui' 8 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 9 | import Top from './Top' 10 | 11 | const baseURL = 'https://tel9au775k.execute-api.ap-northeast-2.amazonaws.com/prod'; 12 | // const baseURL = 'http://localhost:3000'; 13 | const App = styled.div`` 14 | 15 | const Input = styled.div` 16 | align-items: center; 17 | display: flex; 18 | padding: 0 1rem 1rem; 19 | ` 20 | const StyledTextField = styled(TextField)` 21 | flex: 1; 22 | ` 23 | const StyledRaisedButton = styled(RaisedButton)` 24 | height: 2.25rem; 25 | margin: 1rem 0 0 2rem; 26 | ` 27 | const StyledRaisedActionButton = styled(RaisedButton)` 28 | height: 2.25rem; 29 | margin: 0rem 0rem 0.4rem 0rem; 30 | margin-top: -3rem; 31 | ` 32 | const StyledListItem = styled(ListItem)` 33 | padding: 4rem; 34 | flex: 1; 35 | ` 36 | 37 | export default class extends React.Component { 38 | constructor(props) { 39 | super(props) 40 | this.state = { 41 | items: [], 42 | inputText: '', 43 | open: false, 44 | isUploading: false, 45 | } 46 | this.fetchItems = this.fetchItems.bind(this) 47 | this.addItem = this.addItem.bind(this) 48 | this.handleChange = this.handleChange.bind(this) 49 | this.handleKeyUp = this.handleKeyUp.bind(this) 50 | this.deleteItem = this.deleteItem.bind(this) 51 | this.updateItem = this.updateItem.bind(this) 52 | this.handleOpen = this.handleOpen.bind(this) 53 | this.handleClose = this.handleClose.bind(this) 54 | this.handleChangeItem = this.handleChangeItem.bind(this) 55 | } 56 | componentWillMount() { 57 | this.fetchItems() 58 | } 59 | handleOpen () { 60 | this.setState({ open: true }); 61 | } 62 | handleClose () { 63 | this.setState({ open: false }); 64 | } 65 | addItem() { 66 | if(this.state.isUploading) return; 67 | 68 | this.setState({ isUploading: true }); 69 | axios.post(`${baseURL}/todo/`, { 70 | content: this.state.inputText, 71 | // content: '', 72 | }).then(() => { 73 | this.setState({inputText: ""}); 74 | this.fetchItems(); 75 | }).finally(() => { 76 | this.setState({ isUploading: false }); 77 | }); 78 | } 79 | fetchItems() { 80 | axios.get(`${baseURL}/todo/`).then(({ data }) => { 81 | console.log(data); 82 | this.setState({ 83 | items: data, 84 | }) 85 | }) 86 | } 87 | handleChange(e) { 88 | const nextState = {}; 89 | nextState[e.target.name] = e.target.value 90 | this.setState(nextState); 91 | } 92 | handleKeyUp(e) { 93 | if(e.keyCode === 13) { 94 | this.addItem(); 95 | } 96 | } 97 | deleteItem(createdAt) { 98 | axios.delete(`${baseURL}/todo/${createdAt}`).then(this.fetchItems); 99 | } 100 | updateItem(createdAt, content) { 101 | axios.put(`${baseURL}/todo/${createdAt}`, { 102 | content, 103 | }).then(this.fetchItems); 104 | } 105 | handleChangeItem(e, i) { 106 | const nextItems = [ 107 | ...this.state.items 108 | ]; 109 | nextItems[i].content = e.target.value; 110 | this.setState(Object.assign(this.state, { 111 | items: nextItems 112 | })); 113 | } 114 | render() { 115 | return ( 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | { 125 | this.state.items.map((item, i) => ( 126 |
127 | 128 | this.handleChangeItem(e, i)}/> 129 | this.handleChangeItem(e, i)}/> 130 | 131 | this.updateItem(item.createdAt, item.content)}/> 132 | this.deleteItem(item.createdAt)}/> 133 | 134 | 135 |
136 | )) 137 | } 138 |
139 |
140 |
141 | ) 142 | } 143 | } -------------------------------------------------------------------------------- /sls-ts-static-web-front/src/components/Top.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import oc from 'open-color' 4 | import { AppBar, Drawer, MenuItem } from 'material-ui' 5 | import LinkIcon from 'material-ui/svg-icons/content/link' 6 | const Top = styled.div` 7 | align-items: center; 8 | background-color: ${oc.gray[8]}; 9 | color: ${oc.white}; 10 | display: flex; 11 | font-size: 1.1rem; 12 | left: 0; 13 | height: 3rem; 14 | justify-content: center; 15 | position: fixed; 16 | top: 0; 17 | width: 100%; 18 | ` 19 | 20 | export default class extends React.Component { 21 | constructor(props) { 22 | super(props) 23 | this.state = { 24 | isDrawerOpened: false, 25 | } 26 | this.toggleDrawer = this.toggleDrawer.bind(this) 27 | } 28 | toggleDrawer() { 29 | return this.setState({ 30 | isDrawerOpened: !this.state.isDrawerOpened, 31 | }) 32 | } 33 | render() { 34 | return [ 35 | Serverless Todo} 38 | onLeftIconButtonClick={this.toggleDrawer} 39 | />, 40 | this.setState({ isDrawerOpened })} 44 | docked={false} 45 | > 46 | }>AWSKRUG Facebook 47 | }>AWSKRUG Slack 48 | }>Github 49 | 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /sls-ts-static-web-front/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 5 | } 6 | 7 | a.nostyle:link { 8 | text-decoration: inherit; 9 | color: inherit; 10 | cursor: auto; 11 | } 12 | 13 | a.nostyle:visited { 14 | text-decoration: inherit; 15 | color: inherit; 16 | cursor: auto; 17 | } -------------------------------------------------------------------------------- /sls-ts-static-web-front/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sls-ts-static-web-front/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import styled from 'styled-components' 4 | 5 | import App from './components/App' 6 | 7 | ReactDOM.render(, document.getElementById('app')) 8 | 9 | // Hot Module Replacement 10 | if (module.hot) { 11 | module.hot.accept(); 12 | } 13 | -------------------------------------------------------------------------------- /static-web-front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["babel-plugin-styled-components"] 4 | } -------------------------------------------------------------------------------- /static-web-front/README.md: -------------------------------------------------------------------------------- 1 | # Todo Web Client 2 | Lambda와 Express.js로 만든 RESTful API에 요청을 날리고 받아오는 웹 클라이언트입니다. 3 | 4 | # 할 일 목록 5 | > 2018년 3월 24일 기준 6 | 7 | ### 개발 스펙 8 | - [ ] React project scaffolding 9 | - [ ] UI Design (Responsive) 10 | - [ ] Markup 및 CSS 작성 11 | - [ ] CORS 이슈 해결 12 | - [ ] Endpoint에 Data Fetching & Binding 13 | - [ ] S3 Static Website Hosting을 통해 배포 14 | - [ ] AWS SDK를 통한 배포 Automation 15 | 16 | ### 가이드 작성 스펙 17 | - [ ] Endpoint URL 변경 18 | - [ ] `npm run build` (`yarn build`) 19 | - [ ] AWS Console을 이용해 S3에 업로드 20 | - [ ] Static Website Hosting 설정 -------------------------------------------------------------------------------- /static-web-front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-todo-demo-app", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Jee Hyuk Won (Tony) ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "npx parcel src/index.html" 9 | }, 10 | "devDependencies": { 11 | "babel-plugin-styled-components": "^1.5.1", 12 | "babel-preset-env": "^1.6.1", 13 | "parcel-bundler": "^1.7.0" 14 | }, 15 | "dependencies": { 16 | "@material-ui/core": "^1.2.1", 17 | "axios": "^0.18.0", 18 | "jquery": "^3.5.0", 19 | "material-ui": "^0.20.0", 20 | "open-color": "^1.6.3", 21 | "react": "^16.3.0", 22 | "react-dom": "^16.3.0", 23 | "setimmediate": "^1.0.5", 24 | "styled-components": "^3.2.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /static-web-front/src/components/App.js: -------------------------------------------------------------------------------- 1 | import 'setimmediate' 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import axios from 'axios' 5 | import MaterialUiThemeProvider from 'material-ui/styles/MuiThemeProvider' 6 | import { List, ListItem } from 'material-ui/List' 7 | import { TextField, RaisedButton } from 'material-ui' 8 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 9 | import Top from './Top' 10 | 11 | const baseURL = 'https://aixvm0ki39.execute-api.ap-northeast-2.amazonaws.com/dev'; 12 | const App = styled.div`` 13 | 14 | const Input = styled.div` 15 | align-items: center; 16 | display: flex; 17 | padding: 0 1rem 1rem; 18 | ` 19 | const StyledTextField = styled(TextField)` 20 | flex: 1; 21 | ` 22 | const StyledRaisedButton = styled(RaisedButton)` 23 | height: 2.25rem; 24 | margin: 1rem 0 0 2rem; 25 | ` 26 | const StyledRaisedActionButton = styled(RaisedButton)` 27 | height: 2.25rem; 28 | margin: 0rem 0rem 0.4rem 0rem; 29 | ` 30 | const StyledListItem = styled(ListItem)` 31 | padding: 4rem; 32 | flex: 1; 33 | ` 34 | 35 | export default class extends React.Component { 36 | constructor(props) { 37 | super(props) 38 | this.state = { 39 | items: [], 40 | inputText: '', 41 | open: false, 42 | } 43 | this.fetchItems = this.fetchItems.bind(this) 44 | this.addItem = this.addItem.bind(this) 45 | this.handleChange = this.handleChange.bind(this) 46 | this.handleKeyUp = this.handleKeyUp.bind(this) 47 | this.deleteItem = this.deleteItem.bind(this) 48 | this.updateItem = this.updateItem.bind(this) 49 | this.handleOpen = this.handleOpen.bind(this) 50 | this.handleClose = this.handleClose.bind(this) 51 | this.handleChangeItem = this.handleChangeItem.bind(this) 52 | } 53 | componentWillMount() { 54 | this.fetchItems() 55 | } 56 | handleOpen () { 57 | this.setState({ open: true }); 58 | } 59 | handleClose () { 60 | this.setState({ open: false }); 61 | } 62 | addItem() { 63 | axios.post(`${baseURL}/todo/`, { 64 | title: this.state.inputText, 65 | content: '', 66 | }).then(() => { 67 | this.setState({inputText: ""}); 68 | this.fetchItems(); 69 | }) 70 | } 71 | fetchItems() { 72 | axios.get(`${baseURL}/todo/`).then(({ data }) => { 73 | this.setState({ 74 | items: data, 75 | }) 76 | }) 77 | } 78 | handleChange(e) { 79 | const nextState = {}; 80 | nextState[e.target.name] = e.target.value 81 | this.setState(nextState); 82 | } 83 | handleKeyUp(e) { 84 | if(e.keyCode === 13) { 85 | this.addItem(); 86 | } 87 | } 88 | deleteItem(createdAt) { 89 | axios.delete(`${baseURL}/todo/${createdAt}`).then(this.fetchItems); 90 | } 91 | updateItem(createdAt, title) { 92 | axios.put(`${baseURL}/todo/${createdAt}`, { 93 | title, 94 | }).then(this.fetchItems); 95 | } 96 | handleChangeItem(e, i) { 97 | const nextItems = [ 98 | ...this.state.items 99 | ]; 100 | nextItems[i].title = e.target.value; 101 | this.setState(Object.assign(this.state, { 102 | items: nextItems 103 | })); 104 | } 105 | render() { 106 | return ( 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | { 116 | this.state.items.map((item, i) => ( 117 | 118 | this.handleChangeItem(e, i)}/> 119 | 120 | this.updateItem(item.createdAt, item.title)}/> 121 | this.deleteItem(item.createdAt)}/> 122 | 123 | 124 | )) 125 | } 126 | 127 | 128 | 129 | ) 130 | } 131 | } -------------------------------------------------------------------------------- /static-web-front/src/components/Top.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import oc from 'open-color' 4 | import { AppBar, Drawer, MenuItem } from 'material-ui' 5 | import LinkIcon from 'material-ui/svg-icons/content/link' 6 | const Top = styled.div` 7 | align-items: center; 8 | background-color: ${oc.gray[8]}; 9 | color: ${oc.white}; 10 | display: flex; 11 | font-size: 1.1rem; 12 | left: 0; 13 | height: 3rem; 14 | justify-content: center; 15 | position: fixed; 16 | top: 0; 17 | width: 100%; 18 | ` 19 | 20 | export default class extends React.Component { 21 | constructor(props) { 22 | super(props) 23 | this.state = { 24 | isDrawerOpened: false, 25 | } 26 | this.toggleDrawer = this.toggleDrawer.bind(this) 27 | } 28 | toggleDrawer() { 29 | return this.setState({ 30 | isDrawerOpened: !this.state.isDrawerOpened, 31 | }) 32 | } 33 | render() { 34 | return [ 35 | Serverless Todo} 38 | onLeftIconButtonClick={this.toggleDrawer} 39 | />, 40 | this.setState({ isDrawerOpened })} 44 | docked={false} 45 | > 46 | }>AWSKRUG Facebook 47 | }>AWSKRUG Slack 48 | }>Github 49 | 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /static-web-front/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 5 | } 6 | 7 | a.nostyle:link { 8 | text-decoration: inherit; 9 | color: inherit; 10 | cursor: auto; 11 | } 12 | 13 | a.nostyle:visited { 14 | text-decoration: inherit; 15 | color: inherit; 16 | cursor: auto; 17 | } -------------------------------------------------------------------------------- /static-web-front/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /static-web-front/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import styled from 'styled-components' 4 | 5 | import App from './components/App' 6 | 7 | ReactDOM.render(, document.getElementById('app')) 8 | 9 | // Hot Module Replacement 10 | if (module.hot) { 11 | module.hot.accept(); 12 | } 13 | -------------------------------------------------------------------------------- /ws-static-web-front/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /ws-static-web-front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /ws-static-web-front/README.md: -------------------------------------------------------------------------------- 1 | # ws-static-web-front 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /ws-static-web-front/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /ws-static-web-front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ws-static-web-front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "deploy": "aws s3 cp ./dist/ s3://awskrug-sls-chat-app-front/ --recursive --acl public-read" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.0", 12 | "bootstrap": "^4.3.1", 13 | "bootstrap-vue": "^2.0.0-rc.18", 14 | "core-js": "^2.6.5", 15 | "node-sass": "^4.12.0", 16 | "sass-loader": "^7.1.0", 17 | "sockette": "^2.0.5", 18 | "vue": "^2.6.6", 19 | "vuex": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "^3.5.0", 23 | "@vue/cli-service": "^3.5.0", 24 | "vue-template-compiler": "^2.5.21" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ws-static-web-front/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ws-static-web-front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/ws-static-web-front/public/favicon.ico -------------------------------------------------------------------------------- /ws-static-web-front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ws-static-web-front 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ws-static-web-front/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /ws-static-web-front/src/assets/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novemberde/serverless-todo-demo/2077a02633263b3c14d2101741032b3b5cffcd28/ws-static-web-front/src/assets/logo.jpeg -------------------------------------------------------------------------------- /ws-static-web-front/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 96 | 97 | 98 | 145 | -------------------------------------------------------------------------------- /ws-static-web-front/src/main.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css' 2 | import 'bootstrap-vue/dist/bootstrap-vue.css' 3 | 4 | import Vue from 'vue' 5 | import App from './App.vue' 6 | import store from './store' 7 | import BootstrapVue from 'bootstrap-vue' 8 | 9 | Vue.use(BootstrapVue) 10 | 11 | Vue.config.productionTip = false 12 | 13 | new Vue({ 14 | store, 15 | render: h => h(App) 16 | }).$mount('#app') 17 | -------------------------------------------------------------------------------- /ws-static-web-front/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | 9 | }, 10 | mutations: { 11 | 12 | }, 13 | actions: { 14 | 15 | } 16 | }) 17 | --------------------------------------------------------------------------------