├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── README_ja.md ├── batch ├── Dockerfile ├── README.md ├── README_ja.md ├── requirements.txt └── src │ └── sample │ ├── __init__.py │ └── batch.py ├── docs └── images │ ├── job.png │ ├── keypair_command_en.png │ ├── keypair_command_ja.png │ ├── prerequirsite_en.png │ ├── prerequirsite_ja.png │ ├── repository_url_en.png │ ├── repository_url_ja.png │ ├── template_architecture_en.png │ ├── template_architecture_ja.png │ ├── template_architecture_privatelink_en.png │ ├── template_architecture_privatelink_ja.png │ ├── template_architecture_serverless_en.png │ ├── template_architecture_serverless_ja.png │ ├── template_architecture_serverless_privatelink_en.png │ └── template_architecture_serverless_privatelink_ja.png ├── functions ├── .gitignore ├── get.ts ├── init.ts ├── lib │ └── connect.ts ├── package-lock.json ├── package.json └── post.ts ├── infra ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── README.md ├── README_ja.md ├── README_serverless.md ├── README_serverless_ja.md ├── bin │ ├── base.ts │ ├── batch.ts │ ├── serverless-webapp.ts │ └── webapp.ts ├── buildWebEnv.mjs ├── cdk.json ├── docker │ └── nginx │ │ ├── Dockerfile │ │ ├── default.conf │ │ └── static-content │ │ └── index.html ├── gulpfile.js ├── jest.config.js ├── lib │ ├── base-stack.ts │ ├── batch-stack.ts │ ├── constructs │ │ ├── aurora │ │ │ ├── README.md │ │ │ ├── README_dbinitlambda.md │ │ │ ├── aurora.ts │ │ │ └── dbinitlambda.ts │ │ ├── codepipeline │ │ │ ├── README_java.md │ │ │ ├── README_react.md │ │ │ ├── codepipeline-webapp-java.ts │ │ │ └── codepipeline-webapp-react.ts │ │ ├── ec2 │ │ │ ├── README.md │ │ │ └── bastion.ts │ │ ├── ecr │ │ │ ├── README.md │ │ │ └── ecr.ts │ │ ├── ecs │ │ │ ├── README_BASE.md │ │ │ ├── README_JOB.md │ │ │ ├── README_SERVICE.md │ │ │ ├── ecs-app-base.ts │ │ │ ├── ecs-app-service.ts │ │ │ └── ecs-job.ts │ │ ├── kms │ │ │ ├── README.md │ │ │ └── key.ts │ │ ├── network │ │ │ ├── README.md │ │ │ └── network.ts │ │ ├── s3 │ │ │ ├── README.md │ │ │ ├── README_webappbucket.md │ │ │ ├── bucket.ts │ │ │ └── webappbucket.ts │ │ └── serverless │ │ │ ├── README_apigw.md │ │ │ ├── README_lambda.md │ │ │ ├── README_serverless_app.md │ │ │ ├── apigw.ts │ │ │ ├── lambda.ts │ │ │ └── serverless-app.ts │ ├── serverlessapp-stack.ts │ └── webapp-stack.ts ├── package-lock.json ├── package.json ├── ssl │ └── openssl_sign_inca.cnf ├── stages.js └── tsconfig.json ├── package-lock.json ├── webapp-java ├── .gitignore ├── Dockerfile ├── README.md ├── README_ja.md ├── build.gradle ├── buildspec.yaml ├── docs │ └── images │ │ └── screenshot.png ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── log4j2.xml ├── settings.gradle └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── sampleapp │ │ └── webapp │ │ ├── ServletInitializer.java │ │ ├── WebappApplication.java │ │ ├── controller │ │ ├── SampleAppController.java │ │ └── form │ │ │ ├── SampleAppForm.java │ │ │ └── SampleAppFormList.java │ │ ├── domain │ │ ├── SampleAppService.java │ │ └── dto │ │ │ ├── SampleAppDto.java │ │ │ └── SampleAppListDto.java │ │ └── repository │ │ ├── SampleAppRepository.java │ │ └── model │ │ └── SampleApp.java │ └── resources │ ├── application.properties │ ├── data.sql │ ├── schema.sql │ ├── static │ ├── css │ │ └── bootstrap.min.css │ └── js │ │ └── bootstrap.min.js │ └── templates │ ├── sampleappform.html │ └── sampleapplist.html └── webapp-react ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc.json ├── README.md ├── README_ja.md ├── buildspec.yaml ├── docs └── images │ └── screenshot.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── sample.env ├── src ├── components │ ├── Dashboard.tsx │ ├── RecordForm.tsx │ ├── RecordFormRow.tsx │ └── RecordList.tsx ├── index.css ├── index.tsx ├── modules │ └── requests.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── types │ └── record.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | */.DS_Store 2 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "interactive" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Template of Closed Network System Works on AWS 2 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template for Closed Network System Workloads on AWS 2 | 3 | [日本語で読む](./README_ja.md) 4 | 5 | It assumes a closed network scenario and is a template for deploying applications and batch systems accessible from that environment to AWS. 6 | It consists of AWS CDK and Web server sample applications that via CI/CD (AWS CodePipelie) will be deployed to private networks. 7 | 8 | In local government systems that require a high level security and network restrictions, we need to configure our architecture with characteristics from on-premise, like "Closed area networks" and "Allow NW access routes from AWS to on-premise network". We designed the template so that these type of systems can be deployed on AWS. 9 | 10 | We will adopt REPLATFORM, one of the 6Rs, which is AWS's migration strategy, and aims to migrate from an existing on-premise environment to computing and managed DB using containers. REPLATFORM has advantages such as improving performance and reducing costs. The template uses several AWS managed services that will help us to reduce cost and operational workload. 11 | (Ref:[Migrating to AWS Best Practices and Strategies](https://pages.awscloud.com/rs/112-TZM-766/images/Migrating-to-AWS_Best-Practices-and-Strategies_eBook.pdf) 12 | 13 | And we added serverless application version of infra that uses AWS Lambda and React application instead of container. 14 | Please see here you want to know how to deploy serverless application version. 15 | 16 | ## Scope 17 | 18 | ### What the template provides 19 | 20 | - Container execution environment for running Java applications (Spring boot) on Amazon ECS/Fargate 21 | 22 | - In addition to this, a sample application using Spring Boot 23 | - A sample Dockerfile to turn that sample application into a container image 24 | - For sample applications, see [`Webapp-java/readme.md`](./webapp-java/README.md) 25 | 26 | - Serverless application environment for running React application hosted on Amazon S3 and REST API on API Gateway and AWS Lambda.(\*) 27 | 28 | - A sample application using React 29 | - For sample react application, see [`Webapp-react/readme.md`](./webapp-react/README.md). 30 | - Sample REST APIs code is in `functions/` 31 | 32 | - CI/CD environment for continuous application development 33 | 34 | - Pipeline for building and deploying the above sample applications using CodePipeline, CodeCommit, and CodeBuild 35 | - A job execution platform combining Step Functions and Amazon ECS/Fargate that can execute simple job flows 36 | 37 | - In addition to this, a Python sample job script 38 | 39 | - A sample Dockerfile for turning the sample job script into a container image 40 | - For a sample job script, see [`batch/README.md`](./batch/README.md) 41 | 42 | - Maintenance environment for checking application operation and managing RDB 43 | - A secure access where you can test applications and manage databases combining SystemsManager and EC2 44 | - Provides remote desktop connections (Windows Server Instances) and SSH connections (Amazon Linux Instances) 45 | 46 | ### What the template doesn't provide 47 | 48 | - Settings and implementation on the AWS side involved in on-premise connections such as AWS Direct Connect (DX) and AWS Site-to-Site VPN (VPN) 49 | - Please design and implement DX and VPN, which are likely to be necessary for actual use on the user's side 50 | - Application authentication function 51 | - Since this application is a sample, it does not have authentication or authorization functions such as login/logout 52 | - DNS settings for applications 53 | - To check the operation of this template, we will use an endpoint that AWS automatically creates for the ALB 54 | - Operation features 55 | - It does not have integrated management of application and AWS resource logs or the ability to alert and monitor applications 56 | 57 | ## Directories 58 | 59 | This is the directory tree and its overview. 60 | 61 | | Directory | Sub directory | Description | 62 | | ----------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 63 | | batch | | Creates a batch container application with Dockerfile | 64 | | | src | python scripts sample app | 65 | | infra | | CDK source code for provisioning the following AWS resources
- Network (VPC and subnet)
- DB (Aurora)
- Compute resources for containers (Amazon ECS, Fargate)
- CI/CD tools (CodePipeline, CodeCommit, CodeDeploy)
- Batch Job Management ( Step Functions, DynamoDB, SNS) | 66 | | | bin | CDK app source code | 67 | | | lib/constructs | Constructs used to build AWS resources
The [Core concept](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/core_concepts.html) explains about what is the difference between Stack and Construct. | 68 | | webapp-java | | Source code of SpringBoot web app with Dockerfile | 69 | 70 | ## Requirement 71 | 72 | - `Node.js` >= `16.13.0` 73 | - `npm` >= `9.2.0` 74 | - `aws-cdk` >= `2.65.0` 75 | - `aws-cdk-lib` >= `2.65.0` 76 | - `OpenSSL` >= `3.0.8` 77 | - `Docker` 78 | 79 | ## Architecture 80 | 81 | ### NW configuration assumptions 82 | 83 | It is assumed that the on-premise NW (on the right side of the image bellow) exists and the AWS network will be connected via Direct Connect or VPN. 84 | 85 | ![Connection scheme overview diagram](./docs/images/prerequirsite_en.png) 86 | 87 | ### Architecture diagram 88 | 89 | This template will deploy AWS resources in the application NW connected by AWS Direct Connect (DX) or AWS Site-to-Site VPN (VPN). 90 | 91 | ![Architecture Diagram](./docs/images/template_architecture_en.png) 92 | 93 | Is important to mention that in addition to configuring NW routes on DX and VPNs, please have a look at using private links for better network desing in this blog post: [an AWS Transit Gateway that can also be used with a “shared” AWS DirectConnect](https://aws.amazon.com/jp/blogs/news/aws-transit-gateway-with-shared-directconnect/). 94 | 95 | ### Using Private Link 96 | 97 | The template, optionally allows you to provision the architecture by using Private Links. It is recommended for an extra layer of security when designing applications that are deployed in Private networks. 98 | 99 | This is the architecture diagram that is slightly modified by using private links for the services: 100 | 101 | ![Private Link Version](./docs/images/template_architecture_privatelink_en.png) 102 | 103 | ## How to Deploy 104 | 105 | Please see the following document: [infra/README.md](./infra/README.md) 106 | If you want to deploy serverless application version, please see the following document: [infra/README_serverless.md](./infra/README_serverless.md) 107 | 108 | ## Security 109 | 110 | See [CONTRIBUTING](CONTRIBUTING.md#Security-issue-notifications) for more information. 111 | 112 | ## License 113 | 114 | This library is licensed under the MIT-0 License. See the LICENSE file. 115 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # Template for Closed Network System Workloads on AWS 2 | 3 | [View this page in English](./README.md) 4 | 5 | 閉域網を前提環境とし、その環境からアクセス可能な Web アプリケーションとバッチシステムを AWS 上に展開するためのテンプレートとなります。 6 | CDK で構築されるインフラ とサンプルアプリのソースコードで構成されています。 7 | 8 | 地方自治体のようなシステムがデプロイされている環境の特性である、「閉域網」や「AWS からオンプレへの NW アクセス経路の確保」を考慮した上で、 9 | AWS のマイグレーション戦略である 6R の一つ、REPLATFORM を採用し、既存のオンプレ環境から、コンテナを利用したコンピューティングやマネージドな DB への移行を目指します。 10 | (ご参考:[AWS への移行:ベストプラクティスと戦略](https://pages.awscloud.com/rs/112-TZM-766/images/Migrating-to-AWS_Best-Practices-and-Strategies_eBook.pdf) 11 | ) 12 | REPLATFORM では、サーバの運用負荷の軽減などがメリットになります。本テンプレートでも、AWS のマネージドサービスを活用した形で運用コストの軽減を目指しました。 13 | 14 | また、Container の代わりに、AWS Lambda を利用して API を構築し、React アプリケーションを利用した、サーバーレスアプリケーション版を追加しました。 15 | デプロイ方法については、[こちら](./infra/README_serverless_ja.md)を参照ください。 16 | 17 | ## テンプレートのスコープ 18 | 19 | ### テンプレートで提供されるもの 20 | 21 | - Java アプリケーション(Spring boot)を Amazon ECS/Fargate 上で稼働させるためのコンテナ実行環境(\*) 22 | - これに加え、上記環境下で動作する Spring boot を利用したサンプルアプリケーション 23 | - そのサンプルアプリケーションをコンテナイメージにするためのサンプル Dockerfile 24 | - サンプルアプリケーションについては、[`webapp-java/README.md`](../webapp-java/README_ja.md)をご参照ください 25 | - 閉域網で SPA + REST API を動かすための、Amazon S3、Amazon API Gateway、AWS Lambda を利用したサーバーレスな実行環境(\*) 26 | - React のサンプルアプリケーション 27 | - 詳しくは、[`Webapp-react/readme_ja.md`](./webapp-react/README_ja.md)を参照ください 28 | - React サンプルアプリケーションから呼び出される REST API のサンプルコード 29 | - アプリケーションを継続開発するための CI/CD 環境 30 | - AWS CodePipeline や AWS CodeCommit, AWS CodeBuild を利用した、上記サンプルアプリケーションをビルド、デプロイするためのパイプライン 31 | - 簡易なジョブフローが実行できる、AWS Step Functions、Amazon ECS/Fargate を組み合わせたジョブ実行基盤 32 | - これに加え、上記環境下で動作する、Python のサンプルジョブスクリプト 33 | - サンプルジョブスクリプトをコンテナイメージにするためのサンプル Dockerfile 34 | - サンプルジョブスクリプトについては、[`batch/README.md`](../batch/README_ja.md)をご参照ください 35 | - アプリケーションの動作確認や RDB を管理するためのメンテナンス環境 36 | - SystemsManager と EC2 を組み合わせたアプリケーションのテストや DB の管理を実施できる環境 37 | - リモートデスクトップ接続(Windows Server Instance)と コンソール接続(Amazon Linux Instance)を提供 38 | 39 | \* コンテナ実行環境とサーバーレスな実行環境は、どちらか選んでデプロイしていただく手順をそれぞれの README に記載しています。 40 | 41 | ### テンプレートで提供されないもの 42 | 43 | - AWS Direct Connect(DX)や AWS Site-to-Site VPN(VPN) といったオンプレとの接続に関わる AWS 側の設定や実装 44 | - 本番利用において必要になると思われる DX や VPN の設計・導入については、別途実施ください 45 | - アプリケーションの認証機能 46 | - 本アプリケーションはサンプルのため、ログイン・ログアウトなどの認証機能を持ちません 47 | - アプリケーションの運用機能 48 | - アプリケーションや AWS リソースのログの統合的な管理やアプリケーションに対するアラートや監視の機能は持ちません 49 | 50 | ## ディレクトリ構成 51 | 52 | ディレクトリ構成とその概要です。 53 | 54 | | ディレクトリ | サブディレクトリ | 概要 | 55 | | ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 56 | | batch | | batch コンテナを作成するためのソースファイルと Dockerfile | 57 | | | src | python のスクリプト | 58 | | infra | | AWS リソースをプロビジョニングするための CDK のソースコード
- ネットワーク(VPC やサブネット)
- DB(Aurora)
- コンテナ向けコンピューティングリソース(Amazon ECS、Fargate)
- CI/CD ツール(CodePipeline、CodeCommit、CodeDeploy)
- バッチジョブ管理(Step Functions、DynamoDB、SNS)
が生成される。 | 59 | | | bin | CDK を実行するためのソースコード。 CDK で定義されるところの app に該当 | 60 | | | lib/constructs | AWS リソースを生成するための Stack, Construct が定義されたソースコード
Stack と Construct の違いは[ドキュメント](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/core_concepts.html)をご参照ください | 61 | | webapp-java | | Spring boot 製のウェブアプリのサンプルソースと Dockerfile | 62 | 63 | ## 前提条件 64 | 65 | - `Node.js` >= `16.13.0` 66 | - `npm` >= `9.2.0` 67 | - `aws-cdk` >= `2.65.0` 68 | - `aws-cdk-lib` >= `2.65.0` 69 | - `OpenSSL` >= `3.0.8` 70 | - `Docker` 71 | 72 | ## アーキテクチャ 73 | 74 | ### NW 構成の前提 75 | 76 | オンプレミス NW と AWS とは、Direct Connect または、VPN で接続されることを前提としています。 77 | 78 | ![接続方式概要図](./docs/images/prerequirsite_ja.png) 79 | 80 | ### テンプレートのアーキテクチャ図 81 | 82 | 本テンプレートでは、AWS Direct Connect(DX) や AWS Site-to-Site VPN(VPN) で接続されるアプリケーション NW 内の AWS リソースが構築されます。 83 | 84 | ![アーキテクチャ図](./docs/images/template_architecture_ja.png) 85 | 86 | ### Private Link を利用する場合 87 | 88 | また、前述の構成において、既存 NW との CIDR 重複を回避するために、Private Link の利用を検討するケースもあるかと思います。その場合には、以下の図に示すように、オプションとして Private Link を経由した接続の構築が可能になっています。 89 | Private Link を利用する場合には、[“共有型”AWS DirectConnect でも使える AWS Transit Gateway](https://aws.amazon.com/jp/blogs/news/aws-transit-gateway-with-shared-directconnect/)を参照いただき、最適な NW 設計をご検討ください。 90 | 91 | ![Private Link Version](./docs/images/template_architecture_privatelink_ja.png) 92 | 93 | ## テンプレートのデプロイ方法 94 | 95 | [infra/README.md](./infra/README_ja.md)を参照ください。 96 | サーバーレスアプリケーション版を利用したい方は、[infra/README_serverless_ja.md](./infra/README_serverless_ja.md)を参照ください。 97 | 98 | ## Security 99 | 100 | See [CONTRIBUTING](CONTRIBUTING.md#Security-issue-notifications) for more information. 101 | 102 | ## License 103 | 104 | This library is licensed under the MIT-0 License. See the LICENSE file. 105 | -------------------------------------------------------------------------------- /batch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.1-alpine 2 | 3 | WORKDIR /usr/src/app 4 | VOLUME /usr/src/app 5 | 6 | COPY requirements.txt ./ 7 | 8 | RUN apk update && apk add --upgrade sqlite-libs && apk add --upgrade libcrypto3 && apk add --upgrade libssl3 && apk add curl && apk add -f python3 py3-pip && pip install --no-cache-dir -r requirements.txt 9 | 10 | RUN curl https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -o /usr/src/app/root.pem 11 | 12 | RUN addgroup -S python && adduser -S python -G python 13 | USER python 14 | 15 | COPY . . 16 | 17 | CMD [ "python", "./src/sample/batch.py" ] -------------------------------------------------------------------------------- /batch/README.md: -------------------------------------------------------------------------------- 1 | # batch 2 | 3 | [日本語で読む](./README_ja.md) 4 | 5 | It's a sample job script that is called by Step Functions that are created by the CDK infra application. 6 | 7 | ## What does it do 8 | 9 | The script will invoke SQL commands by `JOBID` given by Step Functions. 10 | The queries will test `true` or `false` values that are stored in the database. If any returned value is `false` the script will output the record names to a file in S3. Also the job execution result is returned back to the step functions. 11 | 12 | ## How to use 13 | 14 | The job is packed into a docker image that is pushed by this CDK application into ECR. You can trigger the job by the AWS Console or modifiy the job and deploy again to reflect the changes. 15 | 16 | ## Path to Production 17 | 18 | - Please modify the script and modify the Dockerfile follow your environment. 19 | - Please consider CI/CD for job scripts. 20 | -------------------------------------------------------------------------------- /batch/README_ja.md: -------------------------------------------------------------------------------- 1 | # batch 2 | 3 | [View this page in English](./README.md) 4 | 5 | このソースコードは、infra で構築されたジョブ実行基盤から呼び出されるジョブスクリプトを定義したものです。 6 | 7 | ## 概要 8 | 9 | ジョブ実行基盤から渡される JOBID をもとに異なる処理を呼び出すことが可能です。 10 | 本サンプルでは、データベースに格納されたテストデータの `true/false` を確認し、ジョブの成否を判断し、`false` の設定されているレコード名のリストを S3 に出力し、ジョブの実行結果を Step Functions に返却します。 11 | 12 | ## 利用方法 13 | 14 | infra のデプロイ時に、自動的にジョブスクリプトを含んだコンテナイメージがビルドされ、ECR にプッシュされます。 15 | そのため、本ディレクトリでなにかする必要はありません。 16 | コンテナイメージについては、`Dockerfile` を参照ください。 17 | 18 | ## 本番利用に向けて 19 | 20 | - 環境に合わせ、スクリプトの改修や Dockerfile の修正を実施ください。 21 | - ジョブスクリプトの更新を個別に適用するための、ジョブスクリプトの CI/CD をご検討ください。 22 | -------------------------------------------------------------------------------- /batch/requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary 2 | boto3 -------------------------------------------------------------------------------- /batch/src/sample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/batch/src/sample/__init__.py -------------------------------------------------------------------------------- /batch/src/sample/batch.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import datetime 3 | import json 4 | import os 5 | import sys 6 | import psycopg2 7 | 8 | s3 = boto3.resource('s3') 9 | 10 | ENDPOINT=os.environ['DB_ENDPOINT'] 11 | PORT="5432" 12 | USER=os.environ['DB_USERNAME'] 13 | PASS=os.environ['DB_PASSWORD'] 14 | DBNAME="postgres" 15 | 16 | JOB_ID=os.environ['JOB_ID'] 17 | BUCKET_NAME=os.environ['BUCKET_NAME'] 18 | 19 | KEYS = ('id', 'name', 'job0001_flag', 'job0002_flag', 'job0003_flag', 'job0004_flag', 'job0005_flag') 20 | 21 | CHECK_ERROR_QUERIES={ 22 | "Job0001": """SELECT name FROM sampleapp_table WHERE job0001_flag = false;""", 23 | "Job0002": """SELECT name FROM sampleapp_table WHERE job0002_flag = false;""", 24 | "Job0003": """SELECT name FROM sampleapp_table WHERE job0003_flag = false;""", 25 | "Job0004": """SELECT name FROM sampleapp_table WHERE job0004_flag = false;""", 26 | "Job0005": """SELECT name FROM sampleapp_table WHERE job0005_flag = false;""", 27 | } 28 | 29 | TODAY = datetime.date.today() 30 | 31 | def datetime_encoder(datetime_object): 32 | if isinstance(datetime_object, datetime.date): 33 | return datetime_object.isoformat() 34 | 35 | try: 36 | conn = psycopg2.connect(host=ENDPOINT, port=PORT, database=DBNAME, user=USER, password=PASS, sslmode='verify-full', sslrootcert = './root.pem') 37 | cur = conn.cursor() 38 | cur.execute(CHECK_ERROR_QUERIES[JOB_ID]) 39 | query_results = cur.fetchall() 40 | ret = [] 41 | for qresult in query_results: 42 | ret.append({key:value for key, value in zip(KEYS, qresult)}) 43 | except Exception as e: 44 | print("Database connection failed due to {}".format(e)) 45 | 46 | if len(query_results) > 0: 47 | try: 48 | key_name = "{0}_failure_result_{1}.json".format(JOB_ID, str(TODAY)) 49 | s3_obj = s3.Object(BUCKET_NAME, key_name) 50 | s3_obj.put(Body=json.dumps(ret, ensure_ascii=False, default=datetime_encoder), ContentEncoding='utf-8', ContentType='application/json') 51 | print("Job was failed. Please check the failure records in {0}/{1}".format(BUCKET_NAME, key_name)) 52 | except Exception as e: 53 | print("Couldn't put object to S3: {}".format(e)) 54 | 55 | sys.exit(1) 56 | 57 | else: 58 | print("Job was success!") 59 | sys.exit(0) -------------------------------------------------------------------------------- /docs/images/job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/job.png -------------------------------------------------------------------------------- /docs/images/keypair_command_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/keypair_command_en.png -------------------------------------------------------------------------------- /docs/images/keypair_command_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/keypair_command_ja.png -------------------------------------------------------------------------------- /docs/images/prerequirsite_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/prerequirsite_en.png -------------------------------------------------------------------------------- /docs/images/prerequirsite_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/prerequirsite_ja.png -------------------------------------------------------------------------------- /docs/images/repository_url_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/repository_url_en.png -------------------------------------------------------------------------------- /docs/images/repository_url_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/repository_url_ja.png -------------------------------------------------------------------------------- /docs/images/template_architecture_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_en.png -------------------------------------------------------------------------------- /docs/images/template_architecture_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_ja.png -------------------------------------------------------------------------------- /docs/images/template_architecture_privatelink_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_privatelink_en.png -------------------------------------------------------------------------------- /docs/images/template_architecture_privatelink_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_privatelink_ja.png -------------------------------------------------------------------------------- /docs/images/template_architecture_serverless_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_serverless_en.png -------------------------------------------------------------------------------- /docs/images/template_architecture_serverless_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_serverless_ja.png -------------------------------------------------------------------------------- /docs/images/template_architecture_serverless_privatelink_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_serverless_privatelink_en.png -------------------------------------------------------------------------------- /docs/images/template_architecture_serverless_privatelink_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/docs/images/template_architecture_serverless_privatelink_ja.png -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /functions/get.ts: -------------------------------------------------------------------------------- 1 | import Connection from "./lib/connect"; 2 | import { Logger } from "@aws-lambda-powertools/logger"; 3 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; 4 | 5 | const logger = new Logger({ serviceName: "getLambda" }); 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | try { 11 | const client = await Connection(); 12 | // Connection 13 | await client.connect(); 14 | logger.info("connected"); 15 | 16 | // Query 17 | const res = await client.query( 18 | "SELECT * FROM sampleapp_table WHERE id = 1" 19 | ); 20 | const response = { 21 | statusCode: 200, 22 | body: 23 | res.rows.length > 0 ? JSON.stringify(res.rows[0]) : JSON.stringify(""), 24 | }; 25 | return response; 26 | } catch (e) { 27 | logger.error(e.toString()); 28 | const response = { 29 | statusCode: 500, 30 | body: JSON.stringify("Server error"), 31 | }; 32 | return response; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /functions/init.ts: -------------------------------------------------------------------------------- 1 | import Connection from "./lib/connect"; 2 | import { Logger } from "@aws-lambda-powertools/logger"; 3 | import cfnResponse from "cfn-response"; 4 | 5 | const logger = new Logger({ serviceName: "initLambda" }); 6 | 7 | export const handler = async (event: any, context: any): Promise => { 8 | if (event.RequestType == "Create" || event.RequestType == "Update") { 9 | try { 10 | const client = await Connection(); 11 | // Connection 12 | await client.connect(); 13 | logger.info("connected"); 14 | 15 | // Query 16 | const res1 = await client.query("DROP TABLE IF EXISTS sampleapp_table;"); 17 | logger.info(res1); 18 | const res2 = await client.query( 19 | 'CREATE TABLE IF NOT EXISTS sampleapp_table(id serial NOT NULL,name text COLLATE pg_catalog."default" NOT NULL,job0001_flag boolean NOT NULL DEFAULT false,job0002_flag boolean NOT NULL DEFAULT false,job0003_flag boolean NOT NULL DEFAULT false,job0004_flag boolean NOT NULL DEFAULT false,job0005_flag boolean NOT NULL DEFAULT false,CONSTRAINT sample_app_pkey PRIMARY KEY (id));' 20 | ); 21 | logger.info(res2); 22 | const res3 = await client.query( 23 | "INSERT INTO sampleapp_table(name, job0001_flag, job0002_flag, job0003_flag, job0004_flag, job0005_flag) VALUES ('test record 1',true,true,true,true,true);" 24 | ); 25 | logger.info(res3); 26 | return cfnResponse.send( 27 | event, 28 | context, 29 | cfnResponse.SUCCESS, 30 | { message: Date.now().toString() }, 31 | event.PhysicalResourceId 32 | ); 33 | } catch (e) { 34 | logger.error(e.toString()); 35 | return cfnResponse.send( 36 | event, 37 | context, 38 | cfnResponse.SUCCESS, 39 | { message: Date.now().toString() }, 40 | event.PhysicalResourceId 41 | ); 42 | } 43 | } 44 | return cfnResponse.send( 45 | event, 46 | context, 47 | cfnResponse.SUCCESS, 48 | { message: Date.now().toString() }, 49 | event.PhysicalResourceId 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /functions/lib/connect.ts: -------------------------------------------------------------------------------- 1 | const referSecrets = async () => { 2 | const { SecretsManagerClient, GetSecretValueCommand } = await import( 3 | "@aws-sdk/client-secrets-manager" 4 | ); 5 | const secretsManager = new SecretsManagerClient({ 6 | region: process.env.REGION!, 7 | }); 8 | const response = await secretsManager.send( 9 | new GetSecretValueCommand({ 10 | SecretId: process.env.SECRET_NAME!, 11 | }) 12 | ); 13 | return JSON.parse(response.SecretString!); 14 | }; 15 | 16 | export default async function Connection() { 17 | const { Client } = await import("pg"); 18 | const secrets = await referSecrets(); 19 | const { Signer } = await import("@aws-sdk/rds-signer"); 20 | const signer = new Signer({ 21 | region: process.env.REGION!, 22 | username: secrets.username, 23 | hostname: process.env.HOST!, 24 | port: secrets.port, 25 | }); 26 | const token = await signer.getAuthToken(); 27 | // client settings 28 | const client = new Client({ 29 | host: process.env.HOST!, 30 | port: secrets.port, 31 | user: secrets.username, 32 | password: token, //secrets.password, 33 | ssl: true, 34 | }); 35 | return client; 36 | } 37 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "init.ts", 6 | "dependencies": { 7 | "@aws-lambda-powertools/logger": "^1.13.0", 8 | "@aws-sdk/client-secrets-manager": "^3.427.0", 9 | "@aws-sdk/rds-signer": "^3.427.0", 10 | "cfn-response": "^1.0.1", 11 | "lodash": "^4.17.21", 12 | "pg": "^8.11.3" 13 | }, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "devDependencies": { 18 | "@types/cfn-response": "^1.0.8", 19 | "@types/lodash": "^4.14.202", 20 | "@types/node": "^20.10.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions/post.ts: -------------------------------------------------------------------------------- 1 | import Connection from "./lib/connect"; 2 | import { Logger } from "@aws-lambda-powertools/logger"; 3 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; 4 | import { isBoolean } from "lodash"; 5 | 6 | const logger = new Logger({ serviceName: "postLambda" }); 7 | 8 | export const handler = async ( 9 | event: APIGatewayProxyEvent 10 | ): Promise => { 11 | if (!event.body) { 12 | const response = { 13 | statusCode: 400, 14 | body: JSON.stringify("Body is null"), 15 | }; 16 | logger.error("Body is null"); 17 | return response; 18 | } 19 | const body = JSON.parse(event.body); 20 | const { 21 | id, 22 | job0001_flag, 23 | job0002_flag, 24 | job0003_flag, 25 | job0004_flag, 26 | job0005_flag, 27 | } = body; 28 | 29 | // check if there is data 30 | if ( 31 | !id || 32 | job0001_flag == undefined || 33 | job0002_flag == undefined || 34 | job0003_flag == undefined || 35 | job0004_flag == undefined || 36 | job0005_flag == undefined 37 | ) { 38 | const response = { 39 | statusCode: 400, 40 | body: JSON.stringify("Some parameters are undefined"), 41 | }; 42 | logger.error("Some parameters are undefined"); 43 | return response; 44 | } 45 | // check their types and formats 46 | if (Number.isNaN(parseInt(id))) { 47 | logger.error("id is not a number"); 48 | return { statusCode: 400, body: JSON.stringify("id is not a number") }; 49 | } 50 | if ( 51 | !isBoolean(job0001_flag) || 52 | !isBoolean(job0002_flag) || 53 | !isBoolean(job0003_flag) || 54 | !isBoolean(job0004_flag) || 55 | !isBoolean(job0005_flag) 56 | ) { 57 | logger.error("Any flag parameters are not Boolean"); 58 | return { 59 | statusCode: 400, 60 | body: JSON.stringify("Any flag parameters are not Boolean"), 61 | }; 62 | } 63 | 64 | try { 65 | const client = await Connection(); 66 | // Connection 67 | await client.connect(); 68 | logger.info("connected"); 69 | 70 | // Query 71 | const res = await client.query( 72 | "UPDATE sampleapp_table SET job0001_flag = $1, job0002_flag = $2, job0003_flag = $3, job0004_flag = $4, job0005_flag = $5 WHERE id = $6", 73 | [job0001_flag, job0002_flag, job0003_flag, job0004_flag, job0005_flag, id] 74 | ); 75 | const response = { 76 | statusCode: 200, 77 | body: JSON.stringify(res), 78 | }; 79 | return response; 80 | } catch (e) { 81 | logger.error(e.toString()); 82 | const response = { 83 | statusCode: 500, 84 | body: JSON.stringify("Server error"), 85 | }; 86 | return response; 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /infra/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /cdk.out -------------------------------------------------------------------------------- /infra/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | rules: {}, 14 | }; 15 | -------------------------------------------------------------------------------- /infra/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !.eslintrc.js 3 | !jest.config.js 4 | !stages.js 5 | !gulpfile.js 6 | *.d.ts 7 | node_modules 8 | 9 | # CDK asset staging directory 10 | .cdk.staging 11 | cdk.out 12 | cdk.context.json 13 | cdk-*-outputs.json 14 | .DS_Store 15 | .env 16 | scoutsuite-report* 17 | 18 | # infra 19 | !ssl/openssl_sign_inca.cnf 20 | ssl/* 21 | certificate_arn.json -------------------------------------------------------------------------------- /infra/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /infra/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /infra/README_serverless.md: -------------------------------------------------------------------------------- 1 | # Serverless application version 2 | 3 | This is CDK code to build an environment to run serverless sample applications and batch systems on AWS. 4 | 5 | ## Overview 6 | 7 | If the number of accesses is low or there is a time period where there is almost no access, running servers all the time using ECS/Fargate costs a lot compared to the actual usage amount. There are also operating costs for container images. 8 | 9 | In such cases, reduce costs and operational troubles by using serverless by S3 and Lambda. 10 | 11 | Hosting an internal HTTPS static website using ALB, S3, and PrivateLink in a closed network is described in [this blog](https://aws.amazon.com/jp/blogs/networking-and-content-delivery/hosting-internal-https-static-websites-with-alb-s3-and-privatelink/). 12 | This template includes architecture of this blog. 13 | By using this source code, you can automate complicated Internal ALB settings, etc. 14 | 15 | The architecture diagram is as follows. (The area circled red is the difference from the container version) 16 | 17 | ![architecture diagram](../docs/images/template_architecture_serverless_en.png) 18 | 19 | When using Private Link, it looks like this: 20 | ![architecture diagram](../docs/images/template_architecture_serverless_privatelink_en.png) 21 | 22 | ## Preparation 23 | ### 1. Configuring the AWS CLI 24 | 25 | In order to use the CDK and deploy this application it is necessary to configure the `AWS CLI`. On a terminal run the following command: 26 | 27 | ```bash 28 | $ aws configure --profile {profile name} 29 | ``` 30 | 31 | Run and enter the required information in response to the prompts that appear. 32 | 33 | The access key, secret key, and default region that are displayed when an IAM user is created are checked. 34 | For more information, see [Quick Setup with aws configure - Profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-profiles). 35 | 36 | ### 2. Rewrite stages.js 37 | 38 | This template is using Task Runner [gulp](https://gulpjs.com/) for deployment. 39 | The variables referred to from gulp are defined in `stages.js`, so they can be changed according to each environment. 40 | 41 | ```javascript 42 | default: { 43 | appName, 44 | awsProfile: 'myProfile', 45 | alias: 'default', 46 | deployEnv: 'dev', 47 | notifyEmail: 'default-mail@default-mail.com', 48 | enabledPrivateLink: false, 49 | windowsBastion: true, 50 | linuxBastion: true, 51 | domainName: 'templateapp.local', 52 | }, 53 | alias: { 54 | appName: '', // application's name ex: demoapp 55 | awsProfile: '', // aws profile that you configured in step 1 56 | alias: '', // identifier to deploy to same aws account by other teammates. ex: ysuzuki 57 | deployEnv: '' // deploy stage ex: dev, stage, prod 58 | notifyEmail: '', // This e-mail to send message when job was failed. 59 | enabledPrivateLink: , // Whether using PrivateLink or not. true is using PrivateLink, and false is not. 60 | windowsBastion: true, // Whether using Windows Bastion instance or not. true is using it, and false is not. 61 | linuxBastion: true, // Whether using Amazon Linux Bastion instance or not. true is using it, and false is not. 62 | domainName: 'templateapp.local', // It will be registered to Private Hosted Zone. 63 | } 64 | ``` 65 | 66 | ### 3. Create self-signed certificate 67 | 68 | Self-signed certificate will be used in this sample to use HTTPS. 69 | Please run this command to import certificate to Amazon Certificate Manager in `infra` dir. 70 | Please install `OpenSSL` to your local environment befeore running these commands. 71 | 72 | ```bash 73 | $ npm install 74 | $ npm run create-certificate -- --{alias} 75 | ``` 76 | 77 | ## How to deploy 78 | 79 | ### 1. CDK 80 | 81 | After deployment, the comannds to get keypairs will be shown in same terminal. 82 | If you want to use ssh from your client or RDP connection via FleetManager, please get keypairs by commands like below. 83 | 84 | (Correct keypair ID will be included in fact.) 85 | 86 | 1. The case of Windows instance in ap-northeast-1 region. 87 | 88 | ``` 89 | {alias}{stage}{appName}Webapp.WindowsGetSSHKeyForWindowsInstanceCommand = aws ssm get-parameter --name /ec2/keypair/key-XXXXXXXXXXXXXXXXX --region ap-northeast-1 --with-decryption --query Parameter.Value --output text 90 | ``` 91 | 92 | 2. The case of Amazon Linux instance in ap-northeast-1 region. 93 | 94 | ``` 95 | {alias}{stage}{appName}Webapp.LinuxGetSSHKeyForLinuxInstanceCommand = aws ssm get-parameter --name /ec2/keypair/key-XXXXXXXXXXXXXXXXX --region ap-northeast-1 --with-decryption --query Parameter.Value --output text 96 | ``` 97 | 98 | > NOTE: 99 | > If you deploy this template at first, there are many outputs in your terminal. 100 | > So, you may not find these commands in your terminal. 101 | > In this case, please go to CloudFormation's console in your browser. 102 | > And open the `Output` tab of `Webapp stack`. You can see commands in your screen like below image. 103 | > ![How to get key pair command](../docs/images/keypair_command_en.png) 104 | 105 | And mail adderess you put in `stages.js` will receive email from Amazon SNS after CDK deployment. 106 | Please do confirmation of this email follow these steps in email to receive notification of job failed. 107 | And job will be start at 21:00 JST on weekdays. The initial data sets registered by deployment of sample web application is set so that all jobs succeed. So no notification is sent. 108 | If you want to confirm the failure notification, please change one of the 5 `trues` to `false` in the sample web application that will be deployed later. 109 | 110 | ### 2. Sample web apps 111 | 112 | Your source code repository was created after deploying CDK. 113 | 114 | > NOTE: 115 | > Your source code repository URL will be shown in your console after deploying CDK or in CloudFormation Console of AWS Management Console like below. 116 | > ![Source Code URL](../docs/images/repository_url_en.png) 117 | 118 | Set `REACT_APP_ENDPOINT_URL` defined in the `.env` file in the `webapp-react` directory as `https://app.{domainName}/apigw/` ( use `domainName` in `stages.js`.) 119 | 120 | You can deploy sample web application via pipeline by following steps to push source code to your repository. 121 | 122 | ```bash 123 | $ cd./webapp-react 124 | $ git init 125 | $ git remote add origin https://git-codecommit.{your region}.amazonaws.com/v1/repos/{your repository name} 126 | $ git add. 127 | $ git commit -m "Initial commit" 128 | $ git push --set-upstream origin main 129 | $ git checkout -b develop 130 | $ git push --set-upstream origin develop 131 | ``` 132 | 133 | > NOTE: 134 | > When the develop branch was changed, this pipeline will be invoked. So, you have to create develop branch. 135 | 136 | If you want to confirm pipeline situation, please access AWS CodePipeline via management console. 137 | 138 | 139 | #### CI/CD Pipeline 140 | 141 | The implementation of this CI/CD is based on the BlackBelt sample: [(Black Belt AWS - Page 52)](https://d1.awsstatic.com/webinars/jp/pdf/services/20201111_BlackBelt_AWS%20CodeStar_AWS_CodePipeline.pdf?page=52) 142 | 143 | If you want to replace it with your own web application or job script, replace the source code you push to CodeCommit with your own and modify the Dockerfile to suit your environment and application. 144 | 145 | ### 3. Testing 146 | 147 | When you want to check web application, you can access the app through the Bastion server on EC2. 148 | To access to the Bastion server via Fleet Manager Remote Desktop, you use the keypair that you've gotten in section [1. CDK]. 149 | If you want to know about how to access the Bastion server via Fleet Manager Remote Desktop, please see [Connect to a managed node using Remote Desktop](https://docs.aws.amazon.com/systems-manager/latest/userguide/fleet-rdp.html#fleet-rdp-connect-to-node). 150 | 151 | If you can access to Bastion server, open your browser and enter the domain specified by `app.{domainName}` in `stages.js` to access the web application. 152 | 153 | If the following screen is displayed, it is successful. 154 | 155 | ![application screenshot](../webapp-react/docs/images/screenshot.png) 156 | 157 | ### 4. Delete environment 158 | 159 | If you want to delete the created environment, execute the following command: 160 | 161 | 162 | ``` 163 | $ npm run destroy-serverless -- --{alias} 164 | ``` 165 | 166 | Some resources that like a ECR may remain due to the status. So you may need to delete them manually. 167 | Ref:[(ecr): add option to auto delete images upon ECR repository removal #12618 ](https://github.com/aws/aws-cdk/issues/12618) 168 | If destroy command was failed, please check the error message or CloudFormation console to understand what happend and root cause of errors to solve them. 169 | 170 | 171 | ### Additional commands 172 | 173 | Since `diff, list`, which is the CDK command, has already been implemented in gulp, these commands can also be executed via gulp. 174 | 175 | ``` 176 | $ npm run diff-serverless -- --{alias} 177 | $ npm run list-serverless -- --{alias} 178 | ``` 179 | 180 | ## Considerations in production 181 | 182 | ### S3 bucket names 183 | 184 | In order to communicate, the S3 bucket name must match the website domain name. 185 | S3 bucket names must be unique across all AWS accounts, and this limitation may prevent you from deploying a website with your preferred domain name. 186 | 187 | ### Migration steps from the container version 188 | 189 | If you are using the container version and are thinking about moving to serverless, you need to follow the following steps. 190 | 191 | - Get the latest source code from GitHub, including the serverless version 192 | - `npm run destroy-webapp -- --{alias}` command to delete the deployed Webapp stack 193 | - The certificate has been created, so Implement the deployment according to the ”1. CDK” after `npm install` in function folder 194 | - Since Java application code has been deployed in the existing CodeCommit repository for webapps, delete only the source code while leaving git related files in the webapp-java directory, and copy the webapp-react source code to the webapp-java directory. 195 | - Next, rename the webapp-java directory to webapp-react 196 | - change `.env` by domain name in `stages.js` 197 | - Run the following command to push the react source code 198 | 199 | ``` 200 | $ cd webapp-react 201 | $ git add. 202 | $ git commit -m "Initial commit" 203 | $ git push 204 | ``` 205 | -------------------------------------------------------------------------------- /infra/README_serverless_ja.md: -------------------------------------------------------------------------------- 1 | # サーバーレスアプリケーション版 2 | 3 | AWS 上にサーバーレスなサンプルアプリケーションやバッチシステムを動かす環境を構築する CDK のコードです。 4 | 5 | ## 概要 6 | 7 | アクセス数が少ない、またはほとんどアクセスしない時間帯があるようなアプリケーションを、ECS/Fargate を用いて常時稼働しておくと、実際の利用量に対し、費用がかかります。また、コンテナイメージなどの運用コストもあります。 8 | 9 | そのような場合に、ウェブサイト部分を S3 や Lambda を用いてサーバーレスで構成することによって、費用や運用の手間を減らすことができます。 10 | 11 | 閉域網における ALB、S3、PrivateLink による内部 HTTPS 静的 Web サイトのホスティングは、[こちらのブログ](https://aws.amazon.com/jp/blogs/news/hosting-internal-https-static-websites-with-alb-s3-and-privatelink/)に記載されており、このブログをもとに CDK 化したものが、サーバーレス版の本ソースコード群の一部となります。本ソースコードをご利用いただくことで、煩雑な Internal ALB の設定などを自動化することができます。 12 | 13 | 構成図は以下のとおりです。(赤枠で囲ったところがコンテナ版との差分です) 14 | 15 | ![構成図](../docs/images/template_architecture_serverless_ja.png) 16 | 17 | Private Link を用いた場合、以下の通りになります。 18 | ![構成図](../docs/images/template_architecture_serverless_privatelink_ja.png) 19 | 20 | ### 1. AWS CLI の設定 21 | 22 | CDK を利用するため、コマンドを実行する端末で AWS の設定が必要になります。 23 | 24 | ```bash 25 | $ aws configure --profile {プロファイル名} 26 | ``` 27 | 28 | と実行し、表示されるプロンプトに応じて、必要な情報を入力してください。 29 | 30 | IAM ユーザ作成時に表示される、アクセスキーとシークレットキー、デフォルトのリージョンが確認されます。 31 | 詳しくは[aws configure を使用したクイック設定 - プロファイル](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-profiles)をご参照ください。 32 | 33 | ### 2. stages.js の書き換え 34 | 35 | 本テンプレートは、タスクランナーの[gulp](https://gulpjs.com/)を利用してデプロイを行います。 36 | gulp から参照される変数が`stages.js`で定義されているため、各自の環境に合わせて変更します。 37 | 38 | ```javascript 39 | default: { 40 | appName, 41 | awsProfile: 'myProfile', 42 | alias: 'default', 43 | deployEnv: 'dev', 44 | notifyEmail: 'johndoe@johndoe.mail.com', 45 | enabledPrivateLink: false, 46 | windowsBastion: true, 47 | linuxBastion: true, 48 | domainName: 'templateapp.local', 49 | }, 50 | alias: { 51 | appName: '', // アプリの名前を入力します。 例: demoapp, など 52 | awsProfile: '', // 1で設定したProfile名を入力します。 53 | alias: '', // 個々人で環境面が被るのを回避するため、ユーザ名などの識別子を入力してください。 例: ysuzuki, など 54 | deployEnv: '' // デプロイする環境の面を記載します。例: dev, stage, prod, など 55 | notifyEmail: '', // ジョブが失敗した際の通知先メールアドレス 56 | enabledPrivateLink: false, // PrivateLinkを利用するかどうか。trueは利用し、falseは利用しない 57 | windowsBastion: true, // WindowsのBastionインスタンスを利用する場合はtrue、利用しない場合はfalse 58 | linuxBastion: true, // Amazon LinuxのBastionインスタンスを利用する場合はtrue、利用しない場合はfalse 59 | domainName: '', // Private Hosted Zoneに登録されるドメイン名(このドメイン名がS3のバケット名になり、S3 のバケット名はユニークである必要があるため、必ず変更してください。) 60 | } 61 | ``` 62 | 63 | ### 3. 自己署名付き証明書の作成 64 | 65 | HTTPS 通信を実装するために、今回は自己署名付き証明書を用います。 66 | `infra`ディレクトリで次のコマンドを実行し、Amazon Certificate Manager に証明書をインポートしてください。 67 | また、以下のコマンド実行前に、`OpenSSL`のインストールを実施してください。 68 | 69 | ```bash 70 | $ npm install 71 | $ npm run create-certificate -- --{alias} 72 | ``` 73 | 74 | ### 4. Lambda 関数に必要なモジュールのインストール 75 | 76 | `functions`ディレクトリで次のコマンドを実行し、Lambda 関数に必要なモジュールをインストールしてください。 77 | 78 | ```bash 79 | $ npm install 80 | ``` 81 | 82 | ## デプロイ 83 | 84 | ### 1. CDK 85 | 86 | `infra`ディレクトリで以下のコマンドを実行してください。 87 | 自動的に CDK が実行され、AWS の各リソースが生成されます。 88 | 89 | ```bash 90 | $ npm run deploy-serverless -- --{alias} 91 | ``` 92 | 93 | デプロイ後、ターミナル上に以下に示すようなコマンドが出力されますので、コピーして実行してください。 94 | 生成された EC2 インスタンス 用の Keypair がそれぞれ取得できます。 95 | コンソール接続する場合や Fleet Manager から RDP 接続する際には、Keypair の取得を行ってください。(コマンド実行時には Profile の指定をお願いします) 96 | 97 | ``` 98 | // regionがap-northeast-1のWindowsインスタンスの場合 99 | $ {alias}{stage}{appName}Webapp.WindowsGetSSHKeyForWindowsInstanceCommand = aws ssm get-parameter --name /ec2/keypair/key-XXXXXXXXXXXXXXXXX --region ap-northeast-1 --with-decryption --query Parameter.Value --output text 100 | 101 | // regionがap-northeast-1のAmazonLinuxインスタンスの場合 102 | $ {alias}{stage}{appName}Webapp.LinuxGetSSHKeyForLinuxInstanceCommand = aws ssm get-parameter --name /ec2/keypair/key-XXXXXXXXXXXXXXXXX --region ap-northeast-1 --with-decryption --query Parameter.Value --output text 103 | ``` 104 | 105 | > NOTE: 106 | > 初回デプロイ時は、ターミナルの出力が多いため、Keypair を取得するためのコマンドが見えなくなってしまうことがあります。 107 | > その場合は、ブラウザから CloudFormation のコンソールを開き、Webapp スタックの出力タブからご確認ください。 108 | > ![How to get key pair command](../docs/images/keypair_command_ja.png) 109 | 110 | また、CDK のデプロイが完了すると、`stages.js` に登録したメールアドレス宛に、Amazon SNS よりサブスクリプションの確認メールが届きます。 111 | 112 | ジョブが失敗した通知を受けるために、届いたメールの内容に従い、サブスクリプションの Confirmation を実施してください。 113 | 114 | また、バッチジョブは平日 21 時に実行される設定になっています。デプロイ時に登録される初期データは、ジョブがすべて成功する設定になっているため、メールは送信されません。 115 | もし、失敗を確認したい場合は、この後デプロイするサンプル Web アプリで、5 つある`true`のいずれかを`false`へ変更してください。 116 | 117 | ### 2. サンプル Web アプリ 118 | 119 | CDK のデプロイが完了したことで、AWS CodeCommit に サンプル Web アプリ用のリポジトリが作成されています。 120 | 121 | > NOTE: 122 | > リポジトリの URL はデプロイをしたターミナルもしくは、CloudFormation のコンソールに表示されます。 123 | > CloudFormation のコンソールを参照する場合は、`baseStack`の`出力`タブを参照ください。 124 | > ![Repository Url](../docs/images/repository_url_ja.png) 125 | 126 | `webapp-react` ディレクトリの`.env`ファイルに定義された`REACT_APP_ENDPOINT_URL`を、`stages.js`で設定した`domainName`を使って`https://app.{domainName}/apigw/`に置き換えてください。 127 | 128 | その後、以下の手順で、`webapp-react` ディレクトリのソースコードをプッシュすることで、サンプル Web アプリがパイプラインからデプロイされます。 129 | 130 | ```bash 131 | $ cd ./webapp-react 132 | $ git init 133 | $ git remote add origin https://git-codecommit.{your region}.amazonaws.com/v1/repos/{your repository name} 134 | $ git add . 135 | $ git commit -m "Initial commit" 136 | $ git push --set-upstream origin main 137 | $ git checkout -b develop 138 | $ git push --set-upstream origin develop 139 | ``` 140 | 141 | > NOTE: 142 | > CodePipeline のトリガーは develop ブランチを監視しています。そのため、develop ブランチの作成が必要になります。 143 | 144 | パイプラインの状況を確認したい場合は、マネジメントコンソールより AWS CodePipeline へアクセスしてください。 145 | 146 | #### CI/CD パイプラインについて 147 | 148 | Web アプリ向けの CI/CD は BlackBelt で紹介されている[構成例(Page 52)](https://d1.awsstatic.com/webinars/jp/pdf/services/20201111_BlackBelt_AWS%20CodeStar_AWS_CodePipeline.pdf)を元に実装しています。 149 | 150 | ご自身の Web アプリケーションに差し替えたい場合は、CodeCommit にプッシュするソースコードをご自身のものに差し替え、ご自身の環境やアプリケーションに合わせ、Dockerfile を修正してください。 151 | 152 | ### 3. 動作確認 153 | 154 | デプロイした Web アプリの動作を確認したい場合、Bastion として構築した Windows が起動している EC2 上でブラウザを起動し、アプリケーションにアクセスします。 155 | 156 | Bastion にアクセスする Keypair は [デプロイ - 1. CDK](#1-cdk) で取得したものを利用し、Fleet Manager 経由でアクセスします。 157 | Fleet Manager を利用した RDP 接続の方法は、[リモートデスクトップを使用してマネージドノードへ接続する](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/fleet-rdp.html#fleet-rdp-connect-to-node)を参照ください。 158 | 159 | Bastion への RDP 接続ができたら、ブラウザを起動し、`stages.js`の`domainName` で `app.{domainName}` を入力し、アプリケーションにアクセスしてください。 160 | 161 | 次のような画面が表示されたら成功です。 162 | 163 | ![アプリケーション動作画面](../webapp-react/docs/images/screenshot.png) 164 | 165 | ### 4. 作成した環境の削除 166 | 167 | 生成した環境を削除したい場合は、以下のコマンドを実行してください。 168 | ECR など、状況によっては残ってしまうリソースもあるため、手動での削除が必要な場合があります。 169 | ご参考:[(ecr): add option to auto delete images upon ECR repository removal #12618 ](https://github.com/aws/aws-cdk/issues/12618) 170 | コマンドが失敗した場合は、エラーメッセージや CloudFormation のコンソールで内容をご確認の上、対応ください。 171 | 172 | ``` 173 | $ npm run destroy-serverless -- --{alias} 174 | ``` 175 | 176 | ### その他のコマンド 177 | 178 | CDK のコマンドである、`diff, list`は、gulp で実装済みのため、これらのコマンドも gulp 経由で実行可能です。 179 | 180 | ``` 181 | $ npm run diff-serverless -- --{alias} 182 | $ npm run list-serverless -- --{alias} 183 | ``` 184 | 185 | ## 本番利用時の考慮点 186 | 187 | ### S3 のバケット名について 188 | 189 | 通信を疎通させるために、S3 のバケット名をウェブサイトのドメイン名と一致させる必要があります。 190 | S3 のバケット名は全ての AWS アカウント間でユニークである必要があり、この制約により希望のドメイン名でウェブサイトをデプロイできない場合があります。 191 | 192 | ### コンテナ版からの移行手順 193 | 194 | コンテナ版を使っていて、サーバーレスへの移行を考えているときは、大まかには次のような手順を踏む必要があります。 195 | 196 | - GitHub からサーバーレス版のソースコードを含んだ、最新のソースコードを取得する 197 | - `npm run destroy-webapp -- --{alias}` コマンドを利用し、デプロイ済みの Webapp Stack を削除する 198 | - 証明書の作成は完了しているため、`functions`ディレクトリでLambda 関数に必要なモジュールをインストールしてから、本 README の 1. CDK に従い、デプロイを実施する 199 | - 既存の Webapp 用の CodeCommit リポジトリは、Java アプリケーションコードがデプロイされているため、webapp-java のディレクトリ内の git 関連ファイルを残したまま、ソースコードだけを削除し、webapp-react のソースコードを webapp-java ディレクトリにコピーする。 200 | - 続いて、webapp-java のディレクトリ名を webapp-react に変更する 201 | - `.env` のドメインを `stages.js` のものと一致させる 202 | - 以下のコマンドを実行し、react のソースコードを push する 203 | 204 | ``` 205 | $ cd webapp-react 206 | $ git add . 207 | $ git commit -m "Initial commit" 208 | $ git push 209 | ``` 210 | -------------------------------------------------------------------------------- /infra/bin/base.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from 'lodash'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | 4 | import { BaseStack } from '../lib/base-stack'; 5 | import { DefaultStackSynthesizer } from 'aws-cdk-lib'; 6 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; 7 | import { Aspects } from 'aws-cdk-lib'; 8 | 9 | const env = { 10 | account: process.env.CDK_DEFAULT_ACCOUNT, 11 | region: process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION, 12 | }; 13 | 14 | const app = new cdk.App(); 15 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 16 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true, reports: true })); 17 | 18 | const stageAlias = app.node.tryGetContext('stage_alias') || 'defaultAlias'; 19 | const appName = app.node.tryGetContext('app_name') || 'defaultApp'; 20 | const deployEnv = app.node.tryGetContext('deploy_env') || 'defaultEnv'; 21 | 22 | const qualifier = `${stageAlias.slice(0, 5)}${deployEnv.slice(0, 5)}`; 23 | 24 | const id = `${capitalize(stageAlias)}${capitalize(deployEnv)}${capitalize(appName)}`; 25 | 26 | const base = new BaseStack(app, `${id}Base`, { 27 | env, 28 | synthesizer: new DefaultStackSynthesizer({ 29 | qualifier, 30 | }), 31 | description: 32 | 'BaseStack will provision static resourcse, like a database, repository, and vpc (uksb-1tupboc54) (tag:base).', 33 | }); 34 | 35 | // cdk-nag suppressions 36 | NagSuppressions.addStackSuppressions(base, [ 37 | { 38 | id: 'CdkNagValidationFailure', 39 | reason: 'refer to https://github.com/cdklabs/cdk-nag/issues/817', 40 | }, 41 | ]); 42 | -------------------------------------------------------------------------------- /infra/bin/batch.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from 'lodash'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | 4 | import { BatchStack } from '../lib/batch-stack'; 5 | import { DefaultStackSynthesizer } from 'aws-cdk-lib'; 6 | import { AwsSolutionsChecks } from 'cdk-nag'; 7 | import { Aspects } from 'aws-cdk-lib'; 8 | 9 | const env = { 10 | account: process.env.CDK_DEFAULT_ACCOUNT, 11 | region: process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION, 12 | }; 13 | 14 | const app = new cdk.App(); 15 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 16 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true, reports: true })); 17 | 18 | const stageAlias = app.node.tryGetContext('stage_alias') || 'defaultAlias'; 19 | const appName = app.node.tryGetContext('app_name') || 'defaultApp'; 20 | const deployEnv = app.node.tryGetContext('deploy_env') || 'defaultEnv'; 21 | const notifyEmail = app.node.tryGetContext('notify_email'); 22 | const appVpcId = app.node.tryGetContext('app_vpc_id') || 'defaultVpc'; 23 | 24 | if (!notifyEmail) { 25 | throw new Error('No notify email address in stages.js'); 26 | } 27 | 28 | const repositoryName = cdk.Fn.importValue('BatchContainerRepositoryName'); 29 | const auroraSecretName = cdk.Fn.importValue('SecretName'); 30 | const auroraSecurityGroupId = cdk.Fn.importValue('AuroraSecurityGroupId'); 31 | const auroraSecretEncryptionKeyArn = cdk.Fn.importValue('AuroraSecretEncryptionKeyArn'); 32 | 33 | const qualifier = `${stageAlias.slice(0, 5)}${deployEnv.slice(0, 5)}`; 34 | 35 | const id = `${capitalize(stageAlias)}${capitalize(deployEnv)}${capitalize(appName)}`; 36 | 37 | new BatchStack(app, `${id}Batch`, { 38 | env, 39 | synthesizer: new DefaultStackSynthesizer({ 40 | qualifier, 41 | }), 42 | description: 43 | 'BatchStack will provision stepfunctions statemachine and ecs cluster for batch (uksb-1tupboc54) (tag:batch).', 44 | notifyEmail, 45 | repositoryName, 46 | vpcId: appVpcId, 47 | auroraSecretName, 48 | auroraSecurityGroupId, 49 | auroraSecretEncryptionKeyArn, 50 | }); 51 | -------------------------------------------------------------------------------- /infra/bin/serverless-webapp.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from 'lodash'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { ServerlessappStack } from '../lib/serverlessapp-stack'; 4 | import { DefaultStackSynthesizer } from 'aws-cdk-lib'; 5 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; 6 | import { Aspects } from 'aws-cdk-lib'; 7 | 8 | const env = { 9 | account: process.env.CDK_DEFAULT_ACCOUNT, 10 | region: process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION, 11 | }; 12 | 13 | const app = new cdk.App(); 14 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 15 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true, reports: true })); 16 | 17 | const stageAlias = app.node.tryGetContext('stage_alias') || 'defaultAlias'; 18 | const appName = app.node.tryGetContext('app_name') || 'defaultApp'; 19 | const deployEnv = app.node.tryGetContext('deploy_env') || 'defaultEnv'; 20 | const enabledPrivateLink = app.node.tryGetContext('enabled_privatelink') || false; 21 | const vpcId = app.node.tryGetContext('app_vpc_id') || 'defaultVpc'; 22 | const windowsBastion = app.node.tryGetContext('windows_bastion') || false; 23 | const linuxBastion = app.node.tryGetContext('linux_bastion') || false; 24 | const domainName = app.node.tryGetContext('domain_name') || 'defaultDomain'; 25 | const certificateArn = app.node.tryGetContext('certificate_arn') || 'defaultCert'; 26 | 27 | const auroraSecretName = cdk.Fn.importValue('SecretName'); 28 | const auroraSecretArn = cdk.Fn.importValue('SecretArn'); 29 | const auroraSecurityGroupId = cdk.Fn.importValue('AuroraSecurityGroupId'); 30 | const auroraSecretEncryptionKeyArn = cdk.Fn.importValue('AuroraSecretEncryptionKeyArn'); 31 | const auroraEdition = cdk.Fn.importValue('AuroraEdition'); 32 | const rdsProxyEndpoint = cdk.Fn.importValue('RdsProxyEndpoint'); 33 | const rdsProxyArn = cdk.Fn.importValue('RdsProxyArn'); 34 | const containerRepositoryName = cdk.Fn.importValue('WebappContainerRepositoryName'); 35 | const sourceRepositoryName = cdk.Fn.importValue('WebappSourceRepositoryName'); 36 | 37 | const qualifier = `${stageAlias.slice(0, 5)}${deployEnv.slice(0, 5)}`; 38 | 39 | const id = `${capitalize(stageAlias)}${capitalize(deployEnv)}${capitalize(appName)}`; 40 | const webappStack = new ServerlessappStack(app, `${id}Webapp`, { 41 | env: env, 42 | synthesizer: new DefaultStackSynthesizer({ 43 | qualifier, 44 | }), 45 | description: 'ServerlessappStack will provision APIGW, Lambda function, bastions, and CI/CD pipeline (uksb-1tupboc54) (tag:webapp-serverless).', 46 | auroraSecretName, 47 | auroraSecretArn, 48 | auroraSecurityGroupId, 49 | auroraSecretEncryptionKeyArn, 50 | auroraEdition, 51 | rdsProxyEndpoint, 52 | rdsProxyArn, 53 | containerRepositoryName, 54 | enabledPrivateLink: enabledPrivateLink.toLowerCase() === 'true', 55 | testVpcCidr: '10.2.0.0/16', 56 | sourceRepositoryName, 57 | vpcId, 58 | windowsBastion, 59 | linuxBastion, 60 | domainName, 61 | certificateArn, 62 | }); 63 | // cdk-nag suppressions 64 | NagSuppressions.addStackSuppressions(webappStack, [ 65 | { 66 | id: 'AwsSolutions-IAM5', 67 | reason: 'To use ManagedPolicy', 68 | }, 69 | ]); 70 | -------------------------------------------------------------------------------- /infra/bin/webapp.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from 'lodash'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { WebappStack } from '../lib/webapp-stack'; 4 | import { DefaultStackSynthesizer } from 'aws-cdk-lib'; 5 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; 6 | import { Aspects } from 'aws-cdk-lib'; 7 | 8 | const env = { 9 | account: process.env.CDK_DEFAULT_ACCOUNT, 10 | region: process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION, 11 | }; 12 | 13 | const app = new cdk.App(); 14 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 15 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true, reports: true })); 16 | 17 | const stageAlias = app.node.tryGetContext('stage_alias') || 'defaultAlias'; 18 | const appName = app.node.tryGetContext('app_name') || 'defaultApp'; 19 | const deployEnv = app.node.tryGetContext('deploy_env') || 'defaultEnv'; 20 | const enabledPrivateLink = app.node.tryGetContext('enabled_privatelink') || false; 21 | const vpcId = app.node.tryGetContext('app_vpc_id') || 'defaultVpc'; 22 | const windowsBastion = app.node.tryGetContext('windows_bastion') || false; 23 | const linuxBastion = app.node.tryGetContext('linux_bastion') || false; 24 | const domainName = app.node.tryGetContext('domain_name') || 'defaultDomain'; 25 | const certificateArn = app.node.tryGetContext('certificate_arn') || 'defaultCert'; 26 | 27 | const auroraSecretName = cdk.Fn.importValue('SecretName'); 28 | const auroraSecurityGroupId = cdk.Fn.importValue('AuroraSecurityGroupId'); 29 | const auroraSecretEncryptionKeyArn = cdk.Fn.importValue('AuroraSecretEncryptionKeyArn'); 30 | const containerRepositoryName = cdk.Fn.importValue('WebappContainerRepositoryName'); 31 | const sourceRepositoryName = cdk.Fn.importValue('WebappSourceRepositoryName'); 32 | 33 | const qualifier = `${stageAlias.slice(0, 5)}${deployEnv.slice(0, 5)}`; 34 | 35 | const id = `${capitalize(stageAlias)}${capitalize(deployEnv)}${capitalize(appName)}`; 36 | const webappStack = new WebappStack(app, `${id}Webapp`, { 37 | env: env, 38 | synthesizer: new DefaultStackSynthesizer({ 39 | qualifier, 40 | }), 41 | description: 42 | 'WebappStack will provision ecs cluster for webapp, load balancers, bastions, and CI/CD pipeline (uksb-1tupboc54) (tag:webapp-container).', 43 | auroraSecretName, 44 | auroraSecurityGroupId, 45 | auroraSecretEncryptionKeyArn, 46 | containerRepositoryName, 47 | enabledPrivateLink: enabledPrivateLink.toLowerCase() === 'true', 48 | testVpcCidr: '10.2.0.0/16', 49 | sourceRepositoryName, 50 | vpcId, 51 | windowsBastion, 52 | linuxBastion, 53 | domainName, 54 | certificateArn, 55 | }); 56 | // cdk-nag suppressions 57 | NagSuppressions.addStackSuppressions(webappStack, [ 58 | { 59 | id: 'AwsSolutions-IAM5', 60 | reason: 'To use ManagedPolicy', 61 | }, 62 | ]); 63 | -------------------------------------------------------------------------------- /infra/buildWebEnv.mjs: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash'; 2 | import { readFile, writeFile } from 'fs/promises'; 3 | 4 | const { endsWith, startsWith } = lodash; 5 | 6 | const envFile = '../webapp/.env.production'; 7 | const stageName = process.argv[2]; 8 | 9 | let cdkOutputData = {}; 10 | 11 | async function append(data) { 12 | await writeFile(envFile, data, { flag: 'a' }, (err) => { 13 | if (err) { 14 | console.error(err); 15 | return; 16 | } 17 | }); 18 | } 19 | 20 | async function buildWebAppEnv() { 21 | let data = await readFile(new URL('./cdk-infra-outputs.json', import.meta.url)); 22 | cdkOutputData = data ? JSON.parse(data) : {}; 23 | let stageKeys = {}; 24 | 25 | Object.keys(cdkOutputData).forEach((key) => { 26 | if (endsWith(key, 'baseline') && startsWith(key, stageName)) { 27 | stageKeys = cdkOutputData[key]; 28 | } 29 | }); 30 | 31 | Object.keys(stageKeys).forEach((key) => { 32 | key.includes('userpoolid') ? append(`VITE_COGNITO_USERPOOLID=${stageKeys[key]}\n`) : ''; 33 | key.includes('userpoolclientid') ? append(`VITE_COGNITO_WEBCLIENTID=${stageKeys[key]}\n`) : ''; 34 | key.includes('identitypoolid') ? append(`VITE_COGNITO_IDENTITYPOOLID=${stageKeys[key]}\n`) : ''; 35 | key.includes('cognitourl') ? append(`VITE_COGNITO_ENDPOINT=${stageKeys[key]}\n`) : ''; 36 | key.includes('graphqlurl') ? append(`VITE_GRAPHQL_URL=${stageKeys[key]}\n`) : ''; 37 | key.includes('region') ? append(`VITE_COGNITO_REGION=${stageKeys[key]}\n`) : ''; 38 | }); 39 | } 40 | 41 | await writeFile(envFile, '', { flag: 'w+' }, (err) => { 42 | if (err) { 43 | console.error(err); 44 | return; 45 | } 46 | }); 47 | 48 | await buildWebAppEnv(); 49 | -------------------------------------------------------------------------------- /infra/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/base.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 25 | "@aws-cdk/core:checkSecretUsage": true, 26 | "@aws-cdk/aws-iam:minimizePolicies": true, 27 | "@aws-cdk/core:target-partitions": ["aws"], 28 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /infra/docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | VOLUME /var/cache/nginx 4 | VOLUME /var/run 5 | VOLUME /etc/nginx/conf.d 6 | VOLUME /usr/share/nginx/html 7 | 8 | COPY default.conf /etc/nginx/conf.d/default.conf 9 | COPY static-content /usr/share/nginx/html 10 | -------------------------------------------------------------------------------- /infra/docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | listen [::]:8080; 4 | server_name localhost; 5 | 6 | location / { 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | } 10 | } -------------------------------------------------------------------------------- /infra/docker/nginx/static-content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

