├── .gitignore
├── README.md
├── backend
├── Dockerfile
└── app.py
├── diagram.png
├── docker-compose.yaml
├── drawio_diagram.xml
├── frontend
├── Dockerfile
├── package-lock.json
├── package.json
└── server.js
├── nginx
├── Dockerfile
└── default.conf
└── stack.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | backend/Pipfile
2 | backend/Pipfile.lock
3 | frontend/node_modules
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ECS Full Stack
2 |
3 | Independently Scalable Multi-Container Microservices Architecture on AWS Fargate
4 |
5 | ## Overview
6 |
7 | 
8 |
9 | Information and walkthrough coming soon.
10 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:alpine
2 |
3 | RUN pip install Flask
4 |
5 | WORKDIR /app
6 |
7 | COPY app.py ./
8 |
9 | ENV FLASK_APP app.py
10 |
11 | CMD flask run --host=0.0.0.0
12 |
--------------------------------------------------------------------------------
/backend/app.py:
--------------------------------------------------------------------------------
1 | import time
2 | import random
3 |
4 | from flask import Flask
5 |
6 | app = Flask(__name__)
7 |
8 | @app.route('/')
9 | def greet():
10 | interval = time.time() + 1
11 |
12 | # Simulates some CPU load.
13 | while time.time() < interval:
14 | x = 435344
15 | x * x
16 | x = x + random.randint(-12314, 10010)
17 |
18 | return 'Hello from the backend. Backend computed %d' % x
19 |
--------------------------------------------------------------------------------
/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eulersson/ecsfs/15ca63af737878fa3f4aa080bb58c61a4ad75296/diagram.png
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | frontend:
4 | restart: always
5 | build: ./frontend
6 | networks:
7 | ecsfs:
8 | aliases:
9 | - ecsfs-frontend.local
10 | backend:
11 | restart: always
12 | build: ./backend
13 | networks:
14 | ecsfs:
15 | aliases:
16 | - ecsfs-backend.local
17 | nginx:
18 | restart: always
19 | build: ./nginx
20 | ports:
21 | - "80:80"
22 | networks:
23 | ecsfs:
24 |
25 | networks:
26 | ecsfs:
27 |
--------------------------------------------------------------------------------
/drawio_diagram.xml:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json package-lock.json ./
6 |
7 | RUN npm install
8 |
9 | COPY server.js ./
10 |
11 | CMD npm start
12 |
--------------------------------------------------------------------------------
/frontend/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "accepts": {
8 | "version": "1.3.5",
9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
11 | "requires": {
12 | "mime-types": "~2.1.18",
13 | "negotiator": "0.6.1"
14 | }
15 | },
16 | "ajv": {
17 | "version": "6.6.2",
18 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
19 | "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
20 | "requires": {
21 | "fast-deep-equal": "^2.0.1",
22 | "fast-json-stable-stringify": "^2.0.0",
23 | "json-schema-traverse": "^0.4.1",
24 | "uri-js": "^4.2.2"
25 | }
26 | },
27 | "array-flatten": {
28 | "version": "1.1.1",
29 | "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
30 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
31 | },
32 | "asn1": {
33 | "version": "0.2.4",
34 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
35 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
36 | "requires": {
37 | "safer-buffer": "~2.1.0"
38 | }
39 | },
40 | "assert-plus": {
41 | "version": "1.0.0",
42 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
43 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
44 | },
45 | "asynckit": {
46 | "version": "0.4.0",
47 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
48 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
49 | },
50 | "aws-sign2": {
51 | "version": "0.7.0",
52 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
53 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
54 | },
55 | "aws4": {
56 | "version": "1.8.0",
57 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
58 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
59 | },
60 | "bcrypt-pbkdf": {
61 | "version": "1.0.2",
62 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
63 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
64 | "requires": {
65 | "tweetnacl": "^0.14.3"
66 | }
67 | },
68 | "body-parser": {
69 | "version": "1.18.3",
70 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
71 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
72 | "requires": {
73 | "bytes": "3.0.0",
74 | "content-type": "~1.0.4",
75 | "debug": "2.6.9",
76 | "depd": "~1.1.2",
77 | "http-errors": "~1.6.3",
78 | "iconv-lite": "0.4.23",
79 | "on-finished": "~2.3.0",
80 | "qs": "6.5.2",
81 | "raw-body": "2.3.3",
82 | "type-is": "~1.6.16"
83 | }
84 | },
85 | "bytes": {
86 | "version": "3.0.0",
87 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
88 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
89 | },
90 | "caseless": {
91 | "version": "0.12.0",
92 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
93 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
94 | },
95 | "combined-stream": {
96 | "version": "1.0.7",
97 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
98 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
99 | "requires": {
100 | "delayed-stream": "~1.0.0"
101 | }
102 | },
103 | "content-disposition": {
104 | "version": "0.5.2",
105 | "resolved": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
106 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
107 | },
108 | "content-type": {
109 | "version": "1.0.4",
110 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
111 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
112 | },
113 | "cookie": {
114 | "version": "0.3.1",
115 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
116 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
117 | },
118 | "cookie-signature": {
119 | "version": "1.0.6",
120 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
121 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
122 | },
123 | "core-util-is": {
124 | "version": "1.0.2",
125 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
126 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
127 | },
128 | "dashdash": {
129 | "version": "1.14.1",
130 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
131 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
132 | "requires": {
133 | "assert-plus": "^1.0.0"
134 | }
135 | },
136 | "debug": {
137 | "version": "2.6.9",
138 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
139 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
140 | "requires": {
141 | "ms": "2.0.0"
142 | }
143 | },
144 | "delayed-stream": {
145 | "version": "1.0.0",
146 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
147 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
148 | },
149 | "depd": {
150 | "version": "1.1.2",
151 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
152 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
153 | },
154 | "destroy": {
155 | "version": "1.0.4",
156 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
157 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
158 | },
159 | "ecc-jsbn": {
160 | "version": "0.1.2",
161 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
162 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
163 | "requires": {
164 | "jsbn": "~0.1.0",
165 | "safer-buffer": "^2.1.0"
166 | }
167 | },
168 | "ee-first": {
169 | "version": "1.1.1",
170 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
171 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
172 | },
173 | "encodeurl": {
174 | "version": "1.0.2",
175 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
176 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
177 | },
178 | "escape-html": {
179 | "version": "1.0.3",
180 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
181 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
182 | },
183 | "etag": {
184 | "version": "1.8.1",
185 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
186 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
187 | },
188 | "express": {
189 | "version": "4.16.4",
190 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
191 | "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
192 | "requires": {
193 | "accepts": "~1.3.5",
194 | "array-flatten": "1.1.1",
195 | "body-parser": "1.18.3",
196 | "content-disposition": "0.5.2",
197 | "content-type": "~1.0.4",
198 | "cookie": "0.3.1",
199 | "cookie-signature": "1.0.6",
200 | "debug": "2.6.9",
201 | "depd": "~1.1.2",
202 | "encodeurl": "~1.0.2",
203 | "escape-html": "~1.0.3",
204 | "etag": "~1.8.1",
205 | "finalhandler": "1.1.1",
206 | "fresh": "0.5.2",
207 | "merge-descriptors": "1.0.1",
208 | "methods": "~1.1.2",
209 | "on-finished": "~2.3.0",
210 | "parseurl": "~1.3.2",
211 | "path-to-regexp": "0.1.7",
212 | "proxy-addr": "~2.0.4",
213 | "qs": "6.5.2",
214 | "range-parser": "~1.2.0",
215 | "safe-buffer": "5.1.2",
216 | "send": "0.16.2",
217 | "serve-static": "1.13.2",
218 | "setprototypeof": "1.1.0",
219 | "statuses": "~1.4.0",
220 | "type-is": "~1.6.16",
221 | "utils-merge": "1.0.1",
222 | "vary": "~1.1.2"
223 | }
224 | },
225 | "extend": {
226 | "version": "3.0.2",
227 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
228 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
229 | },
230 | "extsprintf": {
231 | "version": "1.3.0",
232 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
233 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
234 | },
235 | "fast-deep-equal": {
236 | "version": "2.0.1",
237 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
238 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
239 | },
240 | "fast-json-stable-stringify": {
241 | "version": "2.0.0",
242 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
243 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
244 | },
245 | "finalhandler": {
246 | "version": "1.1.1",
247 | "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
248 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
249 | "requires": {
250 | "debug": "2.6.9",
251 | "encodeurl": "~1.0.2",
252 | "escape-html": "~1.0.3",
253 | "on-finished": "~2.3.0",
254 | "parseurl": "~1.3.2",
255 | "statuses": "~1.4.0",
256 | "unpipe": "~1.0.0"
257 | }
258 | },
259 | "forever-agent": {
260 | "version": "0.6.1",
261 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
262 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
263 | },
264 | "form-data": {
265 | "version": "2.3.3",
266 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
267 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
268 | "requires": {
269 | "asynckit": "^0.4.0",
270 | "combined-stream": "^1.0.6",
271 | "mime-types": "^2.1.12"
272 | }
273 | },
274 | "forwarded": {
275 | "version": "0.1.2",
276 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
277 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
278 | },
279 | "fresh": {
280 | "version": "0.5.2",
281 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
282 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
283 | },
284 | "getpass": {
285 | "version": "0.1.7",
286 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
287 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
288 | "requires": {
289 | "assert-plus": "^1.0.0"
290 | }
291 | },
292 | "har-schema": {
293 | "version": "2.0.0",
294 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
295 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
296 | },
297 | "har-validator": {
298 | "version": "5.1.3",
299 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
300 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
301 | "requires": {
302 | "ajv": "^6.5.5",
303 | "har-schema": "^2.0.0"
304 | }
305 | },
306 | "http-errors": {
307 | "version": "1.6.3",
308 | "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
309 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
310 | "requires": {
311 | "depd": "~1.1.2",
312 | "inherits": "2.0.3",
313 | "setprototypeof": "1.1.0",
314 | "statuses": ">= 1.4.0 < 2"
315 | }
316 | },
317 | "http-signature": {
318 | "version": "1.2.0",
319 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
320 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
321 | "requires": {
322 | "assert-plus": "^1.0.0",
323 | "jsprim": "^1.2.2",
324 | "sshpk": "^1.7.0"
325 | }
326 | },
327 | "iconv-lite": {
328 | "version": "0.4.23",
329 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
330 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
331 | "requires": {
332 | "safer-buffer": ">= 2.1.2 < 3"
333 | }
334 | },
335 | "inherits": {
336 | "version": "2.0.3",
337 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
338 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
339 | },
340 | "ipaddr.js": {
341 | "version": "1.8.0",
342 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
343 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
344 | },
345 | "is-typedarray": {
346 | "version": "1.0.0",
347 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
348 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
349 | },
350 | "isstream": {
351 | "version": "0.1.2",
352 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
353 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
354 | },
355 | "jsbn": {
356 | "version": "0.1.1",
357 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
358 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
359 | },
360 | "json-schema": {
361 | "version": "0.2.3",
362 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
363 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
364 | },
365 | "json-schema-traverse": {
366 | "version": "0.4.1",
367 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
368 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
369 | },
370 | "json-stringify-safe": {
371 | "version": "5.0.1",
372 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
373 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
374 | },
375 | "jsprim": {
376 | "version": "1.4.1",
377 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
378 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
379 | "requires": {
380 | "assert-plus": "1.0.0",
381 | "extsprintf": "1.3.0",
382 | "json-schema": "0.2.3",
383 | "verror": "1.10.0"
384 | }
385 | },
386 | "media-typer": {
387 | "version": "0.3.0",
388 | "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
389 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
390 | },
391 | "merge-descriptors": {
392 | "version": "1.0.1",
393 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
394 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
395 | },
396 | "methods": {
397 | "version": "1.1.2",
398 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
399 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
400 | },
401 | "mime": {
402 | "version": "1.4.1",
403 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
404 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
405 | },
406 | "mime-db": {
407 | "version": "1.37.0",
408 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
409 | "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
410 | },
411 | "mime-types": {
412 | "version": "2.1.21",
413 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
414 | "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
415 | "requires": {
416 | "mime-db": "~1.37.0"
417 | }
418 | },
419 | "ms": {
420 | "version": "2.0.0",
421 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
422 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
423 | },
424 | "negotiator": {
425 | "version": "0.6.1",
426 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
427 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
428 | },
429 | "oauth-sign": {
430 | "version": "0.9.0",
431 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
432 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
433 | },
434 | "on-finished": {
435 | "version": "2.3.0",
436 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
437 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
438 | "requires": {
439 | "ee-first": "1.1.1"
440 | }
441 | },
442 | "parseurl": {
443 | "version": "1.3.2",
444 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
445 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
446 | },
447 | "path-to-regexp": {
448 | "version": "0.1.7",
449 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
450 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
451 | },
452 | "performance-now": {
453 | "version": "2.1.0",
454 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
455 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
456 | },
457 | "proxy-addr": {
458 | "version": "2.0.4",
459 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
460 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
461 | "requires": {
462 | "forwarded": "~0.1.2",
463 | "ipaddr.js": "1.8.0"
464 | }
465 | },
466 | "psl": {
467 | "version": "1.1.31",
468 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
469 | "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
470 | },
471 | "punycode": {
472 | "version": "2.1.1",
473 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
474 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
475 | },
476 | "qs": {
477 | "version": "6.5.2",
478 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
479 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
480 | },
481 | "range-parser": {
482 | "version": "1.2.0",
483 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
484 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
485 | },
486 | "raw-body": {
487 | "version": "2.3.3",
488 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
489 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
490 | "requires": {
491 | "bytes": "3.0.0",
492 | "http-errors": "1.6.3",
493 | "iconv-lite": "0.4.23",
494 | "unpipe": "1.0.0"
495 | }
496 | },
497 | "request": {
498 | "version": "2.88.0",
499 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
500 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
501 | "requires": {
502 | "aws-sign2": "~0.7.0",
503 | "aws4": "^1.8.0",
504 | "caseless": "~0.12.0",
505 | "combined-stream": "~1.0.6",
506 | "extend": "~3.0.2",
507 | "forever-agent": "~0.6.1",
508 | "form-data": "~2.3.2",
509 | "har-validator": "~5.1.0",
510 | "http-signature": "~1.2.0",
511 | "is-typedarray": "~1.0.0",
512 | "isstream": "~0.1.2",
513 | "json-stringify-safe": "~5.0.1",
514 | "mime-types": "~2.1.19",
515 | "oauth-sign": "~0.9.0",
516 | "performance-now": "^2.1.0",
517 | "qs": "~6.5.2",
518 | "safe-buffer": "^5.1.2",
519 | "tough-cookie": "~2.4.3",
520 | "tunnel-agent": "^0.6.0",
521 | "uuid": "^3.3.2"
522 | }
523 | },
524 | "safe-buffer": {
525 | "version": "5.1.2",
526 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
527 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
528 | },
529 | "safer-buffer": {
530 | "version": "2.1.2",
531 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
532 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
533 | },
534 | "send": {
535 | "version": "0.16.2",
536 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
537 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
538 | "requires": {
539 | "debug": "2.6.9",
540 | "depd": "~1.1.2",
541 | "destroy": "~1.0.4",
542 | "encodeurl": "~1.0.2",
543 | "escape-html": "~1.0.3",
544 | "etag": "~1.8.1",
545 | "fresh": "0.5.2",
546 | "http-errors": "~1.6.2",
547 | "mime": "1.4.1",
548 | "ms": "2.0.0",
549 | "on-finished": "~2.3.0",
550 | "range-parser": "~1.2.0",
551 | "statuses": "~1.4.0"
552 | }
553 | },
554 | "serve-static": {
555 | "version": "1.13.2",
556 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
557 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
558 | "requires": {
559 | "encodeurl": "~1.0.2",
560 | "escape-html": "~1.0.3",
561 | "parseurl": "~1.3.2",
562 | "send": "0.16.2"
563 | }
564 | },
565 | "setprototypeof": {
566 | "version": "1.1.0",
567 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
568 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
569 | },
570 | "sshpk": {
571 | "version": "1.16.0",
572 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
573 | "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
574 | "requires": {
575 | "asn1": "~0.2.3",
576 | "assert-plus": "^1.0.0",
577 | "bcrypt-pbkdf": "^1.0.0",
578 | "dashdash": "^1.12.0",
579 | "ecc-jsbn": "~0.1.1",
580 | "getpass": "^0.1.1",
581 | "jsbn": "~0.1.0",
582 | "safer-buffer": "^2.0.2",
583 | "tweetnacl": "~0.14.0"
584 | }
585 | },
586 | "statuses": {
587 | "version": "1.4.0",
588 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
589 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
590 | },
591 | "tough-cookie": {
592 | "version": "2.4.3",
593 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
594 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
595 | "requires": {
596 | "psl": "^1.1.24",
597 | "punycode": "^1.4.1"
598 | },
599 | "dependencies": {
600 | "punycode": {
601 | "version": "1.4.1",
602 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
603 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
604 | }
605 | }
606 | },
607 | "tunnel-agent": {
608 | "version": "0.6.0",
609 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
610 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
611 | "requires": {
612 | "safe-buffer": "^5.0.1"
613 | }
614 | },
615 | "tweetnacl": {
616 | "version": "0.14.5",
617 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
618 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
619 | },
620 | "type-is": {
621 | "version": "1.6.16",
622 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
623 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
624 | "requires": {
625 | "media-typer": "0.3.0",
626 | "mime-types": "~2.1.18"
627 | }
628 | },
629 | "unpipe": {
630 | "version": "1.0.0",
631 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
632 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
633 | },
634 | "uri-js": {
635 | "version": "4.2.2",
636 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
637 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
638 | "requires": {
639 | "punycode": "^2.1.0"
640 | }
641 | },
642 | "utils-merge": {
643 | "version": "1.0.1",
644 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
645 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
646 | },
647 | "uuid": {
648 | "version": "3.3.2",
649 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
650 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
651 | },
652 | "vary": {
653 | "version": "1.1.2",
654 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
655 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
656 | },
657 | "verror": {
658 | "version": "1.10.0",
659 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
660 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
661 | "requires": {
662 | "assert-plus": "^1.0.0",
663 | "core-util-is": "1.0.2",
664 | "extsprintf": "^1.2.0"
665 | }
666 | }
667 | }
668 | }
669 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "description": "Frontend mocked up.",
5 | "private": true,
6 | "scripts": {
7 | "start": "node server.js"
8 | },
9 | "author": "Ramon Blanquer (http://www.ramonblanquer.com)",
10 | "license": "ISC",
11 | "dependencies": {
12 | "express": "^4.16.4",
13 | "request": "^2.88.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const request = require('request')
3 |
4 | const app = express()
5 | const port = 3000
6 |
7 | app.get('/', (req, res) => {
8 |
9 | // The frontend greets with the following:
10 | const message = "Hello from the frontend..."
11 |
12 | // The backend should greet us with "Hello from the backend."
13 | request('http://ecsfs-backend.local:5000', (err, response, body) => {
14 | // We send both greetings together on a GET request to /
15 | res.send(message + " " + body);
16 | })
17 | })
18 |
19 | app.listen(port, () => console.log(`Fontend app listening on port ${port}.`))
20 |
--------------------------------------------------------------------------------
/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:alpine
2 |
3 | COPY default.conf /etc/nginx/conf.d/default.conf
4 |
--------------------------------------------------------------------------------
/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | location / {
4 | proxy_pass http://ecsfs-frontend.local:3000;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/stack.yaml:
--------------------------------------------------------------------------------
1 | # == ecsfs ====================================================================
2 | # Independently Scalable Multi-Container Microservices Architecture on Fargate
3 | # =============================================================================
4 | # This CloudFormation stack shows how to deploy a full-stack application
5 | # consisting of a backend, a frontend and an nginx server. Each defined as an
6 | # independent Fargate service. The backend will auto-scale between 1 and 3.
7 | #
8 | # Two articles have been written explaining what this stack is for:
9 | #
10 | # -
11 | # -
12 | #
13 | # How to deploy this YAML template
14 | # --------------------------------
15 | #
16 | # Either using command line (1) or from the web console (2).
17 | #
18 | # 1. From the command line.
19 | # First you will need to install and configure the AWS CLI. Read the docs:
20 | #
21 | # - https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html
22 | # - https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html
23 | #
24 | # TL;DR Basically you will need to install it with pip:
25 | #
26 | # pip install awscli --upgrade --user
27 | #
28 | # And configure it (specify your default region too so you don't have to type
29 | # it on each subsequent command):
30 | #
31 | # aws configure
32 | #
33 | # To actually deploy the stack you have two choices (a) and (b)...
34 | #
35 | # a) If you have no hosted zones set up (associated with a Route 53 domain):
36 | #
37 | # aws cloudformation create-stack \
38 | # --stack-name ecsfs \
39 | # --template-body file://$(pwd)/stack.yaml \
40 | # --capabilities CAPABILITY_NAMED_IAM \
41 | # --parameters ParameterKey=HostedZoneName,ParameterValue=
42 | #
43 | # b) If you have a hosted zone, you can pass it in an the application will
44 | # be available under the subdomain ecsfs., e.g.
45 | # ecsfs.example.com. Simply pass the parameter flag instead of leaving it
46 | # empty:
47 | #
48 | # --parameters ParameterKey=HostedZoneName,ParameterValue=foo.com.
49 | # (!) the final dot is needed ^
50 | #
51 | # 2. From the CloudFormation section on your AWS web console.
52 | # - Click the "Create Stack" button.
53 | # - Click on "Choose File" and upload this stack.yaml file.
54 | # - Give the Stack a name: "ecsfs".
55 | # - In the parameters section, you will see "HostedZoneName". It is up to you
56 | # if you want to use one of your hosted zones (domains) for instance
57 | # 'foo.com.' so the application would then be configured to run on a
58 | # subdomain of it (ecsfs.foo.com). You can leave it empty.
59 | # - Click "Next".
60 | # - Click "Next" one more time.
61 | # - On the "Capabilities" section check the box "I acknowledge that..."
62 | #
63 | # Deleting all the resources that have been created
64 | # -------------------------------------------------
65 | # Either from the web console or from CLI. To do it from the web console go to
66 | # the CloudFormation section and delete it there. The command line equivalent
67 | # is:
68 | #
69 | # aws cloudformation delete-stack --stack-name ecsfs
70 | #
71 | # PARAMETERS ==================================================================
72 | # Options the user can provide when deploying the stack that can be accessed
73 | # within the resource definitions.
74 | Parameters:
75 | HostedZoneName:
76 | Type: String
77 | Description:
78 | (Optional) If you have a domain available registered with Route 53 you
79 | can type it (e.g. 'foo.com.'; do not miss the final dot!). Then a DNS
80 | record gets created on subdomain ecsfs.foo.com which will route to the
81 | load balancer (the entry point of this application).
82 |
83 | # CONDITIONS ==================================================================
84 | # Allows to define boolean variables that we can use to conditionally build
85 | # some resources. You can set conditions for resource building by adding the
86 | # Conditions yaml property under the resource.
87 | Conditions:
88 | HasHostedZoneName: !Not [ !Equals [ !Ref HostedZoneName, '']]
89 |
90 | Resources:
91 | # VIRTUAL PRIVATE CLOUD (VPC) ===============================================
92 | # A VPC is simply a logically isolated chunk of the AWS Cloud.
93 | #
94 | # Our VPC has two public subnetworks since it's a requirement for an
95 | # Application Load Balancer. The nginx container will use them too.
96 | #
97 | # Then we will isolate backend and frontend to a private subnet so they can't
98 | # be reached directly from the Internet.
99 | #
100 | # You will the word CIDR in various places, it is used for subnet masking.
101 | #
102 | # CIDR blocks describe the Network ID and IP ranges to assign in our
103 | # I subnets. It basically tells what part of the address is reserved for
104 | # IPs and what part is for the network ID.
105 | #
106 | # E.g. 10.0.0.0/24 would mean that the first 3 octets (3 x 8 = 24) are
107 | # going to be exclusively defining the Network ID, which would result in
108 | # all the IPs that are given out would start with 10.0.0.x.
109 | #
110 | # This video explains it very well:
111 | #
112 | # - IPv4 Addressing: Network IDs and Subnet Masks
113 | # https://youtu.be/XQ3T14SIlV4
114 | #
115 | VPC:
116 | Type: AWS::EC2::VPC
117 | Properties:
118 | EnableDnsSupport: true
119 | EnableDnsHostnames: true
120 | CidrBlock: 10.0.0.0/16
121 | Tags: # You can give
122 | - Key: Name # pretty names to
123 | Value: ECSFS VPC # your resources.
124 |
125 | PublicSubnetOne:
126 | Type: AWS::EC2::Subnet
127 | Properties:
128 | # Select the first availability zone on our current region.
129 | AvailabilityZone: !Select # !Select chooses an item from a list.
130 | - 0 # First availability zone, since...
131 | - Fn::GetAZs: !Ref AWS::Region # ...a region has various zones (list).
132 | CidrBlock: 10.0.0.0/24
133 | VpcId: !Ref VPC
134 |
135 | PublicSubnetTwo:
136 | Type: AWS::EC2::Subnet
137 | Properties:
138 | AvailabilityZone: !Select
139 | - 1 # Second availability zone under the same region.
140 | - Fn::GetAZs: !Ref AWS::Region
141 | CidrBlock: 10.0.1.0/24
142 | VpcId: !Ref VPC
143 |
144 | PrivateSubnet:
145 | Type: AWS::EC2::Subnet
146 | Properties:
147 | AvailabilityZone: !Select
148 | - 0
149 | - Fn::GetAZs: !Ref AWS::Region
150 | CidrBlock: 10.0.2.0/24
151 | VpcId: !Ref VPC
152 |
153 | # NETWORK SETUP: ROUTING AND SUBNETTING =====================================
154 | # Let's revisit the main elements that comform a subnet and how we are going
155 | # to use them in our application.
156 | #
157 | # - Internet Gateway:
158 | #
159 | # Allows communication between the containers and the internet. All the
160 | # outbound traffic goes through it. In AWS it must get attached to a VPC.
161 | #
162 | # All requests from a instances runnning on the public subnet must be
163 | # routed to the internet gateway. This is done by defining routes on
164 | # route tables.
165 | #
166 | # - Network Address Translation (NAT) Gateway:
167 | #
168 | # When an application is running on a private subnet it cannot talk to
169 | # the outside world. The NAT Gateway remaps the IP address of the packets
170 | # sent from the private instance assigning them a public IP so when the
171 | # service the instance wants to talk you replies, the NAT can receive the
172 | # information (since the NAT itself is public-facing and rechable from
173 | # the Internet) and hand it back to the private instance.
174 | #
175 | # An Elastic IP needs to be associated with each NAT Gateway we create.
176 | #
177 | # The reason why we traffic private tasks' traffic through a NAT is so
178 | # tasks can pull the images from Docker Hub whilst keeping protection
179 | # since connections cannot be initiated from the Internet, just outbound
180 | # traffic will be allowed through the NAT.
181 | #
182 | # - Routes and Route Tables:
183 | #
184 | # Route tables gather together a set of routes. A route describes where
185 | # do packets need to go based on rules. You can for instance send
186 | # any packets with destination address starting with 10.0.4.x to a NAT
187 | # while others with destination address 10.0.5.x to another NAT or
188 | # internet gateway (I cannot find a proper example, I apologize). You can
189 | # describe both in and outbound routes.
190 | #
191 | # The way we associate a route table with a subnet is by using "Subnet
192 | # Route Table Association" resources, pretty descriptive.
193 | #
194 | # Public routing ------------------------------------------------------------
195 | InternetGateway:
196 | Type: AWS::EC2::InternetGateway
197 |
198 | GatewayAttachment:
199 | Type: AWS::EC2::VPCGatewayAttachment
200 | Properties:
201 | InternetGatewayId: !Ref InternetGateway
202 | VpcId: !Ref VPC
203 |
204 | PublicRouteTable:
205 | Type: AWS::EC2::RouteTable
206 | Properties:
207 | VpcId: !Ref VPC
208 |
209 | PublicRoute:
210 | Type: AWS::EC2::Route
211 | Properties:
212 | RouteTableId: !Ref PublicRouteTable
213 | DestinationCidrBlock: 0.0.0.0/0
214 | GatewayId: !Ref InternetGateway
215 |
216 | PublicSubnetOneRouteTableAssociation:
217 | Type: AWS::EC2::SubnetRouteTableAssociation
218 | Properties:
219 | RouteTableId: !Ref PublicRouteTable
220 | SubnetId: !Ref PublicSubnetOne
221 |
222 | PublicSubnetTwoRouteTableAssociation:
223 | Type: AWS::EC2::SubnetRouteTableAssociation
224 | Properties:
225 | RouteTableId: !Ref PublicRouteTable
226 | SubnetId: !Ref PublicSubnetTwo
227 |
228 | # Private routing -----------------------------------------------------------
229 | NatElasticIP:
230 | Type: AWS::EC2::EIP
231 |
232 | NatGateway:
233 | Type: AWS::EC2::NatGateway
234 | Properties:
235 | AllocationId: !GetAtt NatElasticIP.AllocationId
236 | SubnetId: !Ref PublicSubnetOne
237 |
238 | PrivateRouteTable:
239 | Type: AWS::EC2::RouteTable
240 | Properties:
241 | VpcId: !Ref VPC
242 |
243 | PrivateRoute:
244 | Type: AWS::EC2::Route
245 | Properties:
246 | RouteTableId: !Ref PrivateRouteTable
247 | DestinationCidrBlock: 0.0.0.0/0
248 | NatGatewayId: !Ref NatGateway
249 |
250 | PrivateSubnetRouteTableAssociation:
251 | Type: AWS::EC2::SubnetRouteTableAssociation
252 | Properties:
253 | RouteTableId: !Ref PrivateRouteTable
254 | SubnetId: !Ref PrivateSubnet
255 |
256 | # SECURITY ==================================================================
257 | # A security group shared by all containers running on Fargate. Security
258 | # groups act as firewalls between inbound and outbound communications of the
259 | # instances we run.
260 | #
261 | # The stack has one security group with two ingress (inbound traffic) rules:
262 | #
263 | # 1. To allow traffic coming from the Application Load Balancer.
264 | # (PublicLoadBalancerSecurityGroup)
265 | #
266 | # 2. To allow traffic between running containers.
267 | # (FargateContainerSecurityGroup)
268 | #
269 | FargateContainerSecurityGroup:
270 | Type: AWS::EC2::SecurityGroup
271 | Properties:
272 | GroupDescription: Access to Fargate containers.
273 | VpcId: !Ref VPC
274 |
275 | IngressFromPublicALBSecurityGroup:
276 | Type: AWS::EC2::SecurityGroupIngress
277 | Properties:
278 | Description: Ingress from the public Application Load Balancer.
279 | GroupId: !Ref FargateContainerSecurityGroup
280 | IpProtocol: -1 # Means all protocols (TCD, UDP or any ICMP/ICMPv6 number).
281 | SourceSecurityGroupId: !Ref PublicLoadBalancerSecurityGroup
282 |
283 | IngressFromSelfSecurityGroup:
284 | Type: AWS::EC2::SecurityGroupIngress
285 | Properties:
286 | Description: Ingress from other containers in the same security group.
287 | GroupId: !Ref FargateContainerSecurityGroup
288 | IpProtocol: -1
289 | SourceSecurityGroupId: !Ref FargateContainerSecurityGroup
290 |
291 | PublicLoadBalancerSecurityGroup:
292 | Type: AWS::EC2::SecurityGroup
293 | Properties:
294 | GroupDescription: Access to the public facing load balancer.
295 | VpcId: !Ref VPC
296 | SecurityGroupIngress:
297 | - CidrIp: 0.0.0.0/0 # Allows all IPs. Traffic from anywhere.
298 | IpProtocol: -1
299 |
300 | # LOAD BALANCER =============================================================
301 | # The Application Load Balancer (ALB) is the single point of contact for
302 | # clients (users). Its duty is to relay the request to the right running task
303 | # (think of a task as an instance for now).
304 | #
305 | # In our case all requests on port 80 are forwarded to nginx task.
306 | #
307 | # To configure a load balancer we need to specify a listener and a target
308 | # group. The listener is described through rules, where you can specify
309 | # different targets to route to based on port or URL. The target group is the
310 | # set of resources that would receive the routed requests from the ALB.
311 | #
312 | # This target group will be managed by Fargate and every time a new instance
313 | # of nginx spins up then it will register it automatically on this group, so
314 | # we don't have to worry about adding instances to the target group at all.
315 | #
316 | # Read more:
317 | #
318 | # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html
319 | #
320 | TargetGroup:
321 | Type: AWS::ElasticLoadBalancingV2::TargetGroup
322 | Properties:
323 | Name: ecsfs-target-group
324 | Port: 80
325 | Protocol: HTTP
326 | TargetType: ip
327 | VpcId: !Ref VPC
328 |
329 | ListenerHTTP:
330 | Type: AWS::ElasticLoadBalancingV2::Listener
331 | Properties:
332 | DefaultActions:
333 | - TargetGroupArn: !Ref TargetGroup
334 | Type: forward
335 | LoadBalancerArn: !Ref LoadBalancer
336 | Port: 80
337 | Protocol: HTTP
338 |
339 | LoadBalancer:
340 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer
341 | Properties:
342 | Scheme: internet-facing
343 | SecurityGroups:
344 | - !Ref PublicLoadBalancerSecurityGroup
345 | Subnets:
346 | - !Ref PublicSubnetOne
347 | - !Ref PublicSubnetTwo
348 |
349 | # If a hosted zone got specified when running this stack, we create a
350 | # subdomain on that zone and route it to the load balancer. For instance, say
351 | # 'example.com.' is specified as HostedZoneName, then all the traffic going to
352 | # ecsfs.example.com would go to the load balancer.
353 | DNSRecord:
354 | Type: AWS::Route53::RecordSet
355 | Condition: HasHostedZoneName
356 | Properties:
357 | HostedZoneName: !Ref HostedZoneName
358 | Name: !Join ['.', [ecsfs, !Ref HostedZoneName]]
359 | Type: A
360 | AliasTarget:
361 | DNSName: !GetAtt LoadBalancer.DNSName
362 | HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID
363 |
364 | # ELASTIC CONTAINER SERVICE =================================================
365 | # ECS is a container management system. It basically removes the headache of
366 | # having to setup and provision another management infrastructure such as
367 | # Kubernetes or similar.
368 | #
369 | # You define your application in ECS through **task definitions**, they act
370 | # as blueprints which describe what containers to use, ports to open, what
371 | # launch type to use (EC2 instances or Fargate), and what memory and CPU
372 | # requirements need to be met.
373 | #
374 | # Then a service is in charge of taking those tasks definitions to generate
375 | # and manage running processes from them in a **cluster**. Those running
376 | # processes instanciated by the service are called **tasks**.
377 | #
378 | # Key ideas:
379 | # * A cluster is a grouping of resources: services, task definitions, etc...
380 | # * On a _task definition_...
381 | # - You can describe one or more containers.
382 | # - Desired CPU and memory needed to run that process.
383 | # * A service takes a _task definition_ and instanciates it into running _tasks_.
384 | # * _Task definitions_ and _services_ are configured per-cluster.
385 | # * _Tasks_ run in a cluster.
386 | # * Auto-scaling is configured on the service-level.
387 | #
388 | # Learn more:
389 | #
390 | # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html
391 | #
392 | # Cluster -------------------------------------------------------------------
393 | ECSCluster:
394 | Type: AWS::ECS::Cluster
395 | Properties:
396 | ClusterName: ecsfs-cluster
397 |
398 | # Logging -------------------------------------------------------------------
399 | # Throws all logs from tasks within our cluster under the same group. There
400 | # is one log stream per task running. An aggregated result can be viewed from
401 | # the web console under the page for the service the task is part of.
402 | LogGroup:
403 | Type: AWS::Logs::LogGroup
404 | Properties:
405 | LogGroupName: ecsfs-logs
406 |
407 | # IAM Roles -----------------------------------------------------------------
408 | # We need to allow Fargate to perform specific actions on our behalf.
409 | #
410 | # - ECS Task Execution Role: This role enables AWS Fargate to pull container
411 | # images from Amazon ECR and to forward logs to Amazon CloudWatch Logs.
412 | #
413 | # - ECS Auto Scaling Role: Role needed to perform the scaling operations on
414 | # our behalf, that is, to change the desired count state on the services.
415 | #
416 | # Read more:
417 | #
418 | # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
419 | # https://serverfault.com/questions/854413/confused-by-the-role-requirement-of-ecs
420 | #
421 | ExecutionRole:
422 | Type: AWS::IAM::Role
423 | Properties:
424 | RoleName: ecsfs-execution-role
425 | AssumeRolePolicyDocument:
426 | Statement:
427 | - Effect: Allow
428 | Principal:
429 | Service: ecs-tasks.amazonaws.com
430 | Action: sts:AssumeRole
431 | ManagedPolicyArns:
432 | - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
433 |
434 | AutoScalingRole:
435 | Type: AWS::IAM::Role
436 | Properties:
437 | RoleName: backend-auto-scaling-role
438 | AssumeRolePolicyDocument:
439 | Statement:
440 | - Effect: Allow
441 | Principal:
442 | Service: ecs-tasks.amazonaws.com
443 | Action: sts:AssumeRole
444 |
445 | # Task Definitions ----------------------------------------------------------
446 | BackendTaskDefinition:
447 | Type: AWS::ECS::TaskDefinition
448 | DependsOn: LogGroup
449 | Properties:
450 | Family: ecsfs-backend-td
451 | Cpu: 256
452 | Memory: 1024
453 | NetworkMode: awsvpc
454 | RequiresCompatibilities:
455 | - FARGATE
456 | ExecutionRoleArn: !Ref ExecutionRole
457 | ContainerDefinitions:
458 | - Name: ecsfs-backend-container
459 | Image: docwhite/ecsfs-backend
460 | PortMappings:
461 | - ContainerPort: 5000
462 | LogConfiguration:
463 | LogDriver: awslogs
464 | Options:
465 | awslogs-group: ecsfs-logs
466 | awslogs-region: !Ref AWS::Region
467 | awslogs-stream-prefix: backend
468 |
469 | FrontendTaskDefinition:
470 | Type: AWS::ECS::TaskDefinition
471 | DependsOn: LogGroup
472 | Properties:
473 | Family: ecsfs-frontend-td
474 | Cpu: 256
475 | Memory: 512
476 | NetworkMode: awsvpc
477 | RequiresCompatibilities:
478 | - FARGATE
479 | ExecutionRoleArn: !Ref ExecutionRole
480 | ContainerDefinitions:
481 | - Name: ecsfs-frontend-container
482 | Image: docwhite/ecsfs-frontend
483 | PortMappings:
484 | - ContainerPort: 3000
485 | LogConfiguration:
486 | LogDriver: awslogs
487 | Options:
488 | awslogs-group: ecsfs-logs
489 | awslogs-region: !Ref AWS::Region
490 | awslogs-stream-prefix: frontend
491 |
492 | NginxTaskDefinition:
493 | Type: AWS::ECS::TaskDefinition
494 | DependsOn: LogGroup
495 | Properties:
496 | Family: ecsfs-nginx-td
497 | Cpu: 256
498 | Memory: 512
499 | NetworkMode: awsvpc
500 | RequiresCompatibilities:
501 | - FARGATE
502 | ExecutionRoleArn: !Ref ExecutionRole
503 | ContainerDefinitions:
504 | - Name: ecsfs-nginx-container
505 | Image: docwhite/ecsfs-nginx
506 | PortMappings:
507 | - ContainerPort: 80
508 | LogConfiguration:
509 | LogDriver: awslogs
510 | Options:
511 | awslogs-group: ecsfs-logs
512 | awslogs-region: !Ref AWS::Region
513 | awslogs-stream-prefix: nginx
514 |
515 | # Service Discovery ---------------------------------------------------------
516 | # In our application, we want the backend to be reachable at
517 | # ecsfs-backend.local, the frontend at ecsfs-backend.local, etc... You can
518 | # see the names are suffixed with .local. In AWS we can create a
519 | # PrivateDnsService resource and add services to them, and that would produce
520 | # the aforementioned names, that is, ..
521 | #
522 | # By creating various DNS names under the same namespace, services that get
523 | # assigned those names can talk between them, i.e. the frontend talking to
524 | # a backend, or nginx talking to the frontend.
525 | #
526 | # The IPs for each service task are dynamic, they change, and sometimes more
527 | # than task might be running for the same service... so... how do we associate
528 | # the DNS name with the right task? Well we don't! Fargate does it all for us.
529 | #
530 | # There is a whole section on the documentation explaining it in detail:
531 | #
532 | # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-discovery.html
533 | #
534 | LocalNamespace:
535 | Type: AWS::ServiceDiscovery::PrivateDnsNamespace
536 | Properties:
537 | Vpc: !Ref VPC
538 | Name: local
539 |
540 | NginxLocalDiscoveryService:
541 | Type: AWS::ServiceDiscovery::Service
542 | Properties:
543 | Name: ecsfs-nginx
544 | HealthCheckCustomConfig:
545 | FailureThreshold: 1
546 | DnsConfig:
547 | DnsRecords:
548 | - Type: A
549 | TTL: 60
550 | NamespaceId: !GetAtt LocalNamespace.Id
551 |
552 | BackendLocalDiscoveryService:
553 | Type: AWS::ServiceDiscovery::Service
554 | Properties:
555 | Name: ecsfs-backend
556 | HealthCheckCustomConfig:
557 | FailureThreshold: 1
558 | DnsConfig:
559 | DnsRecords:
560 | - Type: A
561 | TTL: 60
562 | NamespaceId: !GetAtt LocalNamespace.Id
563 |
564 | FrontendLocalDiscoveryService:
565 | Type: AWS::ServiceDiscovery::Service
566 | Properties:
567 | Name: ecsfs-frontend
568 | HealthCheckCustomConfig:
569 | FailureThreshold: 1
570 | DnsConfig:
571 | DnsRecords:
572 | - Type: A
573 | TTL: 60
574 | NamespaceId: !GetAtt LocalNamespace.Id
575 |
576 | # Services ------------------------------------------------------------------
577 | BackendService:
578 | Type: AWS::ECS::Service
579 | Properties:
580 | ServiceName: ecsfs-backend-service
581 | Cluster: !Ref ECSCluster
582 | LaunchType: FARGATE
583 | DesiredCount: 1
584 | ServiceRegistries: # And that's how you associate ecsfs-backend.local!
585 | - RegistryArn: !GetAtt BackendLocalDiscoveryService.Arn
586 | NetworkConfiguration:
587 | AwsvpcConfiguration:
588 | AssignPublicIp: DISABLED
589 | SecurityGroups:
590 | - !Ref FargateContainerSecurityGroup
591 | Subnets:
592 | - !Ref PrivateSubnet
593 | TaskDefinition: !Ref BackendTaskDefinition
594 |
595 | FrontendService:
596 | Type: AWS::ECS::Service
597 | Properties: # Associates it with the DNS name ecsfs-frontend.local.
598 | ServiceName: ecsfs-frontend-service
599 | Cluster: !Ref ECSCluster
600 | LaunchType: FARGATE
601 | DesiredCount: 1
602 | ServiceRegistries:
603 | - RegistryArn: !GetAtt FrontendLocalDiscoveryService.Arn
604 | NetworkConfiguration:
605 | AwsvpcConfiguration:
606 | AssignPublicIp: DISABLED
607 | SecurityGroups:
608 | - !Ref FargateContainerSecurityGroup
609 | Subnets:
610 | - !Ref PrivateSubnet
611 | TaskDefinition: !Ref FrontendTaskDefinition
612 |
613 | # The application load balancer routes the requests to the nginx service,
614 | # therefore we need to wait for the ALB to finish before we can actually spin
615 | # up the nginx service.
616 | NginxService:
617 | Type: AWS::ECS::Service
618 | DependsOn: ListenerHTTP
619 | Properties:
620 | ServiceName: ecsfs-nginx-service
621 | Cluster: !Ref ECSCluster
622 | LaunchType: FARGATE
623 | DesiredCount: 1
624 | ServiceRegistries: # Associate it with ecsfs-nginx.local DNS name.
625 | - RegistryArn: !GetAtt NginxLocalDiscoveryService.Arn
626 | NetworkConfiguration:
627 | AwsvpcConfiguration:
628 | AssignPublicIp: ENABLED
629 | SecurityGroups:
630 | - !Ref FargateContainerSecurityGroup
631 | Subnets:
632 | - !Ref PublicSubnetOne
633 | - !Ref PublicSubnetTwo
634 | TaskDefinition: !Ref NginxTaskDefinition
635 | LoadBalancers:
636 | - ContainerName: ecsfs-nginx-container
637 | ContainerPort: 80
638 | TargetGroupArn: !Ref TargetGroup
639 |
640 | # AUTO-SCALING -------------------------------------------------------------
641 | # We are just interested in scaling the backend. For scaling a service you
642 | # need to define a *Scalable Target*, which is where you specify *what*
643 | # service do you want to scale, and a *ScalingPolicy*, where you describe
644 | # *how* and *when* do you want to scale it.
645 | #
646 | # There's two modes when scaling a service, we use 'Target Tracking Scaling',
647 | # in which you specify a target value for a metric (say for instance 75% of
648 | # CPU usage) and then Fargate would spin more instances when the average of
649 | # all the tasks running that service exceed the threshold.
650 | #
651 | # In our case we will scale the backend between 1 and 3 instances and we will
652 | # specify a target CPU usage percentage of 50%.
653 | #
654 | # Usually each service task spits out metrics every 1 minute. You can see
655 | # these metrics on the CloudWatch page on the AWS web console. Use that for
656 | # inspecting how Fargate reacts to changes when you stress the application.
657 | #
658 | # Read more:
659 | #
660 | # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-autoscaling-targettracking.html
661 | #
662 |
663 | # Specifies a resource that Application Auto Scaling can scale. In our case
664 | # it's just the backend.
665 | AutoScalingTarget:
666 | Type: AWS::ApplicationAutoScaling::ScalableTarget
667 | Properties:
668 | MinCapacity: 1
669 | MaxCapacity: 3
670 | ResourceId: !Join ['/', [service, !Ref ECSCluster, !GetAtt BackendService.Name]]
671 | ScalableDimension: ecs:service:DesiredCount
672 | ServiceNamespace: ecs
673 | RoleARN: !GetAtt AutoScalingRole.Arn
674 |
675 | # Describes the rules for ECS to check and decide when it should scale up or
676 | # down a service. In our application we just scale the backend.
677 | AutoScalingPolicy:
678 | Type: AWS::ApplicationAutoScaling::ScalingPolicy
679 | Properties:
680 | PolicyName: BackendAutoScalingPolicy
681 | PolicyType: TargetTrackingScaling
682 | ScalingTargetId: !Ref AutoScalingTarget
683 | TargetTrackingScalingPolicyConfiguration:
684 | PredefinedMetricSpecification:
685 | PredefinedMetricType: ECSServiceAverageCPUUtilization
686 | ScaleInCooldown: 10
687 | ScaleOutCooldown: 10
688 | TargetValue: 50
689 |
690 | # STRESSING THE APPLICATION =================================================
691 | # You can use the 'ab' unix command (Apache Benchmark) to send many requests
692 | # to you application load balancer and see how Fargate starts scaling up the
693 | # backend service.
694 | #
695 | # First go to the web console under the EC2 page and look for the Load
696 | # Balancers category.
697 | #
698 | # In there look for the DNS name. You can also click the *Outputs* tab from
699 | # the CloudFormation stack to see that URL. It should look like:
700 | #
701 | # http://ecsfs-loadb-1g27mx21p6h8d-1015414055.us-west-2.elb.amazonaws.com/
702 | #
703 | # Then run the following command to stress the application. It will perform
704 | # 10,000 requests (1 per second) printing all the responses.
705 | #
706 | # ab -n 10000 -c 1 -v 3 http:///
707 | #
708 | # I noticed that CloudWatch will wait until it has 3 consecutive measurements
709 | # (metric points) exceeding the target value specified (50%). It is then when
710 | # it then when it sets an alarm and Fargate reacts by adding extra running
711 | # tasks until the metrics stabilize.
712 | #
713 | # If then the CPU decreases and doesn't need many tasks running anymore it
714 | # would wait some minutes (around 15 metric points, that is, 15min) to start
715 | # scaling down.
716 |
717 | # OUTPUTS =====================================================================
718 | # The entries defined as outputs will show in the stack page in the Amazon
719 | # web console under the CloudFormation section. We expose the load balancer DNS
720 | # name so you can copy-paste it on your browser to see the app running:
721 | #
722 | # http://
723 | #
724 | Outputs:
725 | LoadBalancerDNSName:
726 | Description: Copy and paste this value into your browser to access the app.
727 | Value: !GetAtt LoadBalancer.DNSName
728 |
--------------------------------------------------------------------------------