├── CODE_OF_CONDUCT.md ├── hexagonal-architecture ├── ports │ ├── Repository.js │ ├── HTTPHandler.js │ └── CurrenciesService.js ├── app.js ├── adapters │ ├── CurrencyConverter.js │ ├── GetStocksRequest.js │ ├── StocksDB.js │ └── CurrencyConverterWithCache.js ├── package.json ├── domains │ └── StocksLogic.js └── package-lock.json ├── samconfig.toml ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── README.md └── template.yaml /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /hexagonal-architecture/ports/Repository.js: -------------------------------------------------------------------------------- 1 | const getStockValue = require("../adapters/StocksDB"); 2 | 3 | const getStockData = async (stockID) => { 4 | try{ 5 | const data = await getStockValue(stockID); 6 | return data; 7 | } catch(err) { 8 | return err 9 | } 10 | } 11 | 12 | module.exports = { 13 | getStockData 14 | } -------------------------------------------------------------------------------- /hexagonal-architecture/app.js: -------------------------------------------------------------------------------- 1 | const getStocksRequest = require("./adapters/GetStocksRequest"); 2 | 3 | exports.lambdaHandler = async (event) => { 4 | try{ 5 | const stockID = event.pathParameters.StockID; 6 | const response = await getStocksRequest(stockID); 7 | return response 8 | } catch (err) { 9 | console.log(err) 10 | return err; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | [default] 3 | [default.deploy] 4 | [default.deploy.parameters] 5 | stack_name = "stock-app" 6 | s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-11dh6gr0t53lj" 7 | s3_prefix = "stock-app" 8 | region = "eu-west-1" 9 | confirm_changeset = true 10 | capabilities = "CAPABILITY_IAM" 11 | resolve_s3 = true 12 | profile = "hexagonal" 13 | image_repositories = [] 14 | -------------------------------------------------------------------------------- /hexagonal-architecture/ports/HTTPHandler.js: -------------------------------------------------------------------------------- 1 | const stock = require("../domains/StocksLogic"); 2 | 3 | const retrieveStock = async (stockID) => { 4 | try{ 5 | const stockWithCurrencies = await stock.getStockWithCurrencies(stockID) 6 | return stockWithCurrencies; 7 | } 8 | catch(err){ 9 | return err 10 | } 11 | } 12 | 13 | module.exports = { 14 | retrieveStock 15 | } -------------------------------------------------------------------------------- /hexagonal-architecture/adapters/CurrencyConverter.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios") 2 | 3 | const CURRENCIES_BASE_PATH = process.env.CURRENCIES_PATH 4 | 5 | const getCurrencies = async () => { 6 | try{ 7 | const getCurr = await axios.get(`${CURRENCIES_BASE_PATH}/currencies`) 8 | return getCurr.data 9 | } catch(err) { 10 | return err; 11 | } 12 | } 13 | 14 | module.exports = getCurrencies; -------------------------------------------------------------------------------- /hexagonal-architecture/ports/CurrenciesService.js: -------------------------------------------------------------------------------- 1 | const getCurrencies = require("../adapters/CurrencyConverter"); 2 | // const getCurrencies = require("../adapters/CurrencyConverterWithCache"); 3 | 4 | const getCurrenciesData = async () => { 5 | try{ 6 | const data = await getCurrencies(); 7 | return data 8 | } catch(err) { 9 | return err 10 | } 11 | } 12 | 13 | module.exports = { 14 | getCurrenciesData 15 | } -------------------------------------------------------------------------------- /hexagonal-architecture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexagonal_architecture_for_lambda", 3 | "version": "2.0.0", 4 | "description": "hexagonal architecture implemented in AWS Lambda", 5 | "main": "app.js", 6 | "author": "luca mezzalira", 7 | "license": "MIT-0", 8 | "dependencies": { 9 | "axios": "^1.4.0", 10 | "redis": "^4.6.8" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /hexagonal-architecture/adapters/GetStocksRequest.js: -------------------------------------------------------------------------------- 1 | const HTTPHandler = require("../ports/HTTPHandler") 2 | 3 | const getStocksRequest = async(stockID) => { 4 | let res; 5 | 6 | try { 7 | const stockData = await HTTPHandler.retrieveStock(stockID) 8 | 9 | res = { 10 | 'statusCode': 200, 11 | 'body': JSON.stringify({ 12 | message: stockData, 13 | }) 14 | } 15 | } catch (err) { 16 | console.log(err); 17 | return err; 18 | } 19 | 20 | return res; 21 | } 22 | 23 | module.exports = getStocksRequest -------------------------------------------------------------------------------- /hexagonal-architecture/domains/StocksLogic.js: -------------------------------------------------------------------------------- 1 | const Currency = require("../ports/CurrenciesService"); 2 | const Repository = require("../ports/Repository"); 3 | 4 | const getStockWithCurrencies = async (stockID) => { 5 | try{ 6 | const stock = await Repository.getStockData(stockID); 7 | const currencyList = await Currency.getCurrenciesData(); 8 | 9 | const stockWithCurrencies = { 10 | stock: stock.id, 11 | values: { 12 | "USD": stock.value 13 | } 14 | }; 15 | 16 | for(const currency in currencyList.rates){ 17 | stockWithCurrencies.values[currency] = (stock.value * currencyList.rates[currency]).toFixed(2) 18 | } 19 | 20 | return stockWithCurrencies; 21 | 22 | } catch(err) { 23 | return err; 24 | } 25 | } 26 | 27 | module.exports = { 28 | getStockWithCurrencies 29 | }; -------------------------------------------------------------------------------- /hexagonal-architecture/adapters/StocksDB.js: -------------------------------------------------------------------------------- 1 | const { DynamoDBClient } = require( "@aws-sdk/client-dynamodb"); 2 | const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb"); 3 | 4 | const DB_TABLE = process.env.DB_TABLE; 5 | 6 | const client = new DynamoDBClient({}); 7 | const docClient = DynamoDBDocumentClient.from(client); 8 | 9 | const getStockValue = async (stockID = "AMZN") => { 10 | 11 | let params = { 12 | TableName : DB_TABLE, 13 | Key:{ 14 | 'STOCK_ID': stockID 15 | } 16 | } 17 | 18 | const command = new GetCommand(params); 19 | 20 | try { 21 | const stockData = await docClient.send(command); 22 | return { 23 | id: stockData.Item.STOCK_ID, 24 | value: stockData.Item.VALUE 25 | } 26 | } 27 | catch (err) { 28 | console.log(err) 29 | return err 30 | } 31 | 32 | } 33 | 34 | module.exports = getStockValue; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ -------------------------------------------------------------------------------- /hexagonal-architecture/adapters/CurrencyConverterWithCache.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios") 2 | const redis = require("redis"); 3 | 4 | const REDIS_URL = process.env.CACHE_URL 5 | const REDIS_PORT = process.env.CACHE_PORT 6 | const CURRENCIES_BASE_PATH = process.env.CURRENCIES_PATH 7 | 8 | const client = redis.createClient({ 9 | url: `redis://${REDIS_URL}:${REDIS_PORT}` 10 | }); 11 | 12 | client.on('error', (err) => console.log('Redis Cluster Error', err)); 13 | 14 | const getCurrencies = async () => { 15 | 16 | try{ 17 | if(!client.isOpen) 18 | await client.connect(); 19 | 20 | let res = await client.get("CURRENCIES"); 21 | 22 | if(res){ 23 | return JSON.parse(res); 24 | } 25 | 26 | const getCurr = await axios.get(`${CURRENCIES_BASE_PATH}/currencies`) 27 | await client.set( 28 | "CURRENCIES", 29 | JSON.stringify(getCurr.data), 30 | { 31 | EX: 10, 32 | NX: true 33 | }); 34 | 35 | return getCurr.data 36 | 37 | } catch(err) { 38 | return err; 39 | } 40 | } 41 | 42 | module.exports = getCurrencies 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # node-waf configuration 19 | .lock-wscript 20 | 21 | # IDEs and editors 22 | .idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | # misc 38 | .sass-cache 39 | connect.lock 40 | typings 41 | 42 | # Logs 43 | logs 44 | *.log 45 | npm-debug.log* 46 | yarn-debug.log* 47 | yarn-error.log* 48 | 49 | 50 | # Dependency directories 51 | node_modules/ 52 | jspm_packages/ 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | 72 | # next.js build output 73 | .next 74 | 75 | # Lerna 76 | lerna-debug.log 77 | 78 | # System Files 79 | .DS_Store 80 | Thumbs.db 81 | 82 | .aws-sam -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Developing evolutionary architecture with AWS Lambda 2 | 3 | ## Context 4 | Agility enables you to evolve a workload quickly, adding new features, or introducing new infrastructure as required. The key characteristics for achieving agility in a code base are loosely coupled components and strong encapsulation. 5 | 6 | Loose coupling can help improve test coverage and create atomic refactoring. With encapsulation, you expose only what is needed to interact with a service without revealing the implementation logic. 7 | 8 | Evolutionary architectures can help achieve agility in your design. In the book [“Building Evolutionary Architectures”](https://learning.oreilly.com/library/view/building-evolutionary-architectures/9781491986356/), this architecture is defined as one that “supports guided, incremental change across multiple dimensions”. 9 | 10 | The project is set up to work on eu-west-1, if you want to deploy in another region please change the code accordingly 11 | 12 | If you are interested to learn more about this approach, please read the [blog post associated to this code example](https://aws.amazon.com/blogs/compute/developing-evolutionary-architecture-with-aws-lambda). 13 | 14 | ## Project 15 | This example provides an idea on how to implement a hexagonal architecture with [AWS Lambda](https://aws.amazon.com/lambda/) using Node.js. 16 | The folder structure represents the three key elements that characterizes the first implementation of an hexagonal architecture: ports, adapters and domain logic. 17 | 18 | In order to run the project in your AWS account, you have to follow these steps: 19 | 20 | 1. Download [AWS SAM](https://aws.amazon.com/serverless/sam/) 21 | 22 | 2. Build the project with the command ```sam build``` 23 | 24 | 3. Deploy the project in your account ```sam deploy --guided``` 25 | 26 | 4. Go to DynamoDB console, add an item to the stock table: 27 | 28 | - __STOCK_ID__: AMZN 29 | - __VALUE__: 1234.56 30 | 31 | 5. in the ```template.yaml``` add the URL for an API to find the live value of the currencies, it can be as simple as a mock API or a service that provides the live values such as [fixer](https://fixer.io/). Independently from the service you want to use, remember the payload response should be structured similar to the following snippet: 32 | 33 | ```json 34 | { 35 | "base": "USD", 36 | "date": "2023-08-22", 37 | "rates": { 38 | "CAD": 1.260046, 39 | "CHF": 0.933058, 40 | "EUR": 0.806942, 41 | "GBP": 0.719154 42 | } 43 | } 44 | ``` 45 | 46 | After these changes you are able to test the API retrieving the URL from the API gateway console and appending ```/stock/AMZN``` 47 | 48 | ## Evolving the project 49 | 50 | When we want to evolve the application adding a cache-aside pattern using an ElastiCache cluster for reducing the throughput towards a 3rd party service, we can do it applying some changes to the current architecture. 51 | You can also watch [a presentation]((https://youtu.be/kRFg6fkVChQ?si=5ZazsmXmKvspQZp9)) where I describe the evolutionary nature more in depth. 52 | If you deployed successfully the infrastructure in the previous step, there is only a thing to change. In the ```ports/CurrenciesService``` we comment the first import and uncomment the second one. This will use a new adapter called CurrencyConverterWithCache that contains the logic for the cache-aside pattern with ElastiCache Redis cluster 53 | 54 | ``` 55 | //const getCurrencies = require("../adapters/CurrencyConverter"); 56 | const getCurrencies = require("../adapters/CurrencyConverterWithCache"); 57 | ``` 58 | 59 | Thanks to hexagonal architecture we were able to atomically change an adapter and a port without changing anything else in the code base. 60 | 61 | ## Contributing 62 | 63 | Please create a new GitHub issue for any feature requests, bugs, or documentation improvements. 64 | 65 | Where possible, please also submit a pull request for the change. 66 | 67 | ## License 68 | 69 | This library is licensed under the MIT-0 License. See the LICENSE file. 70 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: hexagonal-arch-sample 4 | 5 | Globals: 6 | Function: 7 | Timeout: 20 8 | 9 | Resources: 10 | #DynamoDB table 11 | StocksTable: 12 | Type: AWS::DynamoDB::Table 13 | Properties: 14 | AttributeDefinitions: 15 | - AttributeName: STOCK_ID 16 | AttributeType: S 17 | KeySchema: 18 | - AttributeName: STOCK_ID 19 | KeyType: HASH 20 | BillingMode: PAY_PER_REQUEST 21 | #Network configuration 22 | Vpc: 23 | Type: AWS::EC2::VPC 24 | Properties: 25 | CidrBlock: '10.0.0.0/16' 26 | 27 | SecurityGroup: 28 | Type: AWS::EC2::SecurityGroup 29 | Properties: 30 | GroupDescription: 'Lambda Security Group' 31 | VpcId: !Ref Vpc 32 | SecurityGroupEgress: 33 | - CidrIp: '0.0.0.0/0' 34 | FromPort: 0 35 | ToPort: 65535 36 | IpProtocol: tcp 37 | SecurityGroupIngress: 38 | - CidrIp: '0.0.0.0/0' 39 | FromPort: 0 40 | ToPort: 65535 41 | IpProtocol: tcp 42 | SubnetA: 43 | Type: AWS::EC2::Subnet 44 | Properties: 45 | VpcId: !Ref Vpc 46 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 47 | MapPublicIpOnLaunch: false 48 | CidrBlock: '10.0.0.0/24' 49 | SubnetB: 50 | Type: AWS::EC2::Subnet 51 | Properties: 52 | VpcId: !Ref Vpc 53 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 54 | MapPublicIpOnLaunch: false 55 | CidrBlock: '10.0.1.0/24' 56 | SubnetC: 57 | Type: AWS::EC2::Subnet 58 | Properties: 59 | VpcId: !Ref Vpc 60 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 61 | MapPublicIpOnLaunch: false 62 | CidrBlock: '10.0.2.0/24' 63 | 64 | RouteTable: 65 | Type: AWS::EC2::RouteTable 66 | Properties: 67 | VpcId: !Ref Vpc 68 | 69 | PublicSubnetCRouteTableAssociation: 70 | Type: AWS::EC2::SubnetRouteTableAssociation 71 | Properties: 72 | SubnetId: !Ref SubnetC 73 | RouteTableId: !Ref RouteTable 74 | 75 | InternetGateway: 76 | Type: 'AWS::EC2::InternetGateway' 77 | 78 | VPCGatewayAttachment: 79 | Type: 'AWS::EC2::VPCGatewayAttachment' 80 | Properties: 81 | VpcId: !Ref Vpc 82 | InternetGatewayId: !Ref InternetGateway 83 | 84 | InternetRoute: 85 | Type: 'AWS::EC2::Route' 86 | Properties: 87 | DestinationCidrBlock: '0.0.0.0/0' 88 | GatewayId: !Ref InternetGateway 89 | RouteTableId: !Ref RouteTable 90 | 91 | EIP: 92 | Type: 'AWS::EC2::EIP' 93 | Properties: 94 | Domain: 'vpc' 95 | 96 | Nat: 97 | Type: 'AWS::EC2::NatGateway' 98 | Properties: 99 | AllocationId: !GetAtt EIP.AllocationId 100 | SubnetId: !Ref SubnetC 101 | 102 | NatRouteTable: 103 | Type: 'AWS::EC2::RouteTable' 104 | Properties: 105 | VpcId: !Ref Vpc 106 | 107 | NatRoute: 108 | Type: 'AWS::EC2::Route' 109 | Properties: 110 | DestinationCidrBlock: '0.0.0.0/0' 111 | NatGatewayId: !Ref Nat 112 | RouteTableId: !Ref NatRouteTable 113 | 114 | SubnetARouteTableAssociation: 115 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 116 | Properties: 117 | RouteTableId: !Ref NatRouteTable 118 | SubnetId: !Ref SubnetA 119 | 120 | SubnetBRouteTableAssociation: 121 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 122 | Properties: 123 | RouteTableId: !Ref NatRouteTable 124 | SubnetId: !Ref SubnetB 125 | 126 | DDBEndpoint: 127 | Type: 'AWS::EC2::VPCEndpoint' 128 | Properties: 129 | PolicyDocument: 130 | Version: 2012-10-17 131 | Statement: 132 | - Effect: Allow 133 | Principal: '*' 134 | Action: 135 | - 'dynamodb:GetItem' 136 | - 'dynamodb:DescribeTable' 137 | Resource: 138 | - '*' 139 | ServiceName: !Sub 'com.amazonaws.${AWS::Region}.dynamodb' 140 | RouteTableIds: 141 | - !Ref NatRouteTable 142 | VpcId: !Ref Vpc 143 | 144 | InstanceSecurityGroup: 145 | Type: 'AWS::EC2::SecurityGroup' 146 | Properties: 147 | GroupName: 'Security Group for outbound traffic' 148 | GroupDescription: 'Lambda Traffic' 149 | VpcId: !Ref Vpc 150 | SecurityGroupEgress: 151 | - IpProtocol: '-1' 152 | CidrIp: '0.0.0.0/0' 153 | 154 | InstanceSecurityGroupIngress: 155 | Type: 'AWS::EC2::SecurityGroupIngress' 156 | DependsOn: 'InstanceSecurityGroup' 157 | Properties: 158 | GroupId: !Ref InstanceSecurityGroup 159 | IpProtocol: 'tcp' 160 | FromPort: 0 161 | ToPort: 65535 162 | SourceSecurityGroupId: !Ref InstanceSecurityGroup 163 | #ElastiCache Redis 164 | ElastiCacheSubnetGroup: 165 | Type: 'AWS::ElastiCache::SubnetGroup' 166 | Properties: 167 | Description: Cache Subnet Group 168 | SubnetIds: 169 | - !Ref SubnetA 170 | - !Ref SubnetB 171 | ElastiCacheRedisCluster: 172 | Type: AWS::ElastiCache::ReplicationGroup 173 | Properties: 174 | AutoMinorVersionUpgrade: true 175 | AtRestEncryptionEnabled: true 176 | CacheNodeType: cache.t2.micro 177 | CacheSubnetGroupName: !Ref ElastiCacheSubnetGroup 178 | Engine: redis 179 | NumNodeGroups: 1 180 | Port: 6379 181 | ReplicasPerNodeGroup: 1 182 | ReplicationGroupDescription: cache-aside pattern with Redis 183 | SecurityGroupIds: 184 | - !Ref InstanceSecurityGroup 185 | 186 | #Lambda function 187 | StocksConverterFunction: 188 | Type: AWS::Serverless::Function 189 | DependsOn: ElastiCacheRedisCluster 190 | Properties: 191 | CodeUri: hexagonal-architecture/ 192 | Handler: app.lambdaHandler 193 | Runtime: nodejs18.x 194 | MemorySize: 256 195 | Policies: 196 | - DynamoDBReadPolicy: 197 | TableName: !Ref StocksTable 198 | - Version: '2012-10-17' 199 | Statement: 200 | - Effect: Allow 201 | Action: 202 | - 'logs:CreateLogGroup' 203 | - 'logs:CreateLogStream' 204 | - 'logs:PutLogEvents' 205 | - 'ec2:CreateNetworkInterface' 206 | - 'ec2:DescribeNetworkInterfaces' 207 | - 'ec2:DeleteNetworkInterface' 208 | - 'ec2:AssignPrivateIpAddresses' 209 | - 'ec2:UnassignPrivateIpAddresses' 210 | Resource: '*' 211 | Environment: 212 | Variables: 213 | DB_TABLE: !Ref StocksTable 214 | CACHE_URL: !GetAtt ElastiCacheRedisCluster.PrimaryEndPoint.Address 215 | CACHE_PORT: !GetAtt ElastiCacheRedisCluster.PrimaryEndPoint.Port 216 | # change with a mock API URL or an 3rd Party API 217 | # CURRENCIES_PATH: 'https://www.myAPI.com' 218 | VpcConfig: 219 | SecurityGroupIds: 220 | - !Ref InstanceSecurityGroup 221 | SubnetIds: 222 | - !Ref SubnetA 223 | - !Ref SubnetB 224 | Events: 225 | StocksConverter: 226 | Type: HttpApi 227 | Properties: 228 | ApiId: !Ref StocksGateway 229 | Path: /stock/{StockID} 230 | Method: get 231 | #HTTP API Gateway 232 | StocksGateway: 233 | Type: AWS::Serverless::HttpApi 234 | Properties: 235 | CorsConfiguration: 236 | AllowMethods: 237 | - GET 238 | - POST 239 | AllowOrigins: 240 | - '*' 241 | 242 | Outputs: 243 | HttpApiUrl: 244 | Description: URL of your API endpoint, add stock ID like AMZN 245 | Value: 246 | Fn::Sub: 'https://${StocksGateway}.execute-api.${AWS::Region}.${AWS::URLSuffix}/stock/' 247 | -------------------------------------------------------------------------------- /hexagonal-architecture/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexagonal_architecture_for_lambda", 3 | "version": "2.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hexagonal_architecture_for_lambda", 9 | "version": "2.0.0", 10 | "license": "MIT-0", 11 | "dependencies": { 12 | "axios": "^1.4.0", 13 | "redis": "^4.6.8" 14 | } 15 | }, 16 | "node_modules/@redis/bloom": { 17 | "version": "1.2.0", 18 | "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", 19 | "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", 20 | "peerDependencies": { 21 | "@redis/client": "^1.0.0" 22 | } 23 | }, 24 | "node_modules/@redis/client": { 25 | "version": "1.5.9", 26 | "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.9.tgz", 27 | "integrity": "sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ==", 28 | "dependencies": { 29 | "cluster-key-slot": "1.1.2", 30 | "generic-pool": "3.9.0", 31 | "yallist": "4.0.0" 32 | }, 33 | "engines": { 34 | "node": ">=14" 35 | } 36 | }, 37 | "node_modules/@redis/graph": { 38 | "version": "1.1.0", 39 | "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", 40 | "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", 41 | "peerDependencies": { 42 | "@redis/client": "^1.0.0" 43 | } 44 | }, 45 | "node_modules/@redis/json": { 46 | "version": "1.0.4", 47 | "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", 48 | "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", 49 | "peerDependencies": { 50 | "@redis/client": "^1.0.0" 51 | } 52 | }, 53 | "node_modules/@redis/search": { 54 | "version": "1.1.3", 55 | "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.3.tgz", 56 | "integrity": "sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==", 57 | "peerDependencies": { 58 | "@redis/client": "^1.0.0" 59 | } 60 | }, 61 | "node_modules/@redis/time-series": { 62 | "version": "1.0.5", 63 | "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", 64 | "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", 65 | "peerDependencies": { 66 | "@redis/client": "^1.0.0" 67 | } 68 | }, 69 | "node_modules/asynckit": { 70 | "version": "0.4.0", 71 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 72 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 73 | }, 74 | "node_modules/axios": { 75 | "version": "1.4.0", 76 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", 77 | "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", 78 | "dependencies": { 79 | "follow-redirects": "^1.15.0", 80 | "form-data": "^4.0.0", 81 | "proxy-from-env": "^1.1.0" 82 | } 83 | }, 84 | "node_modules/cluster-key-slot": { 85 | "version": "1.1.2", 86 | "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", 87 | "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", 88 | "engines": { 89 | "node": ">=0.10.0" 90 | } 91 | }, 92 | "node_modules/combined-stream": { 93 | "version": "1.0.8", 94 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 95 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 96 | "dependencies": { 97 | "delayed-stream": "~1.0.0" 98 | }, 99 | "engines": { 100 | "node": ">= 0.8" 101 | } 102 | }, 103 | "node_modules/delayed-stream": { 104 | "version": "1.0.0", 105 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 106 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 107 | "engines": { 108 | "node": ">=0.4.0" 109 | } 110 | }, 111 | "node_modules/follow-redirects": { 112 | "version": "1.15.2", 113 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", 114 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", 115 | "funding": [ 116 | { 117 | "type": "individual", 118 | "url": "https://github.com/sponsors/RubenVerborgh" 119 | } 120 | ], 121 | "engines": { 122 | "node": ">=4.0" 123 | }, 124 | "peerDependenciesMeta": { 125 | "debug": { 126 | "optional": true 127 | } 128 | } 129 | }, 130 | "node_modules/form-data": { 131 | "version": "4.0.0", 132 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 133 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 134 | "dependencies": { 135 | "asynckit": "^0.4.0", 136 | "combined-stream": "^1.0.8", 137 | "mime-types": "^2.1.12" 138 | }, 139 | "engines": { 140 | "node": ">= 6" 141 | } 142 | }, 143 | "node_modules/generic-pool": { 144 | "version": "3.9.0", 145 | "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", 146 | "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", 147 | "engines": { 148 | "node": ">= 4" 149 | } 150 | }, 151 | "node_modules/mime-db": { 152 | "version": "1.52.0", 153 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 154 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 155 | "engines": { 156 | "node": ">= 0.6" 157 | } 158 | }, 159 | "node_modules/mime-types": { 160 | "version": "2.1.35", 161 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 162 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 163 | "dependencies": { 164 | "mime-db": "1.52.0" 165 | }, 166 | "engines": { 167 | "node": ">= 0.6" 168 | } 169 | }, 170 | "node_modules/proxy-from-env": { 171 | "version": "1.1.0", 172 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 173 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 174 | }, 175 | "node_modules/redis": { 176 | "version": "4.6.8", 177 | "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.8.tgz", 178 | "integrity": "sha512-S7qNkPUYrsofQ0ztWlTHSaK0Qqfl1y+WMIxrzeAGNG+9iUZB4HGeBgkHxE6uJJ6iXrkvLd1RVJ2nvu6H1sAzfQ==", 179 | "dependencies": { 180 | "@redis/bloom": "1.2.0", 181 | "@redis/client": "1.5.9", 182 | "@redis/graph": "1.1.0", 183 | "@redis/json": "1.0.4", 184 | "@redis/search": "1.1.3", 185 | "@redis/time-series": "1.0.5" 186 | } 187 | }, 188 | "node_modules/yallist": { 189 | "version": "4.0.0", 190 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 191 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 192 | } 193 | } 194 | } 195 | --------------------------------------------------------------------------------