It works!

4 | 5 | -------------------------------------------------------------------------------- /infra/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 | -------------------------------------------------------------------------------- /infra/lib/base-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, StackProps, Stack, aws_codecommit, aws_ec2 } from 'aws-cdk-lib'; 2 | import { DatabaseClusterEngine, AuroraPostgresEngineVersion } from 'aws-cdk-lib/aws-rds'; 3 | import { Construct } from 'constructs'; 4 | 5 | // Constructs 6 | import { Network } from './constructs/network/network'; 7 | import { Aurora } from './constructs/aurora/aurora'; 8 | import { Ecr } from './constructs/ecr/ecr'; 9 | 10 | export class BaseStack extends Stack { 11 | public readonly vpc: aws_ec2.Vpc; 12 | constructor(scope: Construct, id: string, props?: StackProps) { 13 | super(scope, id, props); 14 | 15 | // Create networking resources 16 | const network = new Network(this, `AppVpc`, { 17 | cidr: '10.0.0.0/16', 18 | cidrMask: 24, 19 | publicSubnet: false, 20 | isolatedSubnet: true, 21 | maxAzs: 2, 22 | }); 23 | this.vpc = network.vpc; 24 | 25 | // Create Aurora 26 | new Aurora(this, 'Aurora', { 27 | enabledServerless: false, 28 | enabledProxy: false, // If you want to use Lambda Proxy, This parameter is true. And If you want to use `serverless-webapp`, Please set `true`. 29 | auroraEdition: DatabaseClusterEngine.auroraPostgres({ 30 | version: AuroraPostgresEngineVersion.VER_12_9, 31 | }), 32 | vpc: network.vpc, 33 | dbUserName: 'postgres', 34 | }); 35 | 36 | // Create ECR 37 | new Ecr(this, 'Webapp').containerRepository; 38 | new Ecr(this, 'Batch').containerRepository; 39 | 40 | // Create Pipeline 41 | const codecommitRepository = new aws_codecommit.Repository(this, 'WebappSourceRepository', { 42 | repositoryName: `${id.toLowerCase()}-webapp-source`, 43 | }); 44 | new CfnOutput(this, 'SourceRepositoryName', { 45 | exportName: 'WebappSourceRepositoryName', 46 | value: codecommitRepository.repositoryName, 47 | }); 48 | new CfnOutput(this, 'SourceRepositoryUrl', { 49 | exportName: 'WebappSourceRepositoryUrl', 50 | value: codecommitRepository.repositoryCloneUrlHttp, 51 | }); 52 | new CfnOutput(this, 'AuroraEdition', { 53 | exportName: 'AuroraEdition', 54 | value: 'postgresql', 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /infra/lib/constructs/aurora/README.md: -------------------------------------------------------------------------------- 1 | # Aurora Construct 2 | 3 | ## Purpose 4 | 5 | - Create a credential for Aurora 6 | - Create Aurora Cluster or Aurora Serverless (v1) 7 | - Create RDS Proxy for Aurora Cluster 8 | 9 | ## Required resources 10 | 11 | - VPC and private isolated subnet 12 | 13 | ## Required parameters (props) 14 | 15 | - `enabledServerless ` : Select whether Aurora Serverless or not 16 | - `auroraEdition ` : Define Aurora Engine 17 | > ### Note 18 | > 19 | > Aurora Serverless v1 supports `PostgreSQL Ver 10.18` or` MySQL 5.6/5.7` 20 | - `vpc ` : Define the vpc including isolated subnets 21 | - `dbUserName ` : Database username for db credentials 22 | 23 | ## Optional parameters (props) 24 | 25 | - `enabledProxy` : Create RDS proxy and this proxy attached to Aurora clustrer if this props is `true` 26 | 27 | ## Properties 28 | 29 | | Name | Type | Description | 30 | | ------------------- | :----------------------------------: | -----------------------------------------------: | 31 | | aurora | DatabaseCluster or ServerlessCluster | An Aurora | 32 | | proxy | DatabaseProxy | A RDS proxy | 33 | | databaseCredentials | Credentials | A Secrets Manager for storing databse credential | 34 | | proxyRole | Role | | 35 | -------------------------------------------------------------------------------- /infra/lib/constructs/aurora/README_dbinitlambda.md: -------------------------------------------------------------------------------- 1 | # DBinitLambda Construct 2 | 3 | ## Purpose 4 | 5 | - Lambda for initialize DB 6 | 7 | ## Required resources 8 | 9 | - RDS 10 | - RDS proxy 11 | 12 | ## Required parameters (props) 13 | 14 | - `vpc ` : Define the vpc including RDS 15 | - `sgForLambda ` : Security Group for Lambda 16 | - `auroraSecretName ` : Secret Name including RDS information 17 | - `auroraSecretArn ` : Secret Arn including RDS information 18 | - `auroraSecretEncryptionKeyArn ` : KMS Key Arn which encrypt secret including RDS information 19 | - `rdsProxyEndpoint ` : Endpoint url of RDS proxy endpoint 20 | - `rdsProxyArn ` : ARN of RDS proxy endpoint 21 | 22 | ## Optional parameters (props) 23 | 24 | None 25 | 26 | ## Properties 27 | 28 | None 29 | -------------------------------------------------------------------------------- /infra/lib/constructs/aurora/aurora.ts: -------------------------------------------------------------------------------- 1 | import { aws_rds, RemovalPolicy, CfnOutput, aws_iam, aws_ec2 } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { isEmpty } from 'lodash'; 4 | import { EncryptionKey } from '../kms/key'; 5 | import { ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 6 | import { NagSuppressions } from 'cdk-nag'; 7 | 8 | export class Aurora extends Construct { 9 | public readonly aurora: aws_rds.DatabaseCluster | aws_rds.ServerlessCluster; 10 | public readonly proxy: aws_rds.DatabaseProxy; 11 | public readonly databaseCredentials: aws_rds.Credentials; 12 | public readonly proxyRole: aws_iam.Role; 13 | constructor( 14 | scope: Construct, 15 | id: string, 16 | props: { 17 | enabledServerless: boolean; 18 | auroraEdition: aws_rds.IClusterEngine; 19 | vpc: aws_ec2.Vpc; 20 | dbUserName: string; 21 | enabledProxy?: boolean; 22 | } 23 | ) { 24 | super(scope, id); 25 | 26 | // Check whether isolated subnets which you chose or not 27 | if (isEmpty(props.vpc.isolatedSubnets)) { 28 | throw new Error('You should specify the isolated subnets in subnets'); 29 | } 30 | 31 | const secretName = 'AuroraSecret'; 32 | this.databaseCredentials = aws_rds.Credentials.fromGeneratedSecret(props.dbUserName, { 33 | secretName, 34 | encryptionKey: new EncryptionKey(this, 'AuroraSecretEncryptionKey', { 35 | servicePrincipals: [new ServicePrincipal('secretsmanager.amazonaws.com')], 36 | }).encryptionKey, 37 | }); 38 | 39 | // Add VPC Endpoint to use SecretRotation 40 | props.vpc.addInterfaceEndpoint('SecretsmanagerEndpoint', { 41 | service: aws_ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, 42 | subnets: { 43 | subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED, 44 | }, 45 | }); 46 | 47 | if (props.enabledServerless) { 48 | this.aurora = new aws_rds.ServerlessCluster(this, 'Serverless', { 49 | engine: props.auroraEdition, 50 | vpc: props.vpc, 51 | vpcSubnets: { 52 | subnets: props.vpc.isolatedSubnets, 53 | }, 54 | credentials: this.databaseCredentials, 55 | removalPolicy: RemovalPolicy.DESTROY, // For development env only 56 | deletionProtection: false, // In production, we have to set true. 57 | }); 58 | } else { 59 | this.aurora = new aws_rds.DatabaseCluster(this, `Cluster`, { 60 | engine: props.auroraEdition, 61 | iamAuthentication: true, 62 | vpc: props.vpc, 63 | vpcSubnets: { 64 | subnets: props.vpc.isolatedSubnets, 65 | }, 66 | writer: aws_rds.ClusterInstance.provisioned('Writer', { 67 | instanceType: aws_ec2.InstanceType.of( 68 | aws_ec2.InstanceClass.T3, 69 | aws_ec2.InstanceSize.MEDIUM 70 | ), 71 | }), 72 | readers: [ 73 | aws_rds.ClusterInstance.provisioned('Reader', { 74 | instanceType: aws_ec2.InstanceType.of( 75 | aws_ec2.InstanceClass.T3, 76 | aws_ec2.InstanceSize.MEDIUM 77 | ), 78 | }), 79 | ], 80 | storageEncrypted: true, 81 | credentials: this.databaseCredentials, 82 | removalPolicy: RemovalPolicy.DESTROY, // For development env only 83 | deletionProtection: false, // In production, we have to set true. 84 | parameters: { 85 | 'rds.force_ssl': '1', 86 | }, 87 | cloudwatchLogsExports: ['postgresql'], 88 | }); 89 | 90 | if (props.enabledProxy && this.aurora.secret) { 91 | this.proxyRole = new aws_iam.Role(this, 'RdsProxyRole', { 92 | assumedBy: new aws_iam.ServicePrincipal('rds.amazonaws.com'), 93 | }); 94 | 95 | this.proxy = this.aurora.addProxy('RdsProxy', { 96 | vpc: props.vpc, 97 | iamAuth: true, 98 | secrets: [this.aurora.secret], 99 | securityGroups: this.aurora.connections.securityGroups, 100 | }); 101 | 102 | this.proxy.grantConnect(this.proxyRole); 103 | } 104 | } 105 | 106 | this.aurora.addRotationSingleUser(); 107 | this.databaseCredentials.encryptionKey?.grantDecrypt(new ServicePrincipal('rds.amazonaws.com')); 108 | 109 | if (this.aurora.secret && this.aurora.clusterEndpoint) { 110 | new CfnOutput(this, 'SecretName', { 111 | exportName: 'SecretName', 112 | value: this.aurora.secret.secretName, 113 | }); 114 | new CfnOutput(this, 'SecretArn', { 115 | exportName: 'SecretArn', 116 | value: this.aurora.secret.secretArn, 117 | }); 118 | 119 | new CfnOutput(this, 'AuroraClusterIdentifier', { 120 | exportName: 'AuroraClusterIdentifier', 121 | value: this.aurora.clusterIdentifier, 122 | }); 123 | 124 | new CfnOutput(this, 'AuroraEndpoint', { 125 | exportName: 'AuroraEndpoint', 126 | value: this.aurora.clusterEndpoint.hostname, 127 | }); 128 | 129 | new CfnOutput(this, 'AuroraSecurityGroupId', { 130 | exportName: 'AuroraSecurityGroupId', 131 | value: this.aurora.connections.securityGroups[0].securityGroupId, 132 | }); 133 | 134 | new CfnOutput(this, 'AuroraSecretEncryptionKeyArn', { 135 | exportName: 'AuroraSecretEncryptionKeyArn', 136 | value: this.aurora.secret.encryptionKey ? this.aurora.secret.encryptionKey.keyArn : '', 137 | }); 138 | 139 | if (props.enabledProxy) { 140 | new CfnOutput(this, 'RDSProxyEndpoint', { 141 | exportName: 'RdsProxyEndpoint', 142 | value: this.proxy.endpoint, 143 | }); 144 | new CfnOutput(this, 'RDSProxyArn', { 145 | exportName: 'RdsProxyArn', 146 | value: this.proxy.dbProxyArn, 147 | }); 148 | } 149 | } 150 | NagSuppressions.addResourceSuppressions( 151 | this.aurora, 152 | [ 153 | { 154 | id: 'AwsSolutions-RDS10', 155 | reason: 'for Development purpose only', 156 | }, 157 | ], 158 | true 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /infra/lib/constructs/aurora/dbinitlambda.ts: -------------------------------------------------------------------------------- 1 | import { aws_ec2, custom_resources, CustomResource } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { NagSuppressions } from 'cdk-nag'; 4 | import { DefaultLambda } from '../serverless/lambda'; 5 | import * as path from 'path'; 6 | 7 | export class DbInitLambda extends Construct { 8 | constructor( 9 | scope: Construct, 10 | id: string, 11 | props: { 12 | vpc: aws_ec2.IVpc; 13 | sgForLambda: aws_ec2.SecurityGroup; 14 | auroraSecretName: string; 15 | auroraSecretArn: string; 16 | auroraSecretEncryptionKeyArn: string; 17 | rdsProxyEndpoint: string; 18 | rdsProxyArn: string; 19 | } 20 | ) { 21 | super(scope, id); 22 | 23 | const initLambda = new DefaultLambda(this, 'dbInitLambda', { 24 | entry: path.join(__dirname, '../../../../functions/init.ts'), 25 | vpc: props.vpc, 26 | auroraSecretName: props.auroraSecretName, 27 | auroraSecretArn: props.auroraSecretArn, 28 | auroraSecretEncryptionKeyArn: props.auroraSecretEncryptionKeyArn, 29 | rdsProxyEndpoint: props.rdsProxyEndpoint, 30 | rdsProxyArn: props.rdsProxyArn, 31 | sgForLambda: props.sgForLambda, 32 | }); 33 | 34 | const provider = new custom_resources.Provider(this, 'DBInitProvider', { 35 | onEventHandler: initLambda.lambda, 36 | }); 37 | 38 | new CustomResource(this, 'DBInitResource', { 39 | serviceToken: provider.serviceToken, 40 | properties: { 41 | time: Date.now().toString(), 42 | }, 43 | }); 44 | NagSuppressions.addResourceSuppressions( 45 | provider, 46 | [ 47 | { 48 | id: 'AwsSolutions-L1', 49 | reason: 'This is Custom Resource managed by AWS', 50 | }, 51 | ], 52 | true 53 | ); 54 | NagSuppressions.addResourceSuppressions( 55 | provider, 56 | [ 57 | { 58 | id: 'AwsSolutions-IAM4', 59 | reason: 'This is Custom Resource managed by AWS', 60 | }, 61 | ], 62 | true 63 | ); 64 | NagSuppressions.addResourceSuppressions( 65 | provider, 66 | [ 67 | { 68 | id: 'AwsSolutions-IAM5', 69 | reason: 'This is Custom Resource managed by AWS', 70 | }, 71 | ], 72 | true 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /infra/lib/constructs/codepipeline/README_java.md: -------------------------------------------------------------------------------- 1 | # CodePipelineWebappJava Construct 2 | 3 | ## Purpose 4 | 5 | - Create CodePipeline to deploy to ECS/Fargate 6 | - Include CodeCommit and CodeBuild 7 | 8 | ## Required resources 9 | 10 | - CodeCommit repository 11 | - ECS TaskDefinition 12 | - ECS Service 13 | - ECS Cluster on Fargate. 14 | 15 | ## Required parameters (props) 16 | 17 | - `codeCommitRepository` : CodeCOmmit Repository, Get source code from this repository 18 | - `ecsService` : ECS Service for Fargate, deploy destination 19 | 20 | ## Properties 21 | 22 | None 23 | -------------------------------------------------------------------------------- /infra/lib/constructs/codepipeline/README_react.md: -------------------------------------------------------------------------------- 1 | # CodePipelineWebappReact Construct 2 | 3 | ## Purpose 4 | 5 | - Create CodePipeline to deploy to S3 6 | - Include CodeCommit and CodeBuild 7 | 8 | ## Required resources 9 | 10 | - CodeCommit repository 11 | - S3 bucket to deploy 12 | 13 | ## Required parameters (props) 14 | 15 | - `codeCommitRepository` : CodeCOmmit Repository, Get source code from this repository 16 | - `s3bucket` : S3 bucket, deploy destination 17 | 18 | ## Properties 19 | 20 | None 21 | -------------------------------------------------------------------------------- /infra/lib/constructs/codepipeline/codepipeline-webapp-java.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_codebuild, 3 | aws_codecommit, 4 | aws_codepipeline, 5 | aws_codepipeline_actions, 6 | aws_ecr, 7 | aws_ecs, 8 | aws_iam, 9 | aws_kms, 10 | aws_logs, 11 | } from 'aws-cdk-lib'; 12 | import { NagSuppressions } from 'cdk-nag'; 13 | import { Construct } from 'constructs'; 14 | import { EncryptionKey } from '../kms/key'; 15 | 16 | export class CodePipelineWebappJava extends Construct { 17 | constructor( 18 | scope: Construct, 19 | id: string, 20 | props: { 21 | codeCommitRepository: aws_codecommit.IRepository; 22 | ecrRepository: aws_ecr.IRepository; 23 | ecsService: aws_ecs.FargateService; 24 | containerName: string; 25 | } 26 | ) { 27 | super(scope, id); 28 | 29 | const pipeline = new aws_codepipeline.Pipeline(this, 'WebappPipeline', { 30 | enableKeyRotation: true, 31 | }); 32 | 33 | // Source stage 34 | const sourceOutput = new aws_codepipeline.Artifact('SourceArtifact'); 35 | const sourceAction = new aws_codepipeline_actions.CodeCommitSourceAction({ 36 | actionName: 'GetSourceCodeFromCodeCommit', 37 | repository: props.codeCommitRepository, 38 | branch: 'develop', 39 | output: sourceOutput, 40 | trigger: aws_codepipeline_actions.CodeCommitTrigger.POLL, 41 | }); 42 | 43 | pipeline.addStage({ 44 | stageName: 'Source', 45 | actions: [sourceAction], 46 | }); 47 | 48 | // Build stage 49 | const buildLogGroup = new aws_logs.LogGroup(this, 'BuildLogGroup', { 50 | encryptionKey: new EncryptionKey(this, 'BuildLogGroupEncryptionKey', { 51 | servicePrincipals: [new aws_iam.ServicePrincipal('logs.amazonaws.com')], 52 | }).encryptionKey, 53 | }); 54 | 55 | const buildActionProject = new aws_codebuild.PipelineProject(this, 'BuildProject', { 56 | buildSpec: aws_codebuild.BuildSpec.fromSourceFilename('buildspec.yaml'), 57 | encryptionKey: new aws_kms.Key(this, 'BuildActionProjectKey', { enableKeyRotation: true }), 58 | logging: { 59 | cloudWatch: { 60 | enabled: true, 61 | logGroup: buildLogGroup, 62 | }, 63 | }, 64 | environment: { 65 | privileged: true, 66 | buildImage: aws_codebuild.LinuxBuildImage.AMAZON_LINUX_2_4, 67 | }, 68 | environmentVariables: { 69 | REPOSITORY_URI: { 70 | type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, 71 | value: props.ecrRepository.repositoryUri, 72 | }, 73 | ECS_APP_CONTAINER: { 74 | type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT, 75 | value: props.containerName, 76 | }, 77 | }, 78 | }); 79 | props.ecrRepository.grantPullPush(buildActionProject); 80 | 81 | const buildOutput = new aws_codepipeline.Artifact(); 82 | const buildAction = new aws_codepipeline_actions.CodeBuildAction({ 83 | actionName: 'BuildDockerImageOnCodeBuild', 84 | project: buildActionProject, 85 | input: sourceOutput, 86 | outputs: [buildOutput], 87 | }); 88 | pipeline.addStage({ 89 | stageName: 'Build', 90 | actions: [buildAction], 91 | }); 92 | 93 | // Deploy stage 94 | const deployAction = new aws_codepipeline_actions.EcsDeployAction({ 95 | actionName: 'DeployNewImageToECS', 96 | service: props.ecsService, 97 | input: buildOutput, 98 | }); 99 | 100 | pipeline.addStage({ 101 | stageName: 'Deploy', 102 | actions: [deployAction], 103 | }); 104 | 105 | // cdk-nag suppressions 106 | NagSuppressions.addResourceSuppressions(buildActionProject, [ 107 | { 108 | id: 'AwsSolutions-CB3', 109 | reason: 'To build docker image on CodeBuild host.', 110 | }, 111 | ]); 112 | NagSuppressions.addResourceSuppressions(pipeline.artifactBucket, [ 113 | { 114 | id: 'AwsSolutions-S1', 115 | reason: "This bucket doesn't store sensitive data", 116 | }, 117 | ]); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /infra/lib/constructs/codepipeline/codepipeline-webapp-react.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_codebuild, 3 | aws_codecommit, 4 | aws_codepipeline, 5 | aws_codepipeline_actions, 6 | aws_iam, 7 | aws_kms, 8 | aws_logs, 9 | aws_s3, 10 | } from 'aws-cdk-lib'; 11 | import { NagSuppressions } from 'cdk-nag'; 12 | import { Construct } from 'constructs'; 13 | import { EncryptionKey } from '../kms/key'; 14 | 15 | export class CodePipelineWebappReact extends Construct { 16 | constructor( 17 | scope: Construct, 18 | id: string, 19 | props: { 20 | codeCommitRepository: aws_codecommit.IRepository; 21 | s3bucket: aws_s3.Bucket; 22 | } 23 | ) { 24 | super(scope, id); 25 | 26 | const pipeline = new aws_codepipeline.Pipeline(this, 'WebappPipeline', { 27 | enableKeyRotation: true, 28 | }); 29 | 30 | // Source stage 31 | const sourceOutput = new aws_codepipeline.Artifact('SourceArtifact'); 32 | const sourceAction = new aws_codepipeline_actions.CodeCommitSourceAction({ 33 | actionName: 'GetSourceCodeFromCodeCommit', 34 | repository: props.codeCommitRepository, 35 | branch: 'develop', 36 | output: sourceOutput, 37 | trigger: aws_codepipeline_actions.CodeCommitTrigger.POLL, 38 | }); 39 | 40 | pipeline.addStage({ 41 | stageName: 'Source', 42 | actions: [sourceAction], 43 | }); 44 | 45 | // Build stage 46 | const buildLogGroup = new aws_logs.LogGroup(this, 'BuildLogGroup', { 47 | encryptionKey: new EncryptionKey(this, 'BuildLogGroupEncryptionKey', { 48 | servicePrincipals: [new aws_iam.ServicePrincipal('logs.amazonaws.com')], 49 | }).encryptionKey, 50 | }); 51 | 52 | const buildActionProject = new aws_codebuild.PipelineProject(this, 'BuildProject', { 53 | buildSpec: aws_codebuild.BuildSpec.fromSourceFilename('buildspec.yaml'), 54 | encryptionKey: new aws_kms.Key(this, 'BuildActionProjectKey', { enableKeyRotation: true }), 55 | logging: { 56 | cloudWatch: { 57 | enabled: true, 58 | logGroup: buildLogGroup, 59 | }, 60 | }, 61 | environment: { 62 | privileged: true, 63 | buildImage: aws_codebuild.LinuxBuildImage.AMAZON_LINUX_2_4, 64 | }, 65 | }); 66 | 67 | const buildOutput = new aws_codepipeline.Artifact(); 68 | const buildAction = new aws_codepipeline_actions.CodeBuildAction({ 69 | actionName: 'BuildReactOnCodeBuild', 70 | project: buildActionProject, 71 | input: sourceOutput, 72 | outputs: [buildOutput], 73 | }); 74 | pipeline.addStage({ 75 | stageName: 'Build', 76 | actions: [buildAction], 77 | }); 78 | 79 | // Deploy stage 80 | const deployAction = new aws_codepipeline_actions.S3DeployAction({ 81 | actionName: 'DeployBuildFileToS3', 82 | input: buildOutput, 83 | bucket: props.s3bucket, 84 | }); 85 | 86 | pipeline.addStage({ 87 | stageName: 'Deploy', 88 | actions: [deployAction], 89 | }); 90 | 91 | // cdk-nag suppressions 92 | NagSuppressions.addResourceSuppressions(buildActionProject, [ 93 | { 94 | id: 'AwsSolutions-CB3', 95 | reason: 'To build docker image on CodeBuild host.', 96 | }, 97 | ]); 98 | NagSuppressions.addResourceSuppressions(pipeline.artifactBucket, [ 99 | { 100 | id: 'AwsSolutions-S1', 101 | reason: "This bucket doesn't store sensitive data", 102 | }, 103 | ]); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /infra/lib/constructs/ec2/README.md: -------------------------------------------------------------------------------- 1 | # Bastion Construct 2 | 3 | ## Purpose 4 | 5 | - Create Windows/Linux bastion instance 6 | 7 | ## Required resources 8 | 9 | - VPC and private isolated subnet 10 | 11 | ## Required parameters (props) 12 | 13 | - `os <"Linux"|"Windows">` : Select whether Linux or Windows 14 | - `vpc ` : Define destionation to create an instance. It required isolated subnet 15 | - `region ` : Region ID 16 | - `auroraSecurityGroupId ` : To create security group for bastion to allow access to RDS from bastion 17 | 18 | ## Optional parameters (props) 19 | 20 | - `instanceType ` : If you want to change instance type, please add it. Default is `t2-small` 21 | 22 | ## Properties 23 | 24 | - None 25 | -------------------------------------------------------------------------------- /infra/lib/constructs/ec2/bastion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CfnKeyPair, 3 | BlockDeviceVolume, 4 | Instance, 5 | InstanceClass, 6 | InstanceSize, 7 | InstanceType, 8 | MachineImage, 9 | Port, 10 | SecurityGroup, 11 | SubnetType, 12 | WindowsVersion, 13 | IVpc, 14 | } from 'aws-cdk-lib/aws-ec2'; 15 | import { 16 | Policy, 17 | PolicyStatement, 18 | Role, 19 | ServicePrincipal, 20 | ManagedPolicy, 21 | } from 'aws-cdk-lib/aws-iam'; 22 | import { CfnOutput, Stack } from 'aws-cdk-lib'; 23 | import { Construct } from 'constructs'; 24 | import { NagSuppressions } from 'cdk-nag'; 25 | 26 | export class Bastion extends Construct { 27 | public readonly bastionInstance: Instance; 28 | constructor( 29 | scope: Construct, 30 | id: string, 31 | props: { 32 | os: 'Linux' | 'Windows'; 33 | vpc: IVpc; 34 | region: string; 35 | auroraSecurityGroupId?: string; 36 | instanceType?: InstanceType; 37 | } 38 | ) { 39 | super(scope, id); 40 | 41 | // keypair 42 | const keyPair = new CfnKeyPair(this, `${id}InstanceKeypair`, { 43 | keyName: `${id}-instance-keypair`, 44 | }); 45 | 46 | // Create EC2 instance to do testing for PrivateLink 47 | const instanceRole = new Role(this, `${id}instanceRole`, { 48 | assumedBy: new ServicePrincipal('ec2.amazonaws.com'), 49 | managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')], 50 | }); 51 | 52 | // If this instance is Linux, If this instance is Linux, this instance is geven grant to access to S3 repo. 53 | if (props.os === 'Linux') { 54 | instanceRole.attachInlinePolicy( 55 | new Policy(this, 'AccessYumRepoPolicy', { 56 | statements: [ 57 | new PolicyStatement({ 58 | actions: ['s3:GetObject'], 59 | resources: [`arn:aws:s3:::al2023-repos-${Stack.of(this).region}-de612dc2/*`], 60 | }), 61 | ], 62 | }) 63 | ); 64 | } 65 | 66 | const bastionInstance = new Instance(this, `${id}BastionInstance`, { 67 | vpc: props.vpc, 68 | instanceType: props.instanceType 69 | ? props.instanceType 70 | : InstanceType.of(InstanceClass.T2, InstanceSize.SMALL), // default is t2-small 71 | machineImage: 72 | props.os === 'Linux' 73 | ? MachineImage.latestAmazonLinux2023() 74 | : MachineImage.latestWindows(WindowsVersion.WINDOWS_SERVER_2022_JAPANESE_FULL_BASE), 75 | vpcSubnets: { 76 | subnetType: SubnetType.PRIVATE_ISOLATED, 77 | }, 78 | role: instanceRole, 79 | keyName: keyPair.keyName, 80 | blockDevices: [ 81 | { 82 | deviceName: props.os === 'Linux' ? '/dev/xvda' : '/dev/sda1', 83 | volume: BlockDeviceVolume.ebs(30, { 84 | encrypted: true, 85 | }), 86 | }, 87 | ], 88 | requireImdsv2: true, 89 | }); 90 | this.bastionInstance = bastionInstance; 91 | 92 | // Allow access to RDS 93 | if (props.auroraSecurityGroupId) { 94 | bastionInstance.connections.allowTo( 95 | SecurityGroup.fromSecurityGroupId(this, 'AuroraSecurityGroup', props.auroraSecurityGroupId), 96 | Port.tcp(5432) 97 | ); 98 | } 99 | 100 | new CfnOutput(this, `${id}BastionInstanceId`, { 101 | value: bastionInstance.instanceId, 102 | exportName: `${id}BastionInstanceId`, 103 | }); 104 | 105 | // Command to get SSH Key 106 | new CfnOutput(this, `GetSSHKeyFor${id}InstanceCommand`, { 107 | value: `aws ssm get-parameter --name /ec2/keypair/${keyPair.getAtt('KeyPairId')} --region ${ 108 | props.region 109 | } --with-decryption --query Parameter.Value --output text`, 110 | }); 111 | 112 | // cdk-nag suppressions 113 | NagSuppressions.addResourceSuppressions(instanceRole, [ 114 | { 115 | id: 'AwsSolutions-IAM4', 116 | reason: 'To use SSM for instance, this managed policy is required.', 117 | }, 118 | ]); 119 | NagSuppressions.addResourceSuppressions(bastionInstance, [ 120 | { 121 | id: 'AwsSolutions-EC29', 122 | reason: 123 | "This instance is to use for maintenance of this system. It's no problem if it was deleted.", 124 | }, 125 | { 126 | id: 'AwsSolutions-EC28', 127 | reason: 128 | 'This instance is to use for maintenance of this system. Detailed monitoring is not required.', 129 | }, 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecr/README.md: -------------------------------------------------------------------------------- 1 | # Ecr Construct 2 | 3 | ## Purpose 4 | 5 | Create ECR repository. 6 | 7 | ## Required resources 8 | 9 | None 10 | 11 | ## Required parameters (props) 12 | 13 | None 14 | 15 | ## Properties 16 | 17 | | Name | Type | Description | 18 | | ------------------- | :-------------: | ----------: | 19 | | containerRepository | ecr.Repository; | | 20 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecr/ecr.ts: -------------------------------------------------------------------------------- 1 | import { aws_ecr, CfnOutput, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | 4 | export class Ecr extends Construct { 5 | public readonly containerRepository: aws_ecr.Repository; 6 | constructor(scope: Construct, id: string) { 7 | super(scope, id); 8 | 9 | this.containerRepository = new aws_ecr.Repository(this, `${id}Repository`, { 10 | imageScanOnPush: true, 11 | encryption: aws_ecr.RepositoryEncryption.KMS, 12 | removalPolicy: RemovalPolicy.DESTROY, 13 | }); 14 | 15 | new CfnOutput(this, 'RepositoryName', { 16 | exportName: `${id}ContainerRepositoryName`, 17 | value: this.containerRepository.repositoryName, 18 | }); 19 | new CfnOutput(this, 'RepositoryUri', { 20 | exportName: `${id}ContainerRepositoryUri`, 21 | value: this.containerRepository.repositoryUri, 22 | }); 23 | new CfnOutput(this, 'EcrRegion', { 24 | exportName: `${id}Region`, 25 | value: this.containerRepository.env.region, 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecs/README_BASE.md: -------------------------------------------------------------------------------- 1 | # EcsAppBase Construct 2 | 3 | ## Purpose 4 | 5 | Create ECS Cluster on Fargate and ALB to access ECS Services on its cluster. 6 | (Optional) Create Private Link to access to ALB. 7 | 8 | ## Required resources 9 | 10 | - VPC that includes private isolated subnet 11 | 12 | ## Required parameters (props) 13 | 14 | - `enabledPrivateLink`: Whether using private link or not 15 | - `vpc`: Define the vpc including isolated subnets 16 | 17 | ## Optional parameters (props) 18 | 19 | - `privateLinkVpc`: It's required when `enabledPrivateLink` is true. 20 | - `testVpcCidr`: CIDR of VPC that Test stack's instance(Windows Server) works on 21 | 22 | ## Properties 23 | 24 | | Name | Type | Description | 25 | | ----------- | :--------------------------------------------: | --------------------------------------------: | 26 | | cluster | ecs.Cluster | Created ECS CLuster on Fargate | 27 | | targetGroup | elaticloadbalancingv2.ApplicationTargetGroup | It includes services and tasks on ECS/Fargate | 28 | | alb | elasticloadbalancingv2.ApplicationLoadBalancer | front of ECS Services/Tasks | 29 | | nlb | elasticloadbalancingv2.NetworkLoadBalancer | Front of alb to connect via PrivateLink | 30 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecs/README_JOB.md: -------------------------------------------------------------------------------- 1 | # EcsJob Construct 2 | 3 | ## Purpose 4 | 5 | Create Job that uses Task of ECS Cluster on Fargate. 6 | The Job has rerunability, state management, and error notification function. 7 | 8 | ## Required resources 9 | 10 | - VPC that includes private isolated subnet 11 | - VPC Endpoint for ECR, CloudWatch Logs, SNS, and SecretsManager 12 | - ECR Repository 13 | - DynamoDB table 14 | - SNS Topic 15 | 16 | ## Required parameters (props) 17 | 18 | - `auroraSecretEncryptionKeyArn` : Encryption key ARN of aurora secret 19 | - `cluster` : Cluster of ECS on Fargate 20 | - `image` : Container's image 21 | - `table` : Store job invokation status each days 22 | - `topic` : Sending error notification 23 | 24 | ## Optional parameters (props) 25 | 26 | - `taskDefinitionEnvironments` <{[key: string]: string}>: Environments variables for task 27 | - `taskDefinitionSecrets` <{[key: string]: ecs.Secret}>: Secrets environments variables for task 28 | - `taskInput` <{[key: string]: any}>: Environments variables for statemachine of stepfunctions 29 | 30 | ## Properties 31 | 32 | | Name | Type | Description | 33 | | ------------ | :----------------------------------------------: | -------------------------------------------------------------------------------------------------: | 34 | | statemachine | stepfunctions.StateMachine | | 35 | | job | stepfunctions_tasks.Step FunctionsStartExecution | A Step Functions Task to call StartExecution on child state machine. | 36 | | task | aws_stepfunctions_tasks.EcsRunTask | It's just task of stepfunctions. This task is defined as ECS's task that called from stepfunctions | 37 | | taskRole | aws_iam.Role | The role is to execute AWS APIs from task. | 38 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecs/README_SERVICE.md: -------------------------------------------------------------------------------- 1 | # EcsAppService Construct 2 | 3 | ## Purpose 4 | 5 | Create Service, TaskDefinition, and Container for ECS on Fargate. 6 | And register the service to TargetGroup of ALB to connect from it. 7 | 8 | ## Required resources 9 | 10 | - VPC that includes private isolated subnet 11 | - VPC Endpoint for ECR, CloudWatch Logs, and SecretsManager 12 | - RDS(Aurora or others) 13 | - ECR Repository 14 | 15 | ## Required parameters (props) 16 | 17 | - `auroraSecretName`: Aurora's secrets name(key) 18 | - `auroraSecurityGroupId`: To define TaskDefinition's SecurityGroup 19 | - `auroraSecretEncryptionKeyArn`: Encryption key ARN of aurora secret 20 | - `cluster`: Service and Tasks works on this cluster 21 | - `repository`: Container Registory to get container image 22 | - `targetGroup`: Destination to register ECS services 23 | 24 | ## Properties 25 | 26 | | Name | Type | Description | 27 | | ------------ | :---------------------: | ----------: | 28 | | ecsService | ecs.FargateService | | 29 | | ecsContainer | ecs.ContainerDefinition | | 30 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecs/ecs-app-base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_ec2, 3 | aws_ecs, 4 | aws_elasticloadbalancingv2, 5 | aws_elasticloadbalancingv2_targets, 6 | aws_iam, 7 | aws_route53, 8 | aws_route53_targets, 9 | CfnOutput, 10 | RemovalPolicy, 11 | Stack, 12 | } from 'aws-cdk-lib'; 13 | import { Bucket } from '../s3/bucket'; 14 | import { Construct } from 'constructs'; 15 | 16 | export class EcsAppBase extends Construct { 17 | public readonly cluster: aws_ecs.Cluster; 18 | public readonly targetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup; 19 | public readonly httpsTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup; 20 | public readonly alb: aws_elasticloadbalancingv2.ApplicationLoadBalancer; 21 | public readonly nlb: aws_elasticloadbalancingv2.NetworkLoadBalancer; 22 | constructor( 23 | scope: Construct, 24 | id: string, 25 | props: { 26 | vpc: aws_ec2.IVpc; 27 | privateLinkVpc?: aws_ec2.IVpc; 28 | domainName: string; 29 | certificateArn: string; 30 | } 31 | ) { 32 | super(scope, id); 33 | 34 | // VPC Endpoint 35 | props.vpc.addInterfaceEndpoint('EcrEndpoint', { 36 | service: aws_ec2.InterfaceVpcEndpointAwsService.ECR, 37 | privateDnsEnabled: true, 38 | }); 39 | 40 | props.vpc.addInterfaceEndpoint('EcrDockerEndpoint', { 41 | service: aws_ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, 42 | privateDnsEnabled: true, 43 | }); 44 | 45 | props.vpc.addInterfaceEndpoint('LogVpcEndpoint', { 46 | service: aws_ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS, 47 | privateDnsEnabled: true, 48 | }); 49 | 50 | props.vpc.addGatewayEndpoint('S3Endpoint', { 51 | service: aws_ec2.GatewayVpcEndpointAwsService.S3, 52 | subnets: [ 53 | { 54 | subnets: props.vpc.isolatedSubnets, 55 | }, 56 | ], 57 | }); 58 | 59 | // ECS Cluster 60 | this.cluster = new aws_ecs.Cluster(this, 'Cluster', { 61 | clusterName: `${id.split('-').slice(-1)[0]}Cluster`, 62 | vpc: props.vpc, 63 | containerInsights: true, 64 | enableFargateCapacityProviders: true, 65 | }); 66 | this.cluster.applyRemovalPolicy(RemovalPolicy.DESTROY); 67 | 68 | // Security Group for ALB 69 | const sgForAlb = new aws_ec2.SecurityGroup(this, 'AlbSecurityGroup', { 70 | vpc: props.vpc, 71 | allowAllOutbound: true, 72 | }); 73 | 74 | // ALB 75 | this.alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(this, 'Alb', { 76 | vpc: props.vpc, 77 | internetFacing: false, 78 | securityGroup: sgForAlb, 79 | vpcSubnets: props.vpc.selectSubnets({ 80 | subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED, 81 | }), 82 | dropInvalidHeaderFields: true, 83 | }); 84 | const albLogBucket = new Bucket(this, 'AlbLogBucket'); 85 | 86 | this.alb.logAccessLogs(albLogBucket.bucket, `${id}WebappAlbLog`); 87 | 88 | this.cluster.connections.allowFrom(this.alb, aws_ec2.Port.tcp(80)); 89 | 90 | const httpsListener = this.alb.addListener('WebappHttpsListener', { 91 | port: 443, 92 | protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTPS, 93 | open: false, 94 | sslPolicy: aws_elasticloadbalancingv2.SslPolicy.TLS12, 95 | }); 96 | httpsListener.addCertificates(`${id}Certificate`, [ 97 | aws_elasticloadbalancingv2.ListenerCertificate.fromArn(props.certificateArn), 98 | ]); 99 | 100 | this.httpsTargetGroup = new aws_elasticloadbalancingv2.ApplicationTargetGroup( 101 | this, 102 | 'HttpsTarget', 103 | { 104 | targetType: aws_elasticloadbalancingv2.TargetType.IP, 105 | port: 8080, 106 | vpc: props.vpc, 107 | healthCheck: { path: '/', port: '8080' }, 108 | } 109 | ); 110 | httpsListener.addTargetGroups('HttpsTargetGroup', { 111 | targetGroups: [this.httpsTargetGroup], 112 | }); 113 | 114 | // Enable PrivateLink 115 | let nlbAccessLogBucket; 116 | if (props.privateLinkVpc) { 117 | this.nlb = new aws_elasticloadbalancingv2.NetworkLoadBalancer(this, 'Nlb', { 118 | vpc: props.vpc, 119 | internetFacing: false, 120 | crossZoneEnabled: true, 121 | vpcSubnets: props.vpc.selectSubnets({ 122 | subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED, 123 | }), 124 | }); 125 | 126 | nlbAccessLogBucket = new Bucket(this, 'NlbAccessLogBucket'); 127 | this.nlb.logAccessLogs(nlbAccessLogBucket.bucket); 128 | 129 | const httpsTargetGroup = this.nlb 130 | .addListener('NlbHttpsListener', { 131 | port: 443, 132 | }) 133 | .addTargets('NlbHttpsTargets', { 134 | targets: [new aws_elasticloadbalancingv2_targets.AlbTarget(this.alb, 443)], 135 | port: 443, 136 | }); 137 | 138 | httpsTargetGroup.configureHealthCheck({ 139 | port: '443', 140 | protocol: aws_elasticloadbalancingv2.Protocol.HTTPS, 141 | }); 142 | httpsTargetGroup.node.addDependency(httpsListener); 143 | 144 | const endpointService = new aws_ec2.VpcEndpointService(this, 'PrivateLinkEndpointService', { 145 | vpcEndpointServiceLoadBalancers: [this.nlb], 146 | acceptanceRequired: false, 147 | allowedPrincipals: [ 148 | new aws_iam.ArnPrincipal(`arn:aws:iam::${Stack.of(this).account}:root`), 149 | ], 150 | }); 151 | 152 | this.alb.connections.allowFrom( 153 | aws_ec2.Peer.ipv4(props.vpc.vpcCidrBlock), 154 | aws_ec2.Port.tcp(443), 155 | 'from same vpc like nlb' 156 | ); 157 | 158 | // Create VPC endpoint 159 | const sgForVpcEndpoint = new aws_ec2.SecurityGroup(this, 'VpcEndpointSecurityGroup', { 160 | vpc: props.privateLinkVpc, 161 | }); 162 | 163 | // For Private Link 164 | const privateLink = props.privateLinkVpc.addInterfaceEndpoint('TestVpcEndpoint', { 165 | service: new aws_ec2.InterfaceVpcEndpointService(endpointService.vpcEndpointServiceName), 166 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 167 | securityGroups: [sgForVpcEndpoint], 168 | }); 169 | 170 | // Create Private Hosted Zone for private link domain name 171 | const privateHostedZone = new aws_route53.PrivateHostedZone(this, 'PrivateHostedZone', { 172 | zoneName: props.domainName, 173 | vpc: props.privateLinkVpc, 174 | }); 175 | 176 | new aws_route53.ARecord(this, 'AlbARecord', { 177 | recordName: `app.${props.domainName}`, 178 | target: aws_route53.RecordTarget.fromAlias( 179 | new aws_route53_targets.InterfaceVpcEndpointTarget(privateLink) 180 | ), 181 | zone: privateHostedZone, 182 | }); 183 | 184 | new CfnOutput(this, 'EndpointService', { 185 | exportName: `${id}EndpointService`, 186 | value: endpointService.vpcEndpointServiceName, 187 | }); 188 | } else { 189 | // Create Private Hosted Zone for ALB internal domain name 190 | const privateHostedZone = new aws_route53.PrivateHostedZone(this, 'PrivateHostedZone', { 191 | zoneName: props.domainName, 192 | vpc: props.vpc, 193 | }); 194 | 195 | new aws_route53.ARecord(this, 'AlbARecord', { 196 | recordName: `app.${props.domainName}`, 197 | target: aws_route53.RecordTarget.fromAlias( 198 | new aws_route53_targets.LoadBalancerTarget(this.alb) 199 | ), 200 | zone: privateHostedZone, 201 | }); 202 | } 203 | 204 | new CfnOutput(this, 'ClusterName', { 205 | exportName: `${id}ClusterName`, 206 | value: this.cluster.clusterName, 207 | }); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /infra/lib/constructs/ecs/ecs-app-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_ec2, 3 | aws_ecr, 4 | aws_ecs, 5 | aws_elasticloadbalancingv2, 6 | aws_iam, 7 | aws_logs, 8 | aws_secretsmanager, 9 | } from 'aws-cdk-lib'; 10 | import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; 11 | import { NagSuppressions } from 'cdk-nag'; 12 | import { Construct } from 'constructs'; 13 | import * as path from 'path'; 14 | import { EncryptionKey } from '../kms/key'; 15 | 16 | export class EcsAppService extends Construct { 17 | public readonly ecsService: aws_ecs.FargateService; 18 | public readonly ecsContainer: aws_ecs.ContainerDefinition; 19 | constructor( 20 | scope: Construct, 21 | id: string, 22 | props: { 23 | auroraSecretName: string; 24 | auroraSecurityGroupId: string; 25 | auroraSecretEncryptionKeyArn: string; 26 | cluster: aws_ecs.Cluster; 27 | repository: aws_ecr.IRepository; 28 | targetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup; 29 | httpsTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup; 30 | } 31 | ) { 32 | super(scope, id); 33 | 34 | // Role for ECS Agent 35 | const executionRole = new aws_iam.Role(this, 'EcsTaskExecutionRole', { 36 | assumedBy: new aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 37 | managedPolicies: [ 38 | aws_iam.ManagedPolicy.fromAwsManagedPolicyName( 39 | 'service-role/AmazonECSTaskExecutionRolePolicy' 40 | ), 41 | ], 42 | inlinePolicies: { 43 | PullThroughCache: new aws_iam.PolicyDocument({ 44 | statements: [ 45 | new aws_iam.PolicyStatement({ 46 | actions: ['ecr:BatchImportUpstreamImage', 'ecr:CreateRepository'], 47 | resources: ['*'], 48 | }), 49 | ], 50 | }), 51 | }, 52 | }); 53 | 54 | // Role for Container 55 | const serviceTaskRole = new aws_iam.Role(this, 'EcsServiceTaskRole', { 56 | assumedBy: new aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 57 | }); 58 | 59 | // CloudWatch Logs Group for Container 60 | const logGroupForCluster = new aws_logs.LogGroup(this, 'FargateLogGroup', { 61 | retention: aws_logs.RetentionDays.THREE_MONTHS, 62 | encryptionKey: new EncryptionKey(this, 'AppFargateLogGroupEncryptionKey', { 63 | servicePrincipals: [new aws_iam.ServicePrincipal('logs.amazonaws.com')], 64 | }).encryptionKey, 65 | }); 66 | 67 | // Task definition 68 | const ecsTask = new aws_ecs.FargateTaskDefinition(this, 'EcsTask', { 69 | executionRole: executionRole, 70 | taskRole: serviceTaskRole, 71 | cpu: 1024, 72 | memoryLimitMiB: 2048, 73 | }); 74 | 75 | const auroraSecret = aws_secretsmanager.Secret.fromSecretNameV2( 76 | this, 77 | 'AuroraSecret', 78 | props.auroraSecretName 79 | ); 80 | executionRole.addToPolicy( 81 | new aws_iam.PolicyStatement({ 82 | actions: ['kms:Decrypt'], 83 | resources: [props.auroraSecretEncryptionKeyArn], 84 | }) 85 | ); 86 | 87 | // Add Container 88 | const nginxDockerAsset = new DockerImageAsset(this, 'NginxAsset', { 89 | directory: path.join(__dirname, '../../../docker/nginx'), 90 | }); 91 | 92 | this.ecsContainer = ecsTask.addContainer('EcsApp', { 93 | // Start the ecsTask with a nginx docker image, later CodePipeLine will deploy the actual app. 94 | image: aws_ecs.ContainerImage.fromDockerImageAsset(nginxDockerAsset), 95 | 96 | secrets: { 97 | DB_ENDPOINT: aws_ecs.Secret.fromSecretsManager(auroraSecret, 'host'), 98 | DB_USERNAME: aws_ecs.Secret.fromSecretsManager(auroraSecret, 'username'), 99 | DB_PASSWORD: aws_ecs.Secret.fromSecretsManager(auroraSecret, 'password'), 100 | }, 101 | logging: aws_ecs.LogDriver.awsLogs({ 102 | streamPrefix: `${id}-`, 103 | logGroup: logGroupForCluster, 104 | }), 105 | readonlyRootFilesystem: true, 106 | }); 107 | 108 | this.ecsContainer.addPortMappings({ 109 | containerPort: 8080, 110 | }); 111 | 112 | // Service 113 | const sgForAurora = aws_ec2.SecurityGroup.fromSecurityGroupId( 114 | this, 115 | 'AuroraSecurityGroup', 116 | props.auroraSecurityGroupId 117 | ); 118 | this.ecsService = new aws_ecs.FargateService(this, 'FargateService', { 119 | cluster: props.cluster, 120 | taskDefinition: ecsTask, 121 | desiredCount: 2, 122 | platformVersion: aws_ecs.FargatePlatformVersion.LATEST, 123 | capacityProviderStrategies: [ 124 | { 125 | capacityProvider: 'FARGATE', 126 | weight: 1, 127 | }, 128 | ], 129 | vpcSubnets: props.cluster.vpc.selectSubnets({ 130 | subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED, 131 | }), 132 | deploymentController: { 133 | type: aws_ecs.DeploymentControllerType.ECS, 134 | }, 135 | }); 136 | this.ecsService.connections.allowTo(sgForAurora, aws_ec2.Port.tcp(5432)); 137 | 138 | // props.targetGroup.addTarget(this.ecsService); 139 | props.httpsTargetGroup.addTarget(this.ecsService); 140 | 141 | // cdk-nag suppressions 142 | NagSuppressions.addResourceSuppressions(executionRole, [ 143 | { 144 | id: 'AwsSolutions-IAM4', 145 | reason: 146 | 'Recommended to use in docs. ref: https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html', 147 | }, 148 | ]); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /infra/lib/constructs/kms/README.md: -------------------------------------------------------------------------------- 1 | # EncryptionKey Construct 2 | 3 | ## Purpose 4 | 5 | Create a AWS KMS key to encrypt Amazon Cloudwatch Logs LogGroup. 6 | 7 | ## Required resources 8 | 9 | None 10 | 11 | ## Required parameters (props) 12 | 13 | None 14 | 15 | ## Optional parameters (props) 16 | 17 | - `servicePrincipals` : To grant access from these principals 18 | 19 | ## Properties 20 | 21 | | Name | Type | Description | 22 | | ------------- | :-----: | --------------------------: | 23 | | encryptionKey | kms.Key | The key to encrypt LogGroup | 24 | -------------------------------------------------------------------------------- /infra/lib/constructs/kms/key.ts: -------------------------------------------------------------------------------- 1 | import * as aws_kms from 'aws-cdk-lib/aws-kms'; 2 | import { ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 3 | import { Construct } from 'constructs'; 4 | 5 | export class EncryptionKey extends Construct { 6 | public readonly encryptionKey: aws_kms.Key; 7 | constructor( 8 | scope: Construct, 9 | id: string, 10 | props?: { 11 | servicePrincipals?: ServicePrincipal[]; 12 | } 13 | ) { 14 | super(scope, id); 15 | 16 | this.encryptionKey = new aws_kms.Key(this, id, { 17 | enableKeyRotation: true, 18 | }); 19 | 20 | if (props && props.servicePrincipals) { 21 | props.servicePrincipals.map((servicePrincipal) => { 22 | this.encryptionKey.grantEncryptDecrypt(servicePrincipal); 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /infra/lib/constructs/network/README.md: -------------------------------------------------------------------------------- 1 | # Network Construct 2 | 3 | ## Purpose 4 | 5 | Creates a VPC enviroment with network logs enabled to Cloudwatch. 6 | 7 | ## Required resources 8 | 9 | None 10 | 11 | ## Required parameters (props) 12 | 13 | - `maxAzs` : Max availability zones 14 | - `cidr` : The CIDR address of the network 15 | - `cidrMask` : The mask for the of available IP addresses in each subnet 16 | 17 | ## Optional parameters (props) 18 | 19 | - `publicSubnet` : Create or not a public subnet 20 | - `isolatedSubnet` : Create or not a isolated subnet 21 | - `natSubnet` : Create or not a nat/private subnet 22 | 23 | ## Properties 24 | 25 | | Name | Type | Description | 26 | | ---- | :------: | ----------: | 27 | | vpc | ec2.IVpc | Exposed VPC | 28 | -------------------------------------------------------------------------------- /infra/lib/constructs/network/network.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { 3 | FlowLogDestination, 4 | FlowLogTrafficType, 5 | SubnetType, 6 | Vpc, 7 | VpcProps, 8 | IpAddresses, 9 | } from 'aws-cdk-lib/aws-ec2'; 10 | import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; 11 | import { EncryptionKey } from '../kms/key'; 12 | import { Construct } from 'constructs'; 13 | import { isEmpty } from 'lodash'; 14 | import { ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 15 | 16 | export class Network extends Construct { 17 | public readonly vpc: Vpc; 18 | 19 | constructor( 20 | scope: Construct, 21 | id: string, 22 | props: { 23 | maxAzs: number; 24 | cidr: string; 25 | cidrMask: number; 26 | publicSubnet?: boolean; 27 | isolatedSubnet?: boolean; 28 | natSubnet?: boolean; 29 | } 30 | ) { 31 | super(scope, id); 32 | 33 | // Vpc logging - 60 days 34 | const cwLogs = new LogGroup(this, `${id}VpcLogs`, { 35 | logGroupName: `/vpc/${id}`, 36 | removalPolicy: RemovalPolicy.DESTROY, 37 | retention: RetentionDays.TWO_MONTHS, 38 | encryptionKey: new EncryptionKey(this, `${id}CWLogsEncryptionKey`, { 39 | servicePrincipals: [new ServicePrincipal('logs.amazonaws.com')], 40 | }).encryptionKey, 41 | }); 42 | 43 | const subnetConfiguration: VpcProps['subnetConfiguration'] = []; 44 | 45 | if (props.publicSubnet) { 46 | subnetConfiguration.push({ 47 | cidrMask: props.cidrMask, 48 | name: `${id.toLowerCase()}-public-subnet`, 49 | subnetType: SubnetType.PUBLIC, 50 | }); 51 | } 52 | 53 | if (props.natSubnet) { 54 | subnetConfiguration.push({ 55 | cidrMask: props.cidrMask, 56 | name: `${id.toLowerCase()}-private-subnet`, 57 | subnetType: SubnetType.PRIVATE_WITH_EGRESS, 58 | }); 59 | } 60 | 61 | if (props.isolatedSubnet) { 62 | subnetConfiguration.push({ 63 | cidrMask: props.cidrMask, 64 | name: `${id.toLowerCase()}-isolated-subnet`, 65 | subnetType: SubnetType.PRIVATE_ISOLATED, 66 | }); 67 | } 68 | 69 | if (isEmpty(subnetConfiguration)) { 70 | throw new Error('No subnet configuration enabled'); 71 | } 72 | 73 | // Create VPC - Private and public subnets 74 | this.vpc = new Vpc(this, `Vpc`, { 75 | ipAddresses: IpAddresses.cidr(props.cidr), 76 | subnetConfiguration, 77 | maxAzs: props.maxAzs, 78 | flowLogs: { 79 | s3: { 80 | destination: FlowLogDestination.toCloudWatchLogs(cwLogs), 81 | trafficType: FlowLogTrafficType.ALL, 82 | }, 83 | }, 84 | }); 85 | 86 | new CfnOutput(this, 'VpcId', { 87 | exportName: `${id}VpcId`, 88 | value: this.vpc.vpcId, 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /infra/lib/constructs/s3/README.md: -------------------------------------------------------------------------------- 1 | # Bucket Construct 2 | 3 | ## Purpose 4 | 5 | Creates two s3 buckets(A bucket and B bucket). 6 | A bucket is to store data or something. 7 | B bucket is to store the logs that something access to A bucket. 8 | 9 | ## Required resources 10 | 11 | None 12 | 13 | ## Required parameters (props) 14 | 15 | None 16 | 17 | ## Optional parameters (props) 18 | 19 | None 20 | 21 | ## Properties 22 | 23 | | Name | Type | Description | 24 | | ------ | :-------: | ----------: | 25 | | bucket | s3.Bucket | | 26 | -------------------------------------------------------------------------------- /infra/lib/constructs/s3/README_webappbucket.md: -------------------------------------------------------------------------------- 1 | # WebAppBucket Construct 2 | 3 | ## Purpose 4 | 5 | Creates two s3 buckets(A bucket and B bucket). 6 | A bucket is to store data or something. 7 | B bucket is to store the logs that something access to A bucket. 8 | 9 | ## Required resources 10 | 11 | None 12 | 13 | ## Required parameters (props) 14 | 15 | - `bucketName` : bucket name of A bucket 16 | 17 | ## Optional parameters (props) 18 | 19 | None 20 | 21 | ## Properties 22 | 23 | | Name | Type | Description | 24 | | ------------ | :-------: | ----------: | 25 | | webAppBucket | s3.Bucket | bucket A | 26 | -------------------------------------------------------------------------------- /infra/lib/constructs/s3/bucket.ts: -------------------------------------------------------------------------------- 1 | import * as aws_s3 from 'aws-cdk-lib/aws-s3'; 2 | import { RemovalPolicy } from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | import { NagSuppressions } from 'cdk-nag'; 5 | 6 | export class Bucket extends Construct { 7 | public readonly bucket: aws_s3.Bucket; 8 | constructor(scope: Construct, id: string) { 9 | super(scope, id); 10 | const accessLogBucket = new aws_s3.Bucket(this, `${id}AccessLogBucket`, { 11 | removalPolicy: RemovalPolicy.DESTROY, 12 | blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL, 13 | encryption: aws_s3.BucketEncryption.S3_MANAGED, 14 | enforceSSL: true, 15 | autoDeleteObjects: true, 16 | objectOwnership: aws_s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, 17 | }); 18 | this.bucket = new aws_s3.Bucket(this, `${id}Bucket`, { 19 | removalPolicy: RemovalPolicy.DESTROY, 20 | blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL, 21 | encryption: aws_s3.BucketEncryption.S3_MANAGED, 22 | enforceSSL: true, 23 | serverAccessLogsBucket: accessLogBucket, 24 | autoDeleteObjects: true, 25 | objectOwnership: aws_s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, 26 | }); 27 | 28 | // cdk-nag suppressions 29 | NagSuppressions.addResourceSuppressions(accessLogBucket, [ 30 | { 31 | id: 'AwsSolutions-S1', 32 | reason: 33 | "This bucket is for access logs of the bucket. So it doesn't need more access log bucket.", 34 | }, 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /infra/lib/constructs/s3/webappbucket.ts: -------------------------------------------------------------------------------- 1 | import { aws_s3, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { NagSuppressions } from 'cdk-nag'; 4 | 5 | export class WebAppBucket extends Construct { 6 | public readonly webAppBucket: aws_s3.Bucket; 7 | constructor( 8 | scope: Construct, 9 | id: string, 10 | props: { 11 | bucketName: string; 12 | } 13 | ) { 14 | super(scope, id); 15 | const webAppAccessLogBucket = new aws_s3.Bucket(this, `${id}WebAppAccessLogBucket`, { 16 | removalPolicy: RemovalPolicy.DESTROY, 17 | blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL, 18 | encryption: aws_s3.BucketEncryption.S3_MANAGED, 19 | enforceSSL: true, 20 | autoDeleteObjects: true, 21 | objectOwnership: aws_s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, 22 | }); 23 | this.webAppBucket = new aws_s3.Bucket(this, `${id}WepAppBucket`, { 24 | bucketName: props.bucketName, 25 | removalPolicy: RemovalPolicy.DESTROY, 26 | blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL, 27 | encryption: aws_s3.BucketEncryption.S3_MANAGED, 28 | enforceSSL: true, 29 | serverAccessLogsBucket: webAppAccessLogBucket, 30 | autoDeleteObjects: true, 31 | objectOwnership: aws_s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, 32 | }); 33 | // cdk-nag suppressions 34 | NagSuppressions.addResourceSuppressions(webAppAccessLogBucket, [ 35 | { 36 | id: 'AwsSolutions-S1', 37 | reason: 38 | "This bucket is for access logs of the bucket. So it doesn't need more access log bucket.", 39 | }, 40 | ]); 41 | NagSuppressions.addResourceSuppressions(this.webAppBucket, [ 42 | { 43 | id: 'AwsSolutions-S5', 44 | reason: 'This bucket is used for website hosting through internal-ALB.', 45 | }, 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /infra/lib/constructs/serverless/README_apigw.md: -------------------------------------------------------------------------------- 1 | # ApiGw Construct 2 | 3 | ## Purpose 4 | 5 | Creates API Gateway for Serverless Architecture 6 | 7 | ## Required resources 8 | 9 | - VPC including RDS 10 | 11 | ## Required parameters (props) 12 | 13 | - `vpc ` : Define the vpc including RDS 14 | - `auroraSecretName ` : Secret Name including RDS information 15 | - `auroraSecretArn ` : Secret Arn including RDS information 16 | - `auroraSecurityGroupId `: Security Group Id including RDS 17 | - `auroraSecretEncryptionKeyArn ` : KMS Key Arn which encrypt secret including RDS information 18 | - `auroraEdition `: edition of aurora database 19 | - `rdsProxyEndpoint ` : Endpoint url of RDS proxy endpoint 20 | - `rdsProxyArn ` : ARN of RDS proxy endpoint 21 | 22 | ## Optional parameters (props) 23 | 24 | None 25 | 26 | ## Properties 27 | 28 | | Name | Type | Description | 29 | | ------------------------ | :-----------------------------------------------: | ------------------------------------: | 30 | | vpcEndpointSecurityGroup | aws_ec2.SecurityGroup | sg for API Gateway vpc endpoint | 31 | | privateApiVpcEndpoint | aws_ec2.InterfaceVpcEndpoint | API Gateway vpc endpoint | 32 | | privateApi | aws_apigateway.LambdaRestApi | API Gateway | 33 | | sgForLambda | aws_ec2.SecurityGroup | sg for lambda which has to connect DB | 34 | | addResource | (resourceName: string) => aws_apigateway.Resource | function for adding resource to API | 35 | -------------------------------------------------------------------------------- /infra/lib/constructs/serverless/README_lambda.md: -------------------------------------------------------------------------------- 1 | # DefaultLambda Construct 2 | 3 | ## Purpose 4 | 5 | Creates Lambda connecting RDS 6 | 7 | ## Required resources 8 | 9 | - VPC including RDS 10 | 11 | ## Required parameters (props) 12 | 13 | - `resourceId ` : Some unique name for Lambda function 14 | - `entry ` : path for function code 15 | - `auroraSecretName ` : Secret Name including RDS information 16 | - `auroraSecretArn ` : Secret Arn including RDS information 17 | - `auroraSecurityGroupId `: Security Group Id including RDS 18 | - `auroraSecretEncryptionKeyArn ` : KMS Key Arn which encrypt secret including RDS information 19 | - `rdsProxyEndpoint ` : Endpoint url of RDS proxy endpoint 20 | - `rdsProxyArn ` : ARN of RDS proxy endpoint 21 | - `sgForLambda ` : Security Group for Lambda 22 | 23 | ## Optional parameters (props) 24 | 25 | None 26 | 27 | ## Properties 28 | 29 | | Name | Type | Description | 30 | | ------ | :------------------------------: | --------------: | 31 | | lambda | aws_lambda_nodejs.NodejsFunction | lambda Function | 32 | -------------------------------------------------------------------------------- /infra/lib/constructs/serverless/README_serverless_app.md: -------------------------------------------------------------------------------- 1 | # ServerlessApp Construct 2 | 3 | ## Purpose 4 | 5 | Create Lambda functions , S3 bucket for static contents and ALB to access services. 6 | 7 | (Optional) Create Private Link to access to ALB. 8 | 9 | ## Required resources 10 | 11 | - VPC that includes private isolated subnet 12 | 13 | ## Required parameters (props) 14 | 15 | - `vpc `: Define the vpc including isolated subnets 16 | - `domainName `: Domain for websites 17 | - `certificateArn `: Certificate Arn for ALB 18 | - `auroraSecretName ` : Secret Name including RDS information 19 | - `auroraSecretArn ` : Secret Arn including RDS information 20 | - `auroraSecurityGroupId `: Security Group Id including RDS 21 | - `auroraSecretEncryptionKeyArn ` : KMS Key Arn which encrypt secret including RDS information 22 | - `auroraEdition `: edition of aurora database 23 | - `rdsProxyEndpoint ` : Endpoint url of RDS proxy endpoint 24 | - `rdsProxyArn ` : ARN of RDS proxy endpoint 25 | 26 | ## Optional parameters (props) 27 | 28 | - `privateLinkVpc`: It's required when `enabledPrivateLink` is true. 29 | 30 | ## Properties 31 | 32 | | Name | Type | Description | 33 | | -------------- | :------------------------------------------------: | -------------------------------------------: | 34 | | alb | aws_elasticloadbalancingv2.ApplicationLoadBalancer | | 35 | | nlb | aws_elasticloadbalancingv2.NetworkLoadBalancer | | 36 | | webappS3bucket | aws_s3.Bucket | s3 bucket for static contents | 37 | | sgForLambda | aws_ec2.SecurityGroup | Security group for Lambda which connects RDS | 38 | -------------------------------------------------------------------------------- /infra/lib/constructs/serverless/apigw.ts: -------------------------------------------------------------------------------- 1 | import { aws_apigateway, aws_ec2, aws_iam, aws_logs } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { NagSuppressions } from 'cdk-nag'; 4 | import { EncryptionKey } from '../kms/key'; 5 | 6 | // private ApiGw with vpc endpoint 7 | export class ApiGw extends Construct { 8 | public readonly vpcEndpointSecurityGroup: aws_ec2.SecurityGroup; 9 | public readonly privateApiVpcEndpoint: aws_ec2.InterfaceVpcEndpoint; 10 | public readonly privateApi: aws_apigateway.LambdaRestApi; 11 | 12 | public addResource: (resourceName: string) => aws_apigateway.Resource; 13 | constructor( 14 | scope: Construct, 15 | id: string, 16 | props: { 17 | vpc: aws_ec2.IVpc; 18 | } 19 | ) { 20 | super(scope, id); 21 | this.vpcEndpointSecurityGroup = new aws_ec2.SecurityGroup(this, 'vpcEndpointSecurityGroup', { 22 | vpc: props.vpc, 23 | allowAllOutbound: true, 24 | }); 25 | 26 | // VPC endpoint 27 | this.privateApiVpcEndpoint = new aws_ec2.InterfaceVpcEndpoint(this, 'privateApiVpcEndpoint', { 28 | vpc: props.vpc, 29 | service: aws_ec2.InterfaceVpcEndpointAwsService.APIGATEWAY, 30 | privateDnsEnabled: true, 31 | subnets: { subnets: props.vpc.isolatedSubnets }, 32 | securityGroups: [this.vpcEndpointSecurityGroup], 33 | open: false, 34 | }); 35 | 36 | // API Gateway LogGroup 37 | const restApiLogAccessLogGroup = new aws_logs.LogGroup(this, 'RestApiLogAccessLogGroup', { 38 | retention: aws_logs.RetentionDays.THREE_MONTHS, 39 | encryptionKey: new EncryptionKey(this, 'RestApiLogAccessLogGroupEncryptionKey', { 40 | servicePrincipals: [new aws_iam.ServicePrincipal('logs.amazonaws.com')], 41 | }).encryptionKey, 42 | }); 43 | 44 | // API Gateway 45 | this.privateApi = new aws_apigateway.RestApi(this, 'privateApi', { 46 | restApiName: `${id}-apigateway`, 47 | deployOptions: { 48 | dataTraceEnabled: true, 49 | loggingLevel: aws_apigateway.MethodLoggingLevel.INFO, 50 | accessLogDestination: new aws_apigateway.LogGroupLogDestination(restApiLogAccessLogGroup), 51 | accessLogFormat: aws_apigateway.AccessLogFormat.clf(), 52 | tracingEnabled: true, 53 | }, 54 | endpointConfiguration: { 55 | types: [aws_apigateway.EndpointType.PRIVATE], 56 | vpcEndpoints: [this.privateApiVpcEndpoint], 57 | }, 58 | policy: new aws_iam.PolicyDocument({ 59 | statements: [ 60 | new aws_iam.PolicyStatement({ 61 | principals: [new aws_iam.AnyPrincipal()], 62 | actions: ['execute-api:Invoke'], 63 | resources: ['execute-api:/*'], 64 | effect: aws_iam.Effect.DENY, 65 | conditions: { 66 | StringNotEquals: { 67 | 'aws:SourceVpce': this.privateApiVpcEndpoint.vpcEndpointId, 68 | }, 69 | }, 70 | }), 71 | new aws_iam.PolicyStatement({ 72 | principals: [new aws_iam.AnyPrincipal()], 73 | actions: ['execute-api:Invoke'], 74 | resources: ['execute-api:/*'], 75 | effect: aws_iam.Effect.ALLOW, 76 | }), 77 | ], 78 | }), 79 | }); 80 | 81 | // Request Validator 82 | this.privateApi.addRequestValidator('validate-request', { 83 | requestValidatorName: 'my-request-validator', 84 | validateRequestBody: true, 85 | validateRequestParameters: true, 86 | }); 87 | this.addResource = (resourceName: string) => { 88 | return this.privateApi.root.addResource(resourceName, { 89 | defaultCorsPreflightOptions: { 90 | allowOrigins: aws_apigateway.Cors.ALL_ORIGINS, 91 | allowMethods: aws_apigateway.Cors.ALL_METHODS, 92 | allowHeaders: aws_apigateway.Cors.DEFAULT_HEADERS, 93 | disableCache: true, 94 | }, 95 | }); 96 | }; 97 | 98 | // NagSuppressions 99 | NagSuppressions.addResourceSuppressions( 100 | this.vpcEndpointSecurityGroup, 101 | [ 102 | { 103 | id: 'AwsSolutions-EC23', 104 | reason: 'This is Sample, so API Gateway is open to everyone.', 105 | }, 106 | ], 107 | true 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /infra/lib/constructs/serverless/lambda.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_lambda_nodejs, 3 | aws_kms, 4 | aws_lambda, 5 | aws_ec2, 6 | aws_iam, 7 | aws_secretsmanager, 8 | Duration, 9 | Fn, 10 | Stack, 11 | } from 'aws-cdk-lib'; 12 | import { Construct } from 'constructs'; 13 | import { NagSuppressions } from 'cdk-nag'; 14 | 15 | export class DefaultLambda extends Construct { 16 | public readonly lambda: aws_lambda_nodejs.NodejsFunction; 17 | 18 | constructor( 19 | scope: Construct, 20 | id: string, 21 | props: { 22 | entry: string; 23 | vpc: aws_ec2.IVpc; 24 | auroraSecretName: string; 25 | auroraSecretArn: string; 26 | auroraSecretEncryptionKeyArn: string; 27 | rdsProxyEndpoint: string; 28 | rdsProxyArn: string; 29 | sgForLambda: aws_ec2.SecurityGroup; 30 | } 31 | ) { 32 | super(scope, id); 33 | 34 | const lambdaFunctionRole = new aws_iam.Role(this, 'lambdaFunctionRole', { 35 | assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'), 36 | path: '/service-role/', 37 | }); 38 | lambdaFunctionRole.addManagedPolicy( 39 | aws_iam.ManagedPolicy.fromManagedPolicyArn( 40 | this, 41 | 'awsLambdaBasicExectionRole', 42 | 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 43 | ) 44 | ); 45 | lambdaFunctionRole.addManagedPolicy( 46 | aws_iam.ManagedPolicy.fromManagedPolicyArn( 47 | this, 48 | 'awsLambdaVpcExectionRole', 49 | 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole' 50 | ) 51 | ); 52 | lambdaFunctionRole.addToPolicy( 53 | new aws_iam.PolicyStatement({ 54 | effect: aws_iam.Effect.ALLOW, 55 | actions: ['secretsmanager:GetSecretValue'], 56 | resources: [props.auroraSecretArn], 57 | }) 58 | ); 59 | lambdaFunctionRole.addToPolicy( 60 | new aws_iam.PolicyStatement({ 61 | effect: aws_iam.Effect.ALLOW, 62 | actions: ['kms:Decrypt'], 63 | resources: [props.auroraSecretEncryptionKeyArn], 64 | }) 65 | ); 66 | const lastOfArn = Fn.select(6, Fn.split(':', props.rdsProxyArn)); 67 | const key = aws_kms.Key.fromKeyArn(this, 'ImportedKey', props.auroraSecretEncryptionKeyArn); 68 | const secret = aws_secretsmanager.Secret.fromSecretAttributes(this, 'ImportedSecret', { 69 | secretCompleteArn: props.auroraSecretArn, 70 | encryptionKey: key, 71 | }); 72 | const user = secret.secretValueFromJson('username').unsafeUnwrap().toString(); 73 | const proxyUser = `arn:aws:rds-db:${Stack.of(this).region}:${ 74 | Stack.of(this).account 75 | }:dbuser:${lastOfArn}/${user}`; 76 | lambdaFunctionRole.addToPolicy( 77 | new aws_iam.PolicyStatement({ 78 | effect: aws_iam.Effect.ALLOW, 79 | actions: ['rds-db:connect'], 80 | resources: [proxyUser], 81 | }) 82 | ); 83 | this.lambda = new aws_lambda_nodejs.NodejsFunction(this, `${id}Lambda`, { 84 | vpc: props.vpc, 85 | vpcSubnets: props.vpc.selectSubnets({ subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }), 86 | securityGroups: [props.sgForLambda], 87 | runtime: aws_lambda.Runtime.NODEJS_20_X, 88 | entry: props.entry, 89 | architecture: aws_lambda.Architecture.ARM_64, 90 | memorySize: 256, 91 | role: lambdaFunctionRole, 92 | timeout: Duration.seconds(600), 93 | environment: { 94 | SECRET_NAME: props.auroraSecretName, 95 | HOST: props.rdsProxyEndpoint, 96 | REGION: Stack.of(this).region, 97 | }, 98 | bundling: { 99 | forceDockerBundling: false, 100 | define: {}, 101 | minify: true, 102 | }, 103 | tracing: aws_lambda.Tracing.ACTIVE, 104 | }); 105 | //Suppressions 106 | NagSuppressions.addResourceSuppressions( 107 | lambdaFunctionRole, 108 | [ 109 | { 110 | id: 'AwsSolutions-IAM4', 111 | reason: 'AWSLambdaBasicExecutionRole managed by SDK.', 112 | }, 113 | ], 114 | true 115 | ); 116 | NagSuppressions.addResourceSuppressions( 117 | lambdaFunctionRole, 118 | [ 119 | { 120 | id: 'AwsSolutions-IAM4', 121 | reason: 'This is Custom Resource managed by AWS', 122 | }, 123 | ], 124 | true 125 | ); 126 | NagSuppressions.addResourceSuppressions( 127 | lambdaFunctionRole, 128 | [ 129 | { 130 | id: 'AwsSolutions-IAM5', 131 | reason: 'This is Custom Resource managed by AWS', 132 | }, 133 | ], 134 | true 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /infra/lib/serverlessapp-stack.ts: -------------------------------------------------------------------------------- 1 | import { aws_codecommit, aws_ec2, StackProps, Stack } from 'aws-cdk-lib'; 2 | import { Bastion } from './constructs/ec2/bastion'; 3 | import { Construct } from 'constructs'; 4 | import { ServerlessApp } from './constructs/serverless/serverless-app'; 5 | import { CodePipelineWebappReact } from './constructs/codepipeline/codepipeline-webapp-react'; 6 | import { Network } from './constructs/network/network'; 7 | import { NagSuppressions } from 'cdk-nag'; 8 | import { DbInitLambda } from './constructs/aurora/dbinitlambda'; 9 | 10 | interface ServerlessappStackProps extends StackProps { 11 | auroraSecretName: string; 12 | auroraSecretArn: string; 13 | auroraSecurityGroupId: string; 14 | auroraSecretEncryptionKeyArn: string; 15 | auroraEdition: string; 16 | rdsProxyEndpoint: string; 17 | rdsProxyArn: string; 18 | containerRepositoryName: string; 19 | enabledPrivateLink: boolean; 20 | testVpcCidr: string; 21 | sourceRepositoryName: string; 22 | vpcId: string; 23 | windowsBastion: boolean; 24 | linuxBastion: boolean; 25 | domainName: string; 26 | certificateArn: string; 27 | } 28 | 29 | export class ServerlessappStack extends Stack { 30 | constructor(scope: Construct, id: string, props: ServerlessappStackProps) { 31 | super(scope, id, props); 32 | 33 | // Import vpc 34 | const vpc = aws_ec2.Vpc.fromLookup(this, 'ServerlessAppVpc', { 35 | isDefault: false, 36 | vpcId: props.vpcId, 37 | }); 38 | 39 | // Import repository 40 | const webappSourceRepository = aws_codecommit.Repository.fromRepositoryName( 41 | this, 42 | 'WebappReactSourceRepository', 43 | props.sourceRepositoryName 44 | ); 45 | 46 | // Security Group for Lambda 47 | const sgForAurora = aws_ec2.SecurityGroup.fromSecurityGroupId( 48 | this, 49 | 'AuroraSecurityGroup', 50 | props.auroraSecurityGroupId 51 | ); 52 | const sgForLambda = new aws_ec2.SecurityGroup(this, 'ApiGwSecurityGroup', { 53 | vpc: vpc, 54 | allowAllOutbound: true, 55 | }); 56 | if (props.auroraEdition == 'mysql') { 57 | sgForAurora.addIngressRule(sgForLambda, aws_ec2.Port.tcp(3306)); 58 | } else { 59 | sgForAurora.addIngressRule(sgForLambda, aws_ec2.Port.tcp(5432)); 60 | } 61 | 62 | let serverlessApp; 63 | if (props.enabledPrivateLink) { 64 | const privateLinkVpc = new Network(this, `PrivateLinkNetwork`, { 65 | cidr: '10.0.0.0/16', 66 | cidrMask: 24, 67 | publicSubnet: false, 68 | isolatedSubnet: true, 69 | maxAzs: 2, 70 | }); 71 | serverlessApp = new ServerlessApp(this, `ServerlessApp`, { 72 | vpc: vpc, 73 | privateLinkVpc: privateLinkVpc.vpc, 74 | domainName: props.domainName, 75 | certificateArn: props.certificateArn, 76 | auroraSecretName: props.auroraSecretName, 77 | auroraSecretArn: props.auroraSecretArn, 78 | auroraSecurityGroupId: props.auroraSecurityGroupId, 79 | auroraSecretEncryptionKeyArn: props.auroraSecretEncryptionKeyArn, 80 | rdsProxyEndpoint: props.rdsProxyEndpoint, 81 | rdsProxyArn: props.rdsProxyArn, 82 | sgForLambda: sgForLambda, 83 | }); 84 | } else { 85 | serverlessApp = new ServerlessApp(this, `ServerlessApp`, { 86 | vpc: vpc, 87 | domainName: props.domainName, 88 | certificateArn: props.certificateArn, 89 | auroraSecretName: props.auroraSecretName, 90 | auroraSecretArn: props.auroraSecretArn, 91 | auroraSecurityGroupId: props.auroraSecurityGroupId, 92 | auroraSecretEncryptionKeyArn: props.auroraSecretEncryptionKeyArn, 93 | rdsProxyEndpoint: props.rdsProxyEndpoint, 94 | rdsProxyArn: props.rdsProxyArn, 95 | sgForLambda: sgForLambda, 96 | }); 97 | } 98 | 99 | // Create Deploy Pipeline 100 | new CodePipelineWebappReact(this, `WebappReactCodePipeline`, { 101 | codeCommitRepository: webappSourceRepository, 102 | s3bucket: serverlessApp.webappS3bucket, 103 | }); 104 | 105 | if (props.windowsBastion || props.linuxBastion) { 106 | const bastionSecurityGroup = new aws_ec2.SecurityGroup(this, 'BastionSecurityGroup', { 107 | vpc, 108 | }); 109 | vpc.addInterfaceEndpoint('BastionSsmVpcEndpoint', { 110 | service: aws_ec2.InterfaceVpcEndpointAwsService.SSM, 111 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 112 | securityGroups: [bastionSecurityGroup], 113 | }); 114 | 115 | vpc.addInterfaceEndpoint('BastionSsmMessagesVpcEndpoint', { 116 | service: aws_ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES, 117 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 118 | securityGroups: [bastionSecurityGroup], 119 | }); 120 | 121 | vpc.addInterfaceEndpoint('BastionEc2MessagesVpcEndpoint', { 122 | service: aws_ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES, 123 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 124 | securityGroups: [bastionSecurityGroup], 125 | }); 126 | // S3's vpc endpoint for yum repo has already been attached in serverless-app-base construct 127 | 128 | if (props.windowsBastion) { 129 | const windowsBastion = new Bastion(this, `Windows`, { 130 | os: 'Windows', 131 | vpc: vpc, 132 | region: this.region, 133 | auroraSecurityGroupId: props.auroraSecurityGroupId, 134 | }); 135 | bastionSecurityGroup.addIngressRule( 136 | aws_ec2.Peer.ipv4(`${windowsBastion.bastionInstance.instancePrivateIp}/32`), 137 | aws_ec2.Port.tcp(443) 138 | ); 139 | 140 | serverlessApp.alb.connections.allowFrom( 141 | windowsBastion.bastionInstance, 142 | aws_ec2.Port.tcp(443) 143 | ); 144 | } 145 | 146 | if (props.linuxBastion) { 147 | const linuxBastion = new Bastion(this, `Linux`, { 148 | os: 'Linux', 149 | vpc: vpc, 150 | region: this.region, 151 | auroraSecurityGroupId: props.auroraSecurityGroupId, 152 | }); 153 | bastionSecurityGroup.addIngressRule( 154 | aws_ec2.Peer.ipv4(`${linuxBastion.bastionInstance.instancePrivateIp}/32`), 155 | aws_ec2.Port.tcp(443) 156 | ); 157 | 158 | serverlessApp.alb.connections.allowFrom( 159 | linuxBastion.bastionInstance, 160 | aws_ec2.Port.tcp(443) 161 | ); 162 | } 163 | } 164 | 165 | new DbInitLambda(this, 'DBInitLambdaConstruct', { 166 | vpc: vpc, 167 | sgForLambda: sgForLambda, 168 | auroraSecretName: props.auroraSecretName, 169 | auroraSecretArn: props.auroraSecretArn, 170 | auroraSecretEncryptionKeyArn: props.auroraSecretEncryptionKeyArn, 171 | rdsProxyEndpoint: props.rdsProxyEndpoint, 172 | rdsProxyArn: props.rdsProxyArn, 173 | }); 174 | 175 | // [CHANGE HERE] Nag suppressions with path : you need to change here for deployment... 176 | NagSuppressions.addResourceSuppressionsByPath( 177 | this, 178 | `/${id}/AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/Resource`, 179 | [ 180 | { 181 | id: 'AwsSolutions-IAM4', 182 | reason: 'CDK managed resource', 183 | appliesTo: [ 184 | 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', 185 | ], 186 | }, 187 | ] 188 | ); 189 | NagSuppressions.addResourceSuppressionsByPath( 190 | this, 191 | `/${id}/AWS679f53fac002430cb0da5b7982bd2287/Resource`, 192 | [ 193 | { 194 | id: 'AwsSolutions-L1', 195 | reason: 'CDK managed resource', 196 | }, 197 | ] 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /infra/lib/webapp-stack.ts: -------------------------------------------------------------------------------- 1 | import { aws_codecommit, aws_ec2, aws_ecr, StackProps, Stack } from 'aws-cdk-lib'; 2 | import { Bastion } from './constructs/ec2/bastion'; 3 | import { Construct } from 'constructs'; 4 | import { EcsAppBase } from './constructs/ecs/ecs-app-base'; 5 | import { EcsAppService } from './constructs/ecs/ecs-app-service'; 6 | import { CodePipelineWebappJava } from './constructs/codepipeline/codepipeline-webapp-java'; 7 | import { Network } from './constructs/network/network'; 8 | 9 | interface WebappStackProps extends StackProps { 10 | auroraSecretName: string; 11 | auroraSecurityGroupId: string; 12 | auroraSecretEncryptionKeyArn: string; 13 | containerRepositoryName: string; 14 | enabledPrivateLink: boolean; 15 | testVpcCidr: string; 16 | sourceRepositoryName: string; 17 | vpcId: string; 18 | windowsBastion: boolean; 19 | linuxBastion: boolean; 20 | domainName: string; 21 | certificateArn: string; 22 | } 23 | 24 | export class WebappStack extends Stack { 25 | constructor(scope: Construct, id: string, props: WebappStackProps) { 26 | super(scope, id, props); 27 | 28 | // Import vpc 29 | const vpc = aws_ec2.Vpc.fromLookup(this, 'WebappBaseVpc', { 30 | isDefault: false, 31 | vpcId: props.vpcId, 32 | }); 33 | 34 | // Import repository 35 | const webappSourceRepository = aws_codecommit.Repository.fromRepositoryName( 36 | this, 37 | 'WebappSourceRepository', 38 | props.sourceRepositoryName 39 | ); 40 | const webappContainerRepository = aws_ecr.Repository.fromRepositoryName( 41 | this, 42 | 'WebappContainerRepository', 43 | props.containerRepositoryName 44 | ); 45 | 46 | let ecsBase; 47 | if (props.enabledPrivateLink) { 48 | const privateLinkVpc = new Network(this, `PrivateLinkNetwork`, { 49 | cidr: '10.0.0.0/16', 50 | cidrMask: 24, 51 | publicSubnet: false, 52 | isolatedSubnet: true, 53 | maxAzs: 2, 54 | }); 55 | // Create ECS 56 | ecsBase = new EcsAppBase(this, `WebappBase`, { 57 | vpc: vpc, 58 | privateLinkVpc: privateLinkVpc.vpc, 59 | domainName: props.domainName, 60 | certificateArn: props.certificateArn, 61 | }); 62 | } else { 63 | // Create ECS 64 | ecsBase = new EcsAppBase(this, `WebappBase`, { 65 | vpc: vpc, 66 | domainName: props.domainName, 67 | certificateArn: props.certificateArn, 68 | }); 69 | } 70 | 71 | const ecsAppService = new EcsAppService(this, `WebappService`, { 72 | auroraSecretName: props.auroraSecretName, 73 | auroraSecurityGroupId: props.auroraSecurityGroupId, 74 | auroraSecretEncryptionKeyArn: props.auroraSecretEncryptionKeyArn, 75 | cluster: ecsBase.cluster, 76 | repository: webappContainerRepository, 77 | targetGroup: ecsBase.targetGroup, 78 | httpsTargetGroup: ecsBase.httpsTargetGroup, 79 | }); 80 | 81 | // Create Deploy Pipeline 82 | new CodePipelineWebappJava(this, `WebappCodePipeline`, { 83 | codeCommitRepository: webappSourceRepository, 84 | ecrRepository: webappContainerRepository, 85 | ecsService: ecsAppService.ecsService, 86 | containerName: ecsAppService.ecsContainer.containerName, 87 | }); 88 | 89 | if (props.windowsBastion || props.linuxBastion) { 90 | const bastionSecurityGroup = new aws_ec2.SecurityGroup(this, 'BastionSecurityGroup', { 91 | vpc, 92 | }); 93 | vpc.addInterfaceEndpoint('BastionSsmVpcEndpoint', { 94 | service: aws_ec2.InterfaceVpcEndpointAwsService.SSM, 95 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 96 | securityGroups: [bastionSecurityGroup], 97 | }); 98 | 99 | vpc.addInterfaceEndpoint('BastionSsmMessagesVpcEndpoint', { 100 | service: aws_ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES, 101 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 102 | securityGroups: [bastionSecurityGroup], 103 | }); 104 | 105 | vpc.addInterfaceEndpoint('BastionEc2MessagesVpcEndpoint', { 106 | service: aws_ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES, 107 | subnets: { subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED }, 108 | securityGroups: [bastionSecurityGroup], 109 | }); 110 | // S3's vpc endpoint for yum repo has already been attached in ecs-app-base construct 111 | 112 | if (props.windowsBastion) { 113 | const windowsBastion = new Bastion(this, `Windows`, { 114 | os: 'Windows', 115 | vpc: vpc, 116 | region: this.region, 117 | auroraSecurityGroupId: props.auroraSecurityGroupId, 118 | }); 119 | bastionSecurityGroup.addIngressRule( 120 | aws_ec2.Peer.ipv4(`${windowsBastion.bastionInstance.instancePrivateIp}/32`), 121 | aws_ec2.Port.tcp(443) 122 | ); 123 | 124 | ecsBase.alb.connections.allowFrom(windowsBastion.bastionInstance, aws_ec2.Port.tcp(443)); 125 | ecsAppService.ecsService.connections.allowFrom( 126 | windowsBastion.bastionInstance, 127 | aws_ec2.Port.tcp(8443) 128 | ); 129 | } 130 | 131 | if (props.linuxBastion) { 132 | const linuxBastion = new Bastion(this, `Linux`, { 133 | os: 'Linux', 134 | vpc: vpc, 135 | region: this.region, 136 | auroraSecurityGroupId: props.auroraSecurityGroupId, 137 | }); 138 | bastionSecurityGroup.addIngressRule( 139 | aws_ec2.Peer.ipv4(`${linuxBastion.bastionInstance.instancePrivateIp}/32`), 140 | aws_ec2.Port.tcp(443) 141 | ); 142 | 143 | ecsBase.alb.connections.allowFrom(linuxBastion.bastionInstance, aws_ec2.Port.tcp(443)); 144 | ecsAppService.ecsService.connections.allowFrom( 145 | linuxBastion.bastionInstance, 146 | aws_ec2.Port.tcp(8443) 147 | ); 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infra", 3 | "version": "0.1.0", 4 | "bin": { 5 | "infra": "bin/infra.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "retire": "retire", 12 | "cdk": "cdk", 13 | "gulp": "gulp --color", 14 | "diff": "gulp diff", 15 | "diff-serverless": "gulp diffServerless", 16 | "synth": "gulp synth", 17 | "synth-serverless": "gulp synthServerless", 18 | "synth-base": "gulp synthBase", 19 | "synth-webapp": "gulp synthWebapp", 20 | "synth-serverless-webapp": "gulp synthServerlessWebapp", 21 | "synth-batch": "gulp synthBatch", 22 | "list": "gulp list", 23 | "list-serverless": "gulp listServerless", 24 | "deploy": "gulp deploy", 25 | "deploy-serverless": "gulp deployServerless", 26 | "deploy-base": "gulp deployBase", 27 | "deploy-webapp": "gulp deployWebapp", 28 | "deploy-serverless-webapp": "gulp deployServerlessWebapp", 29 | "deploy-batch": "gulp deployBatch", 30 | "destroy": "gulp destroy", 31 | "destroy-serverless": "gulp destroyServerless", 32 | "destroy-base": "gulp destroyBase", 33 | "destroy-webapp": "gulp destroyWebapp", 34 | "destroy-serverless-webapp": "gulp destroyServerlessWebapp", 35 | "destroy-batch": "gulp destroyBatch", 36 | "create-certificate": "gulp createCertificate" 37 | }, 38 | "devDependencies": { 39 | "@swc/core": "^1.3.25", 40 | "@swc/wasm": "^1.3.25", 41 | "@types/jest": "^27.5.0", 42 | "@types/lodash": "^4.14.182", 43 | "@types/node": "^10.17.60", 44 | "@types/prettier": "2.6.0", 45 | "@typescript-eslint/eslint-plugin": "^5.27.0", 46 | "@typescript-eslint/parser": "^5.27.0", 47 | "aws-sdk": "^2.1354.0", 48 | "bufferutil": "^4.0.7", 49 | "canvas": "^2.11.0", 50 | "encoding": "^0.1.13", 51 | "esbuild": "^0.14.42", 52 | "eslint": "^8.17.0", 53 | "eslint-config-prettier": "^8.5.0", 54 | "jest": "^29.3.1", 55 | "license-checker": "^25.0.1", 56 | "node-notifier": "^10.0.1", 57 | "prettier": "^2.6.2", 58 | "ts-jest": "^29.0.3", 59 | "ts-node": "^10.7.0", 60 | "typescript": "^4.9.4", 61 | "utf-8-validate": "^5.0.10" 62 | }, 63 | "dependencies": { 64 | "@types/aws-lambda": "^8.10.98", 65 | "aws-cdk": "^2.114.1", 66 | "aws-cdk-lib": "^2.114.1", 67 | "cdk-nag": "^2.21.17", 68 | "chalk": "^4.1.2", 69 | "constructs": "^10.0.0", 70 | "dotenv": "^16.0.2", 71 | "esm": "^3.2.25", 72 | "gulp": "^4.0.2", 73 | "loadash": "^1.0.0", 74 | "lodash": "^4.17.21", 75 | "source-map-support": "^0.5.21" 76 | }, 77 | "overrides": { 78 | "glob-parent": "^6.0.2", 79 | "snapdragon": { 80 | "debug": "^3.1.0" 81 | }, 82 | "expand-brackets": { 83 | "debug": "^3.1.0" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /infra/ssl/openssl_sign_inca.cnf: -------------------------------------------------------------------------------- 1 | [ v3_ca ] 2 | basicConstraints = CA:true, pathlen:0 3 | keyUsage = cRLSign, keyCertSign 4 | nsCertType = sslCA, emailCA 5 | -------------------------------------------------------------------------------- /infra/stages.js: -------------------------------------------------------------------------------- 1 | const appName = 'templateapp'; 2 | 3 | exports.stages = { 4 | default: { 5 | appName, 6 | awsProfile: 'defaultProfile', 7 | alias: 'default', 8 | deployEnv: 'dev', 9 | notifyEmail: 'default-mail@default-mail.com', 10 | enabledPrivateLink: false, 11 | windowsBastion: true, 12 | linuxBastion: true, 13 | domainName: 'app.templateapp.local', 14 | }, 15 | johndoe: { 16 | appName, 17 | awsProfile: 'myProfile', 18 | alias: 'johndoe', 19 | deployEnv: 'dev', 20 | notifyEmail: 'johndoe@xxxx.com', 21 | enabledPrivateLink: false, 22 | windowsBastion: true, 23 | linuxBastion: true, 24 | domainName: 'templateapp.local', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-for-closed-network-system-workloads-on-aws", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /webapp-java/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /webapp-java/Dockerfile: -------------------------------------------------------------------------------- 1 | # For build 2 | FROM amazoncorretto:17-alpine AS build 3 | RUN apk update && apk add curl 4 | 5 | ENV HOME=/home/app 6 | RUN mkdir -p $HOME 7 | WORKDIR $HOME 8 | ADD . $HOME 9 | RUN curl https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -o $HOME/root.pem 10 | RUN ./gradlew build 11 | 12 | # For app 13 | FROM amazoncorretto:17-alpine 14 | 15 | VOLUME /home/spring 16 | VOLUME /tmp 17 | 18 | RUN addgroup -S spring && adduser -S spring -G spring 19 | USER spring:spring 20 | 21 | COPY --from=build /home/app/build/libs/webapp-java-0.0.1-SNAPSHOT.jar sampleapp.jar 22 | COPY --from=build /home/app/root.pem /tmp/root.pem 23 | CMD ["java","-jar","/sampleapp.jar"] 24 | -------------------------------------------------------------------------------- /webapp-java/README.md: -------------------------------------------------------------------------------- 1 | # webapp-java 2 | 3 | [日本語で読む](./README_ja.md) 4 | 5 | ## Overview 6 | 7 | This is a sample application in Java to test your deployment. 8 | 9 | You can query the database within the application and you can check the operation of batch processing by selecting records that cause batch processing to fail. 10 | 11 | ## Run on local 12 | 13 | ### Requirements 14 | 15 | - PostgresSQL 16 | 17 | ### Configure environment's variables 18 | 19 | Please define 3 environment variables for the database connection: 20 | 21 | ```sh 22 | export $DB_ENDPOINT=localhost 23 | export $DB_USERNAME= 24 | export $DB_PASSWORD= 25 | ``` 26 | 27 | ### Initialize table and add sample data 28 | 29 | The configuration of `spring.sqll.init.mode=always` is set in `src/resources/application.properties` that will always call `src/main/resources/data.sql` and `src/main/resources/schema.sql` to initialize the database. 30 | 31 | These settings are for the purpose of this sample, please consider separately ways to initialize the database and perform the migration in production enviroments. 32 | 33 | ### Run the application 34 | 35 | You can check the application by running the following command: 36 | 37 | ```sh 38 | ./gradlew bootRun 39 | ``` 40 | 41 | Access in your browser `http://localhost:8080` 42 | 43 | ### Sample screenshots 44 | 45 | The application is running when you can see a list of results from the database: 46 | 47 | ![list page](./docs/images/screenshot.png) 48 | 49 | ## Building 50 | 51 | Executing the following command will generate a jar file in `build/libs`. 52 | 53 | ```sh 54 | ./gradlew build 55 | ``` 56 | -------------------------------------------------------------------------------- /webapp-java/README_ja.md: -------------------------------------------------------------------------------- 1 | # webapp-java 2 | 3 | [View this page in English](./README.md) 4 | 5 | ## 概要 6 | 7 | 本アプリケーションは、infra 上で Java アプリケーションの動作確認を行うためのサンプルアプリケーションになります。 8 | また、アプリケーション内で DB を操作することができ、バッチ処理で失敗させるレコードを選択することで、バッチ処理の動作確認を行うことができます。 9 | 10 | ## ローカルでの動作確認 11 | 12 | ### ローカルに DB を構築する 13 | 14 | 実行させたい環境に、PostgreSQL をインストール、もしくは Docker にて起動させてください。 15 | その際に、DB にアクセスするユーザ名とパスワードは控えておいてください。 16 | 17 | ### 環境変数の設定 18 | 19 | 3 つの環境変数を定義してください。DB の構築時に控えておいた、ユーザ名とパスワードを設定してください。 20 | 21 | ```sh 22 | $ export $DB_ENDPOINT=localhost 23 | $ export $DB_USERNAME={構築時に設定したユーザ名} 24 | $ export $DB_PASSWORD={構築時に設定したパスワード} 25 | ``` 26 | 27 | ### テーブルの初期化とサンプルデータの追加 28 | 29 | `src/resources/application.properties` に設定されている、`spring.sql.init.mode=always`によって、起動時に必ず`src/main/resources/data.sql`と`src/main/resources/schema.sql`が呼び出され、DB が初期化されます。 30 | 本アプリケーションはサンプルのため、このような設定をしていますが、本番利用の際には DB の初期化やマイグレーションの実施方法について別途ご検討ください。 31 | 32 | ### 実行 33 | 34 | 次のコマンドを実行すると、localhost:8080 にアクセスして、アプリケーションの動作が確認できます。 35 | 36 | ```sh 37 | ./gradlew bootRun 38 | ``` 39 | 40 | ### スクリーンショット 41 | 42 | 次の画像のような画面が表示されたら起動しています。 43 | 44 | ![参照画面](./docs/images/screenshot.png) 45 | 46 | ## ビルド 47 | 48 | 次のコマンドを実行すると、`build/libs`に jar ファイルが生成されます。 49 | 50 | ```sh 51 | ./gradlew build 52 | ``` 53 | -------------------------------------------------------------------------------- /webapp-java/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '3.0.0-SNAPSHOT' 3 | id 'io.spring.dependency-management' version '1.0.13.RELEASE' 4 | id 'java' 5 | id 'eclipse' 6 | id "org.owasp.dependencycheck" version "7.4.4" 7 | } 8 | 9 | group = 'com.example.webapp' 10 | version = '0.0.1-SNAPSHOT' 11 | sourceCompatibility = '17' 12 | ext['tomcat.version'] = '10.1.4' 13 | 14 | repositories { 15 | mavenCentral() 16 | maven { url 'https://repo.spring.io/milestone' } 17 | maven { url 'https://repo.spring.io/snapshot' } 18 | } 19 | 20 | dependencies { 21 | implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' 22 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 23 | implementation 'org.springframework.boot:spring-boot-starter-web' 24 | implementation 'org.springframework.boot:spring-boot-starter' 25 | implementation 'org.springframework.boot:spring-boot-starter-log4j2' 26 | implementation 'org.springframework.boot:spring-boot-starter-validation' 27 | implementation 'javax.validation:validation-api:2.0.0.Final' 28 | runtimeOnly 'org.postgresql:postgresql' 29 | compileOnly 'org.projectlombok:lombok' 30 | annotationProcessor 'org.projectlombok:lombok' 31 | implementation 'javax.persistence:javax.persistence-api:2.2' 32 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 33 | compileOnly 'org.springframework.boot:spring-boot-starter-tomcat' 34 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 35 | 36 | modules { 37 | module('org.springframework.boot:spring-boot-starter-logging') { 38 | replacedBy 'org.springframework.boot:spring-boot-starter-log4j2' 39 | } 40 | } 41 | } 42 | 43 | tasks.named('test') { 44 | useJUnitPlatform() 45 | } 46 | 47 | dependencyCheck { 48 | autoUpdate = true 49 | analyzedTypes = ['jar', 'war', 'js'] 50 | cveValidForHours = 24 51 | format = 'HTML' 52 | outputDirectory = "$buildDir/owasp-reports" 53 | scanProjects = [] 54 | skipProjects = [] 55 | scanSet = [ 56 | 'src/main/resources', 57 | 'src/main/java', 58 | 'build/libs' 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /webapp-java/buildspec.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | java: corretto17 7 | pre_build: 8 | commands: 9 | - AWS_ACCOUNT_ID=$(echo ${CODEBUILD_BUILD_ARN} | cut -f 5 -d :) 10 | - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com 11 | - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) 12 | - IMAGE_TAG=${COMMIT_HASH:=latest} 13 | build: 14 | commands: 15 | - docker build . -t $REPOSITORY_URI:latest 16 | - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG 17 | post_build: 18 | commands: 19 | - docker push $REPOSITORY_URI:latest 20 | - docker push $REPOSITORY_URI:$IMAGE_TAG 21 | - printf '[{"name":"%s","imageUri":"%s"}]' $ECS_APP_CONTAINER $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json 22 | 23 | artifacts: 24 | files: imagedefinitions.json 25 | -------------------------------------------------------------------------------- /webapp-java/docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/webapp-java/docs/images/screenshot.png -------------------------------------------------------------------------------- /webapp-java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/webapp-java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /webapp-java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /webapp-java/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /webapp-java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /webapp-java/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /webapp-java/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://repo.spring.io/milestone' } 4 | maven { url 'https://repo.spring.io/snapshot' } 5 | gradlePluginPortal() 6 | } 7 | } 8 | rootProject.name = 'webapp-java' 9 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(WebappApplication.class); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/WebappApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class WebappApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(WebappApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/controller/SampleAppController.java: -------------------------------------------------------------------------------- 1 | 2 | package com.example.sampleapp.webapp.controller; 3 | 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.ModelAttribute; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.ui.Model; 10 | import org.springframework.validation.BindingResult; 11 | import org.springframework.validation.annotation.Validated; 12 | import com.example.sampleapp.webapp.domain.dto.SampleAppListDto; 13 | import com.example.sampleapp.webapp.domain.SampleAppService; 14 | 15 | @Controller 16 | public class SampleAppController { 17 | private final SampleAppService service; 18 | public SampleAppService sampleAppService; 19 | 20 | public SampleAppController(SampleAppService service) { 21 | this.service = service; 22 | } 23 | 24 | @RequestMapping(value = { "/sampleapp/list", "/" }) 25 | @GetMapping 26 | public String sampleAppList(Model model) { 27 | 28 | SampleAppListDto sampleAppList = service.listAll(); 29 | model.addAttribute("sampleapplist", sampleAppList); 30 | return "sampleapplist"; 31 | } 32 | 33 | @RequestMapping("/sampleapp/form") 34 | @GetMapping 35 | public String sampleAppForm(Model model) { 36 | 37 | SampleAppListDto sampleAppList = service.listAll(); 38 | model.addAttribute("sampleapplist", sampleAppList); 39 | return "sampleappform"; 40 | } 41 | 42 | @RequestMapping("/sampleapp/form/update") 43 | @PostMapping 44 | public String sampleAppListUpdate(@Validated @ModelAttribute SampleAppListDto sampleappformlist, 45 | BindingResult result, Model model) { 46 | 47 | service.updateAll(sampleappformlist); 48 | model.addAttribute("sampleapplist", service.listAll()); 49 | return "forward:/"; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/controller/form/SampleAppForm.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.controller.form; 2 | 3 | import java.util.Date; 4 | import lombok.Data; 5 | import javax.validation.constraints.NotBlank; 6 | 7 | 8 | @Data 9 | public class SampleAppForm { 10 | @NotBlank 11 | private Integer id; 12 | @NotBlank 13 | private String name; 14 | @NotBlank 15 | private Boolean job0001Flag; 16 | @NotBlank 17 | private Boolean job0002Flag; 18 | @NotBlank 19 | private Boolean job0003Flag; 20 | @NotBlank 21 | private Boolean job0004Flag; 22 | @NotBlank 23 | private Boolean job0005Flag; 24 | } 25 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/controller/form/SampleAppFormList.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.controller.form; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class SampleAppFormList { 8 | private List sampleAppFormList; 9 | } 10 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/domain/SampleAppService.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import com.example.sampleapp.webapp.repository.*; 9 | import com.example.sampleapp.webapp.repository.model.*; 10 | import com.example.sampleapp.webapp.domain.dto.SampleAppDto; 11 | import com.example.sampleapp.webapp.domain.dto.SampleAppListDto; 12 | 13 | @Service 14 | public class SampleAppService { 15 | @Autowired 16 | private SampleAppRepository repository; 17 | 18 | public SampleAppListDto listAll() { 19 | Iterable data = repository.findAll(); 20 | List list = new ArrayList(); 21 | SampleAppListDto ret = new SampleAppListDto(); 22 | 23 | for (SampleApp sampleApp : data) { 24 | SampleAppDto sampleAppDto = new SampleAppDto(); 25 | 26 | sampleAppDto.setId(sampleApp.getId()); 27 | sampleAppDto.setName(sampleApp.getName()); 28 | sampleAppDto.setJob0001Flag(sampleApp.getJob0001Flag()); 29 | sampleAppDto.setJob0002Flag(sampleApp.getJob0002Flag()); 30 | sampleAppDto.setJob0003Flag(sampleApp.getJob0003Flag()); 31 | sampleAppDto.setJob0004Flag(sampleApp.getJob0004Flag()); 32 | sampleAppDto.setJob0005Flag(sampleApp.getJob0005Flag()); 33 | 34 | list.add(sampleAppDto); 35 | } 36 | 37 | ret.setSampleAppList(list); 38 | return ret; 39 | } 40 | 41 | public void updateAll(SampleAppListDto sampleAppListDto) { 42 | List sampleAppList = new ArrayList(); 43 | 44 | for (SampleAppDto sampleAppDto : sampleAppListDto.getSampleAppList()) { 45 | SampleApp sampleApp = repository.findById(sampleAppDto.getId()).get(); 46 | sampleApp.setJob0001Flag(sampleAppDto.getJob0001Flag()); 47 | sampleApp.setJob0002Flag(sampleAppDto.getJob0002Flag()); 48 | sampleApp.setJob0003Flag(sampleAppDto.getJob0003Flag()); 49 | sampleApp.setJob0004Flag(sampleAppDto.getJob0004Flag()); 50 | sampleApp.setJob0005Flag(sampleAppDto.getJob0005Flag()); 51 | sampleAppList.add(sampleApp); 52 | } 53 | repository.saveAll(sampleAppList); 54 | } 55 | 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/domain/dto/SampleAppDto.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.domain.dto; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | import org.springframework.stereotype.Component; 6 | import lombok.Data; 7 | 8 | @Data 9 | @Component 10 | public class SampleAppDto implements Serializable { 11 | private Integer id; 12 | private String name; 13 | private Boolean job0001Flag; 14 | private Boolean job0002Flag; 15 | private Boolean job0003Flag; 16 | private Boolean job0004Flag; 17 | private Boolean job0005Flag; 18 | } 19 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/domain/dto/SampleAppListDto.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.domain.dto; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | import org.springframework.stereotype.Component; 6 | import lombok.Data; 7 | 8 | @Data 9 | @Component 10 | public class SampleAppListDto implements Serializable { 11 | 12 | private List sampleAppList; 13 | } 14 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/repository/SampleAppRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | import com.example.sampleapp.webapp.repository.model.*;; 6 | 7 | @Repository 8 | public interface SampleAppRepository extends CrudRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /webapp-java/src/main/java/com/example/sampleapp/webapp/repository/model/SampleApp.java: -------------------------------------------------------------------------------- 1 | package com.example.sampleapp.webapp.repository.model; 2 | 3 | import java.util.Date; 4 | import javax.persistence.Column; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.relational.core.mapping.Table; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @RequiredArgsConstructor 14 | @Getter 15 | @Setter 16 | @EqualsAndHashCode(of = {"id"}) 17 | @ToString 18 | @Table(name = "sampleapp_table", schema = "public") 19 | public class SampleApp { 20 | @Id 21 | @Column(name = "id") 22 | private Integer id; 23 | @Column(name = "name") 24 | private String name; 25 | @Column(name = "job0001_flag") 26 | private Boolean job0001Flag; 27 | @Column(name = "job0002_flag") 28 | private Boolean job0002Flag; 29 | @Column(name = "job0003_flag") 30 | private Boolean job0003Flag; 31 | @Column(name = "job0004_flag") 32 | private Boolean job0004Flag; 33 | @Column(name = "job0005_flag") 34 | private Boolean job0005Flag; 35 | } 36 | -------------------------------------------------------------------------------- /webapp-java/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DB_ENDPOINT}:5432/postgres?sslmode=verify-full&sslrootcert=/tmp/root.pem 2 | spring.datasource.username=${DB_USERNAME} 3 | spring.datasource.password=${DB_PASSWORD} 4 | spring.sql.init.mode=always 5 | logging.level.org.springframework.web=INFO -------------------------------------------------------------------------------- /webapp-java/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO sampleapp_table(name, job0001_flag, job0002_flag, job0003_flag, job0004_flag, job0005_flag) VALUES ('test record 1',true,true,true,true,true); 2 | -------------------------------------------------------------------------------- /webapp-java/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS sampleapp_table; 2 | CREATE TABLE IF NOT EXISTS sampleapp_table 3 | ( 4 | id serial NOT NULL, 5 | name text COLLATE pg_catalog."default" NOT NULL, 6 | job0001_flag boolean NOT NULL DEFAULT false, 7 | job0002_flag boolean NOT NULL DEFAULT false, 8 | job0003_flag boolean NOT NULL DEFAULT false, 9 | job0004_flag boolean NOT NULL DEFAULT false, 10 | job0005_flag boolean NOT NULL DEFAULT false, 11 | CONSTRAINT sample_app_pkey PRIMARY KEY (id) 12 | ); 13 | -------------------------------------------------------------------------------- /webapp-java/src/main/resources/templates/sampleappform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | サンプルアプリ 5 | 6 | 7 | 8 | 9 | 10 |
15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 55 | 62 | 69 | 76 | 83 | 90 | 91 | 92 |
ID名前ジョブの成否
JOB0001JOB0002JOB0003JOB0004JOB0005
36 | 44 | 46 | 54 | 56 | 61 | 63 | 68 | 70 | 75 | 77 | 82 | 84 | 89 |
93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /webapp-java/src/main/resources/templates/sampleapplist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | サンプルアプリ 5 | 6 | 7 | 8 | 9 | 10 |
11 | 変更する 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
ID名前ジョブの成否
JOB0001JOB0002JOB0003JOB0004JOB0005
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /webapp-react/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_ENDPOINT_URL="https://app.templateapp.local/apigw/" -------------------------------------------------------------------------------- /webapp-react/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | rules: { 14 | '@typescript-eslint/no-loss-of-precision': 'off', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /webapp-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | -------------------------------------------------------------------------------- /webapp-react/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /webapp-react/README.md: -------------------------------------------------------------------------------- 1 | # webapp-react 2 | 3 | ## Overview 4 | 5 | This application is a sample application for checking the operation of React applications on serverless infra. 6 | Also, you can operate the DB within the application, and you can check the operation of batch processing by selecting records that will fail in batch processing. 7 | 8 | ## screenshot 9 | 10 | The following image will be displayed. 11 | 12 | ![Screenshot](./docs/images/screenshot.png) 13 | 14 | ## Run on local 15 | 16 | ### Execute 17 | 18 | You can check what the application looks like by running the following command and going to localhost:3000. (To check data operation, deployment on AWS is required.) 19 | 20 | ```sh 21 | npm start 22 | ``` 23 | 24 | ## build 25 | 26 | When the following command is executed, a file generated into `build` folder. 27 | 28 | ```sh 29 | npm run build 30 | ``` 31 | -------------------------------------------------------------------------------- /webapp-react/README_ja.md: -------------------------------------------------------------------------------- 1 | # webapp-react 2 | 3 | ## 概要 4 | 5 | 本アプリケーションは、サーバーレスな infra 上で React アプリケーションの動作確認を行うためのサンプルアプリケーションになります。 6 | また、アプリケーション内で DB を操作することができ、バッチ処理で失敗させるレコードを選択することで、バッチ処理の動作確認を行うことができます。 7 | 8 | ## スクリーンショット 9 | 10 | 次の画像のような画面が表示されたら起動しています。 11 | 12 | ![参照画面](./docs/images/screenshot.png) 13 | 14 | ## ローカルでの動作確認 15 | 16 | ### 実行 17 | 18 | 次のコマンドを実行すると、localhost:3000 にアクセスして、アプリケーションの外観が確認できます。(データの操作を確認するには、AWS上にデプロイすることが必要となります。) 19 | 20 | ```sh 21 | npm start 22 | ``` 23 | 24 | ## ビルド 25 | 26 | 次のコマンドを実行すると、`build`にビルドしたファイルが生成されます。 27 | 28 | ```sh 29 | npm run build 30 | ``` 31 | -------------------------------------------------------------------------------- /webapp-react/buildspec.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | commands: 5 | - npm ci 6 | build: 7 | commands: 8 | - npm run build 9 | artifacts: 10 | base-directory: build 11 | files: 12 | - '**/*' 13 | -------------------------------------------------------------------------------- /webapp-react/docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/webapp-react/docs/images/screenshot.png -------------------------------------------------------------------------------- /webapp-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@mui/material": "^5.14.6", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.46", 12 | "@types/react": "^18.2.21", 13 | "@types/react-dom": "^18.2.7", 14 | "axios": "^1.5.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.15.0", 18 | "react-scripts": "^5.0.1", 19 | "typescript": "^4.9.5", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "NODE_OPTIONS='--openssl-legacy-provider' react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "lint": "npx eslint '**/*.ts' ./" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 49 | "@types/react-router-hash-link": "^1.0.0", 50 | "@typescript-eslint/eslint-plugin": "^7.13.0", 51 | "@typescript-eslint/parser": "^7.13.0", 52 | "eslint": "^8.57.0", 53 | "eslint-config-prettier": "^9.1.0", 54 | "eslint-plugin-react": "^7.34.2", 55 | "prettier": "^3.3.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /webapp-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/template-for-closed-network-system-workloads-on-aws/ad70db9f29f7c0f7991645ce2f1d8d403260a5f4/webapp-react/public/favicon.ico -------------------------------------------------------------------------------- /webapp-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | React App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /webapp-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Sample React App", 3 | "name": "Sample App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /webapp-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /webapp-react/sample.env: -------------------------------------------------------------------------------- 1 | REACT_APP_ENDPOINT_URL="https://{your-domain}/apigw/" -------------------------------------------------------------------------------- /webapp-react/src/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Record } from '../types/record'; 3 | import { RecordForm } from './RecordForm'; 4 | import { RecordList } from './RecordList'; 5 | import { get } from '../modules/requests'; 6 | import { post } from '../modules/requests'; 7 | import Button from '@mui/material/Button'; 8 | 9 | const resource = 'sample/'; 10 | 11 | export const Dashboard: React.FC = () => { 12 | const [sampleRecords, setSampleRecords] = useState({} as Record); 13 | const [formState, setFormState] = useState(false); 14 | 15 | const setJobFlag = (id: number, jobFlagKey: string, newFlagValue: boolean) => { 16 | setSampleRecords((prevRecord) => 17 | prevRecord.id === id ? { ...prevRecord, [jobFlagKey]: newFlagValue } : prevRecord, 18 | ); 19 | }; 20 | 21 | useEffect(() => { 22 | (async () => { 23 | const res = await get(resource); 24 | setSampleRecords(res.data as Record); 25 | })(); 26 | }, []); 27 | return ( 28 | 29 |

Hello From S3 through CodePipeline !

30 |
31 | {formState ? ( 32 | <> 33 | 42 | 43 | 44 | ) : ( 45 | <> 46 | 54 | 55 | 56 | )} 57 |
58 | ); 59 | }; 60 | 61 | export default Dashboard; 62 | -------------------------------------------------------------------------------- /webapp-react/src/components/RecordForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Record } from '../types/record'; 3 | import { RecordFormRow } from './RecordFormRow'; 4 | //mui 5 | import Table from '@mui/material/Table'; 6 | import TableBody from '@mui/material/TableBody'; 7 | import TableCell from '@mui/material/TableCell'; 8 | import TableHead from '@mui/material/TableHead'; 9 | import TableRow from '@mui/material/TableRow'; 10 | 11 | export const RecordForm: React.FC<{ 12 | record: Record; 13 | setFlagHandler: (id: number, jobFlagKey: string, newFlagValue: boolean) => void; 14 | }> = ({ record, setFlagHandler }) => { 15 | return ( 16 | 17 | 18 | 19 | ID 20 | 名前 21 | ジョブの成否 22 | 23 | 24 | JOB0001 25 | JOB0002 26 | JOB0003 27 | JOB0004 28 | JOB0005 29 | 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | }; 37 | 38 | export default RecordForm; 39 | -------------------------------------------------------------------------------- /webapp-react/src/components/RecordFormRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Record } from '../types/record'; 3 | import TableCell from '@mui/material/TableCell'; 4 | import TableRow from '@mui/material/TableRow'; 5 | import Checkbox from '@mui/material/Checkbox'; 6 | 7 | export const RecordFormRow: React.FC<{ 8 | record: Record; 9 | setFlagHandler: (id: number, jobFlagKey: string, newFlagValue: boolean) => void; 10 | }> = ({ record, setFlagHandler }) => { 11 | return ( 12 | 13 | {record.id} 14 | {record.name} 15 | 16 | setFlagHandler(record.id, 'job0001_flag', !record.job0001_flag)} 19 | /> 20 | 21 | 22 | setFlagHandler(record.id, 'job0002_flag', !record.job0002_flag)} 25 | /> 26 | 27 | 28 | setFlagHandler(record.id, 'job0003_flag', !record.job0003_flag)} 31 | /> 32 | 33 | 34 | setFlagHandler(record.id, 'job0004_flag', !record.job0004_flag)} 37 | /> 38 | 39 | 40 | setFlagHandler(record.id, 'job0005_flag', !record.job0005_flag)} 43 | /> 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /webapp-react/src/components/RecordList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Record } from '../types/record'; 3 | 4 | //mui 5 | import Table from '@mui/material/Table'; 6 | import TableBody from '@mui/material/TableBody'; 7 | import TableCell from '@mui/material/TableCell'; 8 | import TableHead from '@mui/material/TableHead'; 9 | import TableRow from '@mui/material/TableRow'; 10 | 11 | export const RecordList: React.FC<{ record: Record }> = ({ record }) => { 12 | return ( 13 | 14 | 15 | 16 | ID 17 | 名前 18 | ジョブの成否 19 | 20 | 21 | JOB0001 22 | JOB0002 23 | JOB0003 24 | JOB0004 25 | JOB0005 26 | 27 | 28 | 29 | 30 | {record.id} 31 | {record.name} 32 | {record.job0001_flag ? '成功' : '失敗'} 33 | {record.job0002_flag ? '成功' : '失敗'} 34 | {record.job0003_flag ? '成功' : '失敗'} 35 | {record.job0004_flag ? '成功' : '失敗'} 36 | {record.job0005_flag ? '成功' : '失敗'} 37 | 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default RecordList; 44 | -------------------------------------------------------------------------------- /webapp-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 4 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 11 | } 12 | -------------------------------------------------------------------------------- /webapp-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import Dashboard from './components/Dashboard'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | // / 19 | // /#/sampleapp/form 20 | 21 | root.render(); 22 | 23 | // If you want to start measuring performance in your app, pass a function 24 | // to log results (for example: reportWebVitals(console.log)) 25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 26 | reportWebVitals(); 27 | -------------------------------------------------------------------------------- /webapp-react/src/modules/requests.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_ENDPOINT = process.env.REACT_APP_ENDPOINT_URL; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export const get = async (resource: string, params?: { [key: string]: any }) => { 7 | return await axios.get(`${API_ENDPOINT}${resource}`, { params: params }); 8 | }; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export const post = async (resource: string, data: any) => { 12 | return axios.post(`${API_ENDPOINT}${resource}`, data); 13 | }; 14 | -------------------------------------------------------------------------------- /webapp-react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webapp-react/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /webapp-react/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /webapp-react/src/types/record.ts: -------------------------------------------------------------------------------- 1 | export type Record = { 2 | id: number; 3 | name: string; 4 | job0001_flag: boolean; 5 | job0002_flag: boolean; 6 | job0003_flag: boolean; 7 | job0004_flag: boolean; 8 | job0005_flag: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /webapp-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------