├── .github └── CODEOWNERS ├── LICENSE ├── README.md ├── images ├── APIfrontend.jpeg ├── backend.jpeg ├── mongo-and-aws.jpeg └── termination.png ├── modules ├── cf_public_extension │ ├── main.tf │ ├── variables.tf │ └── versions.tf ├── mongoatlas │ ├── alerts.tf │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── scripts │ │ ├── create_update_data_api.sh │ │ ├── create_update_data_api_security.sh │ │ ├── delete_data_api.sh │ │ ├── delete_data_api_security.sh │ │ ├── get_data_api.sh │ │ └── get_data_api_security.sh │ ├── variables.tf │ └── versions.tf ├── mongoatlas_alert │ ├── main.tf │ ├── variables.tf │ └── versions.tf ├── mongoatlas_instance │ ├── configure_data_api_scripts │ │ ├── create_update.sh │ │ ├── delete.sh │ │ └── get.sh │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── secret │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf └── third_party_vpc_endpoint │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── mongo_atlas.tf ├── provider.tf ├── terraform.tfvars ├── variables.tf └── versions.tf /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @jitsecurity/eng-mgmt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MongoDB Atlas Serverless Deployment with Data API 2 | 3 | This guide details a streamlined, one-click Terraform deployment for MongoDB Atlas serverless instances. It includes configurations for private endpoint connections, facilitating secure, non-customer-facing Lambda interactions with MongoDB, and leverages the Data API with JWT token authentication and tenant isolation for customer-facing Lambdas. 4 | 5 | The full blogpost about this repo can be found [here.](https://www.jit.io/blog/enhance-mongodb-security-for-atlas-with-scalable-tenant-isolation) 6 | 7 | ## Key Features and components 8 | 9 | - **MongoDB Atlas CloudFormation Custom Resources**: Automate the creation of database users and roles using MongoDB Atlas CloudFormation custom resources, streamlining database management and security. 10 | - for more information click [here](https://github.com/mongodb/mongodbatlas-cloudformation-resources) 11 | - **Developer Access Configuration**: Implement IP whitelisting for developer access, including the automatic configuration of NAT Gateway IPs for the Data API IP whitelist, ensuring secure developer interactions with MongoDB Atlas. 12 | - **Private Endpoint Connectivity**: Establish private endpoints between AWS and MongoDB Atlas to allow backend Lambda functions to securely access the database without traversing the public internet, enhancing the security of data access. 13 | - **IP Whitelisting for Data API**: In lieu of private connectivity for app services at deployment, incorporate IP whitelisting as a secure method of accessing MongoDB Atlas, safeguarding data interactions. 14 | - **Alerts and Monitoring**: Set up comprehensive alerts, including pricing alerts, to be delivered to a designated email address, with the capability for notification adjustments through Slack and other communication channels. 15 | - **Data API Configuration**: Fully configure the Data API with JWT authentication and filtering mechanisms for tenant separation, facilitating secure and efficient data access and manipulation. 16 | - **MongoDB Atlas Project Creation**: Establish a dedicated project within MongoDB Atlas to house serverless instances and the Data API, centralizing resources for streamlined management. 17 | - **Serverless Instances on MongoDB Atlas**: Deploy one or more serverless instances within MongoDB Atlas, optimized for scalability and operational efficiency, catering to dynamic workload requirements including AWS Secret Manager secret to assist cloudformation in provisioning atlas resources through CloudFormation. 18 | - **Parameters Management in AWS SSM Parameter Store**: Manage essential AWS parameters within the SSM Parameter Store, simplifying the access and use of Atlas URLs by other services, promoting operational efficiency and inter-service connectivity. 19 | 20 | 21 | ## Deployment Instructions 22 | 23 | ### Prerequisites 24 | - **AWS Account**: You need an AWS account to deploy the resources. 25 | - **VPC**: You need a VPC with private subnets to deploy the resources. 26 | - **Security Groups**: You need security groups to control the traffic to and from AWS and MongoDB Atlas. 27 | - **MongoDB Atlas Account**: You need a MongoDB Atlas account to deploy the serverless instance. 28 | - **Terraform**: You need Terraform installed on your local machine to deploy the resources. 29 | 30 | > **Verify Atlas serverless is supported in your region** - You can check out the supported regions [here](https://www.mongodb.com/docs/atlas/reference/amazon-aws/#std-label-amazon-aws) 31 | 32 | Follow these steps to deploy your MongoDB Atlas serverless instance: 33 | 34 | ### 1. API Key Generation 35 | - Visit the MongoDB Cloud Console Access Manager at `https://cloud.mongodb.com/v2#/org//access/apiKeys`. 36 | - Create a new API key with the "Organization Project Creator" role and note the public and private keys. 37 | 38 | ### 2. Environment Configuration 39 | - Ensure AWS credentials and permissions are properly configured to enable custom CloudFormation resources, manage secrets and SSM parameters, read VPC configurations, and create private endpoints. 40 | 41 | ### 3. Setup your variables 42 | - Fill in the required parameters in the `terraform.tfvars` file, their descriptions is available in the `variables.tf` file. 43 | - Most importantly - obtain the following: 44 | - **organization_id**: Atlas organization to deploy into. from https://cloud.mongodb.com/v2#/preferences/organizations 45 | - **aws_vpc_id**: The VPC ID where the private endpoint will be configured. 46 | - **private_subnet_ids**: The private subnet IDs where the private endpoint will be created. 47 | - **aws_allowed_access_security_groups**: The security groups that will be allowed to access the private endpoint and to MongoDB Atlas. 48 | - **jwt_audience**: The audience for the JWT token used by your users. 49 | - **jwt_public_key**: The public key used to verify the JWT token. 50 | - **tenant_id_field_in_jwt**: The field in the JWT token that contains the tenant ID, this field is applied to all data-api requests as a field filter. 51 | - **display_name_field_in_jwt**: The field in the JWT token that contains the display name of the user, choose a value that will let you identify the user easily in the atlas console. 52 | 53 | ### 4. Deployment Process 54 | Set your MongoDB Atlas API key pair as environment variables and execute the Terraform commands: 55 | 56 | ```bash 57 | export TF_VAR_mongo_atlas_public_key= 58 | export TF_VAR_mongo_atlas_private_key= 59 | terraform init 60 | terraform plan 61 | terraform apply 62 | ``` 63 | 64 | ### Cleanup 65 | To clean everything up - you can comment out everything in `mongo_atlas.tf` or run `terraform destroy` for the module. 66 | 67 | Before cleaning up, if you enabled `enable_termination_protection` you will need to go through the console to each instance and disable the **termination protection**. 68 | ![Termination protection](./images/termination.png) 69 | 70 | 71 | ## Architecture Overview 72 | 73 | ![MongoDB Atlas Architecture](./images/mongo-and-aws.jpeg) 74 | 75 | ## Cost Breakdown 76 | 77 | The cost of deploying MongoDB Atlas serverless instances with the Data API and private endpoint connectivity sums up to few dollars. Let's see some examples (rough estimations for usage) 78 | 79 |
80 | Example 1: 1 Serverless instance with CloudFormation Atlas resources, on 2 AWS availability zones, without any usage or continuous backup 81 | 82 | Let's assume you will deploy the resources on aws - US east 1 region, and you will enable custom CloudFormation resources to provision DB users and roles through cloudformation (as seen [here](https://github.com/mongodb/mongodbatlas-cloudformation-resources)). 83 | You will deploy the solution on 2 AWS availability zones. 84 | 85 | You will then let it run for 1 month. Your charges would be calculated as follows: 86 | 87 | * 1 secret in AWS Secret Manager to manage the secret used by CloudFormation: $0.40 88 | * 1 AWS PrivateLink for secured backend connection, 2 availability zones: ~730 hours in a month * 2 0.01$ = 14.6$ 89 | * The rest of the services are free. 90 | 91 | Monthly total: **14.6$ + 0.40$ = 15$** 92 |
93 | 94 |
95 | Example 2: 1 Serverless instance, 1 availability zone and monthly data transfer of around 1GB and enabling continuous backup 96 | 97 | Let's assume you will deploy the resources on aws - US east 1 region, and you will not enable custom CloudFormation resources.. 98 | You will deploy the solution on 1 AWS availability zone, enable continuous backup, and will transfer around 1GB of data in a month (write only). 99 | 100 | You will then let it run for 1 month. Your charges would be calculated as follows: 101 | 102 | * 1 AWS PrivateLink for secured backend connection, 1 availability zones: ~730 hours in a month * 2 0.01$ = 7.3$ 103 | * 1GB of data transfer: 0.10$ per GB = 0.01$ per GB on same region: 0.01$ 104 | * 1GB of write processing units on atlas: ~1.25$ per 1 million WPU = ~1.31$ 105 | * 1GB of data transfer using data-api (if used) - Free (on monthly free tier) 106 | * Continuous backup for 1GB: around 0.20$ per GB = 0.20$ 107 | 108 | Monthly total: **7.3$ + 0.01$ + 1.31$ + 0.20$ = 8.82** 109 |
110 | 111 | | Service | Cost | Description | Links | 112 | |-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|-------| 113 | | AWS Secret Manager | $0.40 | Secure storage for Atlas credentials for use with third-party CloudFormation templates. | [Pricing Details](https://aws.amazon.com/secrets-manager/pricing/) | 114 | | Third-Party AWS CloudFormation Operations | Free for enabling and for 1000 handler operations per month | Use MongoDB Atlas custom resources like `MongoDB::Atlas::CustomDBRole` and `MongoDB::Atlas::DatabaseUser` in CloudFormation. | [Pricing Details](https://aws.amazon.com/cloudformation/pricing/) | 115 | | AWS IAM | Free | Enables CloudFormation access to MongoDB credentials in Secret Manager and resource provisioning in Atlas. | - | 116 | | AWS SSM Parameter Store | Free for standard parameters | Stores Atlas URLs for easy access by other services. | [Pricing Details](https://aws.amazon.com/systems-manager/pricing/#Parameter_Store) | 117 | | AWS Interface Endpoint | $0.01 per AZ per hour, then depending on usage. | Connects backend Lambdas to MongoDB using private subnets without the Data API. | [Pricing Details](https://aws.amazon.com/privatelink/pricing/#Interface_Endpoint_pricing) | 118 | | MongoDB Atlas Serverless Instance | Free for the instance, then variable according to the usage. | Free for the instance; data usage determines additional costs. | [Pricing Details](https://www.mongodb.com/docs/atlas/billing/serverless-instance-costs/) | 119 | | MongoDB Atlas Data API | Free tier: 1,000,000 requests or 500 hours of compute or 10,000 hours of sync runtime (whichever occurs first), 10GB of data transfer, Then according to the usage. | Shared monthly free tier across all App Services Apps in a project. | [Pricing Details](https://www.mongodb.com/docs/atlas/app-services/billing/) | 120 | | MongoDB Atlas Private Endpoint for Serverless | Free | Connects to AWS private endpoints. | [Pricing Details](https://www.mongodb.com/docs/atlas/billing/additional-services/#private-endpoints-for-serverless-instances) | 121 | | MongoDB Atlas Continuous backup | Range from $0.20 to $0.60 per GB per month. | Continious backup for your instances | [Pricing Details](https://www.mongodb.com/docs/atlas/billing/serverless-instance-costs/) | 122 | 123 | 124 | 125 | ## Sample AWS Lambda configuration that can use either private connection or data-api 126 | This is a [Serverless Framework](https://www.serverless.com/) example, the AWS Lambda must be in a VPC in order to access mongo. 127 | * Note that CloudFormation and atlas has some race condition issues - so we should create the resources one by one (with depends on the previous one) 128 | 129 | > When using data-api in the AWS Lambda, DB user and role are not needed as the current JWT credentials of the user will be used instead of the AWS Lambda role. 130 | 131 | 132 | ```yaml 133 | anchors: 134 | vpc: &vpc 135 | securityGroupIds: 136 | - ${ssm:/${self:custom.stage}/infra/security_groups/security_group_that_can_access_mongo_private_endpoint, ''} 137 | subnetIds: > 138 | mongoEnvironmentVars: &mongoEnvironmentVars 139 | CONNECTION_STRING_SSM_URL: /${self:custom.stage}/infra/mongodb/jit/private-endpoint/connection-string 140 | MONGO_API_URL_PATH: /${self:custom.stage}/infra/mongodb/data-api/url 141 | mongo: # Some common properties for mongo 142 | commonUserProperties: &commonUserProperties 143 | AWSIAMType: ROLE 144 | ProjectId: ${ssm:/${self:custom.env_name}/infra/mongodb/project-id, ''} 145 | Profile: ${ssm:/${self:custom.env_name}/infra/mongodb/organization-id, ''} 146 | DatabaseName: "$external" 147 | commonRoleProperties: &commonRoleProperties 148 | ProjectId: ${ssm:/${self:custom.env_name}/infra/mongodb/project-id, ''} 149 | Profile: ${ssm:/${self:custom.env_name}/infra/mongodb/organization-id, ''} 150 | 151 | 152 | functions: 153 | my-backend-lambda: 154 | handler: handler 155 | vpc: *vpc 156 | environment: *mongoEnvironmentVars 157 | iamRoleStatementsName: get-docs-role 158 | 159 | Resources: 160 | MongoGetRole: 161 | Type: MongoDB::Atlas::CustomDBRole 162 | Properties: 163 | <<: *commonRoleProperties 164 | RoleName: atlas-get-docs-role 165 | Action: FIND 166 | Resources: 167 | - Collection: col 168 | DB: db 169 | 170 | GetDocsUser: 171 | Type: MongoDB::Atlas::DatabaseUser 172 | Properties: 173 | <<: *commonUserProperties 174 | Username: arn:aws:iam::${aws:accountId}:role/-get-docs-role 175 | Roles: 176 | - RoleName: atlas-get-docs-role 177 | DatabaseName: "admin" 178 | DependsOn: [ MongoGetRole ] 179 | 180 | ``` 181 | > **Important caveat here** - While working with CloudFormation, there’s a race condition on those resources, so if they are created in parallel (even for different lambdas) - the deployment might fail. To currently solve it, make sure each resource depends on the previous atlas one (a_role -> a_user -> b_role -> b_lambda) 182 | 183 | ## Technical implementation details 184 | 185 | ### Backend connectivity 186 | For Lambdas Without Data API and JWT Requirements: 187 | 188 | - **Objective:** Connect to MongoDB Atlas serverless via a private endpoint. 189 | - **Method:** Use VPC private endpoint creation and security group access. 190 | 191 | ![Backend architecture](./images/backend.jpeg) 192 | 193 | - **Implementation Steps:** 194 | 1. Create a [CustomDBRole and DatabaseUser](#sample-aws-lambda-configuration-that-can-use-either-private-connection-or-data-api), linking them to the IAM role of the target Lambda/container. 195 | 2. Ensure Lambda/container is within the VPC, private subnets, and `aws_allowed_access_security_groups` includes the resources' security groups. 196 | 3. Use SSM parameter for the connection string: `//infra/mongodb//private-endpoint/connection-string`. 197 | 4. Example [pymongo](https://pymongo.readthedocs.io/en/stable/) connection: 198 | ```python 199 | client = InternalMongoClient(f"{connection_string}/?authSource=%24external&authMechanism=MONGODB-AWS&retryWrites=true&w=majority") 200 | my_collection = client["db"]["col"] 201 | ``` 202 | 203 | ### User facing APIs connectivity 204 | **For User-Triggered Lambdas:** 205 | 206 | - **Objective:** Utilize the Data API for MongoDB Atlas serverless connections - imposing tenant separation using existing users JWT. 207 | - **Method:** Implement IP Whitelisting due to the absence of private connectivity. 208 | ![Frontend/API architecture](./images/APIfrontend.jpeg) 209 | 210 | - **Implementation Steps:** 211 | 1. Ensure Lambda/container uses VPC and NAT GWs for outbound IPs. 212 | 2. Add required IPs to `ip_whitelist` and enable `add_mongo_ips_access_to_data_api`. 213 | 3. Base URL for Data API calls is fetched from SSM: `//infra/mongodb/data-api/url`. 214 | 4. REST API usage example with JWT: 215 | ```bash 216 | curl -X POST "/action/findOne" \ 217 | -H "jwtTokenString: token" \ 218 | -H "Content-Type: application/json" \ 219 | -d '' 220 | ``` 221 | 5. **Retry Logic for Unauthorized Responses:** Implement a decorator for retrying Data API requests encountering 401 errors, due to potential user provisioning delays. This includes a retry count of 3 with a 1-second pause between attempts. 222 | ``` python 223 | DATA_API_UNAUTHORIZED_REQUESTS_RETRY_COUNT = 3 224 | 225 | def retry_mongo_data_api_unauthorized_responses() -> Callable[..., Callable[..., Any]]: 226 | """ 227 | This decorator is used to retry mongo data api requests that return 401 (Unauthorized) responses. 228 | This is used because of a known race condition when creating a new 'user' in mongo side. 229 | We retry 3 times with 1 seconds sleep between each try. 230 | """ 231 | 232 | def func_wrapper(func: Callable[..., Any]) -> Callable[..., Any]: 233 | @functools.wraps(func) 234 | def wrapper(*args: Any, **kwargs: Any) -> Any: 235 | for i in range(DATA_API_UNAUTHORIZED_REQUESTS_RETRY_COUNT): 236 | try: 237 | return func(*args, **kwargs) 238 | except HTTPError as e: 239 | if e.response.status_code == HTTPStatus.UNAUTHORIZED: 240 | logger.error(f"Failed to get data from mongo data api, retrying. Error: {e}") 241 | sleep(1) 242 | continue 243 | raise e 244 | 245 | return wrapper 246 | 247 | return func_wrapper 248 | ``` 249 | 250 | 251 | 252 | ### Shell scripts inside the code 253 | This section outlines the use of shell scripts for configuring the Data API through the [Shell provider](https://registry.terraform.io/providers/scottwinkler/shell/latest/docs) resource. The scripts enable CRUD operations by setting the resource state through environment queries, ensuring alignment with the saved local state. Any detected discrepancies trigger an update script to align the target environment. 254 | 255 | #### Shell Scripts: enable-data-api 256 | The scripts enable the data-api configuration for a specific project, in a designated region with LOCAL deployment model. 257 | 258 | - **client_id**: The Data API client's unique identifier. 259 | - **data_api_configurations**: Encoded configuration for the Data API, detailing versions, system execution permissions, validation techniques, etc. 260 | - **data_api_id**: The Data API's unique ID. 261 | 262 | State parameters: 263 | 264 | ``` json 265 | { 266 | "client_id": "data-xxxxx", 267 | "data_api_configurations": "eyJ2ZXJzaW9ucyI6WyJ2MSJdLCJydW5fYXNfc3lzdGVtIjpmYWxzZSwicnVuX2FzX3VzZXJfaWQiOiIiLCJydW5fYXNfdXNlcl9pZF9zY3JpcHRfc291cmNlIjoiIiwibG9nX2Z1bmN0aW9uX2FyZ3VtZW50cyI6ZmFsc2UsImRpc2FibGVkIjpmYWxzZSwidmFsaWRhdGlvbl9tZXRob2QiOiJOT19WQUxJREFUSU9OIiwic2VjcmV0X25hbWUiOiIiLCJmZXRjaF9jdXN0b21fdXNlcl9kYXRhIjpmYWxzZSwiY3JlYXRlX3VzZXJfb25fYXV0aCI6dHJ1ZSwicmV0dXJuX3R5cGUiOiJKU09OIn0K", 268 | "data_api_id": "1234567890" 269 | } 270 | ``` 271 | 272 | #### Shell Script: configure-data-api-security 273 | Sets the IP whitelist for the Data API, allowing access to the designated IP addresses, Automatically adds the Nat Gateway IPs to the whitelist. 274 | 275 | - **allowed_ips**: The IP addresses allowed to access the Data API. 276 | 277 | ``` json 278 | { 279 | "allowed_ips": "3.209.100.200,1.1.1.1/32" 280 | } 281 | ``` 282 | 283 | #### Shell Script: configure-data-api 284 | The scripts configure data-api per serverless instance, adding the JWT configurations along with tenant separation ruleset. 285 | 286 | State parameters: 287 | - **default_rule_result**: Default rule set for tenant separation across collections. 288 | - **jwt_provider_result**: JWT provider config, detailing audience, public key, and field mappings. 289 | - **secret_id**, **secret_name**: Identify the public key used by your JWT provider. 290 | - **service_id**, **service_name**: Designate the serverless instance with Data API enabled. 291 | 292 | ``` json 293 | { 294 | "default_rule_result": { 295 | "_id": "xxxxxxx", 296 | "filters": [ 297 | { 298 | "name": "tenant id filter", 299 | "query": { 300 | "tenant_id": "%%user.data.tenantId" 301 | }, 302 | "apply_when": { 303 | "%%true": true 304 | } 305 | } 306 | ], 307 | "roles": [ 308 | { 309 | "name": "readAccessDataAPI", 310 | "apply_when": {}, 311 | "read": true, 312 | "write": true, 313 | "insert": true, 314 | "delete": true, 315 | "search": true 316 | } 317 | ] 318 | }, 319 | "jwt_provider_result": { 320 | "_id": "yyyyyyyyy", 321 | "name": "custom-token", 322 | "type": "custom-token", 323 | "metadata_fields": [ 324 | { 325 | "required": true, 326 | "name": "tenantId", 327 | "field_name": "tenantId" 328 | }, 329 | { 330 | "required": false, 331 | "name": "email", 332 | "field_name": "name" 333 | } 334 | ], 335 | "config": { 336 | "audience": [ 337 | "aud" 338 | ], 339 | "requireAnyAudience": true, 340 | "signingAlgorithm": "RS256", 341 | "useJWKURI": false 342 | }, 343 | "secret_config": { 344 | "signingKeys": [ 345 | "jwt-public-key" 346 | ] 347 | }, 348 | "disabled": false 349 | }, 350 | "secret_id": "1234567890", 351 | "secret_name": "jwt_public_key", 352 | "service_id": "1234567890", 353 | "service_name": "my-instance" 354 | } 355 | ``` -------------------------------------------------------------------------------- /images/APIfrontend.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsecurity/mongodb-atlas-aws-terraform/6a5211608701ecbec1252cc146d15ce2f37765a9/images/APIfrontend.jpeg -------------------------------------------------------------------------------- /images/backend.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsecurity/mongodb-atlas-aws-terraform/6a5211608701ecbec1252cc146d15ce2f37765a9/images/backend.jpeg -------------------------------------------------------------------------------- /images/mongo-and-aws.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsecurity/mongodb-atlas-aws-terraform/6a5211608701ecbec1252cc146d15ce2f37765a9/images/mongo-and-aws.jpeg -------------------------------------------------------------------------------- /images/termination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsecurity/mongodb-atlas-aws-terraform/6a5211608701ecbec1252cc146d15ce2f37765a9/images/termination.png -------------------------------------------------------------------------------- /modules/cf_public_extension/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "extension-activation-role" { 2 | name = "${var.policy_name}-role" 3 | 4 | assume_role_policy = jsonencode({ 5 | Version : "2012-10-17" 6 | Statement : [ 7 | { 8 | Effect : "Allow" 9 | Principal : { 10 | Service : ["resources.cloudformation.amazonaws.com"] 11 | } 12 | Action : "sts:AssumeRole" 13 | } 14 | ] 15 | }) 16 | 17 | path = "/" 18 | 19 | max_session_duration = 8400 20 | } 21 | 22 | resource "aws_iam_policy" "extension-activator-policy" { 23 | name = var.policy_name 24 | 25 | policy = jsonencode({ 26 | Version : "2012-10-17" 27 | Statement : [ 28 | { 29 | Effect : "Allow" 30 | Action : var.iam_actions 31 | Resource : var.iam_resources 32 | } 33 | ] 34 | }) 35 | } 36 | 37 | resource "aws_iam_role_policy_attachment" "extension-activator-role-policy-attachment" { 38 | policy_arn = aws_iam_policy.extension-activator-policy.arn 39 | role = aws_iam_role.extension-activation-role.name 40 | } 41 | 42 | resource "shell_script" "cf_activation_script" { 43 | for_each = toset(var.custom_resources_types) 44 | lifecycle_commands { 45 | create = "aws cloudformation activate-type --type RESOURCE --type-name ${each.value} --execution-role-arn ${aws_iam_role.extension-activation-role.arn} --publisher-id ${var.publisher_id} >/dev/null" 46 | update = "aws cloudformation activate-type --type RESOURCE --type-name ${each.value} --execution-role-arn ${aws_iam_role.extension-activation-role.arn} --publisher-id ${var.publisher_id} >/dev/null" 47 | read = "aws cloudformation describe-type --type RESOURCE --type-name ${each.value}" 48 | delete = "aws cloudformation deactivate-type --type RESOURCE --type-name ${each.value}" 49 | } 50 | 51 | //sets current working directory 52 | working_directory = path.module 53 | 54 | depends_on = [aws_iam_policy.extension-activator-policy, aws_iam_role.extension-activation-role, aws_iam_role_policy_attachment.extension-activator-role-policy-attachment] 55 | } 56 | -------------------------------------------------------------------------------- /modules/cf_public_extension/variables.tf: -------------------------------------------------------------------------------- 1 | variable "iam_actions" { 2 | type = list(string) 3 | description = "IAM permissions required by CloudFormation resources in order to provision the resources" 4 | } 5 | 6 | variable "iam_resources" { 7 | type = list(string) 8 | description = "IAM permissions required by CloudFormation resources in order to provision the resources" 9 | } 10 | 11 | variable "publisher_id" { 12 | type = string 13 | description = "extension publisher ID, usually can be retrieved using console (go into a resource and check the URL)" 14 | } 15 | variable "custom_resources_types" { 16 | type = list(string) 17 | description = "List of custom resource to provision, for example - ['MongoDB::Atlas::CustomDBRole]" 18 | } 19 | 20 | variable "policy_name" { 21 | type = string 22 | description = "policy name that provisions the resources" 23 | } -------------------------------------------------------------------------------- /modules/cf_public_extension/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | shell = { 8 | source = "scottwinkler/shell" 9 | version = "1.7.10" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/mongoatlas/alerts.tf: -------------------------------------------------------------------------------- 1 | # All alerts configurations can be seen here: 2 | # https://www.mongodb.com/docs/atlas/reference/api-resources-spec/v2/#tag/Alert-Configurations/operation/createAlertConfiguration 3 | # It's possible to support all sorts of notifications targets (slack, etc..) - we use email here for simplicity 4 | 5 | # This in case data-api returns a rate limit 6 | module "alert_app_services_rate_limit" { 7 | source = "../mongoatlas_alert" 8 | project_id = mongodbatlas_project.main-project.id 9 | event_type = "REQUEST_RATE_LIMIT" 10 | email = var.notification_email 11 | } 12 | 13 | # in case daily billing is bigger than expected (let's say 6 dollars) 14 | module "alert_daily_bill" { 15 | source = "../mongoatlas_alert" 16 | project_id = mongodbatlas_project.main-project.id 17 | event_type = "DAILY_BILL_OVER_THRESHOLD" 18 | threshold = var.daily_price_threshold_alert 19 | alert_interval_minutes = 1440 # one day 20 | email = var.notification_email 21 | } 22 | 23 | 24 | # monthly price alert 25 | module "alert_monthly_bill" { 26 | source = "../mongoatlas_alert" 27 | project_id = mongodbatlas_project.main-project.id 28 | event_type = "PENDING_INVOICE_OVER_THRESHOLD" 29 | threshold = var.monthly_price_threshold 30 | alert_interval_minutes = 10080 # one week 31 | email = var.notification_email 32 | } 33 | 34 | # if 80% of connections are used (500 are max) 35 | module "alert_above_80_percent_connections" { 36 | source = "../mongoatlas_alert" 37 | project_id = mongodbatlas_project.main-project.id 38 | event_type = "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" 39 | metric_name = "SERVERLESS_CONNECTIONS_PERCENT" 40 | threshold = 80 41 | email = var.notification_email 42 | } 43 | 44 | # if 75% of disk was used - there's a cap of 1TB per serverless cluster 45 | module "alert_disk_space_getting_full" { 46 | source = "../mongoatlas_alert" 47 | project_id = mongodbatlas_project.main-project.id 48 | event_type = "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" 49 | metric_name = "SERVERLESS_DATA_SIZE_TOTAL" 50 | threshold = 0.75 51 | threshold_type = "TERABYTES" 52 | email = var.notification_email 53 | } 54 | 55 | # too many reads 56 | module "alert_too_many_rpus" { 57 | source = "../mongoatlas_alert" 58 | project_id = mongodbatlas_project.main-project.id 59 | event_type = "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" 60 | metric_name = "SERVERLESS_TOTAL_READ_UNITS" 61 | threshold = 1 62 | alert_interval_minutes = 120 63 | delay_minutes = 5 64 | threshold_type = "MILLION_RPU" 65 | email = var.notification_email 66 | } 67 | 68 | # too many reads - lower threshold 69 | module "alert_too_many_rpus_lower_threshold" { 70 | source = "../mongoatlas_alert" 71 | project_id = mongodbatlas_project.main-project.id 72 | event_type = "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" 73 | metric_name = "SERVERLESS_TOTAL_READ_UNITS" 74 | threshold = 0.25 75 | alert_interval_minutes = 720 76 | delay_minutes = 30 77 | threshold_type = "MILLION_RPU" 78 | email = var.notification_email 79 | } 80 | 81 | 82 | # too many writes 83 | module "alert_too_many_wpus" { 84 | source = "../mongoatlas_alert" 85 | project_id = mongodbatlas_project.main-project.id 86 | event_type = "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" 87 | metric_name = "SERVERLESS_TOTAL_WRITE_UNITS" 88 | threshold = 1 89 | threshold_type = "MILLION_WPU" 90 | email = var.notification_email 91 | } 92 | 93 | # default alerts came with the projects - defining here to include slack notifications 94 | # ========================================================================== 95 | 96 | # Alert for replication oplog window running out - triggered when the value is LESS_THAN 1 hours 97 | module "alert_replication_oplog_window_running_out" { 98 | source = "../mongoatlas_alert" 99 | project_id = mongodbatlas_project.main-project.id 100 | event_type = "REPLICATION_OPLOG_WINDOW_RUNNING_OUT" 101 | threshold = 1 102 | required_operator = "LESS_THAN" 103 | threshold_type = "HOURS" 104 | delay_minutes = 0 105 | email = var.notification_email 106 | } 107 | 108 | # Alert for no primary 109 | module "alert_no_primary" { 110 | source = "../mongoatlas_alert" 111 | project_id = mongodbatlas_project.main-project.id 112 | event_type = "NO_PRIMARY" 113 | delay_minutes = 15 114 | email = var.notification_email 115 | } 116 | 117 | # Alert for cluster mongos is missing 118 | module "alert_cluster_mongos_is_missing" { 119 | source = "../mongoatlas_alert" 120 | project_id = mongodbatlas_project.main-project.id 121 | event_type = "CLUSTER_MONGOS_IS_MISSING" 122 | delay_minutes = 15 123 | email = var.notification_email 124 | } 125 | 126 | # Alert for outside metric threshold - triggered based on the connections percent when the value is GREATER_THAN 80.0 raw 127 | module "alert_outside_metric_threshold_connections_percent" { 128 | source = "../mongoatlas_alert" 129 | project_id = mongodbatlas_project.main-project.id 130 | event_type = "OUTSIDE_METRIC_THRESHOLD" 131 | metric_name = "CONNECTIONS_PERCENT" 132 | threshold = 80.0 133 | required_operator = "GREATER_THAN" 134 | threshold_type = "RAW" 135 | delay_minutes = 0 136 | email = var.notification_email 137 | } 138 | 139 | # Alert for outside metric threshold - triggered based on the disk partition space used data when the value is GREATER_THAN 90.0 raw 140 | module "alert_outside_metric_threshold_disk_partition_space_used_data" { 141 | source = "../mongoatlas_alert" 142 | project_id = mongodbatlas_project.main-project.id 143 | event_type = "OUTSIDE_METRIC_THRESHOLD" 144 | metric_name = "DISK_PARTITION_SPACE_USED_DATA" 145 | threshold = 90.0 146 | required_operator = "GREATER_THAN" 147 | threshold_type = "RAW" 148 | delay_minutes = 0 149 | email = var.notification_email 150 | } 151 | 152 | # Alert for outside metric threshold - triggered based on the query targeting scanned objects per returned when the value is GREATER_THAN 1000.0 raw 153 | module "alert_outside_metric_threshold_query_targeting_scanned_objects_per_returned" { 154 | source = "../mongoatlas_alert" 155 | project_id = mongodbatlas_project.main-project.id 156 | event_type = "OUTSIDE_METRIC_THRESHOLD" 157 | metric_name = "QUERY_TARGETING_SCANNED_OBJECTS_PER_RETURNED" 158 | threshold = 1000.0 159 | required_operator = "GREATER_THAN" 160 | threshold_type = "RAW" 161 | delay_minutes = 10 162 | email = var.notification_email 163 | } 164 | 165 | # Alert for credit card about to expire 166 | module "alert_credit_card_about_to_expire" { 167 | source = "../mongoatlas_alert" 168 | project_id = mongodbatlas_project.main-project.id 169 | event_type = "CREDIT_CARD_ABOUT_TO_EXPIRE" 170 | delay_minutes = 0 171 | alert_interval_minutes = 1440 172 | email = var.notification_email 173 | } 174 | 175 | # Alert for outside metric threshold - triggered based on the normalized system cpu user when the value is GREATER_THAN 95.0 raw 176 | module "alert_outside_metric_threshold_normalized_system_cpu_user" { 177 | source = "../mongoatlas_alert" 178 | project_id = mongodbatlas_project.main-project.id 179 | event_type = "OUTSIDE_METRIC_THRESHOLD" 180 | metric_name = "NORMALIZED_SYSTEM_CPU_USER" 181 | threshold = 95.0 182 | required_operator = "GREATER_THAN" 183 | threshold_type = "RAW" 184 | delay_minutes = 0 185 | email = var.notification_email 186 | } 187 | 188 | # Alert for host has index suggestions 189 | module "alert_host_has_index_suggestions" { 190 | source = "../mongoatlas_alert" 191 | project_id = mongodbatlas_project.main-project.id 192 | event_type = "HOST_HAS_INDEX_SUGGESTIONS" 193 | delay_minutes = 10 194 | email = var.notification_email 195 | } 196 | 197 | # Alert for host mongot crashing oom 198 | module "alert_host_mongot_crashing_oom" { 199 | source = "../mongoatlas_alert" 200 | project_id = mongodbatlas_project.main-project.id 201 | event_type = "HOST_MONGOT_CRASHING_OOM" 202 | delay_minutes = 0 203 | email = var.notification_email 204 | } 205 | 206 | # Alert for host not enough disk space 207 | module "alert_host_not_enough_disk_space" { 208 | source = "../mongoatlas_alert" 209 | project_id = mongodbatlas_project.main-project.id 210 | event_type = "HOST_NOT_ENOUGH_DISK_SPACE" 211 | delay_minutes = 0 212 | email = var.notification_email 213 | } 214 | 215 | # Alert for outside metric threshold - triggered based on the search max number of lucene docs when the value is GREATER_THAN 1.0 billion 216 | module "alert_outside_metric_threshold_search_max_number_of_lucene_docs" { 217 | source = "../mongoatlas_alert" 218 | project_id = mongodbatlas_project.main-project.id 219 | event_type = "OUTSIDE_METRIC_THRESHOLD" 220 | metric_name = "SEARCH_MAX_NUMBER_OF_LUCENE_DOCS" 221 | threshold = 1.0 222 | required_operator = "GREATER_THAN" 223 | threshold_type = "BILLION" 224 | delay_minutes = 0 225 | email = var.notification_email 226 | } 227 | 228 | # Alert for joined group 229 | module "alert_joined_group" { 230 | source = "../mongoatlas_alert" 231 | project_id = mongodbatlas_project.main-project.id 232 | event_type = "JOINED_GROUP" 233 | delay_minutes = 0 234 | email = var.notification_email 235 | } 236 | 237 | # Alert for trigger failure 238 | module "alert_trigger_failure" { 239 | source = "../mongoatlas_alert" 240 | project_id = mongodbatlas_project.main-project.id 241 | event_type = "TRIGGER_FAILURE" 242 | delay_minutes = 0 243 | email = var.notification_email 244 | } 245 | 246 | # Alert for trigger auto resumed 247 | module "alert_trigger_auto_resumed" { 248 | source = "../mongoatlas_alert" 249 | project_id = mongodbatlas_project.main-project.id 250 | event_type = "TRIGGER_AUTO_RESUMED" 251 | delay_minutes = 0 252 | email = var.notification_email 253 | } 254 | 255 | # Alert for sync failure 256 | module "alert_sync_failure" { 257 | source = "../mongoatlas_alert" 258 | project_id = mongodbatlas_project.main-project.id 259 | event_type = "SYNC_FAILURE" 260 | delay_minutes = 0 261 | email = var.notification_email 262 | } 263 | 264 | # Alert for log forwarder failure 265 | module "alert_log_forwarder_failure" { 266 | source = "../mongoatlas_alert" 267 | project_id = mongodbatlas_project.main-project.id 268 | event_type = "LOG_FORWARDER_FAILURE" 269 | delay_minutes = 0 270 | email = var.notification_email 271 | } 272 | 273 | # Alert for fts index deletion failed 274 | module "alert_fts_index_deletion_failed" { 275 | source = "../mongoatlas_alert" 276 | project_id = mongodbatlas_project.main-project.id 277 | event_type = "FTS_INDEX_DELETION_FAILED" 278 | delay_minutes = 0 279 | email = var.notification_email 280 | } 281 | 282 | # Alert for fts index build complete 283 | module "alert_fts_index_build_complete" { 284 | source = "../mongoatlas_alert" 285 | project_id = mongodbatlas_project.main-project.id 286 | event_type = "FTS_INDEX_BUILD_COMPLETE" 287 | delay_minutes = 0 288 | email = var.notification_email 289 | } 290 | 291 | # Alert for fts index build failed 292 | module "alert_fts_index_build_failed" { 293 | source = "../mongoatlas_alert" 294 | project_id = mongodbatlas_project.main-project.id 295 | event_type = "FTS_INDEX_BUILD_FAILED" 296 | delay_minutes = 0 297 | email = var.notification_email 298 | } 299 | 300 | # Alert for fts indexes restore failed 301 | module "alert_fts_indexes_restore_failed" { 302 | source = "../mongoatlas_alert" 303 | project_id = mongodbatlas_project.main-project.id 304 | event_type = "FTS_INDEXES_RESTORE_FAILED" 305 | delay_minutes = 0 306 | email = var.notification_email 307 | } 308 | 309 | # Alert for outside metric threshold - triggered based on the system memory percent used when the value is GREATER_THAN 90.0 raw 310 | module "alert_outside_metric_threshold_system_memory_percent_used" { 311 | source = "../mongoatlas_alert" 312 | project_id = mongodbatlas_project.main-project.id 313 | event_type = "OUTSIDE_METRIC_THRESHOLD" 314 | metric_name = "SYSTEM_MEMORY_PERCENT_USED" 315 | threshold = 90.0 316 | required_operator = "GREATER_THAN" 317 | threshold_type = "RAW" 318 | delay_minutes = 60 319 | alert_interval_minutes = 10080 320 | email = var.notification_email 321 | } 322 | 323 | # Alert for outside metric threshold - triggered based on the max normalized system cpu user when the value is GREATER_THAN 90.0 raw 324 | module "alert_outside_metric_threshold_max_normalized_system_cpu_user" { 325 | source = "../mongoatlas_alert" 326 | project_id = mongodbatlas_project.main-project.id 327 | event_type = "OUTSIDE_METRIC_THRESHOLD" 328 | metric_name = "MAX_NORMALIZED_SYSTEM_CPU_USER" 329 | threshold = 90.0 330 | required_operator = "GREATER_THAN" 331 | threshold_type = "RAW" 332 | delay_minutes = 60 333 | alert_interval_minutes = 10080 334 | email = var.notification_email 335 | } 336 | 337 | -------------------------------------------------------------------------------- /modules/mongoatlas/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} # data.aws_caller_identity.current.account_id 2 | data "aws_region" "current" {} # data.aws_region.current.name 3 | data "aws_nat_gateways" "nat_gateways" { 4 | vpc_id = var.aws_vpc_id 5 | 6 | filter { 7 | name = "state" 8 | values = ["available"] 9 | } 10 | } 11 | 12 | data "aws_nat_gateway" "example" { 13 | for_each = toset(data.aws_nat_gateways.nat_gateways.ids) 14 | id = each.value 15 | } -------------------------------------------------------------------------------- /modules/mongoatlas/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # This maps aws regions to mongo locations. 3 | aws_region_to_atlas_location_map = { 4 | "us-east-1" = "US-VA" 5 | "us-west-2" = "US-OR" 6 | "eu-central-1" = "DE-FF" 7 | "eu-west-1" = "IE" 8 | "ap-southeast-2" = "AU" 9 | "ap-south-1" = "IN-MB" 10 | "ap-southeast-1" = "SG" 11 | "sa-east-1" = "BR-SP" 12 | } 13 | mongo_atlas_api_base_admin_URL = "https://realm.mongodb.com/api/admin/v3.0" 14 | aws_account_id = data.aws_caller_identity.current.account_id 15 | aws_region = data.aws_region.current.name 16 | nat_gw_public_ips = [for _, ngw in data.aws_nat_gateway.example : ngw.public_ip] 17 | } -------------------------------------------------------------------------------- /modules/mongoatlas/main.tf: -------------------------------------------------------------------------------- 1 | # This is the main project to create under an org, project per env. 2 | resource "mongodbatlas_project" "main-project" { 3 | name = var.stage 4 | org_id = var.organization_id 5 | is_collect_database_specifics_statistics_enabled = true 6 | is_data_explorer_enabled = true 7 | is_performance_advisor_enabled = true 8 | is_realtime_performance_panel_enabled = true 9 | is_schema_advisor_enabled = true 10 | with_default_alerts_settings = false # as we manage the alerts ourselves to allow adding notification targets 11 | } 12 | 13 | # In order to connect to the DBs (from compass), IP cidr is needed. 14 | resource "mongodbatlas_project_ip_access_list" "mongo_ip_access_list" { 15 | project_id = mongodbatlas_project.main-project.id 16 | cidr_block = element(var.mongo_ip_access_list, count.index).cidr 17 | comment = element(var.mongo_ip_access_list, count.index).description 18 | count = length(var.mongo_ip_access_list) 19 | depends_on = [mongodbatlas_project.main-project] 20 | } 21 | 22 | 23 | # This script enables data API (to be used from customer facing APIs). 24 | # NOTE - this resource is a bit weird, if you want to change the script to support something new, do the following: 25 | # change all scripts all together to support the new change: 26 | # 1. create/update - should perform both creation and update (so if it runs several times, it aligns the environment to what you need 27 | # 2. get - to save in state the wanted configuration, terraform will call this each apply and if there's a drift - will call create/update script 28 | # 3. delete - in case of deletion when resource is deleted 29 | # after doing the change, the apply will NOT trigger an update, but WILL CHANGE THE STATE. 30 | # this is unavoidable, in order to trigger a real update change, you will need to change the environment section. 31 | # this can be done only after scripts are deployed. 32 | # add VERSION env var (or change it if it's there) - and then apply again, update will be trigger and align your environment. 33 | # so - 2 deploys will be needed (first scripts, then change a dummy env var) 34 | resource "shell_script" "enable-data-api" { 35 | environment = { 36 | VERSION = 1 37 | PROJECT_ID = mongodbatlas_project.main-project.id 38 | AWS_REGION = var.aws_region 39 | MONGO_LOCATION = local.aws_region_to_atlas_location_map[var.aws_region] 40 | ATLAS_ADMIN_BASE_API_PATH = local.mongo_atlas_api_base_admin_URL 41 | } 42 | 43 | interpreter = ["/bin/bash", "-c"] 44 | 45 | lifecycle_commands { 46 | create = file("${path.module}/scripts/create_update_data_api.sh") 47 | update = file("${path.module}/scripts/create_update_data_api.sh") 48 | read = file("${path.module}/scripts/get_data_api.sh") 49 | delete = file("${path.module}/scripts/delete_data_api.sh") 50 | } 51 | 52 | working_directory = path.module 53 | } 54 | 55 | 56 | resource "shell_script" "configure-data-api-security" { 57 | environment = { 58 | VERSION = 1 59 | PROJECT_ID = mongodbatlas_project.main-project.id 60 | ATLAS_ADMIN_BASE_API_PATH = local.mongo_atlas_api_base_admin_URL 61 | DATA_API_APP_ID = shell_script.enable-data-api.output["data_api_id"] 62 | AWS_NAT_GW_IPS = var.add_mongo_ips_access_to_data_api ? join(",", concat(local.nat_gw_public_ips, [for ip in var.mongo_ip_access_list : ip.cidr])) : join(",", local.nat_gw_public_ips) 63 | } 64 | 65 | interpreter = ["/bin/bash", "-c"] 66 | 67 | lifecycle_commands { 68 | create = file("${path.module}/scripts/create_update_data_api_security.sh") 69 | update = file("${path.module}/scripts/create_update_data_api_security.sh") 70 | read = file("${path.module}/scripts/get_data_api_security.sh") 71 | delete = file("${path.module}/scripts/delete_data_api_security.sh") 72 | } 73 | 74 | working_directory = path.module 75 | } 76 | 77 | # Save the data-api URL so we can later use it in the AWS Lambdas. 78 | resource "aws_ssm_parameter" "data-api-URL" { 79 | name = "/${var.stage}/infra/mongodb/data-api/url" 80 | type = "String" 81 | value = "https://${var.aws_region}.aws.data.mongodb-api.com/app/${shell_script.enable-data-api.output["client_id"]}/endpoint/data/v1" 82 | } 83 | 84 | 85 | # create the atlas instance per variable defined 86 | module "atlas_instance" { 87 | count = length(var.mongo_instances) 88 | source = "../mongoatlas_instance" 89 | stage = var.stage 90 | project_id = mongodbatlas_project.main-project.id 91 | instance_name = element(var.mongo_instances, count.index) 92 | aws_account_id = local.aws_account_id 93 | organization_id = var.organization_id 94 | aws_vpc_id = var.aws_vpc_id 95 | private_subnet_ids = var.private_subnet_ids 96 | aws_allowed_access_security_groups = var.aws_allowed_access_security_groups 97 | enable_termination_protection = var.enable_termination_protection 98 | jwt_audience = var.jwt_audience 99 | jwt_public_key = var.jwt_public_key 100 | data_api_id = shell_script.enable-data-api.output["data_api_id"] 101 | enable_continuous_backup = var.enable_continuous_backup 102 | tenant_id_field_in_jwt = var.tenant_id_field_in_jwt 103 | display_name_field_in_jwt = var.display_name_field_in_jwt 104 | } 105 | -------------------------------------------------------------------------------- /modules/mongoatlas/outputs.tf: -------------------------------------------------------------------------------- 1 | output "project_id" { 2 | value = mongodbatlas_project.main-project.id 3 | } 4 | -------------------------------------------------------------------------------- /modules/mongoatlas/scripts/create_update_data_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Script Name: Create data-api for a specific instance 4 | # Description: This script is responsible of: 5 | # 1. Create data-api application (app services) in mongoDB. 6 | # 2. enable it, while making sure create_user_on_auth=true (so JWT users will be created automatically) 7 | # 8 | # 9 | # Dependencies: curl, jq 10 | ################################################################################ 11 | 12 | # perform login and get access_token 13 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 14 | 15 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 16 | --header 'content-type: application/json' \ 17 | --data-raw "$AUTH") 18 | if [ $? -ne 0 ]; then 19 | echo "Login failed... $response" >&2 20 | exit 1 21 | fi 22 | 23 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 24 | 25 | echo "Login finished successfully." >&2 26 | 27 | function curl_with_auth () { 28 | # ---------------------------------- 29 | # performs curl request with mongo token 30 | # param1 - the url section (without the base URL) to perform 31 | # param2 - method 32 | # param3 - the body of the request 33 | # 34 | # returns: the response from curl 35 | # ---------------------------------- 36 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 37 | method="$2" 38 | data="$3" 39 | echo "SENDING CURL REQUEST - cont url $url" >&2 40 | response=$(curl -sSL --show-error --fail -X "$method" "$url" \ 41 | --header "authorization: Bearer $MONGO_TOKEN" \ 42 | --header 'content-type: application/json' \ 43 | --data-raw "$data" \ 44 | --compressed) 45 | 46 | if [ $? -ne 0 ]; then 47 | echo "$response" >&2 48 | exit 1 49 | fi 50 | 51 | echo "$response" 52 | } 53 | 54 | function try_get () { 55 | # ---------------------------------- 56 | # performs GET request, and tries to find specific key-value in the array of jsons or json. 57 | # if found, returns the relevant json. if not, empty string. 58 | # If no wanted key is supplied, we will just return the response, if empty array/json, will return empty result. 59 | # param1 - the url section (without the base URL) to perform 60 | # param2 - wanted key to search 61 | # param3 - wanted value to search 62 | # 63 | # returns: the relevant json. if not, empty string. 64 | # ---------------------------------- 65 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 66 | wanted_key=$2 67 | wanted_value=$3 68 | response=$(curl -sSL --show-error --fail -X "GET" "$url" \ 69 | --header "authorization: Bearer $MONGO_TOKEN" \ 70 | --header 'content-type: application/json') 71 | 72 | if [ $? -ne 0 ]; then 73 | echo "$response" >&2 74 | exit 1 75 | fi 76 | # If key was not specified 77 | if [ -z "$wanted_key" ]; then 78 | # check if response is an empty JSON object or an empty array 79 | if [[ $(echo "$response" | jq 'length') -eq 0 ]]; then 80 | echo "" 81 | else 82 | echo "$response" 83 | fi 84 | return 85 | fi 86 | 87 | # Search for the relevant JSON object 88 | if [[ "$response" == "["*"]" ]]; then 89 | # Array of JSON objects 90 | matching_objects=$(echo "$response" | jq -r "map(select(.[\"$wanted_key\"] == \"$wanted_value\"))") 91 | if [ "$(echo "$matching_objects" | jq length)" -eq 0 ]; then 92 | echo "" 93 | return 94 | fi 95 | echo "$matching_objects" | jq '.[0]' 96 | else 97 | # Single JSON object 98 | if [[ $(echo "$response" | jq -r ".$wanted_key") == "$wanted_value" ]]; then 99 | echo "$response" | jq . 100 | else 101 | echo "" 102 | fi 103 | fi 104 | } 105 | 106 | echo "Searching if data-api app exists: $PROJECT_ID" >&2 107 | path="groups/$PROJECT_ID/apps?product=data-api" 108 | res=$(try_get $path "product" "data-api") 109 | if [ -z "$res" ]; then 110 | # create data api app if one was not found. 111 | echo "Not found - Creating data api app for project id: $PROJECT_ID" >&2 112 | res=$(curl_with_auth "groups/$PROJECT_ID/apps?product=data-api" "POST" "{\"name\":\"data\",\"deployment_model\":\"LOCAL\",\"location\":\"$MONGO_LOCATION\",\"provider_region\":\"aws-$AWS_REGION\"}") 113 | if [ $? -ne 0 ]; then 114 | exit 1 115 | fi 116 | fi 117 | 118 | 119 | # Extract data api id and the client id 120 | for item in $(echo "${res}" | jq -r 'select(.product == "data-api") | ._id, .client_app_id'); do 121 | if [[ -z "$data_api_id" ]]; then 122 | data_api_id="$item" 123 | else 124 | client_app_id="$item" 125 | fi 126 | done 127 | 128 | echo "Searching for existing data-api configuration: $PROJECT_ID" >&2 129 | path="groups/$PROJECT_ID/apps/$data_api_id/data_api/config" 130 | method="POST" 131 | res=$(try_get $path) 132 | if [ -n "$res" ]; then 133 | echo "Found, Will update." >&2 134 | method="PATCH" 135 | fi 136 | 137 | # enable data API 138 | echo "Enabling data api for project id: $PROJECT_ID, data-api app id is: $data_api_id" >&2 139 | enable_res=$(curl_with_auth $path $method '{ 140 | "versions": [ 141 | "v1" 142 | ], 143 | "run_as_system": false, 144 | "run_as_user_id": "", 145 | "run_as_user_id_script_source": "", 146 | "disabled": false, 147 | "validation_method": "NO_VALIDATION", 148 | "secret_name": "", 149 | "respond_result": false, 150 | "fetch_custom_user_data": false, 151 | "create_user_on_auth": true, 152 | "return_type": "JSON", 153 | "log_function_arguments": false 154 | }') 155 | if [ $? -ne 0 ]; then 156 | exit 1 157 | fi 158 | -------------------------------------------------------------------------------- /modules/mongoatlas/scripts/create_update_data_api_security.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Script Name: configure data-api security preferences - currently just allow list IPs 4 | # Description: This script adjusts received IPs to the data-api 5 | # 6 | # 7 | # Dependencies: curl, jq 8 | ################################################################################ 9 | 10 | # perform login and get access_token 11 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 12 | 13 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 14 | --header 'content-type: application/json' \ 15 | --data-raw "$AUTH") 16 | if [ $? -ne 0 ]; then 17 | echo "Login failed... $response" >&2 18 | exit 1 19 | fi 20 | 21 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 22 | 23 | echo "Login finished successfully." >&2 24 | 25 | function curl_with_auth () { 26 | # ---------------------------------- 27 | # performs curl request with mongo token 28 | # param1 - the url section (without the base URL) to perform 29 | # param2 - method 30 | # param3 - the body of the request 31 | # 32 | # returns: the response from curl 33 | # ---------------------------------- 34 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 35 | method="$2" 36 | data="$3" 37 | echo "SENDING CURL REQUEST - cont url $url" >&2 38 | response=$(curl -sSL --show-error --fail -X "$method" "$url" \ 39 | --header "authorization: Bearer $MONGO_TOKEN" \ 40 | --header 'content-type: application/json' \ 41 | --data-raw "$data" \ 42 | --compressed) 43 | 44 | if [ $? -ne 0 ]; then 45 | echo "$response" >&2 46 | exit 1 47 | fi 48 | 49 | echo "$response" 50 | } 51 | 52 | function try_get () { 53 | # ---------------------------------- 54 | # performs GET request, and tries to find specific key-value in the array of jsons or json. 55 | # if found, returns the relevant json. if not, empty string. 56 | # If no wanted key is supplied, we will just return the response, if empty array/json, will return empty result. 57 | # param1 - the url section (without the base URL) to perform 58 | # param2 - wanted key to search 59 | # param3 - wanted value to search 60 | # 61 | # returns: the relevant json. if not, empty string. 62 | # ---------------------------------- 63 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 64 | wanted_key=$2 65 | wanted_value=$3 66 | response=$(curl -sSL --show-error --fail -X "GET" "$url" \ 67 | --header "authorization: Bearer $MONGO_TOKEN" \ 68 | --header 'content-type: application/json') 69 | 70 | if [ $? -ne 0 ]; then 71 | echo "$response" >&2 72 | exit 1 73 | fi 74 | # If key was not specified 75 | if [ -z "$wanted_key" ]; then 76 | # check if response is an empty JSON object or an empty array 77 | if [[ $(echo "$response" | jq 'length') -eq 0 ]]; then 78 | echo "" 79 | else 80 | echo "$response" 81 | fi 82 | return 83 | fi 84 | 85 | # Search for the relevant JSON object 86 | if [[ "$response" == "["*"]" ]]; then 87 | # Array of JSON objects 88 | matching_objects=$(echo "$response" | jq -r "map(select(.[\"$wanted_key\"] == \"$wanted_value\"))") 89 | if [ "$(echo "$matching_objects" | jq length)" -eq 0 ]; then 90 | echo "" 91 | return 92 | fi 93 | echo "$matching_objects" | jq '.[0]' 94 | else 95 | # Single JSON object 96 | if [[ $(echo "$response" | jq -r ".$wanted_key") == "$wanted_value" ]]; then 97 | echo "$response" | jq . 98 | else 99 | echo "" 100 | fi 101 | fi 102 | } 103 | 104 | # Get current configured IP Access List in mongo. 105 | echo "Getting ips for data-api: $PROJECT_ID" >&2 106 | RESULT=$(curl_with_auth "groups/$PROJECT_ID/apps/$DATA_API_APP_ID/security/access_list" "GET") 107 | if [ $? -ne 0 ]; then 108 | exit 1 109 | fi 110 | 111 | 112 | # Extract the ip list (comma seperated), and do the following: 113 | # search it in mongo's actual list. 114 | # If a requested IP was not found - add it as a new IP entry. 115 | IFS=',' read -ra IPS <<< "$AWS_NAT_GW_IPS" 116 | for ip in "${IPS[@]}"; do 117 | found=false 118 | _id="" 119 | while IFS= read -r line; do 120 | address=$(jq -r '.address' <<< "$line") 121 | _id=$(jq -r '._id' <<< "$line") 122 | if [ "$address" == "$ip" ]; then 123 | found=true 124 | break 125 | fi 126 | done <<< "$(echo "$RESULT" | jq -c '.allowed_ips[]')" 127 | 128 | if [ "$found" == false ]; then 129 | # the requested IP to add was not found, we will add it now. 130 | echo "IP $ip was not found, adding it." >&2 131 | add_res=$(curl_with_auth "groups/$PROJECT_ID/apps/$DATA_API_APP_ID/security/access_list" "POST" '{ 132 | "address": "'$ip'" 133 | }') 134 | if [ $? -ne 0 ]; then 135 | exit 1 136 | fi 137 | fi 138 | done 139 | 140 | # This part goes over all IPs that already exist in mongo, if any do not exist in the input list - delete them. 141 | output=$(jq -r '.allowed_ips[] | "\(.["_id"]) \(.["address"])"' <<< "$RESULT") 142 | while read -r _id address; do 143 | if [[ -n $address ]]; then 144 | if [[ ! " ${IPS[@]} " =~ " ${address} " ]]; then 145 | echo "IP $address exists on mongo but not in requested list: $AWS_NAT_GW_IPS. will remove it.">&2 146 | del_res=$(curl_with_auth "groups/$PROJECT_ID/apps/$DATA_API_APP_ID/security/access_list/$_id" "DELETE") 147 | if [ $? -ne 0 ]; then 148 | exit 1 149 | fi 150 | fi 151 | fi 152 | done <<< "$output" -------------------------------------------------------------------------------- /modules/mongoatlas/scripts/delete_data_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Description: Deletes data api from mongo 4 | # NOTE - if the state is dirty (data api / service is deleted) - this will cause recreation of data-api 5 | # This affects eventually other dependent services which will need to refresh their URLs from SSM. 6 | # It's recommended that dependent services will read at runtime the URLs (and cache it). 7 | # 8 | # 9 | # Dependencies: curl, jq 10 | ################################################################################ 11 | 12 | # perform login and get access_token 13 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 14 | 15 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 16 | --header 'content-type: application/json' \ 17 | --data-raw "$AUTH") 18 | if [ $? -ne 0 ]; then 19 | echo "Login failed... $response" >&2 20 | exit 1 21 | fi 22 | 23 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 24 | 25 | echo "Login finished successfully." >&2 26 | 27 | function curl_with_auth { 28 | # ---------------------------------- 29 | # performs curl request with mongo token, 404 errors are considered valid. 30 | # param1 - the url section (without the base URL) to perform 31 | # param2 - method 32 | # param3 - the body of the request 33 | # 34 | # returns: None 35 | # ---------------------------------- 36 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 37 | method="$2" 38 | data="$3" 39 | 40 | http_code=$(curl -sSL --fail --output /dev/null -w "%{http_code}" -X "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" -d "$data" "$url") 41 | 42 | if [[ "$?" -ne 0 || ("$http_code" != "404" && ("$http_code" -ge 400 || "$http_code" -lt 200)) ]]; then 43 | echo "HTTP request failed with error code $http_code" >&2 44 | exit 1 45 | fi 46 | } 47 | 48 | # Take the state (STDIN), - which mean, take the state. 49 | IN=$(cat) 50 | data_api_id=$(echo "${IN}" | jq -r '.data_api_id') 51 | 52 | # perform delete 53 | echo "Trying to delete data api for project id: $PROJECT_ID, api_id: $data_api_id, service_id: $service_id" >&2 54 | del_req=$(curl_with_auth "groups/$PROJECT_ID/apps/$data_api_id" "DELETE") 55 | if [ $? -ne 0 ]; then 56 | exit 1 57 | fi 58 | exit 0 59 | -------------------------------------------------------------------------------- /modules/mongoatlas/scripts/delete_data_api_security.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Description: Deletes all entries of IP from mongo 4 | # NOTE - This part goes over all IPs that already exist in mongo, if any do not exist in the input list - delete them. 5 | # 6 | # 7 | # Dependencies: curl, jq 8 | ################################################################################ 9 | 10 | # perform login and get access_token 11 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 12 | 13 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 14 | --header 'content-type: application/json' \ 15 | --data-raw "$AUTH") 16 | if [ $? -ne 0 ]; then 17 | echo "Login failed... $response" >&2 18 | exit 1 19 | fi 20 | 21 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 22 | 23 | echo "Login finished successfully." >&2 24 | 25 | function curl_with_auth { 26 | # ---------------------------------- 27 | # performs curl request with mongo token 28 | # param1 - the url section (without the base URL) to perform 29 | # param2 - method 30 | # param3 - the body of the request 31 | # 32 | # returns: the response from curl 33 | # ---------------------------------- 34 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 35 | method="$2" 36 | data="$3" 37 | 38 | if [ -z "$data" ]; then 39 | response=$(curl -sSL --request "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" "$url") 40 | else 41 | response=$(curl -sSL --request "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" -d "$data" "$url") 42 | fi 43 | 44 | if [ $? -ne 0 ]; then 45 | echo "$response" >&2 46 | exit 1 47 | fi 48 | 49 | echo "$response" 50 | } 51 | 52 | # Get current configured IP Access List in mongo. 53 | echo "Getting ips for data-api: $PROJECT_ID" >&2 54 | RESULT=$(curl_with_auth "groups/$PROJECT_ID/apps/$DATA_API_APP_ID/security/access_list" "GET") 55 | if [ $? -ne 0 ]; then 56 | exit 1 57 | fi 58 | 59 | # Go over all ips and delete them from mongo 60 | output=$(jq -r '.allowed_ips[] | "\(.["_id"]) \(.["address"])"' <<< "$RESULT") 61 | while read -r _id address; do 62 | if [[ -n $address ]]; then 63 | echo "Removing $address from mongo. ">&2 64 | del_res=$(curl_with_auth "groups/$PROJECT_ID/apps/$DATA_API_APP_ID/security/access_list/$_id" "DELETE") 65 | if [ $? -ne 0 ]; then 66 | exit 1 67 | fi 68 | fi 69 | done <<< "$output" -------------------------------------------------------------------------------- /modules/mongoatlas/scripts/get_data_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Description: Reads data api configurations to save to state. 4 | # NOTE - if the state is dirty (data api / service is deleted) - this will cause recreation of data-api 5 | # This affects eventually other dependent services which will need to refresh their URLs from SSM. 6 | # It's recommended that dependent services will read at runtime the URLs (and cache it). 7 | # 8 | # 9 | # Dependencies: curl, jq 10 | ################################################################################ 11 | 12 | # perform login and get access_token 13 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 14 | 15 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 16 | --header 'content-type: application/json' \ 17 | --data-raw "$AUTH") 18 | if [ $? -ne 0 ]; then 19 | echo "Login failed... $response" >&2 20 | exit 1 21 | fi 22 | 23 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 24 | 25 | echo "Login finished successfully." >&2 26 | 27 | function curl_with_auth { 28 | # ---------------------------------- 29 | # performs curl request with mongo token 30 | # param1 - the url section (without the base URL) to perform 31 | # param2 - method 32 | # param3 - the body of the request 33 | # 34 | # returns: the response from curl 35 | # ---------------------------------- 36 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 37 | method="$2" 38 | data="$3" 39 | 40 | if [ -z "$data" ]; then 41 | response=$(curl -sSL --request "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" "$url") 42 | else 43 | response=$(curl -sSL --request "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" -d "$data" "$url") 44 | fi 45 | 46 | if [ $? -ne 0 ]; then 47 | echo "$response" >&2 48 | exit 1 49 | fi 50 | 51 | echo "$response" 52 | } 53 | 54 | # Get data-api configurations 55 | echo "Getting data api for project id: $PROJECT_ID" >&2 56 | data_api_res=$(curl_with_auth "groups/$PROJECT_ID/apps?product=data-api" "GET") 57 | if [ $? -ne 0 ]; then 58 | exit 1 59 | fi 60 | 61 | # Extract data api id and the client id 62 | data_api_id=$(echo "$data_api_res" | jq -r '.[] | select(.product == "data-api") | ._id') 63 | client_app_id=$(echo "$data_api_res" | jq -r '.[] | select(.product == "data-api") | .client_app_id') 64 | 65 | if [ -z "$data_api_id" ]; then 66 | echo "data_api_id is empty, this was the result from the get apps: $data_api_res" >&2 67 | exit 1 68 | fi 69 | 70 | echo "Getting data api configuration for : $PROJECT_ID and api id: $data_api_id" >&2 71 | config_res=$(curl_with_auth "groups/$PROJECT_ID/apps/$data_api_id/data_api/config" "GET") 72 | if [ $? -ne 0 ]; then 73 | exit 1 74 | fi 75 | 76 | # save to state 77 | echo '{"data_api_id": "'$data_api_id'", "client_id":"'$client_app_id'", "data_api_configurations":"'$(echo "$config_res" | base64)'"}' 78 | -------------------------------------------------------------------------------- /modules/mongoatlas/scripts/get_data_api_security.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Description: Reads IPs that are restricting data-api access, and saving it to state 4 | # 5 | # 6 | # Dependencies: curl, jq 7 | ################################################################################ 8 | 9 | # perform login and get access_token 10 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 11 | 12 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 13 | --header 'content-type: application/json' \ 14 | --data-raw "$AUTH") 15 | if [ $? -ne 0 ]; then 16 | echo "Login failed... $response" >&2 17 | exit 1 18 | fi 19 | 20 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 21 | 22 | echo "Login finished successfully." >&2 23 | 24 | function curl_with_auth { 25 | # ---------------------------------- 26 | # performs curl request with mongo token 27 | # param1 - the url section (without the base URL) to perform 28 | # param2 - method 29 | # param3 - the body of the request 30 | # 31 | # returns: the response from curl 32 | # ---------------------------------- 33 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 34 | method="$2" 35 | data="$3" 36 | 37 | if [ -z "$data" ]; then 38 | response=$(curl -sSL --request "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" "$url") 39 | else 40 | response=$(curl -sSL --request "$method" -H "authorization: Bearer $MONGO_TOKEN" -H "content-type: application/json" -d "$data" "$url") 41 | fi 42 | 43 | if [ $? -ne 0 ]; then 44 | echo "$response" >&2 45 | exit 1 46 | fi 47 | 48 | echo "$response" 49 | } 50 | 51 | # Get allowed ips for data-api 52 | echo "Getting ips for data-api: $PROJECT_ID for app id: $DATA_API_APP_ID" >&2 53 | data_api_res=$(curl_with_auth "groups/$PROJECT_ID/apps/$DATA_API_APP_ID/security/access_list" "GET") 54 | if [ $? -ne 0 ]; then 55 | exit 1 56 | fi 57 | 58 | # Extract "address" values from the response and build a comma-separated list 59 | addresses=$(echo "$data_api_res" | jq -r '.allowed_ips[].address' | paste -sd "," -) 60 | 61 | # save to state 62 | echo '{"allowed_ips":"'$addresses'"}' 63 | -------------------------------------------------------------------------------- /modules/mongoatlas/variables.tf: -------------------------------------------------------------------------------- 1 | variable "stage" { 2 | type = string 3 | description = "Name of the stage - this can be used to create different environments" 4 | } 5 | 6 | variable "organization_id" { 7 | type = string 8 | description = "Org ID to work on, usually received from the organization part in the UI" 9 | } 10 | 11 | variable "mongo_ip_access_list" { 12 | type = list(map(string)) 13 | description = "List of IPs allowed to access the DBs via API (for example mongo compass, etc..)" 14 | } 15 | 16 | variable "add_mongo_ips_access_to_data_api" { 17 | type = bool 18 | description = "this indicates if we should also let ips from mongo_ip_access_list to use data-api" 19 | default = false 20 | } 21 | 22 | variable "enable_termination_protection" { 23 | type = bool 24 | description = "Enable termination protection all instances" 25 | } 26 | 27 | variable "mongo_instances" { 28 | type = list(string) 29 | description = "instances to be created - they will all be serverless instances" 30 | } 31 | 32 | variable "aws_vpc_id" { 33 | type = string 34 | description = "VPC ID to integrate the private endpoint" 35 | } 36 | 37 | variable "private_subnet_ids" { 38 | type = list(string) 39 | description = "Subnet IDs to integrate the private endpoint" 40 | } 41 | 42 | # We are using it to create data-api, data.aws_region.current.name somehow forces a change to the shell script 43 | # so we'll pre-configure it. 44 | variable "aws_region" { 45 | type = string 46 | default = "us-east-1" 47 | } 48 | 49 | variable "aws_allowed_access_security_groups" { 50 | type = list(string) 51 | description = "A list of AWS security group IDs permitted access to MongoDB Atlas resources. (for example - your lambdas)" 52 | } 53 | 54 | variable "enable_continuous_backup" { 55 | type = bool 56 | default = false 57 | description = "Enable continuous backup for the cluster" 58 | } 59 | 60 | variable "jwt_audience" { 61 | type = string 62 | description = "The audience of the JWT token" 63 | } 64 | 65 | variable "jwt_public_key" { 66 | type = string 67 | description = "The public key to verify the JWT token" 68 | } 69 | 70 | variable "notification_email" { 71 | type = string 72 | description = "The email to send alerts to" 73 | } 74 | 75 | variable "daily_price_threshold_alert" { 76 | type = number 77 | description = "The price threshold to send a slack notification (daily)" 78 | } 79 | 80 | variable "monthly_price_threshold" { 81 | type = number 82 | description = "The price threshold to send a slack notification (monthly)" 83 | default = 100 84 | } 85 | 86 | 87 | variable "tenant_id_field_in_jwt" { 88 | type = string 89 | description = "The field in the JWT that contains the tenant ID" 90 | } 91 | 92 | variable "display_name_field_in_jwt" { 93 | type = string 94 | description = "The field in the JWT that identifies the display name of the user (to be shown in the console)" 95 | } -------------------------------------------------------------------------------- /modules/mongoatlas/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | mongodbatlas = { 4 | source = "mongodb/mongodbatlas" 5 | version = "1.8.1" 6 | } 7 | aws = { 8 | source = "hashicorp/aws" 9 | version = "~> 4.0" 10 | } 11 | shell = { 12 | source = "scottwinkler/shell" 13 | version = "1.7.10" 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/mongoatlas_alert/main.tf: -------------------------------------------------------------------------------- 1 | resource "mongodbatlas_alert_configuration" "mongo_alert" { 2 | enabled = true 3 | event_type = var.event_type 4 | project_id = var.project_id 5 | 6 | notification { 7 | delay_min = var.delay_minutes 8 | email_enabled = true 9 | interval_min = var.alert_interval_minutes 10 | roles = ["GROUP_OWNER"] 11 | type_name = "GROUP" 12 | } 13 | notification { 14 | delay_min = var.delay_minutes 15 | email_enabled = true 16 | interval_min = var.alert_interval_minutes 17 | roles = ["ORG_OWNER"] 18 | type_name = "ORG" 19 | } 20 | 21 | notification { 22 | delay_min = var.delay_minutes 23 | interval_min = var.alert_interval_minutes 24 | type_name = "EMAIL" 25 | email_address = var.email 26 | } 27 | 28 | dynamic "metric_threshold_config" { 29 | for_each = var.event_type == "OUTSIDE_METRIC_THRESHOLD" || var.event_type == "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" ? [1] : [] 30 | content { 31 | metric_name = var.metric_name 32 | mode = "AVERAGE" 33 | operator = var.required_operator 34 | threshold = var.threshold 35 | units = var.threshold_type 36 | } 37 | } 38 | 39 | dynamic "threshold_config" { 40 | for_each = var.event_type != "OUTSIDE_METRIC_THRESHOLD" && var.event_type != "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" && var.threshold != null ? [1] : [] 41 | content { 42 | operator = var.required_operator 43 | threshold = var.threshold 44 | units = var.threshold_type 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /modules/mongoatlas_alert/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | type = string 3 | description = "project ID of atlas" 4 | } 5 | 6 | variable "event_type" { 7 | type = string 8 | description = "Event type - list is here: https://www.mongodb.com/docs/atlas/reference/api-resources-spec/#tag/Alert-Configurations/operation/createAlertConfiguration" 9 | } 10 | 11 | variable "metric_name" { 12 | type = string 13 | description = "Metric name to check https://www.mongodb.com/docs/atlas/reference/api-resources-spec/#tag/Alert-Configurations/operation/createAlertConfiguration" 14 | default = null 15 | } 16 | 17 | variable "threshold" { 18 | type = number 19 | description = "Threshold to send alert, we will always use average" 20 | default = null 21 | } 22 | 23 | variable "threshold_type" { 24 | type = string 25 | description = "Threshold type" 26 | default = "RAW" 27 | } 28 | 29 | variable "email" { 30 | type = string 31 | sensitive = true 32 | description = "email address for notifications" 33 | } 34 | 35 | variable "alert_interval_minutes" { 36 | type = number 37 | description = "Alert interval in minutes, default to 60" 38 | default = 60 39 | } 40 | 41 | variable "required_operator" { 42 | type = string 43 | description = "required operator to test" 44 | default = "GREATER_THAN" 45 | 46 | } 47 | 48 | variable "delay_minutes" { 49 | type = number 50 | description = "time to wait before sending the alert if it continues" 51 | default = 0 # amount of time to wait before sending the alert if it continues 52 | } -------------------------------------------------------------------------------- /modules/mongoatlas_alert/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | mongodbatlas = { 4 | source = "mongodb/mongodbatlas" 5 | version = "1.8.1" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/mongoatlas_instance/configure_data_api_scripts/create_update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Script Name: Creates / updates data-api configuration 4 | # Description: This script works both for update and create, it basically 5 | # configures app service for an instance, relevant JWT token for authentication. 6 | # 7 | # This script uses a pattern of "get configuration", if not exist create else update 8 | # this makes the script support both create and update 9 | # 10 | # 11 | # Dependencies: curl, jq 12 | ################################################################################ 13 | 14 | # perform login and get access_token 15 | ATLAS_ADMIN_BASE_API_PATH="https://services.cloud.mongodb.com/api/admin/v3.0" 16 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 17 | 18 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 19 | --header 'content-type: application/json' \ 20 | --data-raw "$AUTH") 21 | if [ $? -ne 0 ]; then 22 | echo "Login failed... $response" >&2 23 | exit 1 24 | fi 25 | 26 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 27 | 28 | echo "Login finished successfully." >&2 29 | 30 | function curl_with_auth { 31 | # ---------------------------------- 32 | # performs curl request with mongo token 33 | # param1 - the url section (without the base URL) to perform 34 | # param2 - method 35 | # param3 - the body of the request 36 | # 37 | # returns: the response from curl 38 | # ---------------------------------- 39 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 40 | 41 | method="$2" 42 | data="$3" 43 | response=$(curl -sSL --show-error --fail -X "$method" "$url" \ 44 | --header "authorization: Bearer $MONGO_TOKEN" \ 45 | --header 'content-type: application/json' \ 46 | --data-raw "$data" \ 47 | --compressed) 48 | if [ $? -ne 0 ]; then 49 | exit 1 50 | fi 51 | 52 | echo "$response" 53 | } 54 | 55 | function try_get { 56 | # ---------------------------------- 57 | # performs GET request, and tries to find specific key-value in the array of jsons or json. 58 | # if found, returns the relevant json. if not, empty string. 59 | # param1 - the url section (without the base URL) to perform 60 | # param2 - wanted key to search 61 | # param3 - wanted value to search 62 | # 63 | # returns: the relevant json. if not, empty string. 64 | # ---------------------------------- 65 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 66 | wanted_key=$2 67 | wanted_value=$3 68 | response=$(curl -sSL --show-error --fail -X "GET" "$url" \ 69 | --header "authorization: Bearer $MONGO_TOKEN" \ 70 | --header 'content-type: application/json') 71 | 72 | 73 | # If key was not specified 74 | if [ -z "$wanted_key" ]; then 75 | # check if response is an empty JSON object or an empty array 76 | if [[ $(echo "$response" | jq 'length') -eq 0 ]]; then 77 | echo "" 78 | else 79 | echo "$response" 80 | fi 81 | return 82 | fi 83 | 84 | if [ $? -ne 0 ]; then 85 | echo "$response" >&2 86 | exit 1 87 | fi 88 | 89 | # Search for the relevant JSON object 90 | if [[ "$response" == "["*"]" ]]; then 91 | # Array of JSON objects 92 | matching_objects=$(echo "$response" | jq -r "map(select(.[\"$wanted_key\"] == \"$wanted_value\"))") 93 | if [ "$(echo "$matching_objects" | jq length)" -eq 0 ]; then 94 | echo "" 95 | return 96 | fi 97 | echo "$matching_objects" | jq '.[0]' 98 | else 99 | # Single JSON object 100 | if [[ $(echo "$response" | jq -r ".$wanted_key") == "$wanted_value" ]]; then 101 | echo "$response" | jq . 102 | else 103 | echo "" 104 | fi 105 | fi 106 | } 107 | 108 | echo "Checking if DB $DB_INSTANCE_NAME is configured, project_id: $PROJECT_ID" >&2 109 | path="groups/$PROJECT_ID/apps/$APP_ID/services" 110 | res=$(try_get $path "name" "$DB_INSTANCE_NAME") 111 | if [ -z "$res" ]; then 112 | # configure the service for the cluster 113 | echo "Not found, Enabling data api for cluster $DB_INSTANCE_NAME" >&2 114 | res=$(curl_with_auth $path "POST" "{\"name\": \"$DB_INSTANCE_NAME\", \"type\": \"mongodb-atlas\", \"config\": {\"clusterName\": \"$DB_INSTANCE_NAME\"}}") 115 | if [ $? -ne 0 ]; then 116 | exit 1 117 | fi 118 | fi 119 | 120 | service_id=$(echo "$res" | jq -r '._id') 121 | 122 | # create or update the default rule 123 | 124 | 125 | path="groups/$PROJECT_ID/apps/$APP_ID/services/$service_id/default_rule" 126 | echo "Getting the default rule..." >&2 127 | res=$(try_get $path) 128 | default_rule_id=$(echo "$res" | jq -r '._id // ""') 129 | if [ -z "$default_rule_id" ]; then 130 | echo "Not found, creating empty rule." >&2 131 | method="PUT" 132 | res=$(curl_with_auth $path "POST" '{ 133 | "filters": [], 134 | "roles": [] 135 | }') 136 | default_rule_id=$(echo "$res" | jq -r '._id') 137 | fi 138 | 139 | echo "Updating default rule $default_rule_id..." >&2 140 | default_rule_res=$(curl_with_auth $path "PUT" '{ 141 | "_id": "'$default_rule_id'", 142 | "filters": [ 143 | { 144 | "name": "tenant id filter", 145 | "query": { 146 | "tenant_id": "%%user.data.tenantId" 147 | }, 148 | "apply_when": { 149 | "%%true": true 150 | } 151 | } 152 | ], 153 | "roles": [ 154 | { 155 | "name": "readAccessDataAPI", 156 | "apply_when": {}, 157 | "read": true, 158 | "write": true, 159 | "insert": true, 160 | "delete": true, 161 | "search": true 162 | } 163 | ] 164 | }') 165 | if [ $? -ne 0 ]; then 166 | exit 1 167 | fi 168 | 169 | echo "Searching for existing secret jwt-public-key: $PROJECT_ID" >&2 170 | path="groups/$PROJECT_ID/apps/$APP_ID/secrets" 171 | res=$(try_get $path "name" "jwt-public-key") 172 | method="POST" 173 | if [ -n "$res" ]; then 174 | echo "Found, Will update." >&2 175 | secret_id=$(echo "$res" | jq -r '._id') 176 | path+="/$secret_id" 177 | method="PUT" 178 | fi 179 | 180 | # This adds frontegg public key as a secret, so it can be used for JWT auth. 181 | echo "Performing $method on secret (JWT public key) for the JWT token: $PROJECT_ID" >&2 182 | create_secret_res=$(curl_with_auth $path $method "{ 183 | \"name\": \"jwt-public-key\", 184 | \"value\": \"$FRONTEGG_PUBLIC_KEY\" 185 | }") 186 | if [ $? -ne 0 ]; then 187 | exit 1 188 | fi 189 | 190 | echo "Searching for existing custom-token auth-provider: $PROJECT_ID" >&2 191 | path="groups/$PROJECT_ID/apps/$APP_ID/auth_providers" 192 | res=$(try_get $path "type" "custom-token") 193 | method="POST" 194 | if [ -n "$res" ]; then 195 | echo "Found, Will update." >&2 196 | id=$(echo "$res" | jq -r '._id') 197 | path+="/$id" 198 | method="PATCH" 199 | fi 200 | 201 | # This enables JWT token auth for data-api, configures the audience and public key of frontegg. 202 | # Also this maps the tenantId in the token so it can be used for filtering. 203 | echo "Enabling JWT auth for project id: $PROJECT_ID using $method" >&2 204 | jwt_auth_res=$(curl_with_auth $path $method '{ 205 | "name": "custom-token", 206 | "type": "custom-token", 207 | "disabled": false, 208 | "config": { 209 | "audience": [ 210 | "'$FRONTEGG_AUD'" 211 | ], 212 | "requireAnyAudience": true, 213 | "signingAlgorithm": "RS256", 214 | "useJWKURI": false 215 | }, 216 | "secret_config": { 217 | "signingKeys": [ 218 | "jwt-public-key" 219 | ] 220 | }, 221 | "metadata_fields": [ 222 | { 223 | "required": true, 224 | "name": "'$TENANT_ID_FIELD_IN_JWT'", 225 | "field_name": "tenantId" 226 | }, 227 | { 228 | "required": false, 229 | "name": "'$APP_SERVICES_USER_DISPLAY_FIELD_FROM_JWT'", 230 | "field_name": "name" 231 | } 232 | ] 233 | }') 234 | if [ $? -ne 0 ]; then 235 | exit 1 236 | fi 237 | auth_provider_id=$(echo "$jwt_auth_res" | jq -r '._id') -------------------------------------------------------------------------------- /modules/mongoatlas_instance/configure_data_api_scripts/delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Description: Deletes data api configurations from mongo 4 | # 5 | # 6 | # Dependencies: curl, jq 7 | ################################################################################ 8 | 9 | # perform login and get access_token 10 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 11 | 12 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 13 | --header 'content-type: application/json' \ 14 | --data-raw "$AUTH") 15 | if [ $? -ne 0 ]; then 16 | echo "Login failed... $response" >&2 17 | exit 1 18 | fi 19 | 20 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 21 | 22 | echo "Login finished successfully." >&2 23 | 24 | 25 | function curl_with_auth { 26 | # ---------------------------------- 27 | # performs curl request with mongo token, 404 errors are considered valid. 28 | # param1 - the url section (without the base URL) to perform 29 | # param2 - method 30 | # param3 - the body of the request 31 | # 32 | # returns: None 33 | # ---------------------------------- 34 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 35 | method="$2" 36 | data="$3" 37 | http_code=$(curl -sSL --fail --output /dev/null -w "%{http_code}" -X "$method" "$url" \ 38 | --header "authorization: Bearer $MONGO_TOKEN" \ 39 | --header 'content-type: application/json' \ 40 | --data-raw "$data" \ 41 | --compressed) 42 | if [[ -n "$http_code" && "$http_code" =~ ^[0-9]+$ && "$http_code" -ge 400 && "$http_code" != "404" ]]; then 43 | echo "HTTP request failed with error code $http_code" >&2 44 | exit 1 45 | fi 46 | } 47 | 48 | # Take the state (STDIN) 49 | IN=$(cat) 50 | service_id=$(echo "${IN}" | jq -r '.service_id') 51 | auth_provider_id=$(echo "${IN}" | jq -r '.auth_provider_id') 52 | secret_id=$(echo "${IN}" | jq -r '.secret_id') 53 | 54 | # perform delete 55 | echo "Trying to delete data source (service) using service id: $service_id, project $PROJECT_ID" >&2 56 | del_res=$(curl_with_auth "groups/$PROJECT_ID/apps/$APP_ID/services/$service_id" "DELETE") 57 | if [ $? -ne 0 ]; then 58 | exit 1 59 | fi 60 | echo "disabling auth provider $auth_provider_id" >&2 61 | del_req=$(curl_with_auth "groups/$PROJECT_ID/apps/$APP_ID/auth_providers/$auth_provider_id/disable" "PUT") 62 | if [ $? -ne 0 ]; then 63 | exit 1 64 | fi 65 | 66 | echo "deleting auth provider $auth_provider_id" >&2 67 | del_req=$(curl_with_auth "groups/$PROJECT_ID/apps/$APP_ID/auth_providers/$auth_provider_id" "DELETE") 68 | if [ $? -ne 0 ]; then 69 | exit 1 70 | fi 71 | 72 | echo "deleting secret $secret_id" >&2 73 | del_req=$(curl_with_auth "groups/$PROJECT_ID/apps/$APP_ID/secrets/$secret_id" "DELETE") 74 | if [ $? -ne 0 ]; then 75 | exit 1 76 | fi 77 | exit 0 78 | -------------------------------------------------------------------------------- /modules/mongoatlas_instance/configure_data_api_scripts/get.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # Description: read data API configurations, we will just save responses as base64 and some ids. 4 | # Every change on one of the resources outside of terraform should trigger the update. 5 | # If secret content is changed through manually, we cannot identify it (as there's no modification time) 6 | # 7 | # The state is going to be a json 8 | # 9 | # Dependencies: curl, jq 10 | ################################################################################ 11 | 12 | 13 | # perform login and get access_token 14 | echo "Performing login, url: $ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" >&2 15 | 16 | response=$(curl -sSL --show-error --fail -X POST "$ATLAS_ADMIN_BASE_API_PATH/auth/providers/mongodb-cloud/login" \ 17 | --header 'content-type: application/json' \ 18 | --data-raw "$AUTH") 19 | if [ $? -ne 0 ]; then 20 | echo "Login failed... $response" >&2 21 | exit 1 22 | fi 23 | 24 | MONGO_TOKEN=$(echo "$response" | jq -r '.access_token') 25 | 26 | echo "Login finished successfully." >&2 27 | 28 | function append_to_json() { 29 | # ---------------------------------- 30 | # Creates a json and appends key/value to it. 31 | # param1 - the json to modify, if empty - will create new one 32 | # param2 - key to add 33 | # param3 - value to add 34 | # 35 | # returns: the json 36 | # ---------------------------------- 37 | local json="$1" 38 | local key="$2" 39 | local value="$3" 40 | 41 | if [[ -z "$json" ]]; then 42 | json='{}' 43 | fi 44 | 45 | # Append the new key/value pair to the JSON object 46 | json=$(echo "$json" | jq --arg key "$key" --arg value "$value" '. + { ($key): $value }') 47 | 48 | echo "$json" 49 | } 50 | 51 | function try_get { 52 | # ---------------------------------- 53 | # performs GET request, and tries to find specific key-value in the array of jsons or json. 54 | # if found, returns the relevant json. if not, empty string. 55 | # param1 - the url section (without the base URL) to perform 56 | # param2 - wanted key to search 57 | # param3 - wanted value to search 58 | # 59 | # returns: the relevant json. if not, empty string. 60 | # ---------------------------------- 61 | url="$ATLAS_ADMIN_BASE_API_PATH/$1" 62 | wanted_key=$2 63 | wanted_value=$3 64 | response=$(curl -sSL --fail -X "GET" "$url" \ 65 | --header "authorization: Bearer $MONGO_TOKEN" \ 66 | --header 'content-type: application/json') 67 | 68 | if [ $? -ne 0 ]; then 69 | echo "$response" >&2 70 | exit 1 71 | fi 72 | 73 | if [ -z "$wanted_key" ]; then 74 | echo $response 75 | return 76 | fi 77 | 78 | # Search for the relevant JSON object 79 | if [[ "$response" == "["*"]" ]]; then 80 | # Array of JSON objects 81 | matching_objects=$(echo "$response" | jq -r "map(select(.[\"$wanted_key\"] == \"$wanted_value\"))") 82 | if [ "$(echo "$matching_objects" | jq length)" -eq 0 ]; then 83 | echo "" 84 | return 85 | fi 86 | echo "$matching_objects" | jq '.[0]' 87 | else 88 | # Single JSON object 89 | if [[ $(echo "$response" | jq -r ".$wanted_key") == "$wanted_value" ]]; then 90 | echo "$response" | jq . 91 | else 92 | echo "" 93 | fi 94 | fi 95 | } 96 | 97 | 98 | echo "Searching if DB $DB_INSTANCE_NAME is configured, project_id: $PROJECT_ID" >&2 99 | path="groups/$PROJECT_ID/apps/$APP_ID/services" 100 | res=$(try_get $path "name" "$DB_INSTANCE_NAME") 101 | if [ $? -ne 0 ]; then 102 | exit 1 103 | fi 104 | if [ -z "$res" ]; then 105 | echo "service not found..." >&2 106 | exit 1 107 | fi 108 | 109 | service_id=$(echo "$res" | jq -r '._id') 110 | service_name=$(echo "$res" | jq -r '.name') 111 | myjson=$(append_to_json "$myjson" "service_id" "$service_id") 112 | myjson=$(append_to_json "$myjson" "service_name" "$service_name") 113 | 114 | echo "Searching for existing secret jwt-public-key: $PROJECT_ID" >&2 115 | path="groups/$PROJECT_ID/apps/$APP_ID/secrets" 116 | res=$(try_get $path "name" "jwt-public-key") 117 | if [ $? -ne 0 ]; then 118 | exit 1 119 | fi 120 | if [ -z "$res" ]; then 121 | echo "Secret not found..." >&2 122 | exit 1 123 | fi 124 | 125 | secret_id=$(echo "$res" | jq -r '._id') 126 | secret_name=$(echo "$res" | jq -r '.name') 127 | myjson=$(append_to_json "$myjson" "secret_id" $secret_id) 128 | myjson=$(append_to_json "$myjson" "secret_name" $secret_name) 129 | 130 | echo "Searching for existing custom-token auth-provider: $PROJECT_ID" >&2 131 | path="groups/$PROJECT_ID/apps/$APP_ID/auth_providers" 132 | res=$(try_get $path "type" "custom-token") 133 | if [ $? -ne 0 ]; then 134 | exit 1 135 | fi 136 | if [ -z "$res" ]; then 137 | echo "auth provider not found..." >&2 138 | exit 1 139 | fi 140 | 141 | auth_provider_id=$(echo "$res" | jq -r '._id') 142 | 143 | # Use the auth provider id we found to save the full configurations to state 144 | path="groups/$PROJECT_ID/apps/$APP_ID/auth_providers/$auth_provider_id" 145 | res=$(try_get $path) 146 | if [ $? -ne 0 ]; then 147 | exit 1 148 | fi 149 | 150 | myjson=$(append_to_json "$myjson" "jwt_provider_result" "$(echo -n "$res")") 151 | 152 | # We created the default rule before, so we just need to update it. 153 | echo "Getting the default rule..." >&2 154 | path="groups/$PROJECT_ID/apps/$APP_ID/services/$service_id/default_rule" 155 | res=$(try_get $path) 156 | if [ $? -ne 0 ]; then 157 | exit 1 158 | fi 159 | if [ -z "$res" ]; then 160 | echo "default rule not found..." >&2 161 | exit 1 162 | fi 163 | 164 | # This will be saved to state, full json 165 | myjson=$(append_to_json "$myjson" "default_rule_result" "$(echo -n "$res")") 166 | 167 | echo $myjson -------------------------------------------------------------------------------- /modules/mongoatlas_instance/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | mongo_atlas_api_base_admin_URL = "https://realm.mongodb.com/api/admin/v3.0" 3 | } -------------------------------------------------------------------------------- /modules/mongoatlas_instance/main.tf: -------------------------------------------------------------------------------- 1 | # Create the relevant DB instance. 2 | resource "mongodbatlas_serverless_instance" "database_instance" { 3 | project_id = var.project_id 4 | name = var.instance_name 5 | 6 | provider_settings_backing_provider_name = "AWS" 7 | provider_settings_provider_name = "SERVERLESS" 8 | provider_settings_region_name = replace(upper(var.aws_region), "-", "_") 9 | termination_protection_enabled = var.enable_termination_protection 10 | continuous_backup_enabled = var.enable_continuous_backup 11 | 12 | } 13 | 14 | # The 4 resources below define the mutual private connection between AWS and atlas 15 | resource "mongodbatlas_privatelink_endpoint_serverless" "privatelink_ep_sls" { 16 | project_id = var.project_id 17 | instance_name = mongodbatlas_serverless_instance.database_instance.name 18 | provider_name = "AWS" 19 | } 20 | 21 | # This creates all private VPC in AWS 22 | module "third_party_vpc_endpoint" { 23 | source = "../third_party_vpc_endpoint" 24 | name = var.instance_name 25 | service_name = mongodbatlas_privatelink_endpoint_serverless.privatelink_ep_sls.endpoint_service_name 26 | vpc_id = var.aws_vpc_id 27 | subnet_ids = var.private_subnet_ids 28 | allowed_access_security_groups = var.aws_allowed_access_security_groups 29 | } 30 | 31 | # This configures the vpc in mongo side 32 | resource "mongodbatlas_privatelink_endpoint_service_serverless" "sls_service" { 33 | project_id = var.project_id 34 | instance_name = mongodbatlas_serverless_instance.database_instance.name 35 | comment = mongodbatlas_serverless_instance.database_instance.name 36 | endpoint_id = mongodbatlas_privatelink_endpoint_serverless.privatelink_ep_sls.endpoint_id 37 | cloud_provider_endpoint_id = module.third_party_vpc_endpoint.provider_id 38 | provider_name = "AWS" 39 | } 40 | 41 | # This is a trick, we need to re-read the private endpoint connection string, which is available on the INSTANCE 42 | # but only after the sls_service is created. 43 | data "mongodbatlas_serverless_instance" "aws_private_connection" { 44 | project_id = mongodbatlas_serverless_instance.database_instance.project_id 45 | name = mongodbatlas_serverless_instance.database_instance.name 46 | 47 | depends_on = [mongodbatlas_privatelink_endpoint_service_serverless.sls_service] 48 | } 49 | 50 | # Trick #2 - in order not to cause recreation of the resources below, we are defining a local variable. 51 | # If the instance itself doesn't yet have the private ep url, we are taking it from data source. 52 | # if it has (2nd and further deploys) - we are taking it from the resource itself. 53 | # this will not cause recreation of the ssm and AWS Lambda below. 54 | locals { 55 | private_connection_string = coalesce( 56 | mongodbatlas_serverless_instance.database_instance.connection_strings_private_endpoint_srv != null ? 57 | mongodbatlas_serverless_instance.database_instance.connection_strings_private_endpoint_srv[0] : 58 | null, 59 | data.mongodbatlas_serverless_instance.aws_private_connection.connection_strings_private_endpoint_srv[0] 60 | ) 61 | } 62 | 63 | # Save the private EP URL - this will be used in AWS Lambdas with pymongo proxy. 64 | resource "aws_ssm_parameter" "private-endpoint-connection-string" { 65 | name = "/${var.stage}/infra/mongodb/${mongodbatlas_serverless_instance.database_instance.name}/private-endpoint/connection-string" 66 | type = "String" 67 | value = local.private_connection_string 68 | } 69 | 70 | # Save also standard connection, it can help us connecting through compass 71 | resource "aws_ssm_parameter" "standard-endpoint-connection-string" { 72 | name = "/${var.stage}/infra/mongodb/${mongodbatlas_serverless_instance.database_instance.name}/standard-connection/connection-string" 73 | type = "String" 74 | value = mongodbatlas_serverless_instance.database_instance.connection_strings_standard_srv 75 | } 76 | 77 | 78 | # This configures the data-api with JWT token authentication. 79 | # This also configures tenant isolation (takes the tenantId field from the JWT token and uses it as a filter) 80 | # https://www.mongodb.com/docs/atlas/app-services/rules/filters 81 | # NOTE - this resource is a bit weird, if you want to change the script to support something new, do the following: 82 | # change all scripts all together to support the new change: 83 | # 1. create/update - should perform both creation and update (so if it runs several times, it aligns the environment to what you need 84 | # 2. get - to save in state the wanted configuration, terraform will call this each apply and if there's a drift - will call create/update script 85 | # 3. delete - in case of deletion when resource is deleted 86 | # after doing the change, the apply will NOT trigger an update, but WILL CHANGE THE STATE. 87 | # this is unavoidable, in order to trigger a real update change, you will need to change the environment section. 88 | # this can be done only after scripts are deployed. 89 | # add VERSION env var (or change it if it's there) - and then apply again, update will be trigger and align your environment. 90 | # so - 2 deploys will be needed (first scripts, then change a dummy env var) 91 | resource "shell_script" "configure-data-api" { 92 | environment = { 93 | PROJECT_ID = var.project_id 94 | ATLAS_ADMIN_BASE_API_PATH = local.mongo_atlas_api_base_admin_URL 95 | APP_ID = var.data_api_id 96 | DB_INSTANCE_NAME = mongodbatlas_serverless_instance.database_instance.name 97 | FRONTEGG_PUBLIC_KEY = replace(var.jwt_public_key, "\n", "\\n") 98 | FRONTEGG_AUD = var.jwt_audience 99 | TENANT_ID_FIELD_IN_JWT = var.tenant_id_field_in_jwt 100 | APP_SERVICES_USER_DISPLAY_FIELD_FROM_JWT = var.display_name_field_in_jwt 101 | } 102 | 103 | lifecycle_commands { 104 | create = file("${path.module}/configure_data_api_scripts/create_update.sh") 105 | update = file("${path.module}/configure_data_api_scripts/create_update.sh") 106 | read = file("${path.module}/configure_data_api_scripts/get.sh") 107 | delete = file("${path.module}/configure_data_api_scripts/delete.sh") 108 | } 109 | interpreter = ["/bin/bash", "-c"] 110 | working_directory = path.module 111 | } -------------------------------------------------------------------------------- /modules/mongoatlas_instance/outputs.tf: -------------------------------------------------------------------------------- 1 | output "standard_connection_url" { 2 | value = mongodbatlas_serverless_instance.database_instance.connection_strings_standard_srv 3 | } 4 | -------------------------------------------------------------------------------- /modules/mongoatlas_instance/variables.tf: -------------------------------------------------------------------------------- 1 | variable "stage" { 2 | type = string 3 | description = "Name of the stage - this can be used to create different environments" 4 | } 5 | 6 | variable "project_id" { 7 | type = string 8 | description = "Atlas project ID" 9 | } 10 | 11 | variable "instance_name" { 12 | type = string 13 | description = "Name of the MongoDB serverless instance" 14 | } 15 | 16 | variable "aws_account_id" { 17 | type = string 18 | description = "AWS account ID" 19 | } 20 | 21 | variable "enable_termination_protection" { 22 | type = bool 23 | description = "Enable termination protection for the instance" 24 | } 25 | 26 | # We are using it to create data-api, sdata.aws_region.current.name somehow forces a change to the shell script 27 | # so we'll put it hard coded. 28 | variable "aws_region" { 29 | type = string 30 | default = "us-east-1" 31 | } 32 | 33 | variable "aws_vpc_id" { 34 | type = string 35 | description = "The VPC ID to create the private link in" 36 | } 37 | 38 | variable "private_subnet_ids" { 39 | type = list(string) 40 | description = "The private subnet IDs to create the private link in" 41 | } 42 | 43 | variable "tenant_id_field_in_jwt" { 44 | type = string 45 | description = "The field in the JWT that contains the tenant ID" 46 | } 47 | 48 | variable "display_name_field_in_jwt" { 49 | type = string 50 | description = "The field in the JWT that identifies the display name of the user (to be shown in the console)" 51 | } 52 | 53 | variable "aws_allowed_access_security_groups" { 54 | type = list(string) 55 | description = "The security groups that are allowed to access the MongoDB private endpoint" 56 | } 57 | 58 | variable "enable_continuous_backup" { 59 | type = bool 60 | default = false 61 | description = "Continuous backup incurs additional costs, Use only in production" 62 | } 63 | 64 | variable "organization_id" { 65 | type = string 66 | description = "The Atlas organization ID" 67 | } 68 | 69 | variable "jwt_audience" { 70 | type = string 71 | description = "The audience for the JWT" 72 | } 73 | 74 | variable "jwt_public_key" { 75 | type = string 76 | description = "The public key to verify the JWT" 77 | } 78 | 79 | variable "data_api_id" { 80 | type = string 81 | description = "The ID of the data API" 82 | } 83 | -------------------------------------------------------------------------------- /modules/mongoatlas_instance/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | mongodbatlas = { 4 | source = "mongodb/mongodbatlas" 5 | version = "1.8.1" 6 | } 7 | aws = { 8 | source = "hashicorp/aws" 9 | version = "~> 4.0" 10 | } 11 | shell = { 12 | source = "scottwinkler/shell" 13 | version = "1.7.10" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/secret/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_secretsmanager_secret" "secret" { 2 | name = var.secret_name 3 | description = var.description 4 | recovery_window_in_days = "0" 5 | 6 | // this is optional and can be set to true | false 7 | lifecycle { 8 | create_before_destroy = true 9 | } 10 | } 11 | 12 | resource "aws_secretsmanager_secret_version" "secret_value" { 13 | secret_id = aws_secretsmanager_secret.secret.id 14 | secret_string = var.secret_value 15 | } 16 | -------------------------------------------------------------------------------- /modules/secret/outputs.tf: -------------------------------------------------------------------------------- 1 | output "secret_value" { 2 | value = var.secret_value 3 | sensitive = true 4 | } 5 | 6 | output "secret_arn" { 7 | value = aws_secretsmanager_secret.secret.arn 8 | } -------------------------------------------------------------------------------- /modules/secret/variables.tf: -------------------------------------------------------------------------------- 1 | variable "secret_name" { 2 | type = string 3 | description = "The name of the secret" 4 | } 5 | 6 | variable "description" { 7 | type = string 8 | description = "The description of the secret" 9 | } 10 | 11 | variable "secret_value" { 12 | type = string 13 | sensitive = true 14 | description = "The value of the secret - will be encrypted at rest" 15 | } 16 | -------------------------------------------------------------------------------- /modules/secret/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/third_party_vpc_endpoint/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "third_party_service_security_group" { 2 | name = "${var.name}_egress_any" 3 | description = "Allow egress traffic from vpc interface EP of ${var.name}, ingress from specific security groups" 4 | vpc_id = var.vpc_id 5 | ingress { 6 | from_port = 0 7 | to_port = 0 8 | protocol = "-1" 9 | security_groups = var.allowed_access_security_groups 10 | } 11 | egress { 12 | from_port = 0 13 | to_port = 0 14 | protocol = "-1" 15 | cidr_blocks = ["0.0.0.0/0"] 16 | ipv6_cidr_blocks = ["::/0"] 17 | } 18 | tags = { 19 | Name = "${var.name}_egress_any" 20 | } 21 | } 22 | 23 | resource "aws_vpc_endpoint" "third_party_service" { 24 | vpc_id = var.vpc_id 25 | service_name = var.service_name 26 | vpc_endpoint_type = "Interface" 27 | 28 | security_group_ids = [aws_security_group.third_party_service_security_group.id] 29 | 30 | subnet_ids = var.subnet_ids 31 | tags = { 32 | Name = var.name 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/third_party_vpc_endpoint/outputs.tf: -------------------------------------------------------------------------------- 1 | output "provider_id" { 2 | value = aws_vpc_endpoint.third_party_service.id 3 | } 4 | 5 | output "name" { 6 | value = var.name 7 | } 8 | 9 | output "endpoint_service_name" { 10 | value = var.service_name 11 | } 12 | -------------------------------------------------------------------------------- /modules/third_party_vpc_endpoint/variables.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | type = string 3 | description = "The VPC ID where the endpoint will be created" 4 | } 5 | 6 | variable "subnet_ids" { 7 | type = list(string) 8 | description = "The subnet IDs where the endpoint will be created" 9 | } 10 | 11 | variable "allowed_access_security_groups" { 12 | type = list(string) 13 | description = "The security groups that will be allowed to access the endpoint" 14 | } 15 | 16 | variable "service_name" { 17 | type = string 18 | description = "The corresponding service name for the endpoint, obtained from the connected service." 19 | } 20 | 21 | variable "name" { 22 | type = string 23 | description = "The name of the endpoint, used to tag the endpoint resource." 24 | } -------------------------------------------------------------------------------- /modules/third_party_vpc_endpoint/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.15 " 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 4.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mongo_atlas.tf: -------------------------------------------------------------------------------- 1 | module "mongo-cf-secret" { 2 | count = var.enable_cloudformation_atlas_resources ? 1 : 0 3 | source = "./modules/secret" 4 | secret_name = "cfn/atlas/profile/${var.org_id}" 5 | secret_value = jsonencode({ 6 | PublicKey = var.mongo_atlas_public_key 7 | PrivateKey = var.mongo_atlas_private_key 8 | }) 9 | description = "Required to be able to use CloudFormation resources to create mongo resources" 10 | } 11 | 12 | module "mongo-cf-activation" { 13 | count = var.enable_cloudformation_atlas_resources ? 1 : 0 14 | source = "./modules/cf_public_extension" 15 | iam_actions = ["secretsmanager:GetSecretValue"] 16 | iam_resources = [module.mongo-cf-secret[0].secret_arn] 17 | publisher_id = var.mongo_cloudformation_publisher_id 18 | custom_resources_types = ["MongoDB::Atlas::CustomDBRole", 19 | "MongoDB::Atlas::DatabaseUser"] 20 | policy_name = "mongo-resource-activator-cf" 21 | } 22 | 23 | module "mongodb_atlas" { 24 | source = "./modules/mongoatlas" 25 | stage = var.stage 26 | organization_id = var.org_id 27 | mongo_ip_access_list = var.security.ip_whitelist 28 | mongo_instances = var.instances 29 | aws_vpc_id = var.security.aws_vpc_id 30 | private_subnet_ids = var.security.private_subnet_ids 31 | aws_allowed_access_security_groups = var.security.aws_allowed_access_security_groups 32 | jwt_audience = var.data_api_configurations.jwt_audience 33 | jwt_public_key = var.data_api_configurations.jwt_public_key 34 | tenant_id_field_in_jwt = var.data_api_configurations.tenant_id_field_in_jwt 35 | display_name_field_in_jwt = var.data_api_configurations.display_name_field_in_jwt 36 | add_mongo_ips_access_to_data_api = var.data_api_configurations.add_mongo_ips_access_to_data_api 37 | notification_email = var.alerts.email_notification 38 | daily_price_threshold_alert = var.alerts.daily_price_threshold_alert 39 | enable_continuous_backup = var.enable_continuous_backup 40 | enable_termination_protection = var.enable_termination_protection 41 | aws_region = var.region 42 | } 43 | -------------------------------------------------------------------------------- /provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | 5 | provider "mongodbatlas" { 6 | private_key = var.mongo_atlas_private_key 7 | public_key = var.mongo_atlas_public_key 8 | region = var.region 9 | } 10 | 11 | # Setup auth for mongo management rest API 12 | provider "shell" { 13 | sensitive_environment = { 14 | AUTH = jsonencode({ 15 | username = var.mongo_atlas_public_key 16 | apiKey = var.mongo_atlas_private_key 17 | }) 18 | } 19 | } -------------------------------------------------------------------------------- /terraform.tfvars: -------------------------------------------------------------------------------- 1 | org_id = "YOUR_ORG_ID" 2 | instances = ["my-instance"] 3 | enable_continuous_backup = false 4 | enable_termination_protection = true 5 | enable_cloudformation_atlas_resources = false 6 | stage = "test" 7 | 8 | security = { 9 | aws_vpc_id : "vpc-xxxxxxx", 10 | private_subnet_ids : ["subnet-aaa", "subnet-bbb"], 11 | aws_allowed_access_security_groups : ["sg-xxxxxx"], 12 | ip_whitelist : [ 13 | { 14 | cidr : "1.1.1.1/32", 15 | description : "my IP" 16 | } 17 | ] 18 | } 19 | 20 | data_api_configurations = { 21 | jwt_audience : "aud", 22 | jwt_public_key : "-----BEGIN PUBLIC KEY-----\nXXXX\nYYY\n-----END PUBLIC KEY-----\n", 23 | tenant_id_field_in_jwt : "tenantId", 24 | display_name_field_in_jwt : "sub", 25 | add_mongo_ips_access_to_data_api : true 26 | } 27 | 28 | alerts = { 29 | email_notification : "your_email@example.com", 30 | daily_price_threshold_alert : 10 31 | } 32 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "org_id" { 3 | type = string 4 | description = "The unique identifier for your MongoDB Atlas organization." 5 | } 6 | 7 | variable "mongo_cloudformation_publisher_id" { 8 | type = string 9 | description = "The publisher ID for MongoDB Atlas resources within AWS CloudFormation, enabling integration and resource management. Keep the default value unless changed by AWS/MongoDB." 10 | default = "bb989456c78c398a858fef18f2ca1bfc1fbba082" 11 | } 12 | 13 | variable "instances" { 14 | type = list(string) 15 | description = "A list of MongoDB Atlas serverless instance names to create." 16 | } 17 | 18 | variable "enable_continuous_backup" { 19 | type = bool 20 | description = "A boolean flag to enable or disable continuous backups for MongoDB Atlas instances. (Incurs additional costs)" 21 | } 22 | 23 | variable "enable_termination_protection" { 24 | type = bool 25 | description = "A boolean flag to enable or disable termination protection for MongoDB Atlas instances." 26 | } 27 | 28 | variable "enable_cloudformation_atlas_resources" { 29 | type = bool 30 | description = "A boolean flag to enable or disable the creation of MongoDB Atlas resources within AWS CloudFormation." 31 | } 32 | 33 | variable "stage" { 34 | type = string 35 | description = "A string identifier to denote the environment or stage (e.g., development, test, production) for the MongoDB Atlas setup - A project is created for each stage." 36 | } 37 | 38 | 39 | variable "security" { 40 | type = object({ 41 | aws_vpc_id = string 42 | private_subnet_ids = list(string) 43 | aws_allowed_access_security_groups = list(string) 44 | ip_whitelist = list(object({ 45 | cidr = string 46 | description = string 47 | })) 48 | }) 49 | description = <