├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── elastic_beanstalk_cdk_project.ts ├── cdk.json ├── jest.config.js ├── lib ├── elastic_beanstalk_cdk_project-stack.ts ├── rds-init-fn-code │ ├── Dockerfile │ ├── index.js │ └── package.json ├── rds_infrastructure.ts └── rds_initialiser.ts ├── package-lock.json ├── package.json ├── pictures ├── architecture_diagram.png ├── certificate_example.png ├── db_iam_authentication_policy.png ├── db_settings_overview.png ├── eb_environment_overview.png ├── enhanced_health_overview.png ├── environment_variables.png ├── https_connection.png ├── log_storage.png ├── managed_updates_configuration.png ├── post_cdk_output.png ├── route53-entry-detailed.png ├── route53-entry.png ├── sample_app_hiking_overview.png └── sample_app_homepage.png ├── src ├── code │ └── nodejs-express-hiking-example │ │ ├── .ebextensions │ │ └── statisfiles.config │ │ ├── .gitignore │ │ ├── Procfile │ │ ├── app.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ └── stylesheets │ │ │ └── style.css │ │ ├── routes │ │ ├── hike.js │ │ ├── index.js │ │ └── user.js │ │ └── views │ │ ├── hike.pug │ │ ├── index.pug │ │ └── layout.pug └── deployment_zip │ └── nodejs.zip └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | .DS_Store 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2022-02-27 9 | 10 | ### First Release 11 | 12 | - First release for this sample -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elastic Beanstalk - Hardened Security Deployment (CDK v2) 2 | **Repo name:** aws-elastic-beanstalk-hardened-security-cdk-sample 3 | 4 | **Tagline:** This is an open-source sample of a CDK script which deploys an AWS Elastic Beanstalk application with a hardened security configuration, it accompanies this blogpost: https://aws.amazon.com/blogs/security/hardening-the-security-of-your-aws-elastic-beanstalk-application-the-well-architected-way/ 5 | 6 | ## Purpose of sample 7 | In [our aws blogpost](https://aws.amazon.com/blogs/security/hardening-the-security-of-your-aws-elastic-beanstalk-application-the-well-architected-way/), we describe how our customers can harden their Elastic Beanstalk applications according to the Well-Architected Framework. This sample provides a way to explore how one could approach deploying some of the configurations mentioned in the post as Infrastructure as Code (IaC) using the AWS CDK. 8 | 9 | ## DISCLAIMER: 10 | The sample code; software libraries; command line tools; proofs of concept; templates; or other related technology (including any of the foregoing that are provided by our personnel) is provided to you as AWS Content under the AWS Customer Agreement, or the relevant written agreement between you and AWS (whichever applies). You should not use this AWS Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the AWS Content, such as sample code, as appropriate for production grade use based on your specific quality control practices and standards. Deploying AWS Content may incur AWS charges for creating or using AWS chargeable resources, such as running Amazon EC2 instances or using Amazon S3 storage. 11 | 12 | ## About this sample 13 | This sample, shows how to deploy the AWS Elastic Beanstalk environment using the new CDK v2. More specifically, the core of the CDK script deploys the following resources: 14 | 1. An Elasic Beanstalk sample NodeJS application which collects data about hikes and stores it in a database: 15 | 1. Two-tier web application deployed in a custom VPC 16 | 2. ALB Deployed in Public Subnets 17 | 3. Web-servers running on Amazon EC2 deployed in an Auto Scaling group in private subnets (NAT access) 18 | 4. Database deployed in isolated subnets (no NAT access) 19 | 2. Web Servers which use **IAM authentication** (rather than static credentials) to connect to the RDS database 20 | 3. An **encrypted Amazon RDS database**, with a Multi-AZ stand-by replica, IAM authentication, and automated backups 21 | 4. HTTPS connectivity for clients 22 | 5. Automatically generated DB admin credentials using AWS Secrets Manager 23 | 6. A Lambda function which runs a SQL query to automatically initialise the Database with a user for IAM authentication 24 | 7. **Encrypted Amazon S3 bucket** for deployments and log storage 25 | 8. Elastic Beanstalk [Managed Updates](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environment-platform-update-managed.html) settings enabled 26 | 27 | ## Architecture Diagram 28 | ![Architecture Diagram](pictures/architecture_diagram.png) 29 | 30 | ## Table of contents 31 | - [Running the CDK script](#running-the-cdk-script) 32 | - [Core Project Resources](#core-project-resources) 33 | - [Important notes](#important-notes) 34 | - [Setting up HTTPS](#setting-up-https) 35 | - [Summary](#summary) 36 | - [Requesting a public certificate](#requesting-a-public-certificate) 37 | - [Associating your custom domain with your application's load balancer](#associating-your-custom-domain-with-your-applications-load-balancer) 38 | - [Project outputs](#project-outputs) 39 | - [Configuration Settings](#configuration-settings) 40 | - [Application Settings](#application-settings) 41 | - [Database Settings](#database-settings) 42 | - [Cleaning up](#cleaning-up) 43 | - [Reference resources used for script](#reference-resources-used-for-script) 44 | - [Security](#security) 45 | - [License](#license) 46 | ## Running the CDK script 47 | **Important**: Creating the AWS Lambda function to initialise the database uses Docker, please ensure you've got Docker running on your machine before deploying the script. 48 | 49 | 1. Clone this repository 50 | 2. Run `npm install` to install the dependencies specified in `package.json` file 51 | 3. Make sure you've got AWS credentials, and CDK version 2 installed (`npm install -g aws-cdk`) 52 | 4. Verify you're running CDK version 2.1.0 or higher by running `cdk --version` 53 | 5. Bootstrap the CDK environment by running `cdk bootstrap` 54 | 6. **!! Important !!** we recommend encrypting data in transit, and our script's default configuration in `cdk.json` requires some information in order to set this up - see [Setting up HTTPS](#setting-up-https) for more information. If you do not wish to use the secure HTTPS protocol, e.g. for testing purposes, you need to explicitly disable the `lbHTTPSEnabled` setting in `cdk.json` before the script can be run. 55 | 7. Adjust any other setting you like in the `cdk.json` file 56 | 8. Run the CDK script using `cdk deploy`, confirm the IAM changes that will be made 57 | 9. Sit back, and watch the CDK script get deployed. The Amazon RDS instance deployment can take some time, because it is encrypted with backups enabled, so make yourself a cup of tea. Check [Important notes](#important-notes) if you encounter an error. 58 | 10. After the deployment has finished, you should see the following output. It shows the output of the SQL query which was run to insert a USER into the DB for authentication (rather than using admin credentials): 59 | ![CDK output](pictures/post_cdk_output.png) 60 | ## Core Project Resources 61 | 1. `cdk.json` file, which contains the configuration settings used throughout the project 62 | 2. `lib` folder, which contains the code used for CDK Deployment 63 | 1. `elastic_beanstalk_cdk_project-stack.ts`: Main Stack, contains the code to deploy most resources, including: 64 | - Encrypted S3 bucket 65 | - Custom VPC 66 | - Roles and Policies required to run the application 67 | - Security Groups 68 | - Elastic Beanstalk application 69 | 2. `rds_infrastructure.ts`: CDK Construct for the database 70 | - Creates the RDS database with configuration settings defined in `cdk.json`. 71 | - Contains a CDK Custom Resource which is required to add the appropriate policy to the web server IAM role to be able to allow it to use IAM authentication to connect to the database 72 | 3. `rds_initialiser.ts`: CDK Construct which creates a custom AWS Lambda function (see next point) 73 | 2. `lib/rds-init-fn-code` folder: Source code for custom Lambda function 74 | - Source code for lambda function which runs a SQL statement against the RDS database to initialise it with a USER which we can use for IAM Authentication for the web servers. 75 | 3. `src/deployment_zip` folder: Contains the `nodejs.zip` file which is used to run the sample application on Elastic Beanstalk 76 | 4. `src/code` folder: Contains the source code which is included in the `nodejs.zip` file 77 | - `app.js` contains the code which runs an express server, and connects to the RDS database using IAM authentication. 78 | 79 | ## Important notes 80 | - If you get an error regarding the Solution Stack name, ensure to set the latest version from [here](https://docs.aws.amazon.com/elasticbeanstalk/latest/platforms/platforms-supported.html#platforms-supported.nodejs) in `cdk.json` under `solutionStackName`. 81 | - HTTPS connectivity with Load Balancer (default) has some pre-requisite settings (see next section in this readme). Deployment will fail without those pre-requisites. We recommend using encryption wherever possible, however if you want to run this sample with HTTP instead of HTTPS, e.g. to test out the application, disable the HTTPS setting in the `cdk.json` file. 82 | - If your deployment fails, depending on which step you managed to get to, you might have to manually delete the S3 bucket created by Cloudformation before you can run it again. 83 | - Elastic Beanstalk creates two EC2 Security groups: As described in [this issue](https://github.com/aws/elastic-beanstalk-roadmap/issues/44), Elastic Beanstalk currently creates a default security group for the EC2 instances on top of the custom one we create in this script. In terms of ingress/egress, the default security group is the same as the security group we create ourselves (which we need to do to allow access from EC2 instances to RDS database), unfortunately we can't (easily) disable creating this default security group as part of the setup script. 84 | - Sample Express application is for demo purposes only: We've taken the Express NodeJS example from the [AWS Elastic Beanstalk Developer Guide](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/nodejs-getstarted.html) as an easy way to create an application that connects to an RDS database. We cannot guarantee that the source code adheres to recommended best practices when it comes to web development. We recommend that you only use it for inspiration for your own application, in particular, to connect to an RDS database using IAM authentication. Similarly, it is your responsibility to adhere to your (organisation's) policy regarding data storage, PII, etc. 85 | - CDK / Cloudformation might not pick up some changes in the Elastic Beanstalk Settings (L298 in `lib/elastic_beanstalk_cdk_project-stack.ts`) so it might require a redeployment of the setup. 86 | - If you get a 'fatal' error when testing the application, it could be because MySQL might have terminated the connection if it has not been actively used for a couple of hours (more info, see [here](https://aws.amazon.com/blogs/database/best-practices-for-configuring-parameters-for-amazon-rds-for-mysql-part-3-parameters-related-to-security-operational-manageability-and-connectivity-timeout/)). Because this is a sample application, it does not handle re-creating the database connection. For demo purposes, you can restart the application through the Elastic Beanstalk console to create a new DB connection. 87 | 88 | ## Setting up HTTPS 89 | ### Summary 90 | In order to setup HTTPS as part of this example, you will need the following three items: 91 | 1. A custom domain (Amazon Route53 or 3rd-party) 92 | 2. A certificate for your custom domain in Amazon Certificate Manager to associate with your Elastic Load Balancer. 93 | 3. A Hosted Zone in Route53 to route traffic to your custom domain to the Load Balancer associated with your Elastic Beanstalk application. 94 | 95 | ### Requesting a public certificate 96 | For public facing web applications, you can request a free certificate in AWS Certificate Manager for a custom domain you own. Verification can be done via DNS or Email. For more information on how to request a certificate see [here](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html). 97 | 98 | Once you have created a certificate, you should be able to see it in the overview (once it has been associated with a resource, it will also be indicated): 99 | ![certificate example](pictures/certificate_example.png) 100 | 101 | ### Associating your custom domain with your application's load balancer 102 | In order to have HTTPS connectivity between your clients and your load balancer, you need to create an Alias record for your custom domain to your Load Balancer. You can do this using Amazon Route 53's Hosted Zones. In order to setup a Hosted Zone, see this [documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingHostedZone.html). 103 | 104 | After you've created the hosted zone, add an Alias record and point it to the Load Balancer which is created as part of the Elastic Beanstalk application through this CDK script. 105 | 106 | [](pictures/route53-entry-detailed.png) 107 | 108 | And you should see the entry in your Hosted Zone: 109 | ![route53-entry](pictures/route53-entry.png) 110 | 111 | **Note**: it can take a couple of minutes before the DNS Record for your custom domain is fully propagated and resolving to your application. 112 | 113 | ## Project outputs 114 | 1. If you've setup HTTPS, go to the link of your custom domain 115 | ![https_connection](pictures/https_connection.png) 116 | 2. Otherwise, go to your Elastic Beanstalk environment in the console, and find the newly created application 117 | ![EB Console Overview](pictures/eb_environment_overview.png) 118 | 3. If you're not using HTTPS (e.g. for testing purposes), go to the link at the top of the page to visit the application 119 | ![EB App Overview](pictures/sample_app_homepage.png) 120 | 4. Go to the /hikes page, here you can add entries and verify the database is successfully connected 121 | ![EB Hikes Overview](pictures/sample_app_hiking_overview.png) 122 | 5. Check-out the `pictures` folder of this repository for more images of the deployment: 123 | - Policy attached to web instance role which allows IAM authentication to the DB 124 | - Overview of the DB settings 125 | - Enhanced Health overview 126 | - Overview of environment variables for the application 127 | - Log storage 128 | - Configuration of the Managed Updates 129 | 130 | 131 | ## Configuration Settings 132 | In the `cdk.json` file, under the "configuration" section, you can adjust configuration parameters which the CDK script will use for deployment. For Elastic Beanstalk specific settings, configurations have been taken from [General options for all environments 133 | ](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/command-options-general.html): 134 | 135 | ### Application Settings 136 | * **"instanceType"**: Specify Instance type of Web Instances - e.g. "t2.micro", 137 | * **"applicationName"**: Specify the name for your Application - e.g. "MySimpleNodejsExample", 138 | * **"vpcName"**: Specify the name for the VPC - e.g. "MyVPC", 139 | * **"vpcCidr"**: Define the CIDR for your VPC - e.g. "10.0.0.0/16", 140 | * **"loadbalancerInboundCIDR"**: Define the inbound IP address CIDR the load balancer security group, default is "0.0.0.0/0" to allow all connections, adjust to restrict inbound traffic from specific IP addresses. 141 | * **"loadbalancerOutboundCIDR"**: Define the outbound IP address CIDR for load balancer security group, default is "0.0.0.0/0" to allow all connections, adjust to restrict outbound traffic to specific IP addresses. 142 | * **"webserverOutboundCIDR"**: Define the outbound IP address CIDR for the webserver security group, default is "0.0.0.0/0" to allow all connections, adjust to restrict outbound traffic to specific IP addresses. 143 | * **"zipFileName"**: Define the name of the ZIP file for deployment in the `src/deployment_zip` folder. E.g. - "nodejs.zip", 144 | * **"solutionStackName"**: Use the solution stack name so that Elastic Beanstalk can automatically deploy with a software stack running on the EC2 instances. E.g. - "64bit Amazon Linux 2 v5.5.0 running Node.js 14". More information and solution stack names can be found [here](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/concepts.platforms.html). 145 | * **"managedActionsEnabled"**: Boolean string, with managed platform updates you can configure Elastic Beanstalk to automatically upgrade to the latest patch / platform version. E.g. - "true", 146 | * **"updateLevel"**: Highest level of update to apply with managed actions. E.g - "patch", more info [here](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/command-options-general.html). 147 | * **"preferredUpdateStartTime"**: Maintenance window for managed actions in "day:hour:minute" format. E.g. - "Sun:01:00", 148 | * **"streamLogs"**: Specifies whether to create groups in Amazon CloudWatch Logs for proxy and deployment logs, and stream logs from each instance in your environment. E.g. "true", 149 | * **"deleteLogsOnTerminate"**: Whether or not to delete logs after environment is terminated. E.g. - "false", 150 | * **"logRetentionDays"**: Defines number of days logs should be retained, if not automatically deleted on termination. E.g. - "7", 151 | * **"loadBalancerType"**: Specifies Load Balancer type - e.g. "application", 152 | * **"lbHTTPSEnabled"**: Boolean, specifies whether or not to use HTTPS for the connection between clients and Load Balancer. 153 | * **"lbHTTPSCertificateArn"**: If using HTTPS, a valid ARN for a certificate in AWS Certificate Manager needs to be provided. 154 | * **"lbSSLPolicy"**: Specifies which policy to use for the TLS listener. Default is "ELBSecurityPolicy-FS-1-2-Res-2020-10", for more options, see [here](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html)). For your production applications, choose a policy which fits your security requirements. 155 | 156 | 157 | ### Database Settings 158 | * **"dbName"**: RDS Database name, e.g. - "databasename", 159 | * **"dbAdminUsername"**: Admin username for database, e.g. - "admin", 160 | * **"dbWebUsername"**: Username for database initialisation (SQL statement) used by the web servers to connect to database. E.g. - "dbwebuser", 161 | * **"dbStorageGB"**: Storage size in GB for database - e.g. 100, 162 | * **"dbMaxStorageGiB"**: Maximum size for database in GiB - e.g. 200, 163 | * **"dbMultiAZ"**: Boolean, select if you want to deploy DB in a multi-AZ configuration for disaster recovery. E.g. - true, 164 | * **"dbBackupRetentionDays"**: The number of days during which automatic DB snapshots are retained. E.g. 7, 165 | * **"dbDeleteAutomatedBackups"**: Set if automated backups should be deleted when you delete the DB instance. E.g. - true, 166 | * **"dbPreferredBackupWindow"**: Daily time-range for automated backups in "hh24:mi-hh24:mi" format. E.g. - "01:00-01:30", 167 | * **"dbCloudwatchLogsExports"**: Determine which logs are exported to CloudWatch depends on DB type. For MySQL see [this link](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_LogAccess.MySQL.LogFileSize.html). E.g. - ["audit","error","general","slowquery"], 168 | * **"dbIamAuthentication"**: Whether or not to use IAM authentication for the WebInstances to connect to the database (rather than static passwords).e.g. - true, 169 | * **"dbInstanceType"**: Set the instance type for the Database Instance. E.g. - "t2.small", 170 | * **"dbRetentionPolicy"**: Set the deletion policy for the database. Values: "retain", "destroy", and "snapshot". If the stack is deleted, and deletion policy is set to "retain", then the database will not be deleted, including relevant networking infrastructure (such as VPC, Security Groups, etc.). Default value for demo: "destroy" 171 | 172 | ## Cleaning up 173 | To delete the resources created as part of this script, run `cdk destroy`. Depending on your 'dbRetentionPolicy' setting, some resources might be retained after deletion. This would be the RDS instance including networking infrastructure related to the database (i.e. VPC, Subnets, Security Groups, etc.). On top of that, the Elastic Beanstalk S3 bucket is not automatically deleted, regardless of deletion policy, and you will have to empty it first, remove Deletion Protection in the S3 permissions, and then delete the bucket. 174 | 175 | ## Reference resources used for script 176 | Note: below, some links refer to Issues which contain explanations or code-examples for some of the work-arounds. More details can be found in the specific source code files of this project, which refer to these links where applicable. 177 | 178 | Resources used: 179 | 1. General AWS Public Documentation 180 | 2. https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/command-options-general.html 181 | 3. https://issueexplorer.com/issue/aws/aws-cdk/17205 182 | 4. https://github.com/aws/aws-cdk/issues/1430 183 | 5. https://aws.amazon.com/blogs/infrastructure-and-automation/use-aws-cdk-to-initialize-amazon-rds-instances/ 184 | 6. https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/nodejs-getstarted.html 185 | 7. https://github.com/aws/aws-cdk/issues/11851 186 | 8. https://github.com/aws/aws-sdk-js-v3/issues/1823 187 | 188 | ## Security 189 | 190 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 191 | 192 | ## License 193 | 194 | This library is licensed under the MIT-0 License. See the LICENSE file. 195 | 196 | 197 | -------------------------------------------------------------------------------- /bin/elastic_beanstalk_cdk_project.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { ElasticBeanstalkCdkStack, ElasticBeanstalkCdkStackProps } from '../lib/elastic_beanstalk_cdk_project-stack'; 4 | 5 | const app = new cdk.App(); 6 | const settings: ElasticBeanstalkCdkStackProps = app.node.tryGetContext('configuration') 7 | 8 | new ElasticBeanstalkCdkStack(app, 'ElasticBeanstalkCdkStack', settings); -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/elastic_beanstalk_cdk_project.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "configuration": { 21 | "instanceType": "t2.micro", 22 | "applicationName": "MySimpleNodejsExample", 23 | "vpcName": "MyVPC", 24 | "vpcCidr": "10.0.0.0/16", 25 | "loadbalancerInboundCIDR": "0.0.0.0/0", 26 | "loadbalancerOutboundCIDR": "0.0.0.0/0", 27 | "webserverOutboundCIDR": "0.0.0.0/0", 28 | "zipFileName": "nodejs.zip", 29 | "solutionStackName": "64bit Amazon Linux 2 v5.5.0 running Node.js 14", 30 | "managedActionsEnabled": "true", 31 | "updateLevel": "patch", 32 | "preferredUpdateStartTime": "Sun:01:00", 33 | "streamLogs": "true", 34 | "deleteLogsOnTerminate": "false", 35 | "logRetentionDays": "7", 36 | "loadBalancerType": "application", 37 | "lbHTTPSEnabled": true, 38 | "lbHTTPSCertificateArn": "", 39 | "lbSSLPolicy": null, 40 | "databaseSettings": { 41 | "dbName": "databasename", 42 | "dbAdminUsername": "admin", 43 | "dbWebUsername": "dbwebuser", 44 | "dbStorageGB": 100, 45 | "dbMaxStorageGiB": 200, 46 | "dbMultiAZ": true, 47 | "dbBackupRetentionDays": 7, 48 | "dbDeleteAutomatedBackups": true, 49 | "dbPreferredBackupWindow": "01:00-01:30", 50 | "dbCloudwatchLogsExports": ["audit","error","general","slowquery"], 51 | "dbIamAuthentication": true, 52 | "dbInstanceType": "t2.small", 53 | "dbRetentionPolicy": "destroy" 54 | } 55 | }, 56 | "@aws-cdk/core:stackRelativeExports": true, 57 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 58 | "@aws-cdk/aws-lambda:recognizeVersionProps": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/elastic_beanstalk_cdk_project-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, RemovalPolicy, App, Duration, CfnOutput, Token, CfnResource } from 'aws-cdk-lib'; 2 | import * as logs from 'aws-cdk-lib/aws-logs'; 3 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 4 | import * as elasticbeanstalk from 'aws-cdk-lib/aws-elasticbeanstalk'; 5 | import * as s3 from 'aws-cdk-lib/aws-s3'; 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import * as s3Deploy from 'aws-cdk-lib/aws-s3-deployment'; 8 | 9 | import { DockerImageCode } from 'aws-cdk-lib/aws-lambda' 10 | import { CdkResourceInitializer } from './rds_initialiser'; 11 | import { CdkRDSResource, DatabaseProps } from './rds_infrastructure'; 12 | 13 | export interface ElasticBeanstalkCdkStackProps { 14 | readonly instanceType: string; 15 | readonly applicationName: string; 16 | readonly vpcName: string; 17 | readonly vpcCidr: string; 18 | readonly loadbalancerInboundCIDR: string; 19 | readonly loadbalancerOutboundCIDR: string; 20 | readonly webserverOutboundCIDR: string; 21 | readonly zipFileName: string; 22 | readonly solutionStackName: string; 23 | readonly managedActionsEnabled: string; 24 | readonly updateLevel: string; 25 | readonly preferredUpdateStartTime: string; 26 | readonly streamLogs: string; 27 | readonly deleteLogsOnTerminate: string; 28 | readonly logRetentionDays: string; 29 | readonly loadBalancerType: string; 30 | readonly lbHTTPSEnabled: boolean; 31 | readonly lbHTTPSCertificateArn: string; 32 | readonly lbSSLPolicy: string; 33 | readonly databaseSettings: DatabaseProps; 34 | } 35 | 36 | export class ElasticBeanstalkCdkStack extends Stack { 37 | constructor(scope: App, id: string, props: ElasticBeanstalkCdkStackProps) { 38 | super(scope, id); 39 | const { 40 | applicationName, 41 | instanceType, 42 | vpcName, 43 | vpcCidr, 44 | loadbalancerInboundCIDR, 45 | loadbalancerOutboundCIDR, 46 | webserverOutboundCIDR, 47 | zipFileName, 48 | solutionStackName, 49 | managedActionsEnabled, 50 | updateLevel, 51 | preferredUpdateStartTime, 52 | streamLogs, 53 | deleteLogsOnTerminate, 54 | logRetentionDays, 55 | loadBalancerType, 56 | lbHTTPSEnabled, 57 | lbHTTPSCertificateArn, 58 | lbSSLPolicy, 59 | } = props 60 | 61 | if (lbHTTPSEnabled && lbHTTPSCertificateArn === "") { 62 | throw new Error("Please provide a certificate ARN in cdk.json, or disable HTTPS for testing purposes"); 63 | } 64 | 65 | console.log("Configuration settings: ", props) 66 | 67 | const { dbWebUsername, dbName, dbRetentionPolicy } = props.databaseSettings // get some database settings 68 | 69 | let retentionPolicy: RemovalPolicy; 70 | switch (dbRetentionPolicy) { 71 | case "destroy": retentionPolicy = RemovalPolicy.DESTROY; break; 72 | case "snapshot": retentionPolicy = RemovalPolicy.SNAPSHOT; break; 73 | default: retentionPolicy = RemovalPolicy.RETAIN 74 | } 75 | 76 | // Create an encrypted bucket for deployments and log storage 77 | // S3 Bucket needs a specific format for deployment + logs: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.S3.html 78 | const encryptedBucket = new s3.Bucket(this, 'EBEncryptedBucket', { 79 | bucketName: `elasticbeanstalk-${this.region}-${this.account}`, 80 | encryption: s3.BucketEncryption.S3_MANAGED, 81 | serverAccessLogsPrefix: 'server_access_logs', 82 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 83 | enforceSSL: true 84 | }) 85 | 86 | /* 87 | Create a VPC with three subnets, spread across two AZs: 88 | 1. Private subnet with route to NAT Gateway for the webinstances 89 | 2. Private subnet without NAT Gateway (isolated) for the database instance 90 | 3. Public subnet with Internet Gateway + NAT Gateway for public access for ALB and NAT Gateway access from Web instances 91 | 92 | Store VPC flow logs in the encrypted bucket we created above 93 | */ 94 | const vpc = new ec2.Vpc(this, vpcName, { 95 | natGateways: 1, 96 | maxAzs: 2, 97 | cidr: vpcCidr, 98 | flowLogs: { 99 | 's3': { 100 | destination: ec2.FlowLogDestination.toS3(encryptedBucket, 'vpc-flow-logs'), 101 | trafficType: ec2.FlowLogTrafficType.ALL 102 | } 103 | }, 104 | subnetConfiguration: [ 105 | { 106 | name: 'private-with-nat', 107 | subnetType: ec2.SubnetType.PRIVATE_WITH_NAT, 108 | }, 109 | { 110 | name: 'private-isolated', 111 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 112 | }, 113 | { 114 | name: 'public', 115 | subnetType: ec2.SubnetType.PUBLIC, 116 | } 117 | ] 118 | }) 119 | 120 | vpc.node.addDependency(encryptedBucket) 121 | 122 | // Upload the example ZIP file to the deployment bucket 123 | const appDeploymentZip = new s3Deploy.BucketDeployment(this, "DeployZippedApplication", { 124 | sources: [s3Deploy.Source.asset(`${__dirname}/../src/deployment_zip`)], 125 | destinationBucket: encryptedBucket 126 | }); 127 | 128 | // Define a new Elastic Beanstalk application 129 | const app = new elasticbeanstalk.CfnApplication(this, 'Application', { 130 | applicationName: applicationName, 131 | }); 132 | 133 | // Create role for the web-instances 134 | const webtierRole = new iam.Role(this, `${applicationName}-webtier-role`, { 135 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), 136 | }); 137 | 138 | // Add a managed policy for the ELastic Beanstalk web-tier to the webTierRole 139 | const managedPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName('AWSElasticBeanstalkWebTier') 140 | webtierRole.addManagedPolicy(managedPolicy); 141 | 142 | // Create an instance profile for the web-instance role 143 | const ec2ProfileName = `${applicationName}-EC2WebInstanceProfile` 144 | const ec2InstanceProfile = new iam.CfnInstanceProfile(this, ec2ProfileName, { 145 | instanceProfileName: ec2ProfileName, 146 | roles: [webtierRole.roleName] 147 | }); 148 | 149 | // Create Security Group for load balancer 150 | const lbSecurityGroup = new ec2.SecurityGroup(this, 'LbSecurityGroup', { 151 | vpc: vpc, 152 | description: "Security Group for the Load Balancer", 153 | securityGroupName: "lb-security-group-name", 154 | allowAllOutbound: false 155 | }) 156 | 157 | // Determine if HTTP or HTTPS port should be used for LB 158 | const lbPort = lbHTTPSEnabled === true ? 443 : 80 159 | 160 | // Allow Security Group outbound traffic for load balancer 161 | lbSecurityGroup.addEgressRule( 162 | ec2.Peer.ipv4(loadbalancerOutboundCIDR), 163 | ec2.Port.tcp(lbPort), 164 | `Allow outgoing traffic over port ${lbPort}` 165 | ); 166 | 167 | // Allow Security Group inbound traffic for load balancer 168 | lbSecurityGroup.addIngressRule( 169 | ec2.Peer.ipv4(loadbalancerInboundCIDR), 170 | ec2.Port.tcp(lbPort), 171 | `Allow incoming traffic over port ${lbPort}` 172 | ); 173 | 174 | // Create Security Group for web instances 175 | const webSecurityGroup = new ec2.SecurityGroup(this, 'WebSecurityGroup', { 176 | vpc: vpc, 177 | description: "Security Group for the Web instances", 178 | securityGroupName: "web-security-group", 179 | allowAllOutbound: false 180 | }) 181 | 182 | // Allow Security Group outbound traffic over port 80 instances 183 | webSecurityGroup.addEgressRule( 184 | ec2.Peer.ipv4(webserverOutboundCIDR), 185 | ec2.Port.tcp(80), 186 | 'Allow outgoing traffic over port 80' 187 | ); 188 | 189 | // Allow Security Group inbound traffic over port 80 from the Load Balancer security group 190 | webSecurityGroup.connections.allowFrom( 191 | new ec2.Connections({ 192 | securityGroups: [lbSecurityGroup] 193 | }), 194 | ec2.Port.tcp(80) 195 | ) 196 | 197 | // Create Security Group for Database (+ replica) 198 | const dbSecurityGroup = new ec2.SecurityGroup(this, 'DbSecurityGroup', { 199 | vpc: vpc, 200 | description: "Security Group for the RDS instance", 201 | securityGroupName: "db-security-group", 202 | allowAllOutbound: false 203 | }) 204 | 205 | /* 206 | https://issueexplorer.com/issue/aws/aws-cdk/17205 - retain isolated subnets 207 | If we want to keep the DB, we need to maintain the isolated subnets and corresponding VPC. 208 | There is no easy way to keep the isolated subnets and destroy all the other resources in the VPC (IGW, NAT, EIP, etc.) 209 | Therefore, we're going to keep the whole VPC in case we want to keep the DB alive when running CDK destroy. 210 | */ 211 | if (retentionPolicy === RemovalPolicy.RETAIN) { 212 | dbSecurityGroup.applyRemovalPolicy(retentionPolicy) 213 | vpc.applyRemovalPolicy(retentionPolicy) 214 | vpc.node.findAll().forEach(node => node instanceof CfnResource && node.applyRemovalPolicy(retentionPolicy)) 215 | } 216 | 217 | // Allow inbound traffic on port 3306 from the web instances 218 | dbSecurityGroup.connections.allowFrom( 219 | new ec2.Connections({ 220 | securityGroups: [webSecurityGroup] 221 | }), 222 | ec2.Port.tcp(3306) 223 | ) 224 | 225 | /* 226 | Note for code above ^: We didn't select outbound traffic for DB Security Group above. 227 | Setting no outbound will yield: "out -> ICMP 252-86 -> 255.255.255.255/32" to be added to the security group. 228 | This is used in order to disable the "all traffic" default of Security Groups. No machine can ever actually have 229 | the 255.255.255.255 IP address, but in order to lock it down even more we'll restrict to a nonexistent ICMP traffic type. 230 | Source: https://github.com/aws/aws-cdk/issues/1430 231 | */ 232 | 233 | // Create the RDS instance from the custom resource defined in 'rds_infrastructure.ts' 234 | const rdsResource = new CdkRDSResource(this, 'rdsResource', { 235 | applicationName, 236 | dbSecurityGroup, 237 | vpc: vpc, 238 | databaseProps: props.databaseSettings, 239 | webTierRole: webtierRole, 240 | retentionSetting: retentionPolicy 241 | }); 242 | 243 | // get variables from rds resource 244 | const { rdsInstance, rdsCredentials, rdsCredentialsName } = rdsResource 245 | 246 | /* 247 | Source for initialiser: 248 | https://aws.amazon.com/blogs/infrastructure-and-automation/use-aws-cdk-to-initialize-amazon-rds-instances/ 249 | Initialiser is a Custom Resource which runs a function which executes a Lambda function to create a user 250 | in the RDS database with IAM authentication. Lambda function can be deleted after first execution 251 | */ 252 | const initializer = new CdkResourceInitializer(this, 'MyRdsInit', { 253 | config: { 254 | dbCredentialsName: rdsCredentialsName, 255 | dbWebUsername, 256 | dbName 257 | }, 258 | fnLogRetention: logs.RetentionDays.FIVE_MONTHS, 259 | fnCode: DockerImageCode.fromImageAsset(`${__dirname}/rds-init-fn-code`, {}), 260 | fnTimeout: Duration.minutes(2), 261 | fnSecurityGroups: [], 262 | vpc 263 | }) 264 | 265 | // Add a dependency for the initialiser to make sure it runs only after the RDS instance has been created 266 | initializer.customResource.node.addDependency(rdsInstance) 267 | 268 | // Allow the initializer function to connect to the RDS instance 269 | rdsInstance.connections.allowFrom(initializer.function, ec2.Port.tcp(3306)) 270 | 271 | // Allow initializer function to read RDS instance creds secret 272 | rdsCredentials.grantRead(initializer.function) 273 | 274 | // Output the output of the initialiser, to make sure that the query was executed properly 275 | const output = new CfnOutput(this, 'RdsInitFnResponse', { 276 | value: Token.asString(initializer.response) 277 | }) 278 | 279 | /* 280 | CREATING THE ELASTIC BEANSTALK APPLICATION 281 | */ 282 | 283 | // Get the public and private subnets to deploy Elastic Beanstalk ALB and web servers in. 284 | const publicSubnets = vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }).subnets 285 | const privateWebSubnets = vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_NAT }).subnets 286 | 287 | // A helper function to create a comma separated string from subnets ids 288 | const createCommaSeparatedList = function (subnets: ec2.ISubnet[]): string { 289 | return subnets.map((subnet: ec2.ISubnet) => subnet.subnetId).toString() 290 | } 291 | 292 | const webserverSubnets = createCommaSeparatedList(privateWebSubnets) 293 | const lbSubnets = createCommaSeparatedList(publicSubnets) 294 | 295 | // Define settings for the Elastic Beanstalk application 296 | // Documentation for settings: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/command-options-general.html 297 | const serviceLinkedRole = 'AWSServiceRoleForElasticBeanstalkManagedUpdates' 298 | var ebSettings = [ 299 | ['aws:elasticbeanstalk:environment', 'LoadBalancerType', loadBalancerType], // Set the load balancer type (e.g. 'application' for ALB) 300 | ['aws:autoscaling:launchconfiguration', 'InstanceType', instanceType], // Set instance type for web tier 301 | ['aws:autoscaling:launchconfiguration', 'IamInstanceProfile', ec2InstanceProfile.attrArn], // Set IAM Instance Profile for web tier 302 | ['aws:autoscaling:launchconfiguration', 'SecurityGroups', webSecurityGroup.securityGroupId], // Set Security Group for web tier 303 | ['aws:ec2:vpc', 'VPCId', vpc.vpcId], // Deploy resources in VPC created earlier 304 | ['aws:ec2:vpc', 'Subnets', webserverSubnets], // Deploy Web tier instances in private subnets 305 | ['aws:ec2:vpc', 'ELBSubnets', lbSubnets], // Deploy Load Balancer in public subnets 306 | ['aws:elbv2:loadbalancer', 'SecurityGroups', lbSecurityGroup.securityGroupId], // Attach Security Group to Load Balancer 307 | ['aws:elasticbeanstalk:managedactions', 'ServiceRoleForManagedUpdates', serviceLinkedRole], // Select Service Role for Managed Updates (Elastic Beanstalk will automatically create) 308 | ['aws:elasticbeanstalk:managedactions', 'ManagedActionsEnabled', managedActionsEnabled], // Whether or not to enable managed actions 309 | ['aws:elasticbeanstalk:managedactions:platformupdate', 'UpdateLevel', updateLevel], // Set the update level (e.g. 'patch' or 'minor') 310 | ['aws:elasticbeanstalk:managedactions', 'PreferredStartTime', preferredUpdateStartTime], // Set preferred start time for managed updates 311 | ['aws:elasticbeanstalk:cloudwatch:logs', 'StreamLogs', streamLogs], // Whether or not to stream logs to CloudWatch 312 | ['aws:elasticbeanstalk:cloudwatch:logs', 'DeleteOnTerminate', deleteLogsOnTerminate], // Whether or not to delete log groups when Elastic Beanstalk environment is terminated 313 | ['aws:elasticbeanstalk:cloudwatch:logs', 'RetentionInDays', logRetentionDays], // Number of days logs should be retained 314 | ['aws:elasticbeanstalk:hostmanager', 'LogPublicationControl', 'true'], // Enable Logging to be stored in S3 315 | ['aws:elasticbeanstalk:application:environment', 'RDS_HOSTNAME', rdsInstance.dbInstanceEndpointAddress], // Define Env Variable for HOSTNAME 316 | ['aws:elasticbeanstalk:application:environment', 'RDS_PORT', rdsInstance.dbInstanceEndpointPort], // Define Env Variable for PORT 317 | ['aws:elasticbeanstalk:application:environment', 'RDS_USERNAME', props.databaseSettings.dbWebUsername], // Define Env Variable for DB username to connect (web tier) 318 | ['aws:elasticbeanstalk:application:environment', 'RDS_DATABASE', props.databaseSettings.dbName], // Define Env Variable for DB name (defined when RDS db created) 319 | ['aws:elasticbeanstalk:application:environment', 'REGION', this.region], // Define Env Variable for Region 320 | ] 321 | 322 | if (lbHTTPSEnabled === true) { 323 | const sslPolicy = lbSSLPolicy || "ELBSecurityPolicy-FS-1-2-Res-2020-10" 324 | const httpsSettings = [ 325 | ['aws:elbv2:listener:default', 'ListenerEnabled', "false"], // Disable the default HTTP listener 326 | ['aws:elbv2:listener:443', 'ListenerEnabled', "true"], // Create a new HTTPS listener on port 443 327 | ['aws:elbv2:listener:443', 'SSLCertificateArns', lbHTTPSCertificateArn], // Attach the certificate for the custom domain 328 | ['aws:elbv2:listener:443', 'SSLPolicy', sslPolicy], // Specifies the TLS policy 329 | ['aws:elbv2:listener:443', 'Protocol', "HTTPS"], // Sets the protocol for the listener to HTTPS 330 | ] 331 | ebSettings = ebSettings.concat(httpsSettings) 332 | } 333 | /* Map settings created above, to the format required for the Elastic Beanstalk OptionSettings 334 | [ 335 | { 336 | namespace: "", 337 | optionName: "", 338 | value: "" 339 | }, 340 | .... 341 | ] 342 | */ 343 | const optionSettingProperties: elasticbeanstalk.CfnEnvironment.OptionSettingProperty[] = ebSettings.map( 344 | setting => ({ namespace: setting[0], optionName: setting[1], value: setting[2] }) 345 | ) 346 | 347 | // Create an app version based on the sample application (from https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/nodejs-getstarted.html) 348 | const appVersionProps = new elasticbeanstalk.CfnApplicationVersion(this, 'EBAppVersion', { 349 | applicationName: applicationName, 350 | sourceBundle: { 351 | s3Bucket: encryptedBucket.bucketName, 352 | s3Key: zipFileName, 353 | }, 354 | }); 355 | 356 | // Create Elastic Beanstalk environment 357 | new elasticbeanstalk.CfnEnvironment(this, 'EBEnvironment', { 358 | environmentName: `${applicationName}-env`, 359 | applicationName: applicationName, 360 | solutionStackName: solutionStackName, 361 | versionLabel: appVersionProps.ref, 362 | optionSettings: optionSettingProperties, 363 | }); 364 | 365 | // Make sure we've initialised DB before we deploy EB 366 | appVersionProps.node.addDependency(output) 367 | 368 | // Ensure the app and the example ZIP file exists before adding a version 369 | appVersionProps.node.addDependency(appDeploymentZip) 370 | appVersionProps.addDependsOn(app); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /lib/rds-init-fn-code/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazon/aws-lambda-nodejs:14 2 | WORKDIR ${LAMBDA_TASK_ROOT} 3 | 4 | COPY package.json ./ 5 | RUN npm install --only=production 6 | COPY index.js ./ 7 | 8 | CMD [ "index.handler" ] -------------------------------------------------------------------------------- /lib/rds-init-fn-code/index.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql') 2 | const AWS = require('aws-sdk') 3 | 4 | const secrets = new AWS.SecretsManager({}) 5 | 6 | 7 | // SOURCE: https://aws.amazon.com/blogs/infrastructure-and-automation/use-aws-cdk-to-initialize-amazon-rds-instances/ 8 | // See SQL Statement on line 26 to see what we had to add to the database to make IAM authentication work 9 | 10 | exports.handler = async (e) => { 11 | try { 12 | const { config } = e.params 13 | const { password, username, host } = await getSecretValue(config.dbCredentialsName) 14 | const connection = mysql.createConnection({ 15 | host, 16 | user: username, 17 | password, 18 | multipleStatements: true 19 | }) 20 | 21 | connection.connect() 22 | 23 | // SQL statement to create a user which uses the AWSAuthenticationPlugin 24 | // See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html for more info (and Postgres example) 25 | const sqlStatement = `CREATE USER '${config.dbWebUsername}' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'; 26 | GRANT ALL PRIVILEGES ON ${config.dbName}.* TO '${config.dbWebUsername}' @'%'; 27 | FLUSH PRIVILEGES;` 28 | const res = await query(connection, sqlStatement) 29 | 30 | return { 31 | status: 'OK', 32 | results: res 33 | } 34 | } catch (err) { 35 | return { 36 | status: 'ERROR', 37 | err, 38 | message: err.message 39 | } 40 | } 41 | } 42 | 43 | function query (connection, sql) { 44 | return new Promise((resolve, reject) => { 45 | connection.query(sql, (error, res) => { 46 | if (error) return reject(error) 47 | 48 | return resolve(res) 49 | }) 50 | }) 51 | } 52 | 53 | function getSecretValue (secretId) { 54 | return new Promise((resolve, reject) => { 55 | secrets.getSecretValue({ SecretId: secretId }, (err, data) => { 56 | if (err) return reject(err) 57 | 58 | return resolve(JSON.parse(data.SecretString)) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /lib/rds-init-fn-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rds-init-script", 3 | "version": "1.0.0", 4 | "description": "RDS initialization implementation in Node.js", 5 | "main": "index.js", 6 | "scripts": { 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "mysql": "^2.18.1" 13 | } 14 | } -------------------------------------------------------------------------------- /lib/rds_infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { RemovalPolicy, Duration, Stack } from 'aws-cdk-lib'; 3 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 4 | import * as secretsManager from 'aws-cdk-lib/aws-secretsmanager'; 5 | import * as rds from 'aws-cdk-lib/aws-rds'; 6 | import * as custom from 'aws-cdk-lib/custom-resources' 7 | import * as iam from 'aws-cdk-lib/aws-iam'; 8 | 9 | export interface DatabaseProps { 10 | readonly dbName: string; 11 | readonly dbAdminUsername: string; 12 | readonly dbWebUsername: string; 13 | readonly dbStorageGB: number; 14 | readonly dbMaxStorageGiB: number; 15 | readonly dbMultiAZ: boolean; 16 | readonly dbBackupRetentionDays: number; 17 | readonly dbDeleteAutomatedBackups: boolean; 18 | readonly dbPreferredBackupWindow: string; 19 | readonly dbCloudwatchLogsExports: string[]; 20 | readonly dbIamAuthentication: boolean; 21 | readonly dbInstanceType: string; 22 | readonly dbRetentionPolicy: string; 23 | } 24 | 25 | export interface CdkRDSResourceProps { 26 | readonly applicationName: string; 27 | readonly dbSecurityGroup: ec2.ISecurityGroup; 28 | readonly vpc: ec2.IVpc; 29 | readonly databaseProps: DatabaseProps; 30 | readonly webTierRole: iam.IRole; 31 | readonly retentionSetting: RemovalPolicy; 32 | } 33 | 34 | export class CdkRDSResource extends Construct { 35 | public readonly rdsInstance: rds.IDatabaseInstance; 36 | public readonly rdsCredentials: secretsManager.ISecret; 37 | public readonly rdsCredentialsName: string; 38 | 39 | constructor(scope: Construct, id: string, props: CdkRDSResourceProps) { 40 | super(scope, id) 41 | 42 | const { applicationName, vpc, dbSecurityGroup, webTierRole, retentionSetting } = props 43 | const { 44 | dbName, 45 | dbAdminUsername, 46 | dbWebUsername, 47 | dbStorageGB, 48 | dbMaxStorageGiB, 49 | dbMultiAZ, 50 | dbBackupRetentionDays, 51 | dbDeleteAutomatedBackups, 52 | dbPreferredBackupWindow, 53 | dbCloudwatchLogsExports, 54 | dbIamAuthentication, 55 | dbInstanceType 56 | } = props.databaseProps 57 | 58 | /* 59 | Use Secrets Manager to create credentials for the Admin user for the RDS database 60 | Admin account is only used to create a dbwebusername, which the application uses to connect 61 | Admin credentials are preserved in Secrets Manager, in case of emergency. 62 | For now, credentials are not rotated 63 | */ 64 | const dbCredentialsName = `${applicationName}-database-credentials` 65 | const dbCredentials = new secretsManager.Secret(this, `${applicationName}-DBCredentialsSecret`, { 66 | secretName: dbCredentialsName, 67 | generateSecretString: { 68 | secretStringTemplate: JSON.stringify({ 69 | username: dbAdminUsername, 70 | }), 71 | excludePunctuation: true, 72 | includeSpace: false, 73 | generateStringKey: 'password' 74 | } 75 | }); 76 | 77 | // Define a subnetGroup based on the isolated subnets from the VPC we created 78 | const rdsSubnetGroup = new rds.SubnetGroup(this, 'rds-subnet-group', { 79 | vpc: vpc, 80 | description: 'subnetgroup-db', 81 | vpcSubnets: { 82 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED 83 | } 84 | }) 85 | rdsSubnetGroup.applyRemovalPolicy(retentionSetting) 86 | 87 | // Define the configuration of the RDS instance 88 | const rdsConfig: rds.DatabaseInstanceProps = { 89 | vpc, 90 | engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_25 }), 91 | instanceType: new ec2.InstanceType(dbInstanceType), 92 | instanceIdentifier: `${applicationName}`, 93 | allocatedStorage: dbStorageGB, 94 | maxAllocatedStorage: dbMaxStorageGiB, 95 | securityGroups: [dbSecurityGroup], 96 | credentials: rds.Credentials.fromSecret(dbCredentials), // Get both username and password for Admin user from Secrets manager 97 | storageEncrypted: true, 98 | databaseName: dbName, 99 | multiAz: dbMultiAZ, 100 | backupRetention: Duration.days(dbBackupRetentionDays), // If set to 0, no backup 101 | deleteAutomatedBackups: dbDeleteAutomatedBackups, 102 | preferredBackupWindow: dbPreferredBackupWindow, 103 | publiclyAccessible: false, 104 | removalPolicy: retentionSetting, 105 | cloudwatchLogsExports: dbCloudwatchLogsExports, 106 | cloudwatchLogsRetention: dbBackupRetentionDays, 107 | subnetGroup: rdsSubnetGroup, 108 | iamAuthentication: dbIamAuthentication // Enables IAM authentication for the database 109 | } 110 | 111 | // create the Database instance, assign it to the public attribute so that the stack can read it from the construct 112 | this.rdsInstance = new rds.DatabaseInstance(this, `${applicationName}-instance`, rdsConfig); 113 | this.rdsCredentials = dbCredentials 114 | this.rdsCredentialsName = dbCredentialsName 115 | 116 | /* 117 | There is an issue with rdsInstance.grantConnect(myRole); In a nutshell, the permission created, doesn't actually 118 | create access based on the format defined here: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html 119 | 120 | We still need to add permissions for the web-application to connect to the RDS database with IAM credentials 121 | A workaround was implemented based on: https://github.com/aws/aws-cdk/issues/11851 122 | 123 | For the permissions, we need access to the ResourceId of the instance. 124 | In a nutshell, we create a custom resource, which calls a Lambda function. 125 | This Lambda function calls the describeDBInstances api, and gets the resourceId 126 | We construct a proper policy, and attach it to the web instances' role. 127 | */ 128 | if (dbIamAuthentication) { 129 | const { region, account, stackName } = Stack.of(this) 130 | const customResourceFnRole = new iam.Role(this, 'AwsCustomResourceRoleInfra', { 131 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') 132 | }) 133 | customResourceFnRole.addToPolicy( 134 | new iam.PolicyStatement({ 135 | resources: [`arn:aws:lambda:${region}:${account}:function:*-ResInit${stackName}`], 136 | actions: ['lambda:InvokeFunction'] 137 | }) 138 | ) 139 | const dbResourceId = new custom.AwsCustomResource(this, 'RdsInstanceResourceId', { 140 | onCreate: { 141 | service: 'RDS', 142 | action: 'describeDBInstances', 143 | parameters: { 144 | DBInstanceIdentifier: this.rdsInstance.instanceIdentifier, 145 | }, 146 | physicalResourceId: custom.PhysicalResourceId.fromResponse('DBInstances.0.DbiResourceId'), 147 | outputPaths: ['DBInstances.0.DbiResourceId'], 148 | }, 149 | policy: custom.AwsCustomResourcePolicy.fromSdkCalls({ 150 | resources: custom.AwsCustomResourcePolicy.ANY_RESOURCE, 151 | }), 152 | role: customResourceFnRole 153 | }); 154 | const resourceId = dbResourceId.getResponseField( 155 | 'DBInstances.0.DbiResourceId' 156 | ) 157 | 158 | const dbUserArn = `arn:aws:rds-db:${region}:${account}:dbuser:${resourceId}/${dbWebUsername}` 159 | 160 | webTierRole.addToPrincipalPolicy( 161 | new iam.PolicyStatement({ 162 | actions: ['rds-db:connect'], 163 | resources: [dbUserArn] 164 | }) 165 | ) 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /lib/rds_initialiser.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as ec2 from 'aws-cdk-lib/aws-ec2' 3 | import * as lambda from 'aws-cdk-lib/aws-lambda' 4 | import { Duration, Stack } from 'aws-cdk-lib' 5 | import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from 'aws-cdk-lib/custom-resources' 6 | import { RetentionDays } from 'aws-cdk-lib/aws-logs' 7 | import { PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam' 8 | 9 | export interface CdkResourceInitializerProps { 10 | vpc: ec2.IVpc 11 | fnSecurityGroups: ec2.ISecurityGroup[] 12 | fnTimeout: Duration 13 | fnCode: lambda.DockerImageCode 14 | fnLogRetention: RetentionDays 15 | fnMemorySize?: number 16 | config: any 17 | } 18 | 19 | /** 20 | * The main source for this code: https://aws.amazon.com/blogs/infrastructure-and-automation/use-aws-cdk-to-initialize-amazon-rds-instances/ 21 | * My changes: Removed the function hash calculator when I moved to CDK v2. Getting function physical resource id by the function name instead. 22 | */ 23 | 24 | export class CdkResourceInitializer extends Construct { 25 | public readonly response: string 26 | public readonly customResource: AwsCustomResource 27 | public readonly function: lambda.Function 28 | 29 | constructor (scope: Construct, id: string, props: CdkResourceInitializerProps) { 30 | super(scope, id) 31 | 32 | const stack = Stack.of(this) 33 | 34 | const fnSg = new ec2.SecurityGroup(this, 'ResourceInitializerFnSg', { 35 | securityGroupName: `${id}ResourceInitializerFnSg`, 36 | vpc: props.vpc, 37 | allowAllOutbound: true 38 | }) 39 | 40 | const fn = new lambda.DockerImageFunction(this, 'ResourceInitializerFn', { 41 | memorySize: props.fnMemorySize || 128, 42 | functionName: `${id}-ResInit${stack.stackName}`, 43 | code: props.fnCode, 44 | vpc: props.vpc, 45 | securityGroups: [fnSg, ...props.fnSecurityGroups], 46 | timeout: props.fnTimeout, 47 | logRetention: props.fnLogRetention, 48 | allowAllOutbound: true 49 | }) 50 | 51 | const payload: string = JSON.stringify({ 52 | params: { 53 | config: props.config 54 | } 55 | }) 56 | 57 | const sdkCall: AwsSdkCall = { 58 | service: 'Lambda', 59 | action: 'invoke', 60 | parameters: { 61 | FunctionName: fn.functionName, 62 | Payload: payload 63 | }, 64 | physicalResourceId: PhysicalResourceId.of(fn.functionName) 65 | } 66 | 67 | const customResourceFnRole = new Role(this, 'AwsCustomResourceRoleInit', { 68 | assumedBy: new ServicePrincipal('lambda.amazonaws.com') 69 | }) 70 | customResourceFnRole.addToPolicy( 71 | new PolicyStatement({ 72 | resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:*-ResInit${stack.stackName}`], 73 | actions: ['lambda:InvokeFunction'] 74 | }) 75 | ) 76 | this.customResource = new AwsCustomResource(this, 'AwsCustomResourceInit', { 77 | policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }), 78 | onUpdate: sdkCall, 79 | timeout: Duration.minutes(10), 80 | role: customResourceFnRole 81 | }) 82 | 83 | this.response = this.customResource.getResponseField('Payload') 84 | 85 | this.function = fn 86 | } 87 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elastic_beanstalk_cdk_project", 3 | "version": "0.1.0", 4 | "bin": { 5 | "elastic_beanstalk_cdk_project": "bin/elastic_beanstalk_cdk_project.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^26.0.10", 15 | "@types/node": "10.17.27", 16 | "aws-cdk": "^2.14.0", 17 | "jest": "^26.4.2", 18 | "ts-jest": "^26.2.0", 19 | "ts-node": "^9.0.0", 20 | "typescript": "~3.9.7" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "^2.80.0", 24 | "constructs": "^10.0.0", 25 | "source-map-support": "^0.5.16" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pictures/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/architecture_diagram.png -------------------------------------------------------------------------------- /pictures/certificate_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/certificate_example.png -------------------------------------------------------------------------------- /pictures/db_iam_authentication_policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/db_iam_authentication_policy.png -------------------------------------------------------------------------------- /pictures/db_settings_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/db_settings_overview.png -------------------------------------------------------------------------------- /pictures/eb_environment_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/eb_environment_overview.png -------------------------------------------------------------------------------- /pictures/enhanced_health_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/enhanced_health_overview.png -------------------------------------------------------------------------------- /pictures/environment_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/environment_variables.png -------------------------------------------------------------------------------- /pictures/https_connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/https_connection.png -------------------------------------------------------------------------------- /pictures/log_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/log_storage.png -------------------------------------------------------------------------------- /pictures/managed_updates_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/managed_updates_configuration.png -------------------------------------------------------------------------------- /pictures/post_cdk_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/post_cdk_output.png -------------------------------------------------------------------------------- /pictures/route53-entry-detailed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/route53-entry-detailed.png -------------------------------------------------------------------------------- /pictures/route53-entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/route53-entry.png -------------------------------------------------------------------------------- /pictures/sample_app_hiking_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/sample_app_hiking_overview.png -------------------------------------------------------------------------------- /pictures/sample_app_homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/pictures/sample_app_homepage.png -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/.ebextensions/statisfiles.config: -------------------------------------------------------------------------------- 1 | option_settings: 2 | - namespace: aws:elasticbeanstalk:environment:proxy:staticfiles 3 | option_name: /public 4 | value: /public 5 | - option_name: NODE_ENV 6 | value: production -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .gitignore/ 3 | .elasticbeanstalk/ 4 | -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | const express = require('express') 6 | , routes = require('./routes') 7 | , user = require('./routes/user') 8 | , hike = require('./routes/hike') 9 | , http = require('http') 10 | , path = require('path') 11 | , mysql = require('mysql2') 12 | , async = require('async') 13 | , morgan = require('morgan') 14 | , bodyParser = require('body-parser') 15 | , methodOverride = require('method-override') 16 | , { HttpRequest } = require('@aws-sdk/protocol-http') 17 | , { SignatureV4 } = require('@aws-sdk/signature-v4') 18 | , { defaultProvider } = require("@aws-sdk/credential-provider-node") 19 | , { Hash } = require('@aws-sdk/hash-node') 20 | , { formatUrl } = require('@aws-sdk/util-format-url'); 21 | 22 | const app = express(); 23 | 24 | app.set('port', process.env.PORT || 3000); 25 | app.set('views', __dirname + '/views'); 26 | app.set('view engine', 'pug'); 27 | app.use(morgan('dev')); 28 | app.use(methodOverride()); 29 | app.use(bodyParser.urlencoded({ extended: true })); 30 | app.use(bodyParser.json()); 31 | 32 | const { RDS_HOSTNAME, RDS_PORT, RDS_USERNAME, REGION, RDS_DATABASE } = process.env 33 | 34 | const getIamAuthToken = async() => { 35 | // I don't want to use the older v2 SDK (which had the signer for RDS) 36 | // The code below is inspired by comments from: https://github.com/aws/aws-sdk-js-v3/issues/1823 37 | const signer = new SignatureV4({ 38 | service: 'rds-db', 39 | region: REGION, 40 | credentials: defaultProvider(), 41 | sha256: Hash.bind(null, 'sha256') 42 | }) 43 | 44 | const request = new HttpRequest({ 45 | method: 'GET', 46 | protocol: 'https', 47 | hostname: RDS_HOSTNAME, 48 | port: RDS_PORT, 49 | query: { 50 | Action: 'connect', 51 | DBUser: RDS_USERNAME 52 | }, 53 | headers: { 54 | host: `${RDS_HOSTNAME}:${RDS_PORT}`, 55 | }, 56 | }) 57 | 58 | const presigned = await signer.presign(request, { 59 | expiresIn: 900 60 | }) 61 | 62 | return formatUrl(presigned).replace(`https://`, '') 63 | } 64 | // https://docs.aws.amazon.com/lambda/latest/dg/configuration-database.html 65 | 66 | function init() { 67 | app.get('/', routes.index); 68 | app.get('/users', user.list); 69 | app.get('/hikes', hike.index); 70 | app.post('/add_hike', hike.add_hike); 71 | 72 | http.createServer(app).listen(app.get('port'), function(){ 73 | console.log("Express server listening on port " + app.get('port')); 74 | }); 75 | } 76 | 77 | var client = null; 78 | async.series([ 79 | function initConnection(callback) { 80 | getIamAuthToken().then((token) => { 81 | console.log('Creating connection') 82 | let connectionConfig = { 83 | host : RDS_HOSTNAME, 84 | user : RDS_USERNAME, 85 | password : token, 86 | port : RDS_PORT, 87 | database : RDS_DATABASE, 88 | ssl : 'Amazon RDS', 89 | authPlugins: { mysql_clear_password: () => () => token} 90 | } 91 | client = mysql.createConnection(connectionConfig) 92 | app.set('connection', client) 93 | return callback() 94 | }).catch((error) => { console.log(error); return callback(error) }) 95 | }, 96 | function connect(callback) { 97 | console.log("Connecting to database") 98 | client.connect(callback); 99 | }, 100 | function clear(callback) { 101 | console.log("Dropping existing db") 102 | client.query(`DROP DATABASE IF EXISTS ${RDS_DATABASE}`, callback); 103 | }, 104 | function create_db(callback) { 105 | console.log("Creating new database") 106 | client.query(`CREATE DATABASE ${RDS_DATABASE}`, callback); 107 | }, 108 | function use_db(callback) { 109 | client.query(`USE ${RDS_DATABASE}`, callback); 110 | }, 111 | function create_table(callback) { 112 | client.query('CREATE TABLE HIKES (' + 113 | 'ID VARCHAR(40), ' + 114 | 'HIKE_DATE DATE, ' + 115 | 'NAME VARCHAR(40), ' + 116 | 'DISTANCE VARCHAR(40), ' + 117 | 'LOCATION VARCHAR(40), ' + 118 | 'WEATHER VARCHAR(40), ' + 119 | 'PRIMARY KEY(ID))', callback); 120 | }, 121 | function insert_default(callback) { 122 | var hike = {HIKE_DATE: new Date(), NAME: 'Rainy hike', 123 | LOCATION: 'Mt Rainier', DISTANCE: '4,027m vertical', WEATHER:'Bad'}; 124 | client.query('INSERT INTO HIKES set ?', hike, callback); 125 | } 126 | ], function (err, results) { 127 | if (err) { 128 | console.log('Exception initializing database.'); 129 | throw err; 130 | } else { 131 | console.log('Database initialization complete.'); 132 | init(); 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "name": "application-name", 4 | "version": "0.0.1", 5 | "engines": { "node" : "14.16.0" }, 6 | "private": true, 7 | "scripts": { 8 | "start": "node app" 9 | }, 10 | "dependencies": { 11 | "express": "^4.17.3", 12 | "morgan": "^1.10.0", 13 | "method-override": "^3.0.0", 14 | "body-parser": "^1.19.2", 15 | "pug": "^3.0.2", 16 | "mysql2": "^2.3.3", 17 | "async": "*", 18 | "node-uuid": "*", 19 | "@aws-sdk/client-rds": "^3.350.0", 20 | "@aws-sdk/signature-v4": "^3.53.0", 21 | "@aws-sdk/hash-node": "^3.53.0", 22 | "@aws-sdk/protocol-http": "^3.53.0", 23 | "@aws-sdk/util-format-url": "^3.53.0", 24 | "@aws-sdk/credential-provider-node": "^3.53.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/routes/hike.js: -------------------------------------------------------------------------------- 1 | var uuid = require('node-uuid'); 2 | exports.index = function(req, res) { 3 | res.app.get('connection').query( 'SELECT * FROM HIKES', function(err, 4 | rows) { 5 | if (err) { 6 | res.send(err); 7 | } else { 8 | console.log(JSON.stringify(rows)); 9 | res.render('hike', {title: 'My Hiking Log', hikes: rows}); 10 | }}); 11 | }; 12 | exports.add_hike = function(req, res){ 13 | var input = req.body.hike; 14 | var hike = { HIKE_DATE: new Date(), ID: uuid.v4(), NAME: input.NAME, 15 | LOCATION: input.LOCATION, DISTANCE: input.DISTANCE, WEATHER: input.WEATHER}; 16 | console.log('Request to log hike:' + JSON.stringify(hike)); 17 | req.app.get('connection').query('INSERT INTO HIKES set ?', hike, function(err) { 18 | if (err) { 19 | res.send(err); 20 | } else { 21 | res.redirect('/hikes'); 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/routes/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET home page. 4 | */ 5 | 6 | exports.index = function(req, res){ 7 | res.render('index', { title: 'Express' }); 8 | }; -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/routes/user.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET users listing. 4 | */ 5 | 6 | exports.list = function(req, res){ 7 | res.send("respond with a resource"); 8 | }; -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/views/hike.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | 7 | form(action="/add_hike", method="post") 8 | table(border="1") 9 | tr 10 | td Hike Name 11 | td 12 | input(name="hike[NAME]", type="textbox") 13 | tr 14 | td Location 15 | td 16 | input(name="hike[LOCATION]", type="textbox") 17 | tr 18 | td Distance 19 | td 20 | input(name="hike[DISTANCE]", type="textbox") 21 | tr 22 | td Weather 23 | td 24 | input(name="hike[WEATHER]", type="radio", value="Good") 25 | | Good 26 | input(name="hike[WEATHER]", type="radio", value="Bad") 27 | | Bad 28 | input(name="hike[WEATHER]", type="radio", value="Seattle", checked) 29 | | Seattle 30 | tr 31 | td(colspan="2") 32 | input(type="submit", value="Record Hike") 33 | 34 | div 35 | h3 Hikes 36 | table(border="1") 37 | tr 38 | td Date 39 | td Name 40 | td Location 41 | td Distance 42 | td Weather 43 | each hike in hikes 44 | tr 45 | td #{hike.HIKE_DATE.toDateString()} 46 | td #{hike.NAME} 47 | td #{hike.LOCATION} 48 | td #{hike.DISTANCE} 49 | td #{hike.WEATHER} 50 | -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} -------------------------------------------------------------------------------- /src/code/nodejs-express-hiking-example/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/public/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /src/deployment_zip/nodejs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-elastic-beanstalk-hardened-security-cdk-sample/3b5679de665b5c773ad857e79783bf0ef56c6b91/src/deployment_zip/nodejs.zip -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------