├── .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 | 
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 | 
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 | 
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 | 
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 | 
116 | 2. Otherwise, go to your Elastic Beanstalk environment in the console, and find the newly created application
117 | 
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 | 
120 | 4. Go to the /hikes page, here you can add entries and verify the database is successfully connected
121 | 
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 |
--------------------------------------------------------------------------------