├── .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 | ![AWS Diagram](diagram.png) 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 | --------------------------------------------------------------------------------