├── backend
├── tests
│ └── unit
│ │ ├── __init__.py
│ │ └── test_handler.py
├── src
│ ├── file_uploaded
│ │ ├── __init__.py
│ │ ├── requirements.txt
│ │ └── app.py
│ └── generate_presigned_url
│ │ ├── __init__.py
│ │ ├── requirements.txt
│ │ └── app.py
├── events
│ └── event.json
├── .gitignore
├── template.yaml
└── README.md
├── frontend
├── .env
├── babel.config.js
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── assets
│ │ └── logo.png
│ ├── plugins
│ │ └── vuetify.js
│ ├── main.js
│ ├── App.vue
│ └── components
│ │ └── FileUpload.vue
├── .editorconfig
├── .gitignore
├── vue.config.js
└── package.json
├── architecture.png
└── Readme.md
/backend/tests/unit/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/file_uploaded/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_GENERATE_URL=''
--------------------------------------------------------------------------------
/backend/src/file_uploaded/requirements.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/generate_presigned_url/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/generate_presigned_url/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3==1.13.1
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AWS-Serverless-Projects/s3-upload-with-presigned-using-sam/HEAD/architecture.png
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AWS-Serverless-Projects/s3-upload-with-presigned-using-sam/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AWS-Serverless-Projects/s3-upload-with-presigned-using-sam/HEAD/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/frontend/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuetify from 'vuetify/lib'
3 |
4 | Vue.use(Vuetify)
5 |
6 | export default new Vuetify({
7 | })
8 |
--------------------------------------------------------------------------------
/backend/src/file_uploaded/app.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | def index(event, context):
4 | print(event)
5 | return {
6 | "statusCode": 202,
7 | "body": json.dumps(event)
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import vuetify from './plugins/vuetify'
4 |
5 | Vue.config.productionTip = false
6 |
7 | new Vue({
8 | vuetify,
9 | render: h => h(App)
10 | }).$mount('#app')
11 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
20 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | package-lock.json
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 |
3 | module.exports = {
4 | configureWebpack: {
5 | plugins: [
6 | new webpack.DefinePlugin({
7 | 'process.env': {
8 | NODE_ENV: JSON.stringify(process.env.NODE_ENV)
9 | }
10 | })
11 | ]
12 | },
13 | transpileDependencies: [
14 | 'vuetify'
15 | ]
16 | }
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/backend/src/generate_presigned_url/app.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import uuid
4 | import os
5 | from botocore.exceptions import ClientError
6 | from botocore.client import Config
7 |
8 | s3 = boto3.client('s3', config=Config(
9 | signature_version='s3v4',
10 | s3={'addressing_style': 'virtual'}
11 | ))
12 |
13 |
14 | def index(event, context):
15 | key = str(uuid.uuid4())
16 | bucket = os.getenv('S3BUCKET')
17 |
18 | try:
19 | url = s3.generate_presigned_url('put_object',
20 | Params={'Bucket': bucket, 'Key': key},
21 | ExpiresIn=os.getenv('EXPIRY_TIME'),
22 | HttpMethod='PUT',
23 | )
24 |
25 | response = {
26 | "statusCode": 200,
27 | "body": json.dumps(url),
28 | "headers": {
29 | 'Content-Type': 'application/json',
30 | 'Access-Control-Allow-Origin': '*'
31 | }
32 | }
33 | except ClientError as e:
34 | print(e)
35 | response = {
36 | "statusCode": 500,
37 | "body": 'Error generating the url'
38 | }
39 |
40 | return response
41 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "axios": "^0.19.2",
12 | "core-js": "^3.6.4",
13 | "vue": "^2.6.11",
14 | "vuetify": "^2.2.11",
15 | "webpack": "^4.43.0"
16 | },
17 | "devDependencies": {
18 | "@vue/cli-plugin-babel": "~4.3.0",
19 | "@vue/cli-plugin-eslint": "~4.3.0",
20 | "@vue/cli-service": "~4.3.0",
21 | "@vue/eslint-config-standard": "^5.1.2",
22 | "babel-eslint": "^10.1.0",
23 | "eslint": "^6.7.2",
24 | "eslint-plugin-import": "^2.20.2",
25 | "eslint-plugin-node": "^11.1.0",
26 | "eslint-plugin-promise": "^4.2.1",
27 | "eslint-plugin-standard": "^4.0.0",
28 | "eslint-plugin-vue": "^6.2.2",
29 | "sass": "^1.19.0",
30 | "sass-loader": "^8.0.0",
31 | "vue-cli-plugin-vuetify": "~2.0.5",
32 | "vue-template-compiler": "^2.6.11",
33 | "vuetify-loader": "^1.3.0"
34 | },
35 | "eslintConfig": {
36 | "root": true,
37 | "env": {
38 | "node": true
39 | },
40 | "extends": [
41 | "plugin:vue/essential",
42 | "@vue/standard"
43 | ],
44 | "parserOptions": {
45 | "parser": "babel-eslint"
46 | },
47 | "rules": {}
48 | },
49 | "browserslist": [
50 | "> 1%",
51 | "last 2 versions",
52 | "not dead"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/backend/events/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "{\"message\": \"hello world\"}",
3 | "resource": "/{proxy+}",
4 | "path": "/path/to/resource",
5 | "httpMethod": "POST",
6 | "isBase64Encoded": false,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "pathParameters": {
11 | "proxy": "/path/to/resource"
12 | },
13 | "stageVariables": {
14 | "baz": "qux"
15 | },
16 | "headers": {
17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
18 | "Accept-Encoding": "gzip, deflate, sdch",
19 | "Accept-Language": "en-US,en;q=0.8",
20 | "Cache-Control": "max-age=0",
21 | "CloudFront-Forwarded-Proto": "https",
22 | "CloudFront-Is-Desktop-Viewer": "true",
23 | "CloudFront-Is-Mobile-Viewer": "false",
24 | "CloudFront-Is-SmartTV-Viewer": "false",
25 | "CloudFront-Is-Tablet-Viewer": "false",
26 | "CloudFront-Viewer-Country": "US",
27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
28 | "Upgrade-Insecure-Requests": "1",
29 | "User-Agent": "Custom User Agent String",
30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
33 | "X-Forwarded-Port": "443",
34 | "X-Forwarded-Proto": "https"
35 | },
36 | "requestContext": {
37 | "accountId": "123456789012",
38 | "resourceId": "123456",
39 | "stage": "prod",
40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
41 | "requestTime": "09/Apr/2015:12:34:56 +0000",
42 | "requestTimeEpoch": 1428582896000,
43 | "identity": {
44 | "cognitoIdentityPoolId": null,
45 | "accountId": null,
46 | "cognitoIdentityId": null,
47 | "caller": null,
48 | "accessKey": null,
49 | "sourceIp": "127.0.0.1",
50 | "cognitoAuthenticationType": null,
51 | "cognitoAuthenticationProvider": null,
52 | "userArn": null,
53 | "userAgent": "Custom User Agent String",
54 | "user": null
55 | },
56 | "path": "/prod/path/to/resource",
57 | "resourcePath": "/{proxy+}",
58 | "httpMethod": "POST",
59 | "apiId": "1234567890",
60 | "protocol": "HTTP/1.1"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Upload files to s3 with presigend URLs with AWS SAM
2 |
3 | This project is to create a presigned url to upload files to s3 directory. Whole stack will be created using AWS SAM.
4 |
5 | Architecture is as follows:
6 |
7 | 
8 |
9 | ## Prerequisites
10 |
11 | * Need to have sam cli and npm installed
12 |
13 |
14 | ## How to install
15 |
16 | 1. Within the backend directory, run:
17 | ```
18 | sam build
19 | sam deploy -g
20 | ```
21 | - Parameter *SnsNotificationEmail* : Add an email address to recieve SNS notification from uploaded file processed events
22 |
23 | 2. This will create:
24 | - S3 bucket for static web site hosting
25 | - S3 bucket to upload files
26 | - Lambda function to generate pre-signed url
27 | - API gateway to trigger above lambda
28 | - Lambda function to trigger when file is uploaded into the S3 bucket
29 | - Lambda destination SNS topic for above function when success
30 | - Lambda destination SNS topic for above function when failed
31 |
32 | ** While creating the stack, you will receive two emails to subscribe to above sns topics. Confirm the subscriptions.
33 |
34 | Once the stack is created, note the values of below outputs:
35 |
36 | * `S3WebsiteURL` - This will be the final url to access the system.
37 | * `S3WebsiteBucket` - S3 bucket for static website contents.
38 | * `S3FileUploadBucket` - S3 bucket to store the uploaded files
39 | * `PresignedUrlApi` - API endpoint to generate the presigned url.
40 |
41 | 3. Go to the frontend directory.
42 | Update the variable `VUE_APP_GENERATE_URL` in the .env file with the value of `PresignedUrlApi`.
43 |
44 | 4. Run below commands to install dependancies and build the frontend
45 | ```
46 | npm install
47 | npm run build
48 | ```
49 |
50 | This will generate the project files to deploy in the `dist` directory.
51 |
52 | 5. To copy the built contents to s3, run:
53 | ```
54 | aws s3 cp dist s3:// --recursive
55 | ```
56 |
57 | 6. Access the web site using the `S3WebsiteURL` value.
58 |
59 | 7. Upload a file and you should receive a sns notification to the given email.
60 |
61 |
62 | ## Deleting the stack
63 |
64 | 1. First remove the data in the both s3 buckets:
65 |
66 | ```
67 | aws s3 rm s3:// --recursive
68 | aws s3 rm s3:// --recursive
69 | ```
70 |
71 | 2. Then run:
72 |
73 | ```aws cloudformation delete-stack --stack-name ```
--------------------------------------------------------------------------------
/backend/tests/unit/test_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 |
5 | from hello_world import app
6 |
7 |
8 | @pytest.fixture()
9 | def apigw_event():
10 | """ Generates API GW Event"""
11 |
12 | return {
13 | "body": '{ "test": "body"}',
14 | "resource": "/{proxy+}",
15 | "requestContext": {
16 | "resourceId": "123456",
17 | "apiId": "1234567890",
18 | "resourcePath": "/{proxy+}",
19 | "httpMethod": "POST",
20 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
21 | "accountId": "123456789012",
22 | "identity": {
23 | "apiKey": "",
24 | "userArn": "",
25 | "cognitoAuthenticationType": "",
26 | "caller": "",
27 | "userAgent": "Custom User Agent String",
28 | "user": "",
29 | "cognitoIdentityPoolId": "",
30 | "cognitoIdentityId": "",
31 | "cognitoAuthenticationProvider": "",
32 | "sourceIp": "127.0.0.1",
33 | "accountId": "",
34 | },
35 | "stage": "prod",
36 | },
37 | "queryStringParameters": {"foo": "bar"},
38 | "headers": {
39 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
40 | "Accept-Language": "en-US,en;q=0.8",
41 | "CloudFront-Is-Desktop-Viewer": "true",
42 | "CloudFront-Is-SmartTV-Viewer": "false",
43 | "CloudFront-Is-Mobile-Viewer": "false",
44 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
45 | "CloudFront-Viewer-Country": "US",
46 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
47 | "Upgrade-Insecure-Requests": "1",
48 | "X-Forwarded-Port": "443",
49 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
50 | "X-Forwarded-Proto": "https",
51 | "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==",
52 | "CloudFront-Is-Tablet-Viewer": "false",
53 | "Cache-Control": "max-age=0",
54 | "User-Agent": "Custom User Agent String",
55 | "CloudFront-Forwarded-Proto": "https",
56 | "Accept-Encoding": "gzip, deflate, sdch",
57 | },
58 | "pathParameters": {"proxy": "/examplepath"},
59 | "httpMethod": "POST",
60 | "stageVariables": {"baz": "qux"},
61 | "path": "/examplepath",
62 | }
63 |
64 |
65 | def test_lambda_handler(apigw_event, mocker):
66 |
67 | ret = app.lambda_handler(apigw_event, "")
68 | data = json.loads(ret["body"])
69 |
70 | assert ret["statusCode"] == 200
71 | assert "message" in ret["body"]
72 | assert data["message"] == "hello world"
73 | # assert "location" in data.dict_keys()
74 |
--------------------------------------------------------------------------------
/frontend/src/components/FileUpload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
12 |
17 |
18 | File uploaded successfully.
19 |
20 |
21 | Failed uploading the image. Please try again.
22 |
23 |
24 |
29 | Upload File
30 |
31 |
32 |
33 |
38 |
45 |
46 |
47 |
48 |
49 | Upload
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
125 |
126 |
142 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 | # .nfs files are created when an open file is removed but is still being accessed
17 | .nfs*
18 |
19 | ### OSX ###
20 | *.DS_Store
21 | .AppleDouble
22 | .LSOverride
23 |
24 | # Icon must end with two \r
25 | Icon
26 |
27 | # Thumbnails
28 | ._*
29 |
30 | # Files that might appear in the root of a volume
31 | .DocumentRevisions-V100
32 | .fseventsd
33 | .Spotlight-V100
34 | .TemporaryItems
35 | .Trashes
36 | .VolumeIcon.icns
37 | .com.apple.timemachine.donotpresent
38 |
39 | # Directories potentially created on remote AFP share
40 | .AppleDB
41 | .AppleDesktop
42 | Network Trash Folder
43 | Temporary Items
44 | .apdisk
45 |
46 | ### PyCharm ###
47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
49 |
50 | # User-specific stuff:
51 | .idea/**/workspace.xml
52 | .idea/**/tasks.xml
53 | .idea/dictionaries
54 |
55 | # Sensitive or high-churn files:
56 | .idea/**/dataSources/
57 | .idea/**/dataSources.ids
58 | .idea/**/dataSources.xml
59 | .idea/**/dataSources.local.xml
60 | .idea/**/sqlDataSources.xml
61 | .idea/**/dynamic.xml
62 | .idea/**/uiDesigner.xml
63 |
64 | # Gradle:
65 | .idea/**/gradle.xml
66 | .idea/**/libraries
67 |
68 | # CMake
69 | cmake-build-debug/
70 |
71 | # Mongo Explorer plugin:
72 | .idea/**/mongoSettings.xml
73 |
74 | ## File-based project format:
75 | *.iws
76 |
77 | ## Plugin-specific files:
78 |
79 | # IntelliJ
80 | /out/
81 |
82 | # mpeltonen/sbt-idea plugin
83 | .idea_modules/
84 |
85 | # JIRA plugin
86 | atlassian-ide-plugin.xml
87 |
88 | # Cursive Clojure plugin
89 | .idea/replstate.xml
90 |
91 | # Ruby plugin and RubyMine
92 | /.rakeTasks
93 |
94 | # Crashlytics plugin (for Android Studio and IntelliJ)
95 | com_crashlytics_export_strings.xml
96 | crashlytics.properties
97 | crashlytics-build.properties
98 | fabric.properties
99 |
100 | ### PyCharm Patch ###
101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
102 |
103 | # *.iml
104 | # modules.xml
105 | # .idea/misc.xml
106 | # *.ipr
107 |
108 | # Sonarlint plugin
109 | .idea/sonarlint
110 |
111 | ### Python ###
112 | # Byte-compiled / optimized / DLL files
113 | __pycache__/
114 | *.py[cod]
115 | *$py.class
116 |
117 | # C extensions
118 | *.so
119 |
120 | # Distribution / packaging
121 | .Python
122 | build/
123 | develop-eggs/
124 | dist/
125 | downloads/
126 | eggs/
127 | .eggs/
128 | lib/
129 | lib64/
130 | parts/
131 | sdist/
132 | var/
133 | wheels/
134 | *.egg-info/
135 | .installed.cfg
136 | *.egg
137 |
138 | # PyInstaller
139 | # Usually these files are written by a python script from a template
140 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
141 | *.manifest
142 | *.spec
143 |
144 | # Installer logs
145 | pip-log.txt
146 | pip-delete-this-directory.txt
147 |
148 | # Unit test / coverage reports
149 | htmlcov/
150 | .tox/
151 | .coverage
152 | .coverage.*
153 | .cache
154 | .pytest_cache/
155 | nosetests.xml
156 | coverage.xml
157 | *.cover
158 | .hypothesis/
159 |
160 | # Translations
161 | *.mo
162 | *.pot
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | target/
176 |
177 | # Jupyter Notebook
178 | .ipynb_checkpoints
179 |
180 | # pyenv
181 | .python-version
182 |
183 | # celery beat schedule file
184 | celerybeat-schedule.*
185 |
186 | # SageMath parsed files
187 | *.sage.py
188 |
189 | # Environments
190 | .env
191 | .venv
192 | env/
193 | venv/
194 | ENV/
195 | env.bak/
196 | venv.bak/
197 |
198 | # Spyder project settings
199 | .spyderproject
200 | .spyproject
201 |
202 | # Rope project settings
203 | .ropeproject
204 |
205 | # mkdocs documentation
206 | /site
207 |
208 | # mypy
209 | .mypy_cache/
210 |
211 | ### VisualStudioCode ###
212 | .vscode/*
213 | !.vscode/settings.json
214 | !.vscode/tasks.json
215 | !.vscode/launch.json
216 | !.vscode/extensions.json
217 | .history
218 |
219 | ### Windows ###
220 | # Windows thumbnail cache files
221 | Thumbs.db
222 | ehthumbs.db
223 | ehthumbs_vista.db
224 |
225 | # Folder config file
226 | Desktop.ini
227 |
228 | # Recycle Bin used on file shares
229 | $RECYCLE.BIN/
230 |
231 | # Windows Installer files
232 | *.cab
233 | *.msi
234 | *.msm
235 | *.msp
236 |
237 | # Windows shortcuts
238 | *.lnk
239 |
240 | # Build folder
241 |
242 | */build/*
243 |
244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
245 |
246 | #SAM build files
247 | .aws-sam/
248 | samconfig.toml
--------------------------------------------------------------------------------
/backend/template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Transform: AWS::Serverless-2016-10-31
3 | Description: >
4 | presigned-with-sam
5 |
6 | Sample SAM Template for presigned-with-sam
7 |
8 | Parameters:
9 | SnsNotificationEmail:
10 | Type: String
11 | PresignedUrlExpirySeconds:
12 | Type: Number
13 | Default: 300
14 | Stage:
15 | Type: String
16 | Default: 'dev'
17 |
18 | Globals:
19 | Function:
20 | Timeout: 3
21 |
22 | Resources:
23 | S3Bucket:
24 | Type: AWS::S3::Bucket
25 | Properties:
26 | CorsConfiguration:
27 | CorsRules:
28 | - AllowedMethods:
29 | - "PUT"
30 | - "GET"
31 | AllowedHeaders:
32 | - "X-Forwarded-For"
33 | AllowedOrigins:
34 | - !GetAtt S3WebSiteBucket.WebsiteURL
35 | MaxAge: 60
36 |
37 | S3WebSiteBucket:
38 | Type: AWS::S3::Bucket
39 | Properties:
40 | AccessControl: PublicRead
41 | WebsiteConfiguration:
42 | IndexDocument: index.html
43 | ErrorDocument: 404.html
44 |
45 | S3WebSiteBucketPolicy:
46 | Type: AWS::S3::BucketPolicy
47 | Properties:
48 | PolicyDocument:
49 | Id: S3WebSiteBucketPolicy
50 | Version: 2012-10-17
51 | Statement:
52 | - Sid: PublicReadForGetBucketObjects
53 | Effect: Allow
54 | Principal: '*'
55 | Action: 's3:GetObject'
56 | Resource: !Join
57 | - ''
58 | - - 'arn:aws:s3:::'
59 | - !Ref S3WebSiteBucket
60 | - /*
61 | Bucket: !Ref S3WebSiteBucket
62 |
63 | ApiGatewayApi:
64 | Type: AWS::Serverless::Api
65 | Properties:
66 | StageName:
67 | Ref: Stage
68 | Cors:
69 | AllowMethods: "'OPTIONS,POST,GET'"
70 | AllowHeaders: "'Content-Type'"
71 | AllowOrigin: "'*'"
72 |
73 | GenerateUrlFunction:
74 | Type: AWS::Serverless::Function
75 | Properties:
76 | CodeUri: src/generate_presigned_url/
77 | Handler: app.index
78 | Runtime: python3.8
79 | Environment:
80 | Variables:
81 | S3BUCKET:
82 | Ref: S3Bucket
83 | EXPIRY_TIME:
84 | Ref: PresignedUrlExpirySeconds
85 | Policies:
86 | - S3WritePolicy:
87 | BucketName:
88 | Ref: S3Bucket
89 | Events:
90 | GenerateUrlApi:
91 | Type: Api
92 | Properties:
93 | Path: /generate
94 | Method: get
95 | RestApiId:
96 | Ref: ApiGatewayApi
97 |
98 | FileUploadFunction:
99 | Type: AWS::Serverless::Function
100 | Properties:
101 | CodeUri: src/file_uploaded/
102 | Handler: app.index
103 | Runtime: python3.8
104 | Events:
105 | S3FileUpload:
106 | Type: S3
107 | Properties:
108 | Bucket:
109 | Ref: S3Bucket
110 | Events: s3:ObjectCreated:*
111 | EventInvokeConfig:
112 | DestinationConfig:
113 | OnSuccess:
114 | Type: SNS
115 | OnFailure:
116 | Type: SNS
117 | MaximumEventAgeInSeconds: 3600
118 | MaximumRetryAttempts: 0
119 |
120 | FileUploadNotificationSuccess:
121 | Type: AWS::SNS::Subscription
122 | Properties:
123 | TopicArn:
124 | Ref: FileUploadFunctionEventInvokeConfigOnSuccessTopic
125 | Protocol: email
126 | Endpoint:
127 | Ref: SnsNotificationEmail
128 |
129 | FileUploadNotificationFailure:
130 | Type: AWS::SNS::Subscription
131 | Properties:
132 | TopicArn:
133 | Ref: FileUploadFunctionEventInvokeConfigOnFailureTopic
134 | Protocol: email
135 | Endpoint:
136 | Ref: SnsNotificationEmail
137 |
138 | Outputs:
139 | PresignedUrlApi:
140 | Description: "API Gateway endpoint for generate presigned url"
141 | Value: !Sub "https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/generate"
142 | GenerateUrlFunction:
143 | Description: "GenerateUrlFunction Function ARN"
144 | Value: !GetAtt GenerateUrlFunction.Arn
145 | GenerateUrlFunctionIamRole:
146 | Description: "Implicit IAM Role created for GenerateUrlFunction function"
147 | Value: !GetAtt GenerateUrlFunctionRole.Arn
148 | FileUploadedFunction:
149 | Description: "FileUploadFunction Function ARN"
150 | Value: !GetAtt FileUploadFunction.Arn
151 | FileUploadFunctionIamRole:
152 | Description: "Implicit IAM Role created for FileUploadFunction function"
153 | Value: !GetAtt FileUploadFunctionRole.Arn
154 | FileUploadOnSuccessSNS:
155 | Description: "File upload success sns"
156 | Value: !Ref FileUploadFunctionEventInvokeConfigOnSuccessTopic
157 | FileUploadOnFailSNS:
158 | Description: "File upload failed sns"
159 | Value: !Ref FileUploadFunctionEventInvokeConfigOnFailureTopic
160 | S3FileUploadBucket:
161 | Description: "S3 Bucket to upload files"
162 | Value: !GetAtt S3Bucket.Arn
163 | S3WebsiteBucket:
164 | Value: !Ref S3WebSiteBucket
165 | Description: Website hosted bucket name
166 | S3WebsiteURL:
167 | Value: !GetAtt S3WebSiteBucket.WebsiteURL
168 | Description: URL for website hosted on S3
169 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # backend
2 |
3 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders.
4 |
5 | - hello_world - Code for the application's Lambda function.
6 | - events - Invocation events that you can use to invoke the function.
7 | - tests - Unit tests for the application code.
8 | - template.yaml - A template that defines the application's AWS resources.
9 |
10 | The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code.
11 |
12 | If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit.
13 | The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started.
14 |
15 | * [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
16 | * [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
17 | * [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html)
18 | * [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html)
19 |
20 | ## Deploy the sample application
21 |
22 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API.
23 |
24 | To use the SAM CLI, you need the following tools.
25 |
26 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
27 | * [Python 3 installed](https://www.python.org/downloads/)
28 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community)
29 |
30 | To build and deploy your application for the first time, run the following in your shell:
31 |
32 | ```bash
33 | sam build --use-container
34 | sam deploy --guided
35 | ```
36 |
37 | The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts:
38 |
39 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name.
40 | * **AWS Region**: The AWS region you want to deploy your app to.
41 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes.
42 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modified IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command.
43 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application.
44 |
45 | You can find your API Gateway Endpoint URL in the output values displayed after deployment.
46 |
47 | ## Use the SAM CLI to build and test locally
48 |
49 | Build your application with the `sam build --use-container` command.
50 |
51 | ```bash
52 | backend$ sam build --use-container
53 | ```
54 |
55 | The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder.
56 |
57 | Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project.
58 |
59 | Run functions locally and invoke them with the `sam local invoke` command.
60 |
61 | ```bash
62 | backend$ sam local invoke HelloWorldFunction --event events/event.json
63 | ```
64 |
65 | The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000.
66 |
67 | ```bash
68 | backend$ sam local start-api
69 | backend$ curl http://localhost:3000/
70 | ```
71 |
72 | The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path.
73 |
74 | ```yaml
75 | Events:
76 | HelloWorld:
77 | Type: Api
78 | Properties:
79 | Path: /hello
80 | Method: get
81 | ```
82 |
83 | ## Add a resource to your application
84 | The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types.
85 |
86 | ## Fetch, tail, and filter Lambda function logs
87 |
88 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug.
89 |
90 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM.
91 |
92 | ```bash
93 | backend$ sam logs -n HelloWorldFunction --stack-name backend --tail
94 | ```
95 |
96 | You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html).
97 |
98 | ## Unit tests
99 |
100 | Tests are defined in the `tests` folder in this project. Use PIP to install the [pytest](https://docs.pytest.org/en/latest/) and run unit tests.
101 |
102 | ```bash
103 | backend$ pip install pytest pytest-mock --user
104 | backend$ python -m pytest tests/ -v
105 | ```
106 |
107 | ## Cleanup
108 |
109 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following:
110 |
111 | ```bash
112 | aws cloudformation delete-stack --stack-name backend
113 | ```
114 |
115 | ## Resources
116 |
117 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts.
118 |
119 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/)
120 |
--------------------------------------------------------------------------------