├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yaml
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yaml
└── workflows
│ └── ci.yaml
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── assets
└── test.html
├── bin
└── finch-aws-cdk.ts
├── cdk.context.json
├── cdk.json
├── config
├── env-config.json
├── env-config.ts
├── runner-config.json
└── runner-config.ts
├── integration_test
└── cloudfront_service.test.ts
├── jest.config.integration.js
├── jest.config.js
├── jest.config.unit.js
├── lib
├── artifact-bucket-cloudfront.ts
├── asg-runner-stack.ts
├── aspects
│ └── stack-termination-protection.ts
├── cloudfront_cdn.ts
├── codebuild-stack.ts
├── continuous-integration-stack.ts
├── ecr-repo-stack.ts
├── event-bridge-scan-notifs-stack.ts
├── finch-pipeline-app-stage.ts
├── finch-pipeline-stack.ts
├── image-scanning-notifications-lambda-handler
│ └── main.py
├── pvre-reporting-stack.ts
├── pvre-reporting-template.yml
├── ssm-patching-stack.ts
└── utils.ts
├── package-lock.json
├── package.json
├── scripts
├── setup-linux-runner.sh
├── setup-runner.sh
└── windows-runner-user-data.yaml
├── test
├── artifact-bucket-cloudfront.test.ts
├── asg-runner-stack.test.ts
├── aspects
│ └── stack-termination-protection.test.ts
├── cloudfront_cdn.test.ts
├── codebuild-stack.test.ts
├── ecr-repo-stack.test.ts
├── event-bridge-scan-notifs-stack.test.ts
├── finch-pipeline-stack.test.ts
├── ssm-patching-stack.test.ts
└── utils.test.ts
└── tsconfig.json
/.github/ ISSUE_TEMPLATE /bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug to help improve the infrastructure of Finch
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | Briefly describe the probem you are having.
12 |
13 |
14 | **Steps to reproduce**
15 | A clear, step-by-step set of instructions to reproduce the bug.
16 |
17 |
18 | **Expected behavior**
19 | Description of what you expected to happen.
20 |
21 |
22 | **Screenshots or logs**
23 | If applicable, add screenshots or logs to help explain your problem.
24 |
25 |
26 |
27 | **Additional context**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.github/ ISSUE_TEMPLATE /config.yaml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Ask a question
4 | url: https://github.com/runfinch/infrastructure/discussions
5 | about: Use GitHub Discussions to ask questions, discuss options, or propose new ideas
--------------------------------------------------------------------------------
/.github/ ISSUE_TEMPLATE /feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest a new feature to add to the infrastructure of Finch
4 | title: ''
5 | labels: 'feature'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What is the problem you're trying to solve?**
11 | A clear and concise description of the use case for this feature. Please provide an example, if possible.
12 |
13 |
14 | **Describe the feature you'd like**
15 | A clear and concise description of what you'd like to happen.
16 |
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issue #, if available:*
2 |
3 |
4 | *Description of changes:*
5 |
6 |
7 | *Testing done:*
8 |
9 |
10 |
11 | - [ ] I've reviewed the guidance in CONTRIBUTING.md
12 |
13 |
14 | #### License Acceptance
15 |
16 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
17 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | commit-message:
8 | prefix: "build"
9 | include: "scope"
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 | commit-message:
15 | prefix: "ci"
16 | include: "scope"
17 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths-ignore:
7 | - '*.md'
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | unit-tests:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: 16
20 | - run: npm ci
21 | - run: npm run build
22 | - run: npm run test
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.js
2 | !jest.config.js
3 | !jest.config.integration.js
4 | !jest.config.unit.js
5 | *.d.ts
6 | node_modules
7 | dist
8 |
9 | # CDK asset staging directory
10 | .cdk.staging
11 | cdk.out
12 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.ts
2 | !*.d.ts
3 |
4 | # CDK asset staging directory
5 | .cdk.staging
6 | cdk.out
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "none",
4 | "singleQuote": true,
5 | "printWidth": 120
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AWS CDK repo for Finch
2 |
3 | ## Prerequisites
4 |
5 | - Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configured to the right account and user](https://cdkworkshop.com/15-prerequisites/200-account.html)
6 | - Install Node.js (>= 10.13.0, except for versions 13.0.0 - 13.6.0)
7 | - Install AWS CDK Toolkit with `npm install -g aws-cdk`
8 |
9 | ## Deployment Steps
10 |
11 | **_Step 1:_** Clone the infrastructure repo and open a terminal.
12 | **_Step 2:_** Before the deployment, check whether the key pair `runner-key.pem` exists in your AWS EC2 console. If not, set up a ssh key pair `runner-key.pem` for ssh to the ec2 instance. Go to AWS console > EC2, in the left tab “Network & Security” > “Key Pairs”, click “Create key pair” with name runner-key.pem.
13 | Or create the key pair with the AWS CLI command below.
14 | `aws ec2 create-key-pair --key-name runner-key --output text > runner-key.pem`
15 |
16 | **_Step 3:_** For the first time deployment to an environment, run `cdk bootstrap aws://PIPELINE-ACCOUNT-NUMBER/REGION` to bootstrap the pipeline account and `cdk bootstrap --cloudformation-execution-policies 'arn:aws:iam::aws:policy/AdministratorAccess' --trust PIPELINE-ACCOUNT-NUMBER aws://STAGE-ACCOUNT-NUMBER/REGION` to bootstrap the beta/prod accounts. Then run `cdk deploy` with the pipeline account credentials set up to deploy the pipeline stack, and all the application stacks will be deployed by the pipeline for each commit.
17 |
18 | ### Self-hosted Runners (Mac Arm64 and Mac Amd64)
19 |
20 | The stack `ASGRunnerStack` is used to provision EC2 Mac instances through an autoscaling group. The runner configurations can be edited in `config/runner-config.json`.
21 | When the runners are initialized, a user data script (`scripts/setup-runner.sh`) runs to setup the instance. This script downloads and installs [GitHub actions runner application](https://github.com/actions/runner) on the our self-hosted runner, which is used to connect our runner with the GitHub actions. Then the script connects the instance with our GitHub repos, starting the runner service.
22 |
23 | #### Connect to the runners for troubleshooting
24 |
25 | After the runner is linked to the GitHub repo, you can access a runner to trouble shoot it by noting down the name of the runner (usually `ip172-31-xx-xxx`), access the AWS account that hosts the instance, and find the instance with the private IP address matching the name above. Connect to the runner either by using an SSH service with the key saved in the Secret Manager, or use Session Manager in EC2. Go to the AWS EC2 console, select the instance and click Connect > Session Manager.
26 |
27 | ### S3 Bucket and Cloudfront Distributions
28 |
29 | The S3 buckets are used for storing project artifacts and dependencies that should be publicly accessible. To make the content delivery more effective and secure, we also set up CloudFront to work with the S3 buckets.
30 | The construct `CloudfrontCdn` creates a new CloudFront distribution in front of an existing S3 bucket and adds an OAI to it which makes the content in the bucket can be read by the CloudFront distribution. Users can then access the bucket objects through the CloudFront domain instead of the S3 bucket URL, and benefit from CloudFront's features, like caching.
31 |
32 | - Get the distribution domain from the AWS console.
33 | - Enter the CloudFront URL, concatenated with the path to a file in your browser to download a file.
34 | For example, `*.cloudfront.net/path/to/file`.
35 |
36 | ## Unit and Integration Tests
37 |
38 | The unit tests and integration tests are both executed by the pipeline post-deployment steps in `Beta` stage. Or you can run the tests with the command below.
39 |
40 | ```
41 | npm run test
42 | npm run integration
43 | ```
44 |
45 | Format your code with the command `npm run prettier-format`.
46 |
47 | ## Host Licensing
48 |
49 | When using Auto Scaling Group with macOS instances on EC2, a host license has to be created in AWS License Manager. Create a self-managed license named `MacHostLicense`, set the license type to `sockets`, and save the arn to the `runner-config.json` file.
50 |
51 |
52 | ## Access Tokens
53 |
54 | Overall, 3 access tokens are required - one for the pipeline to access the runfinch/infrastructure code and update when there is a code update, and two for runfinch/finch and runfinch/finch-core to provide the github runner keys for automatically creating and registering the runners
--------------------------------------------------------------------------------
/assets/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello world!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Hello world!
13 |
14 |
15 |
--------------------------------------------------------------------------------
/bin/finch-aws-cdk.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import 'source-map-support/register';
3 | import * as cdk from 'aws-cdk-lib';
4 | import { FinchPipelineStack } from '../lib/finch-pipeline-stack';
5 | import { EnvConfig } from '../config/env-config';
6 |
7 | const app = new cdk.App();
8 | new FinchPipelineStack(app, 'FinchPipelineStack', { env: EnvConfig.envPipeline, terminationProtection: true });
9 |
10 | app.synth();
11 |
--------------------------------------------------------------------------------
/cdk.context.json:
--------------------------------------------------------------------------------
1 | {
2 | "availability-zones:account=820462304213:region=us-west-2": [
3 | "us-west-2a",
4 | "us-west-2b",
5 | "us-west-2c",
6 | "us-west-2d"
7 | ],
8 | "availability-zones:account=090529234398:region=us-west-2": [
9 | "us-west-2a",
10 | "us-west-2b",
11 | "us-west-2c",
12 | "us-west-2d"
13 | ],
14 | "vpc-provider:account=820462304213:filter.isDefault=true:region=us-west-2:returnAsymmetricSubnets=true": {
15 | "vpcId": "vpc-06de4cdd1050128af",
16 | "vpcCidrBlock": "172.31.0.0/16",
17 | "availabilityZones": [],
18 | "subnetGroups": [
19 | {
20 | "name": "Public",
21 | "type": "Public",
22 | "subnets": [
23 | {
24 | "subnetId": "subnet-0ebe88c6840a07821",
25 | "cidr": "172.31.16.0/20",
26 | "availabilityZone": "us-west-2a",
27 | "routeTableId": "rtb-0dc49c558df03064c"
28 | },
29 | {
30 | "subnetId": "subnet-0babc2053bb07dabc",
31 | "cidr": "172.31.32.0/20",
32 | "availabilityZone": "us-west-2b",
33 | "routeTableId": "rtb-0dc49c558df03064c"
34 | },
35 | {
36 | "subnetId": "subnet-0b5c52262bc868779",
37 | "cidr": "172.31.0.0/20",
38 | "availabilityZone": "us-west-2c",
39 | "routeTableId": "rtb-0dc49c558df03064c"
40 | },
41 | {
42 | "subnetId": "subnet-0d19a1a00efb1f4a3",
43 | "cidr": "172.31.48.0/20",
44 | "availabilityZone": "us-west-2d",
45 | "routeTableId": "rtb-0dc49c558df03064c"
46 | }
47 | ]
48 | }
49 | ]
50 | },
51 | "vpc-provider:account=090529234398:filter.isDefault=true:region=us-west-2:returnAsymmetricSubnets=true": {
52 | "vpcId": "vpc-04f2a33e813718bda",
53 | "vpcCidrBlock": "172.31.0.0/16",
54 | "availabilityZones": [],
55 | "subnetGroups": [
56 | {
57 | "name": "Public",
58 | "type": "Public",
59 | "subnets": [
60 | {
61 | "subnetId": "subnet-085ebdeb2a40fb8d8",
62 | "cidr": "172.31.32.0/20",
63 | "availabilityZone": "us-west-2a",
64 | "routeTableId": "rtb-038f353b4f9099666"
65 | },
66 | {
67 | "subnetId": "subnet-02caced18cce39965",
68 | "cidr": "172.31.16.0/20",
69 | "availabilityZone": "us-west-2b",
70 | "routeTableId": "rtb-038f353b4f9099666"
71 | },
72 | {
73 | "subnetId": "subnet-016fe97f7e20db500",
74 | "cidr": "172.31.0.0/20",
75 | "availabilityZone": "us-west-2c",
76 | "routeTableId": "rtb-038f353b4f9099666"
77 | },
78 | {
79 | "subnetId": "subnet-09f074e9d6c16c1f8",
80 | "cidr": "172.31.48.0/20",
81 | "availabilityZone": "us-west-2d",
82 | "routeTableId": "rtb-038f353b4f9099666"
83 | }
84 | ]
85 | }
86 | ]
87 | },
88 | "vpc-provider:account=019528120233:filter.isDefault=true:region=us-east-2:returnAsymmetricSubnets=true": {
89 | "vpcId": "vpc-09e086b046b77b394",
90 | "vpcCidrBlock": "172.31.0.0/16",
91 | "availabilityZones": [],
92 | "subnetGroups": [
93 | {
94 | "name": "Public",
95 | "type": "Public",
96 | "subnets": [
97 | {
98 | "subnetId": "subnet-0ed000ae57a777892",
99 | "cidr": "172.31.0.0/20",
100 | "availabilityZone": "us-east-2a",
101 | "routeTableId": "rtb-03950063f7712710d"
102 | },
103 | {
104 | "subnetId": "subnet-01561873c2253c130",
105 | "cidr": "172.31.16.0/20",
106 | "availabilityZone": "us-east-2b",
107 | "routeTableId": "rtb-03950063f7712710d"
108 | },
109 | {
110 | "subnetId": "subnet-0fd01d9d69c3969ff",
111 | "cidr": "172.31.32.0/20",
112 | "availabilityZone": "us-east-2c",
113 | "routeTableId": "rtb-03950063f7712710d"
114 | }
115 | ]
116 | }
117 | ]
118 | },
119 | "ami:account=090529234398:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.1*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0a18d0d3ba8c9596b",
120 | "ami:account=090529234398:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.1*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0a3536378b0dcbcb0",
121 | "ami:account=019528120233:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.1*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-0fb1a8ce1a0e727d8",
122 | "ami:account=019528120233:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.1*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-0513d1dba13c208f0",
123 | "ami:account=820462304213:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-13.6*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0b71bd84ecf59ddb6",
124 | "ami:account=820462304213:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-13.6*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0bd1dced82749f0c9",
125 | "ami:account=090529234398:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-13.6*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0b71bd84ecf59ddb6",
126 | "ami:account=090529234398:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-13.6*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0bd1dced82749f0c9",
127 | "ami:account=090529234398:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.5*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0ac6cd62ce9ccbae4",
128 | "ami:account=090529234398:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.5*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-034d441e881b05d91",
129 | "ami:account=019528120233:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-13.6*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-0ba140ebbe2896eb2",
130 | "ami:account=019528120233:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-13.6*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-0603a57be9b8fc392",
131 | "ami:account=019528120233:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.5*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-08068ef2743ad7862",
132 | "ami:account=019528120233:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-14.5*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-0b7a317a30ce6d3a0",
133 | "ami:account=019528120233:filters.architecture.0=arm64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-12.7*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-07614028fded10805",
134 | "ami:account=019528120233:filters.architecture.0=x86_64_mac:filters.image-type.0=machine:filters.name.0=amzn-ec2-macos-12.7*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-07a46e0229d50d989",
135 | "ami:account=820462304213:filters.architecture.0=x86_64:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu*22.04*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-07ceaccc0c1916f5f",
136 | "ami:account=820462304213:filters.architecture.0=arm64:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu*22.04*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0d0af2588c3fa1261",
137 | "ami:account=090529234398:filters.architecture.0=x86_64:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu*22.04*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-07ceaccc0c1916f5f",
138 | "ami:account=090529234398:filters.architecture.0=arm64:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu*22.04*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-west-2": "ami-0d0af2588c3fa1261",
139 | "ami:account=019528120233:filters.architecture.0=x86_64:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu*22.04*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-0f09ef696435ff61a",
140 | "ami:account=019528120233:filters.architecture.0=arm64:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu*22.04*:filters.owner-alias.0=amazon:filters.root-device-type.0=ebs:filters.state.0=available:filters.virtualization-type.0=hvm:region=us-east-2": "ami-09e17de8500439c77"
141 | }
142 |
--------------------------------------------------------------------------------
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "npx ts-node --prefer-ts-exts bin/finch-aws-cdk.ts",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "**/*.d.ts",
11 | "**/*.js",
12 | "tsconfig.json",
13 | "package*.json",
14 | "yarn.lock",
15 | "node_modules",
16 | "test"
17 | ]
18 | },
19 | "context": {
20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
21 | "@aws-cdk/core:stackRelativeExports": true,
22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28 | "@aws-cdk/core:checkSecretUsage": true,
29 | "@aws-cdk/aws-iam:minimizePolicies": true,
30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
33 | "@aws-cdk/core:target-partitions": [
34 | "aws",
35 | "aws-cn"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/config/env-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "envPipeline": {"account": "229528898709", "region":"us-west-2"},
3 | "envBeta": {"account": "820462304213", "region":"us-west-2"},
4 | "envProd": {"account": "090529234398", "region":"us-west-2"},
5 | "envRelease": {"account": "019528120233", "region": "us-east-2"}
6 | }
--------------------------------------------------------------------------------
/config/env-config.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import config from './env-config.json';
3 |
4 | /**
5 | * Class for environment configurations. Outlines the account and region.
6 | */
7 | class EnvConfigClass {
8 | public readonly envPipeline: cdk.Environment;
9 | public readonly envBeta: cdk.Environment;
10 | public readonly envProd: cdk.Environment;
11 | public readonly envRelease: cdk.Environment;
12 |
13 | constructor(configFile: any) {
14 | if (!configFile.envPipeline) {
15 | throw new Error('Error: envPipeline must be specified.');
16 | }
17 | this.envPipeline = configFile.envPipeline;
18 |
19 | if (!configFile.envBeta) {
20 | throw new Error('Error: envBeta must be specified.');
21 | }
22 | this.envBeta = configFile.envBeta;
23 |
24 | if (!configFile.envProd) {
25 | throw new Error('Error: envProd must be specified.');
26 | }
27 | this.envProd = configFile.envProd;
28 |
29 | if (!configFile.envRelease) {
30 | throw new Error('Error: envRelease must be specified.');
31 | }
32 | this.envRelease = configFile.envRelease;
33 | }
34 | }
35 |
36 | export const EnvConfig = new EnvConfigClass(config);
37 |
--------------------------------------------------------------------------------
/config/runner-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "runnerBeta": {
3 | "macLicenseArn": "arn:aws:license-manager:us-west-2:820462304213:license-configuration:lic-7ac3e249bd13f71b44ac34963a8c3353",
4 | "windowsLicenseArn": "arn:aws:license-manager:us-west-2:820462304213:license-configuration:lic-56c55a611976d66a1357e58077b19e93",
5 | "runnerTypes": [
6 | {
7 | "platform": "mac",
8 | "version": "13.6",
9 | "arch": "arm",
10 | "repo": "finch",
11 | "desiredInstances": 1,
12 | "availabilityZones": [
13 | "us-west-2a",
14 | "us-west-2b",
15 | "us-west-2c",
16 | "us-west-2d"
17 | ]
18 | },
19 | {
20 | "platform": "mac",
21 | "version": "13.6",
22 | "arch": "x86",
23 | "repo": "finch-core",
24 | "desiredInstances": 1,
25 | "availabilityZones": [
26 | "us-west-2a",
27 | "us-west-2b",
28 | "us-west-2c",
29 | "us-west-2d"
30 | ]
31 | },
32 | {
33 | "platform": "windows",
34 | "version": "2022",
35 | "arch": "x86",
36 | "repo": "finch",
37 | "desiredInstances": 1,
38 | "availabilityZones": [
39 | "us-west-2a",
40 | "us-west-2b",
41 | "us-west-2c",
42 | "us-west-2d"
43 | ]
44 | },
45 | {
46 | "platform": "windows",
47 | "version": "2022",
48 | "arch": "x86",
49 | "repo": "finch-core",
50 | "desiredInstances": 1,
51 | "availabilityZones": [
52 | "us-west-2a",
53 | "us-west-2b",
54 | "us-west-2c",
55 | "us-west-2d"
56 | ]
57 | },
58 | {
59 | "platform": "amazonlinux",
60 | "version": "2023",
61 | "arch": "x86",
62 | "repo": "finch",
63 | "desiredInstances": 1,
64 | "availabilityZones": [
65 | "us-west-2a",
66 | "us-west-2b",
67 | "us-west-2c",
68 | "us-west-2d"
69 | ]
70 | },
71 | {
72 | "platform": "amazonlinux",
73 | "version": "2",
74 | "arch": "x86",
75 | "repo": "finch",
76 | "desiredInstances": 1,
77 | "availabilityZones": [
78 | "us-west-2a",
79 | "us-west-2b",
80 | "us-west-2c",
81 | "us-west-2d"
82 | ]
83 | },
84 | {
85 | "platform": "amazonlinux",
86 | "version": "2023",
87 | "arch": "arm",
88 | "repo": "finch",
89 | "desiredInstances": 1,
90 | "availabilityZones": [
91 | "us-west-2a",
92 | "us-west-2b",
93 | "us-west-2c",
94 | "us-west-2d"
95 | ]
96 | },
97 | {
98 | "platform": "amazonlinux",
99 | "version": "2",
100 | "arch": "arm",
101 | "repo": "finch",
102 | "desiredInstances": 1,
103 | "availabilityZones": [
104 | "us-west-2a",
105 | "us-west-2b",
106 | "us-west-2c",
107 | "us-west-2d"
108 | ]
109 | }
110 | ]
111 | },
112 | "runnerProd": {
113 | "macLicenseArn": "arn:aws:license-manager:us-west-2:090529234398:license-configuration:lic-c0c3e2458f6d2a45b0bcc8d66fbc072e",
114 | "windowsLicenseArn": "arn:aws:license-manager:us-west-2:090529234398:license-configuration:lic-62c55a61d2d9ce6ef100c6ea267216e6",
115 | "runnerTypes": [
116 | {
117 | "platform": "mac",
118 | "version": "13.6",
119 | "arch": "arm",
120 | "repo": "finch",
121 | "desiredInstances": 2,
122 | "availabilityZones": [
123 | "us-west-2a",
124 | "us-west-2b",
125 | "us-west-2c",
126 | "us-west-2d"
127 | ]
128 | },
129 | {
130 | "platform": "mac",
131 | "version": "13.6",
132 | "arch": "x86",
133 | "repo": "finch",
134 | "desiredInstances": 2,
135 | "availabilityZones": [
136 | "us-west-2a",
137 | "us-west-2b",
138 | "us-west-2c",
139 | "us-west-2d"
140 | ]
141 | },
142 | {
143 | "platform": "mac",
144 | "version": "14.5",
145 | "arch": "arm",
146 | "repo": "finch",
147 | "desiredInstances": 2,
148 | "availabilityZones": [
149 | "us-west-2a",
150 | "us-west-2b",
151 | "us-west-2c",
152 | "us-west-2d"
153 | ]
154 | },
155 | {
156 | "platform": "mac",
157 | "version": "14.5",
158 | "arch": "x86",
159 | "repo": "finch",
160 | "desiredInstances": 2,
161 | "availabilityZones": [
162 | "us-west-2a",
163 | "us-west-2b",
164 | "us-west-2c",
165 | "us-west-2d"
166 | ]
167 | },
168 | {
169 | "platform": "mac",
170 | "version": "13.6",
171 | "arch": "arm",
172 | "repo": "finch-core",
173 | "desiredInstances": 1,
174 | "availabilityZones": [
175 | "us-west-2a",
176 | "us-west-2b",
177 | "us-west-2c",
178 | "us-west-2d"
179 | ]
180 | },
181 | {
182 | "platform": "mac",
183 | "version": "13.6",
184 | "arch": "x86",
185 | "repo": "finch-core",
186 | "desiredInstances": 1,
187 | "availabilityZones": [
188 | "us-west-2a",
189 | "us-west-2b",
190 | "us-west-2c",
191 | "us-west-2d"
192 | ]
193 | },
194 | {
195 | "platform": "mac",
196 | "version": "14.5",
197 | "arch": "arm",
198 | "repo": "finch-core",
199 | "desiredInstances": 1,
200 | "availabilityZones": [
201 | "us-west-2a",
202 | "us-west-2b",
203 | "us-west-2c",
204 | "us-west-2d"
205 | ]
206 | },
207 | {
208 | "platform": "mac",
209 | "version": "14.5",
210 | "arch": "x86",
211 | "repo": "finch-core",
212 | "desiredInstances": 1,
213 | "availabilityZones": [
214 | "us-west-2a",
215 | "us-west-2b",
216 | "us-west-2c",
217 | "us-west-2d"
218 | ]
219 | },
220 | {
221 | "platform": "windows",
222 | "version": "2022",
223 | "arch": "x86",
224 | "repo": "finch",
225 | "desiredInstances": 2,
226 | "availabilityZones": [
227 | "us-west-2a",
228 | "us-west-2b",
229 | "us-west-2c",
230 | "us-west-2d"
231 | ]
232 | },
233 | {
234 | "platform": "windows",
235 | "version": "2022",
236 | "arch": "x86",
237 | "repo": "finch-core",
238 | "desiredInstances": 1,
239 | "availabilityZones": [
240 | "us-west-2a",
241 | "us-west-2b",
242 | "us-west-2c",
243 | "us-west-2d"
244 | ]
245 | },
246 | {
247 | "platform": "amazonlinux",
248 | "version": "2023",
249 | "arch": "x86",
250 | "repo": "finch",
251 | "desiredInstances": 1,
252 | "availabilityZones": [
253 | "us-west-2a",
254 | "us-west-2b",
255 | "us-west-2c",
256 | "us-west-2d"
257 | ]
258 | },
259 | {
260 | "platform": "amazonlinux",
261 | "version": "2",
262 | "arch": "x86",
263 | "repo": "finch",
264 | "desiredInstances": 1,
265 | "availabilityZones": [
266 | "us-west-2a",
267 | "us-west-2b",
268 | "us-west-2c",
269 | "us-west-2d"
270 | ]
271 | },
272 | {
273 | "platform": "amazonlinux",
274 | "version": "2023",
275 | "arch": "arm",
276 | "repo": "finch",
277 | "desiredInstances": 1,
278 | "availabilityZones": [
279 | "us-west-2a",
280 | "us-west-2b",
281 | "us-west-2c",
282 | "us-west-2d"
283 | ]
284 | },
285 | {
286 | "platform": "amazonlinux",
287 | "version": "2",
288 | "arch": "arm",
289 | "repo": "finch",
290 | "desiredInstances": 1,
291 | "availabilityZones": [
292 | "us-west-2a",
293 | "us-west-2b",
294 | "us-west-2c",
295 | "us-west-2d"
296 | ]
297 | }
298 | ]
299 | },
300 | "runnerRelease": {
301 | "macLicenseArn": "arn:aws:license-manager:us-east-2:019528120233:license-configuration:lic-94c3e244cb74a70c90c82f432e9e0d91",
302 | "windowsLicenseArn": "arn:aws:license-manager:us-east-2:019528120233:license-configuration:lic-2ec55a6280292474f1ea0d7ad6f64987",
303 | "runnerTypes": [
304 | {
305 | "platform": "mac",
306 | "version": "13.6",
307 | "arch": "arm",
308 | "repo": "finch",
309 | "desiredInstances": 1,
310 | "availabilityZones": [
311 | "us-east-2a",
312 | "us-east-2b",
313 | "us-east-2c"
314 | ]
315 | },
316 | {
317 | "platform": "mac",
318 | "version": "13.6",
319 | "arch": "x86",
320 | "repo": "finch",
321 | "desiredInstances": 1,
322 | "availabilityZones": [
323 | "us-east-2a",
324 | "us-east-2b",
325 | "us-east-2c"
326 | ]
327 | },
328 | {
329 | "platform": "mac",
330 | "version": "14.5",
331 | "arch": "arm",
332 | "repo": "finch",
333 | "desiredInstances": 1,
334 | "availabilityZones": [
335 | "us-east-2a",
336 | "us-east-2b",
337 | "us-east-2c"
338 | ]
339 | },
340 | {
341 | "platform": "mac",
342 | "version": "14.5",
343 | "arch": "x86",
344 | "repo": "finch",
345 | "desiredInstances": 1,
346 | "availabilityZones": [
347 | "us-east-2a",
348 | "us-east-2b",
349 | "us-east-2c"
350 | ]
351 | },
352 | {
353 | "platform": "mac",
354 | "version": "13.6",
355 | "arch": "arm",
356 | "repo": "finch-core",
357 | "desiredInstances": 1,
358 | "availabilityZones": [
359 | "us-east-2a",
360 | "us-east-2b",
361 | "us-east-2c"
362 | ]
363 | },
364 | {
365 | "platform": "mac",
366 | "version": "13.6",
367 | "arch": "x86",
368 | "repo": "finch-core",
369 | "desiredInstances": 1,
370 | "availabilityZones": [
371 | "us-east-2a",
372 | "us-east-2b",
373 | "us-east-2c"
374 | ]
375 | },
376 | {
377 | "platform": "mac",
378 | "version": "12.7",
379 | "arch": "arm",
380 | "repo": "finch-core",
381 | "desiredInstances": 1,
382 | "availabilityZones": [
383 | "us-east-2a",
384 | "us-east-2b",
385 | "us-east-2c"
386 | ]
387 | },
388 | {
389 | "platform": "mac",
390 | "version": "12.7",
391 | "arch": "x86",
392 | "repo": "finch-core",
393 | "desiredInstances": 1,
394 | "availabilityZones": [
395 | "us-east-2a",
396 | "us-east-2b",
397 | "us-east-2c"
398 | ]
399 | },
400 | {
401 | "platform": "windows",
402 | "version": "2022",
403 | "arch": "x86",
404 | "repo": "finch",
405 | "desiredInstances": 1,
406 | "availabilityZones": [
407 | "us-east-2a",
408 | "us-east-2b",
409 | "us-east-2c"
410 | ]
411 | },
412 | {
413 | "platform": "windows",
414 | "version": "2022",
415 | "arch": "x86",
416 | "repo": "finch-core",
417 | "desiredInstances": 1,
418 | "availabilityZones": [
419 | "us-east-2a",
420 | "us-east-2b",
421 | "us-east-2c"
422 | ]
423 | }
424 | ]
425 | }
426 | }
--------------------------------------------------------------------------------
/config/runner-config.ts:
--------------------------------------------------------------------------------
1 | import config from './runner-config.json';
2 |
3 | export interface RunnerProps {
4 | macLicenseArn: string;
5 | windowsLicenseArn: string;
6 | linuxLicenseArn: string;
7 | runnerTypes: Array;
8 | }
9 |
10 | export interface RunnerType {
11 | platform: PlatformType;
12 | /** Different values for different platforms.
13 | * For mac, a version like 13.2
14 | * For windows, a server version like 2022
15 | * For amazonlinux, either 2, 2023, etc.
16 | * For fedora, a version like 40, 41, etc. */
17 | version: string;
18 | arch: string;
19 | repo: string;
20 | desiredInstances: number;
21 | availabilityZones: Array;
22 | }
23 |
24 | export const enum PlatformType {
25 | WINDOWS = 'windows',
26 | MAC = 'mac',
27 | AMAZONLINUX = 'amazonlinux',
28 | }
29 |
30 | /**
31 | * Class for runner configurations. Outlines self hosted license arn and an
32 | * array of runner types to create using an auto scaling group.
33 | */
34 | class RunnerConfigClass {
35 | public readonly runnerBeta: RunnerProps;
36 | public readonly runnerProd: RunnerProps;
37 | public readonly runnerRelease: RunnerProps;
38 |
39 | constructor(configFile: any) {
40 | if (!configFile.runnerBeta) {
41 | throw new Error('Error: Beta runner config must be specified.');
42 | }
43 | this.runnerBeta = configFile.runnerBeta;
44 |
45 | if (!configFile.runnerProd) {
46 | throw new Error('Error: Prod runner config must be specified.');
47 | }
48 | this.runnerProd = configFile.runnerProd;
49 |
50 | if (!configFile.runnerRelease) {
51 | throw new Error('Error: Release runner config must be specified.');
52 | }
53 | this.runnerRelease = configFile.runnerRelease;
54 | }
55 | }
56 |
57 | export const RunnerConfig = new RunnerConfigClass(config);
58 |
--------------------------------------------------------------------------------
/integration_test/cloudfront_service.test.ts:
--------------------------------------------------------------------------------
1 | describe('Integration test', () => {
2 | test('200 Response from CloudFront Distribution', async () => {
3 | const url = process.env.CLOUDFRONT_URL ? `https://${process.env.CLOUDFRONT_URL}/test.html` : 'No URL in env';
4 | console.log('CloudFront URL =>', url);
5 | try {
6 | const response = await fetch(url, {
7 | method: 'GET',
8 | headers: {
9 | Accept: 'application/json'
10 | }
11 | });
12 | if (!response.ok) {
13 | throw new Error(`Request failed ${response.statusText}`);
14 | }
15 | const body = await response.text();
16 | console.log('Response body =>', body);
17 |
18 | expect(response.status).toEqual(200);
19 | } catch (err) {
20 | console.log('ERROR: ', err);
21 | throw err;
22 | }
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/jest.config.integration.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | roots: [
4 | '/integration_test',
5 | ],
6 | testMatch: ['**/*.test.ts'],
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest'
9 | }
10 | };
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | roots: [
4 | '/test',
5 | '/integration_test',
6 | ],
7 | testMatch: ['**/*.test.ts'],
8 | transform: {
9 | '^.+\\.tsx?$': 'ts-jest'
10 | }
11 | };
--------------------------------------------------------------------------------
/jest.config.unit.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | roots: [
4 | '/test',
5 | ],
6 | testMatch: ['**/*.test.ts'],
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest'
9 | }
10 | };
--------------------------------------------------------------------------------
/lib/artifact-bucket-cloudfront.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { CfnOutput } from 'aws-cdk-lib';
3 | import * as s3 from 'aws-cdk-lib/aws-s3';
4 | import { Construct } from 'constructs';
5 |
6 | import { CloudfrontCdn } from './cloudfront_cdn';
7 | import * as s3Deployment from 'aws-cdk-lib/aws-s3-deployment';
8 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
9 |
10 | export class ArtifactBucketCloudfrontStack extends cdk.Stack {
11 | public readonly urlOutput: CfnOutput;
12 | public readonly bucket: s3.Bucket;
13 | constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) {
14 | super(scope, id, props);
15 | applyTerminationProtectionOnStacks([this]);
16 |
17 | const bucketName = `finch-artifact-bucket-${stage.toLowerCase()}-${cdk.Stack.of(this)?.account}`;
18 | const artifactBucket = new s3.Bucket(this, 'ArtifactBucket', {
19 | bucketName,
20 | publicReadAccess: false,
21 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL
22 | });
23 |
24 | // upload the file for integration testing puporse
25 | new s3Deployment.BucketDeployment(this, 'DeployTestFile', {
26 | sources: [s3Deployment.Source.asset('./assets')],
27 | destinationBucket: artifactBucket
28 | });
29 |
30 | const cloudfrontCdn = new CloudfrontCdn(this, 'ArtifactCloudfrontCdn', {
31 | bucket: artifactBucket
32 | });
33 | this.bucket = artifactBucket;
34 | this.urlOutput = cloudfrontCdn.urlOutput;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/asg-runner-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { aws_autoscaling as autoscaling } from 'aws-cdk-lib';
3 | import { UpdatePolicy } from 'aws-cdk-lib/aws-autoscaling';
4 | import * as ec2 from 'aws-cdk-lib/aws-ec2';
5 | import * as iam from 'aws-cdk-lib/aws-iam';
6 | import * as resourcegroups from 'aws-cdk-lib/aws-resourcegroups';
7 | import { Construct } from 'constructs';
8 | import { readFileSync } from 'fs';
9 | import { PlatformType, RunnerType } from '../config/runner-config';
10 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
11 | import { ENVIRONMENT_STAGE } from './finch-pipeline-app-stage';
12 |
13 | interface IASGRunnerStack {
14 | platform: PlatformType;
15 | version: string;
16 | arch: string;
17 | repo: string;
18 | }
19 |
20 | interface ASGRunnerStackProps extends cdk.StackProps {
21 | env: cdk.Environment | undefined;
22 | stage: ENVIRONMENT_STAGE;
23 | /** Only required for dedicated hosts.
24 | * Right now, dedicated hosts should only be used to avoid
25 | * nested virtualization issues, which is only a problem for
26 | * non-Linux usecases. */
27 | licenseArn?: string;
28 | type: RunnerType;
29 | }
30 |
31 | /**
32 | * A stack to provision an autoscaling group for macOS instances. This requires:
33 | * - a self-managed license (manually created as cdk/cfn does not support this)
34 | * - a resource group
35 | * - a launch template
36 | * - an auto scaling group
37 | */
38 | export class ASGRunnerStack extends cdk.Stack implements IASGRunnerStack {
39 | platform: PlatformType;
40 | version: string;
41 | arch: string;
42 | repo: string;
43 |
44 | requiresDedicatedHosts = () => this.platform === PlatformType.MAC || this.platform === PlatformType.WINDOWS;
45 |
46 | userData = (props: ASGRunnerStackProps, setupScriptName: string) =>
47 | `#!/bin/bash
48 | LABEL_STAGE=${props.stage === ENVIRONMENT_STAGE.Release ? 'release' : 'test'}
49 | REPO=${this.repo}
50 | REGION=${props.env?.region}
51 | ` + readFileSync(`./scripts/${setupScriptName}`, 'utf8');
52 |
53 | constructor(scope: Construct, id: string, props: ASGRunnerStackProps) {
54 | super(scope, id, props);
55 |
56 | this.platform = props.type.platform;
57 | this.version = props.type.version;
58 | this.arch = props.type.arch;
59 | this.repo = props.type.repo;
60 |
61 | applyTerminationProtectionOnStacks([this]);
62 |
63 | const amiSearchString = `amzn-ec2-macos-${this.version}*`;
64 |
65 | let instanceType: ec2.InstanceType;
66 | let machineImage: ec2.IMachineImage;
67 | let userDataString = '';
68 | let asgName = '';
69 | let rootDeviceName = '';
70 | switch (this.platform) {
71 | case PlatformType.MAC: {
72 | rootDeviceName = '/dev/sda1';
73 | if (this.arch === 'arm') {
74 | instanceType = ec2.InstanceType.of(ec2.InstanceClass.MAC2, ec2.InstanceSize.METAL);
75 | } else {
76 | instanceType = ec2.InstanceType.of(ec2.InstanceClass.MAC1, ec2.InstanceSize.METAL);
77 | }
78 | const macOSArchLookup = this.arch === 'arm' ? `arm64_${this.platform}` : `x86_64_${this.platform}`;
79 | machineImage = new ec2.LookupMachineImage({
80 | name: amiSearchString,
81 | filters: {
82 | 'virtualization-type': ['hvm'],
83 | 'root-device-type': ['ebs'],
84 | architecture: [macOSArchLookup],
85 | 'owner-alias': ['amazon']
86 | }
87 | });
88 | asgName = 'MacASG';
89 | userDataString = this.userData(props, 'setup-runner.sh');
90 | break;
91 | }
92 | case PlatformType.WINDOWS: {
93 | instanceType = ec2.InstanceType.of(ec2.InstanceClass.M5ZN, ec2.InstanceSize.METAL);
94 | asgName = 'WindowsASG';
95 | rootDeviceName = '/dev/sda1';
96 | machineImage = ec2.MachineImage.latestWindows(ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE);
97 | // We need to provide user data as a yaml file to specify runAs: admin
98 | // Maintain that file as yaml and source here to ensure formatting.
99 | userDataString = readFileSync('./scripts/windows-runner-user-data.yaml', 'utf8')
100 | .replace('', props.stage === ENVIRONMENT_STAGE.Release ? 'release' : 'test')
101 | .replace('', props.type.repo)
102 | .replace('', props.env?.region || '');
103 |
104 | break;
105 | }
106 | case PlatformType.AMAZONLINUX: {
107 | // Linux instances do not have to be metal, since the only mode of operation
108 | // for Finch on linux currently is "native" mode, e.g. no virutal machine on host
109 |
110 | rootDeviceName = '/dev/xvda';
111 | let cpuType: ec2.AmazonLinuxCpuType;
112 | if (this.arch === 'arm') {
113 | instanceType = ec2.InstanceType.of(ec2.InstanceClass.C7G, ec2.InstanceSize.LARGE);
114 | cpuType = ec2.AmazonLinuxCpuType.ARM_64;
115 | } else {
116 | instanceType = ec2.InstanceType.of(ec2.InstanceClass.C7A, ec2.InstanceSize.LARGE);
117 | cpuType = ec2.AmazonLinuxCpuType.X86_64;
118 | }
119 | asgName = 'LinuxASG';
120 | userDataString = this.userData(props, 'setup-linux-runner.sh');
121 | if (this.version === '2') {
122 | machineImage = ec2.MachineImage.latestAmazonLinux2({
123 | cpuType
124 | });
125 | } else {
126 | machineImage = ec2.MachineImage.latestAmazonLinux2023({
127 | cpuType
128 | });
129 | }
130 | break;
131 | }
132 | }
133 |
134 | if (props.env == undefined) {
135 | throw new Error('Runner environment is undefined!');
136 | }
137 |
138 | const vpc = cdk.aws_ec2.Vpc.fromLookup(this, 'VPC', { isDefault: true });
139 |
140 | const securityGroup = new ec2.SecurityGroup(this, 'EC2SecurityGroup', {
141 | vpc,
142 | description: 'Allow only outbound traffic',
143 | allowAllOutbound: true
144 | });
145 |
146 | const role = new iam.Role(this, 'EC2Role', {
147 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com')
148 | });
149 |
150 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'));
151 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AutoScalingFullAccess'));
152 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('ResourceGroupsandTagEditorFullAccess'));
153 |
154 | // Grant EC2 instances access to secretsmanager to retrieve the GitHub api key to register runners
155 | role.addToPolicy(
156 | new iam.PolicyStatement({
157 | effect: iam.Effect.ALLOW,
158 | actions: [
159 | 'secretsmanager:GetResourcePolicy',
160 | 'secretsmanager:GetSecretValue',
161 | 'secretsmanager:DescribeSecret',
162 | 'secretsmanager:ListSecretVersionIds'
163 | ],
164 | resources: [
165 | `arn:aws:secretsmanager:${props.env?.region}:${props.env?.account}:secret:${props.type.repo}-runner-reg-key*`
166 | ]
167 | })
168 | );
169 |
170 | // Create a 100GiB volume to be used as instance root volume
171 | const rootVolume: ec2.BlockDevice = {
172 | deviceName: rootDeviceName,
173 | volume: ec2.BlockDeviceVolume.ebs(100, {
174 | volumeType: ec2.EbsDeviceVolumeType.GP3,
175 | // throughput / 256 KiB per operation
176 | // default is size * 3
177 | iops: 1200,
178 | throughput: 300
179 | })
180 | };
181 |
182 | const ltName = `${asgName}LaunchTemplate`;
183 | const keyPairName = `${asgName}KeyPair`;
184 | const lt = new ec2.LaunchTemplate(this, ltName, {
185 | requireImdsv2: true,
186 | instanceType,
187 | keyPair: ec2.KeyPair.fromKeyPairName(this, keyPairName, 'runner-key'),
188 | machineImage,
189 | role: role,
190 | securityGroup: securityGroup,
191 | userData: ec2.UserData.custom(userDataString),
192 | blockDevices: [rootVolume]
193 | });
194 |
195 | // Create a custom name for this as names for resource groups cannot be repeated
196 | const resourceGroupName = `${this.repo}-${this.platform}-${this.version.split('.')[0]}-${this.arch}HostGroup`;
197 | const resourceGroupDescription = 'Host resource group for finchs infrastructure';
198 |
199 | let ltPlacementConfig = {};
200 | if (this.requiresDedicatedHosts()) {
201 | const hostResourceGroup = this.createHostResourceGroup(resourceGroupName, resourceGroupDescription);
202 | ltPlacementConfig = {
203 | placement: {
204 | tenancy: 'host',
205 | hostResourceGroupArn: hostResourceGroup.attrArn
206 | }
207 | };
208 | }
209 |
210 | // Escape hatch to cfnLaunchTemplate as the L2 construct lacked some required
211 | // configurations.
212 | const cfnLt = lt.node.defaultChild as ec2.CfnLaunchTemplate;
213 | cfnLt.launchTemplateData = {
214 | ...cfnLt.launchTemplateData,
215 | ...(this.requiresDedicatedHosts() && {
216 | ...ltPlacementConfig,
217 | licenseSpecifications: [{ licenseConfigurationArn: props.licenseArn }]
218 | }),
219 | tagSpecifications: [
220 | {
221 | resourceType: 'instance',
222 | tags: [
223 | {
224 | key: 'PVRE-Reporting',
225 | value: 'SSM'
226 | }
227 | ]
228 | }
229 | ]
230 | };
231 |
232 | const asg = new autoscaling.AutoScalingGroup(this, asgName, {
233 | vpc,
234 | desiredCapacity: props.type.desiredInstances,
235 | maxCapacity: props.type.desiredInstances,
236 | minCapacity: 0,
237 | healthCheck: autoscaling.HealthCheck.ec2({
238 | grace: cdk.Duration.seconds(3600)
239 | }),
240 | launchTemplate: lt,
241 | updatePolicy: UpdatePolicy.rollingUpdate({
242 | // Defaults shown here explicitly except for pauseTime
243 | // and minSuccesPercentage
244 | maxBatchSize: 1,
245 | minInstancesInService: 0,
246 | suspendProcesses: [
247 | autoscaling.ScalingProcess.HEALTH_CHECK,
248 | autoscaling.ScalingProcess.REPLACE_UNHEALTHY,
249 | autoscaling.ScalingProcess.AZ_REBALANCE,
250 | autoscaling.ScalingProcess.ALARM_NOTIFICATION,
251 | autoscaling.ScalingProcess.SCHEDULED_ACTIONS
252 | ],
253 | waitOnResourceSignals: false
254 | })
255 | });
256 |
257 | if (!this.requiresDedicatedHosts()) {
258 | this.createTagBasedResourceGroup(resourceGroupName, resourceGroupDescription, asg.autoScalingGroupName);
259 | }
260 |
261 | if (props.stage === ENVIRONMENT_STAGE.Beta) {
262 | new autoscaling.CfnScheduledAction(this, 'SpinDownBetaInstances', {
263 | autoScalingGroupName: asg.autoScalingGroupName,
264 | // 1 day from now
265 | startTime: new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000).toISOString(),
266 | desiredCapacity: 0
267 | });
268 | }
269 | }
270 |
271 | // a host resource group is used by the launch template for placement of instances on dedicated hosts
272 | createHostResourceGroup(resourceGroupName: string, resourceGroupDescription: string) {
273 | return new resourcegroups.CfnGroup(this, resourceGroupName, {
274 | name: resourceGroupName,
275 | description: resourceGroupDescription,
276 | configuration: [
277 | {
278 | // This resource group is only used for management of dedicated hosts, as indicated by
279 | // the "AWS::EC2::HostManagement" type
280 | type: 'AWS::EC2::HostManagement',
281 | parameters: [
282 | {
283 | name: 'auto-allocate-host',
284 | values: ['true']
285 | },
286 | {
287 | name: 'auto-release-host',
288 | values: ['true']
289 | },
290 | {
291 | name: 'any-host-based-license-configuration',
292 | values: ['true']
293 | }
294 | ]
295 | },
296 | {
297 | type: 'AWS::ResourceGroups::Generic',
298 | parameters: [
299 | {
300 | name: 'allowed-resource-types',
301 | values: ['AWS::EC2::Host']
302 | },
303 | {
304 | name: 'deletion-protection',
305 | values: ['UNLESS_EMPTY']
306 | }
307 | ]
308 | }
309 | ]
310 | });
311 | }
312 |
313 | // tag based resource groups filter EC2 instances by tag, anything matching will be included in the group
314 | createTagBasedResourceGroup(resourceGroupName: string, resourceGroupDescription: string, asgName: string) {
315 | return new resourcegroups.CfnGroup(this, resourceGroupName, {
316 | name: resourceGroupName,
317 | description: resourceGroupDescription,
318 | resourceQuery: {
319 | type: 'TAG_FILTERS_1_0',
320 | query: {
321 | resourceTypeFilters: ['AWS::EC2::Instance'],
322 | tagFilters: [
323 | {
324 | key: 'aws:autoscaling:groupName',
325 | values: [asgName]
326 | }
327 | ]
328 | }
329 | }
330 | });
331 | }
332 | }
333 |
--------------------------------------------------------------------------------
/lib/aspects/stack-termination-protection.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // Finch Infrastructure
4 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5 |
6 | import { Aspects, IAspect, Stack } from "aws-cdk-lib";
7 | import { IConstruct } from "constructs";
8 |
9 | /**
10 | * Enable termination protection in all stacks.
11 | */
12 | export class EnableTerminationProtectionOnStacks implements IAspect {
13 | visit(construct: IConstruct): void {
14 | if (Stack.isStack(construct)) {
15 | (construct).terminationProtection = true;
16 | }
17 | }
18 | }
19 |
20 | export function applyTerminationProtectionOnStacks(constructs: IConstruct[]) {
21 | constructs.forEach((construct) => {
22 | Aspects.of(construct).add(new EnableTerminationProtectionOnStacks());
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/lib/cloudfront_cdn.ts:
--------------------------------------------------------------------------------
1 | import * as s3 from 'aws-cdk-lib/aws-s3';
2 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
3 | import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins';
4 | import * as iam from 'aws-cdk-lib/aws-iam';
5 | import { Construct } from 'constructs';
6 | import { CfnOutput, Stack } from 'aws-cdk-lib';
7 |
8 | interface CloudfrontCdnProps {
9 | bucket: s3.Bucket;
10 | }
11 |
12 | export class CloudfrontCdn extends Construct {
13 | public readonly urlOutput: CfnOutput;
14 | constructor(parent: Stack, id: string, props: CloudfrontCdnProps) {
15 | super(parent, id);
16 |
17 | const cloudfrontOAI = new cloudfront.OriginAccessIdentity(this, 'cloudfront-OAI', {
18 | comment: 'OAI for artifact bucket cloudfront'
19 | });
20 |
21 | props.bucket.addToResourcePolicy(
22 | new iam.PolicyStatement({
23 | actions: ['s3:GetObject'],
24 | resources: [props.bucket.arnForObjects('*')],
25 | principals: [new iam.CanonicalUserPrincipal(cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId)]
26 | })
27 | );
28 | new CfnOutput(this, 'S3 bucket', { value: props.bucket.bucketName });
29 |
30 | const distribution = new cloudfront.Distribution(this, 'ArtifactBucketDistribution', {
31 | defaultBehavior: {
32 | origin: new cloudfront_origins.S3Origin(props.bucket, {
33 | originAccessIdentity: cloudfrontOAI
34 | }),
35 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
36 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD
37 | }
38 | });
39 | new CfnOutput(this, 'Distribution Id', {
40 | value: distribution.distributionId
41 | });
42 | this.urlOutput = new CfnOutput(this, 'Distribution Domain', {
43 | value: distribution.domainName
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/codebuild-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import * as codebuild from 'aws-cdk-lib/aws-codebuild';
3 | import * as ec2 from 'aws-cdk-lib/aws-ec2';
4 | import * as iam from 'aws-cdk-lib/aws-iam';
5 | import { Key } from 'aws-cdk-lib/aws-kms';
6 | import { Construct } from 'constructs';
7 | import {
8 | BuildImageOS,
9 | GITHUB_ALLOWLISTED_ACCOUNT_IDS,
10 | LinuxAMIBuildImage,
11 | MacAMIBuildImage,
12 | toStackName,
13 | WindowsAMIBuildImage
14 | } from './utils';
15 |
16 | const webhookFiltersArr: codebuild.FilterGroup[] = [];
17 |
18 | for (const userId of GITHUB_ALLOWLISTED_ACCOUNT_IDS) {
19 | console.log('creating filter group for userid: ', userId);
20 | webhookFiltersArr.push(
21 | codebuild.FilterGroup.inEventOf(codebuild.EventAction.WORKFLOW_JOB_QUEUED).andActorAccountIs(userId)
22 | );
23 | }
24 |
25 | const githHubSource = codebuild.Source.gitHub({
26 | owner: 'runfinch',
27 | repo: 'finch',
28 | webhook: true,
29 | webhookFilters: webhookFiltersArr,
30 | fetchSubmodules: true,
31 | cloneDepth: 0,
32 | });
33 |
34 | interface ImageFilterProps {
35 | 'virtualization-type': string[];
36 | 'root-device-type': string[];
37 | 'owner-alias': string[];
38 | }
39 |
40 | /**
41 | * Default properties for CodeBuildStack configuration.
42 | * Contains static readonly properties that define default values for image filters,
43 | * fleet configuration, and project environment settings.
44 | */
45 | class CodeBuildStackDefaultProps {
46 | static readonly imageFilterProps: ImageFilterProps = {
47 | 'virtualization-type': ['hvm'],
48 | 'root-device-type': ['ebs'],
49 | 'owner-alias': ['amazon']
50 | };
51 | static readonly fleetProps = {
52 | computeType: codebuild.FleetComputeType.MEDIUM,
53 | baseCapacity: 1
54 | };
55 | static readonly projectEnvironmentProps = {
56 | computeType: codebuild.ComputeType.MEDIUM
57 | };
58 | static readonly terminationProtection: boolean = true;
59 | }
60 |
61 | class CodeBuildStackProps {
62 | env: cdk.Environment | undefined;
63 | projectName: string;
64 | region: string;
65 | arch: string;
66 | operatingSystem: string;
67 | amiSearchString: string;
68 | environmentType: codebuild.EnvironmentType;
69 | buildImageOS: BuildImageOS;
70 | imageFilterProps?: ImageFilterProps;
71 | fleetProps?: {
72 | computeType: codebuild.FleetComputeType;
73 | baseCapacity: number;
74 | };
75 | projectEnvironmentProps?: {
76 | computeType: codebuild.ComputeType;
77 | };
78 | }
79 |
80 | export class CodeBuildStack extends cdk.Stack {
81 | constructor(scope: Construct, id: string, props: CodeBuildStackProps) {
82 | super(scope, id, {
83 | ...props,
84 | terminationProtection: CodeBuildStackDefaultProps.terminationProtection,
85 | });
86 | this.createBuildProject(props, id);
87 | }
88 |
89 | private createBuildProject(props: CodeBuildStackProps, id: string): codebuild.Project {
90 | const platformId: string = `${props.operatingSystem}-${toStackName(props.arch)}`;
91 |
92 | const secretArn = this.formatArn({
93 | service: 'secretsmanager',
94 | resource: 'secret',
95 | resourceName: `codebuild-github-access-token-??????`,
96 | arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
97 | });
98 |
99 | const machineImageProps = {
100 | name: props.amiSearchString,
101 | filters: {
102 | ...(props.imageFilterProps || CodeBuildStackDefaultProps.imageFilterProps),
103 | architecture: [props.arch]
104 | }
105 | };
106 | const machineImage = new ec2.LookupMachineImage(machineImageProps);
107 |
108 | const fleetServiceRole = new iam.Role(this, `FleetServiceRole-${platformId}`, {
109 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
110 | managedPolicies: [
111 | iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2FullAccess'),
112 | iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')
113 | ]
114 | });
115 |
116 | const fleet = new codebuild.Fleet(this, `Fleet-${toStackName(props.arch)}`, {
117 | ...(props.fleetProps || CodeBuildStackDefaultProps.fleetProps),
118 | environmentType: props.environmentType
119 | });
120 |
121 | const imageId: string = machineImage.getImage(this).imageId;
122 |
123 | const cfnFleet = fleet.node.defaultChild as cdk.CfnResource;
124 | cfnFleet.addPropertyOverride('ImageId', imageId);
125 | cfnFleet.addPropertyOverride('FleetServiceRole', fleetServiceRole.roleArn);
126 |
127 | const codebuildProject = new codebuild.Project(this, id, {
128 | projectName: props.projectName,
129 | source: githHubSource,
130 | environment: {
131 | ...(props.projectEnvironmentProps || CodeBuildStackDefaultProps.projectEnvironmentProps),
132 | fleet: fleet,
133 | buildImage: this.getBuildImageByOS(props.buildImageOS, props.environmentType, imageId)
134 | },
135 | encryptionKey: new Key(this, `codebuild-${platformId}-key-${props.region}`, {
136 | description: 'Kms Key to encrypt data-at-rest',
137 | alias: `finch-${platformId}-kms-${props.region}`,
138 | enabled: true
139 | })
140 | });
141 |
142 | codebuildProject.addToRolePolicy(
143 | new iam.PolicyStatement({
144 | actions: ['secretsmanager:GetSecretValue'],
145 | resources: [secretArn]
146 | })
147 | );
148 |
149 | return codebuildProject;
150 | }
151 |
152 | private getBuildImageByOS(
153 | os: BuildImageOS,
154 | environmentType: codebuild.EnvironmentType,
155 | imageId: string
156 | ): cdk.aws_codebuild.IBuildImage {
157 | switch (os) {
158 | case BuildImageOS.LINUX:
159 | return new LinuxAMIBuildImage(imageId, environmentType);
160 | case BuildImageOS.MAC:
161 | return new MacAMIBuildImage(imageId, environmentType);
162 | case BuildImageOS.WINDOWS:
163 | return new WindowsAMIBuildImage(imageId, environmentType);
164 | default:
165 | throw new Error(`Unsupported Build Image OS: ${os}`);
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/lib/continuous-integration-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import * as ecr from 'aws-cdk-lib/aws-ecr';
3 | import * as s3 from 'aws-cdk-lib/aws-s3';
4 | import * as iam from 'aws-cdk-lib/aws-iam';
5 | import { Construct } from 'constructs';
6 |
7 | import { CloudfrontCdn } from './cloudfront_cdn';
8 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
9 |
10 | interface ContinuousIntegrationStackProps extends cdk.StackProps {
11 | rootfsEcrRepository: ecr.Repository;
12 | }
13 |
14 | // ContinuousIntegrationStack - AWS stack for supporting Finch's continuous integration process
15 | export class ContinuousIntegrationStack extends cdk.Stack {
16 | constructor(scope: Construct, id: string, stage: string, props: ContinuousIntegrationStackProps) {
17 | super(scope, id, props);
18 | applyTerminationProtectionOnStacks([this]);
19 |
20 | const githubDomain = 'token.actions.githubusercontent.com';
21 |
22 | const ghProvider = new iam.OpenIdConnectProvider(this, 'githubProvider', {
23 | url: `https://${githubDomain}`,
24 | clientIds: ['sts.amazonaws.com']
25 | });
26 |
27 | const githubActionsRole = new iam.Role(this, 'GithubActionsRole', {
28 | assumedBy: new iam.WebIdentityPrincipal(ghProvider.openIdConnectProviderArn),
29 | roleName: 'GithubActionsRole',
30 | description: 'This role is used by GitHub Actions',
31 | maxSessionDuration: cdk.Duration.hours(1)
32 | });
33 |
34 | // Override docs: https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html#cfn_layer_raw
35 | // Condition from: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html#idp_oidc_Create_GitHub
36 | const cfnRole = githubActionsRole.node.defaultChild as iam.CfnRole;
37 | cfnRole.addOverride('Properties.AssumeRolePolicyDocument.Statement.0.Condition', {
38 | StringLike: {
39 | 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
40 | 'token.actions.githubusercontent.com:sub': 'repo:runfinch/*'
41 | }
42 | });
43 |
44 | const bucketName = `finch-dependencies-${stage.toLowerCase()}-${cdk.Stack.of(this)?.account}`;
45 |
46 | const bucket = new s3.Bucket(this, 'Dependencies', {
47 | bucketName,
48 | publicReadAccess: false,
49 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL
50 | });
51 | bucket.grantReadWrite(githubActionsRole);
52 |
53 | const repo = props.rootfsEcrRepository;
54 | repo.grantPullPush(githubActionsRole);
55 | ecr.AuthorizationToken.grantRead(githubActionsRole)
56 |
57 | new CloudfrontCdn(this, 'DependenciesCloudfrontCdn', {
58 | bucket
59 | });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/ecr-repo-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { CfnOutput } from 'aws-cdk-lib';
3 | import * as ecr from 'aws-cdk-lib/aws-ecr';
4 | import { Construct } from 'constructs';
5 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
6 |
7 | export class ECRRepositoryStack extends cdk.Stack {
8 | public readonly repositoryOutput: CfnOutput;
9 | public readonly repository: ecr.Repository;
10 | constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) {
11 | super(scope, id, props);
12 | applyTerminationProtectionOnStacks([this]);
13 |
14 | const repoName = `finch-rootfs-image-${stage.toLowerCase()}`;
15 | const ecrRepository = new ecr.Repository(this, 'finch-rootfs', {
16 | repositoryName:repoName,
17 | imageTagMutability: ecr.TagMutability.IMMUTABLE,
18 | // TODO: CFN does not provide APIs for enhanced image scanning.
19 | // To address, create a custom stack that uses the AWS sdk to change the account ECR
20 | // scanning settings to enhanced.
21 |
22 | // For now, scan on image push is set to true. This means that the image will be scanned
23 | // for vulnerabilites every time it is pushed up to the ECR repo. With enhanced scanning,
24 | // the image would be continously scanned for vulnerabilities.
25 | // See https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-enhanced.html
26 | imageScanOnPush: true,
27 | });
28 |
29 | this.repository = ecrRepository
30 | this.repositoryOutput = new CfnOutput(this, 'ECR repository', { value: ecrRepository.repositoryName });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/event-bridge-scan-notifs-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import * as events from 'aws-cdk-lib/aws-events';
3 | import * as iam from 'aws-cdk-lib/aws-iam';
4 | import * as lambda from 'aws-cdk-lib/aws-lambda';
5 | import * as sns from 'aws-cdk-lib/aws-sns';
6 | import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
7 | import * as targets from 'aws-cdk-lib/aws-events-targets';
8 | import { Construct } from 'constructs';
9 | import path from 'path';
10 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
11 |
12 | export class EventBridgeScanNotifsStack extends cdk.Stack {
13 | constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) {
14 | super(scope, id, props);
15 | applyTerminationProtectionOnStacks([this]);
16 |
17 | const topic = new sns.Topic(this, 'ECR Image Inspector Findings');
18 |
19 | // Let's not expose this on GitHub, will only be visible in AWS logs Finch team owns, which is low risk.
20 | // Secret has to be created only in Prod account.
21 | // unsafeUnwrap is used because SNS does not have any construct that accepts a SecretValue property.
22 | const securityEmail = cdk.SecretValue.secretsManager('security-notifications-email').unsafeUnwrap()
23 | topic.addSubscription(new subscriptions.EmailSubscription(securityEmail.toString()));
24 |
25 | const notificationFn = new lambda.Function(this, 'SendECRImageInspectorFindings', {
26 | runtime: lambda.Runtime.PYTHON_3_11,
27 | handler: 'main.lambda_handler',
28 | code: lambda.Code.fromAsset(path.join(__dirname, 'image-scanning-notifications-lambda-handler')),
29 | environment: {'SNS_ARN': topic.topicArn,},
30 | });
31 |
32 | const snsTopicPolicy = new iam.PolicyStatement({
33 | actions: ['sns:publish'],
34 | resources: ['*'],
35 | });
36 |
37 | notificationFn.addToRolePolicy(snsTopicPolicy);
38 |
39 | // Only publish CRITICAL and HIGH findings (more than 7.0 CVE score) that are ACTIVE
40 | // https://docs.aws.amazon.com/inspector/latest/user/findings-understanding-severity.html
41 | const rule = new events.Rule(this, 'rule', {
42 | eventPattern: {
43 | source: ['aws.inspector2'],
44 | detail: {
45 | severity: ['HIGH', 'CRITICAL'],
46 | status: events.Match.exactString('ACTIVE')
47 | },
48 | detailType: events.Match.exactString('Inspector2 Finding'),
49 | },
50 | });
51 |
52 | rule.addTarget(new targets.LambdaFunction(notificationFn))
53 | }
54 | }
--------------------------------------------------------------------------------
/lib/finch-pipeline-app-stage.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { CfnOutput } from 'aws-cdk-lib';
3 | import * as codebuild from 'aws-cdk-lib/aws-codebuild';
4 | import * as ecr from 'aws-cdk-lib/aws-ecr';
5 | import * as s3 from 'aws-cdk-lib/aws-s3';
6 | import { Construct } from 'constructs';
7 | import { PlatformType, RunnerProps } from '../config/runner-config';
8 | import { ArtifactBucketCloudfrontStack } from './artifact-bucket-cloudfront';
9 | import { ASGRunnerStack } from './asg-runner-stack';
10 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
11 | import { CodeBuildStack } from './codebuild-stack';
12 | import { ContinuousIntegrationStack } from './continuous-integration-stack';
13 | import { ECRRepositoryStack } from './ecr-repo-stack';
14 | import { EventBridgeScanNotifsStack } from './event-bridge-scan-notifs-stack';
15 | import { PVREReportingStack } from './pvre-reporting-stack';
16 | import { SSMPatchingStack } from './ssm-patching-stack';
17 | import { CODEBUILD_STACKS, toStackName } from './utils';
18 |
19 | export enum ENVIRONMENT_STAGE {
20 | Beta,
21 | Prod,
22 | Release
23 | }
24 |
25 | interface FinchPipelineAppStageProps extends cdk.StageProps {
26 | environmentStage: ENVIRONMENT_STAGE;
27 | runnerConfig: RunnerProps;
28 | }
29 |
30 | export class FinchPipelineAppStage extends cdk.Stage {
31 | artifactBucketCloudfrontUrlOutput: CfnOutput;
32 | ecrRepositoryOutput: CfnOutput;
33 | public readonly cloudfrontBucket: s3.Bucket;
34 | public readonly ecrRepository: ecr.Repository;
35 |
36 | constructor(scope: Construct, id: string, props: FinchPipelineAppStageProps) {
37 | super(scope, id, props);
38 | applyTerminationProtectionOnStacks([this]);
39 | props.runnerConfig.runnerTypes.forEach((runnerType) => {
40 | const ASGStackName = `ASG-${runnerType.platform}-${runnerType.repo}-${runnerType.version.split('.')[0]}-${runnerType.arch}Stack`;
41 | let licenseArn: string | undefined;
42 | switch (runnerType.platform) {
43 | case PlatformType.MAC: {
44 | licenseArn = props.runnerConfig.macLicenseArn;
45 | break;
46 | }
47 | case PlatformType.WINDOWS: {
48 | licenseArn = props.runnerConfig.windowsLicenseArn;
49 | break;
50 | }
51 | }
52 | new ASGRunnerStack(this, ASGStackName, {
53 | env: props.env,
54 | stage: props.environmentStage,
55 | type: runnerType,
56 | licenseArn
57 | });
58 | });
59 |
60 | if (props.environmentStage !== ENVIRONMENT_STAGE.Release) {
61 | const artifactBucketCloudfrontStack = new ArtifactBucketCloudfrontStack(
62 | this,
63 | 'ArtifactCloudfront',
64 | this.stageName
65 | );
66 | this.artifactBucketCloudfrontUrlOutput = artifactBucketCloudfrontStack.urlOutput;
67 | this.cloudfrontBucket = artifactBucketCloudfrontStack.bucket;
68 |
69 | const ecrRepositoryStack = new ECRRepositoryStack(this, 'ECRRepositoryStack', this.stageName);
70 |
71 | this.ecrRepositoryOutput = ecrRepositoryStack.repositoryOutput;
72 | this.ecrRepository = ecrRepositoryStack.repository;
73 |
74 | // Only report rootfs image scans in prod to avoid duplicate notifications.
75 | if (props.environmentStage == ENVIRONMENT_STAGE.Prod) {
76 | new EventBridgeScanNotifsStack(this, 'EventBridgeScanNotifsStack', this.stageName);
77 | }
78 |
79 | new ContinuousIntegrationStack(this, 'FinchContinuousIntegrationStack', this.stageName, {
80 | rootfsEcrRepository: this.ecrRepository
81 | });
82 | }
83 |
84 | new PVREReportingStack(this, 'PVREReportingStack', { terminationProtection: true });
85 |
86 | new SSMPatchingStack(this, 'SSMPatchingStack', { terminationProtection: true });
87 |
88 | // Create Ubuntu Codebuild projects for each arch
89 | // CodeBuild credentials are account-wide, so creating them multiple times within the for
90 | // loop causes an error.
91 | // TODO: refactor CodeBuildStack into CodeBuildProjects and loop inside of the constructor.
92 | const codebuildCredsStack = new (class CodeBuildCredentialsStack extends cdk.Stack {
93 | constructor(scope: Construct, id: string) {
94 | super(scope, id, {
95 | ...props,
96 | terminationProtection: true,
97 | });
98 | new codebuild.GitHubSourceCredentials(this, `code-build-credentials`, {
99 | accessToken: cdk.SecretValue.secretsManager('codebuild-github-access-token')
100 | });
101 | }
102 | })(this, 'CodeBuildStack-credentials');
103 |
104 | for (const { arch, operatingSystem, amiSearchString, environmentType, buildImageOS } of CODEBUILD_STACKS) {
105 | const codeBuildStack = new CodeBuildStack(this, `CodeBuildStack-${operatingSystem}-${toStackName(arch)}`, {
106 | env: props.env,
107 | projectName: `finch-${arch}-${props.environmentStage}-instance`,
108 | region: 'us-west-2',
109 | arch,
110 | amiSearchString,
111 | operatingSystem,
112 | buildImageOS: buildImageOS,
113 | environmentType: environmentType
114 | });
115 |
116 | codeBuildStack.addDependency(codebuildCredsStack);
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/lib/finch-pipeline-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { BuildSpec } from 'aws-cdk-lib/aws-codebuild';
3 | import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
4 | import { Construct } from 'constructs';
5 | import { EnvConfig } from '../config/env-config';
6 | import { RunnerConfig } from '../config/runner-config';
7 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
8 | import { ENVIRONMENT_STAGE, FinchPipelineAppStage } from './finch-pipeline-app-stage';
9 |
10 | export class FinchPipelineStack extends cdk.Stack {
11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) {
12 | super(scope, id, props);
13 | applyTerminationProtectionOnStacks([this]);
14 |
15 | const source = CodePipelineSource.gitHub('runfinch/infrastructure', 'main', {
16 | authentication: cdk.SecretValue.secretsManager('pipeline-github-access-token')
17 | });
18 |
19 | const pipeline = new CodePipeline(this, 'FinchPipeline', {
20 | pipelineName: 'FinchPipeline',
21 | crossAccountKeys: true,
22 | synth: new ShellStep('Synth', {
23 | input: source,
24 | commands: ['npm ci', 'npm run build', 'npx cdk synth']
25 | }),
26 | synthCodeBuildDefaults: {
27 | partialBuildSpec: BuildSpec.fromObject({
28 | phases: {
29 | install: {
30 | 'runtime-versions': {
31 | nodejs: '20'
32 | }
33 | }
34 | }
35 | })
36 | }
37 | });
38 |
39 | const betaApp = new FinchPipelineAppStage(this, 'Beta', {
40 | environmentStage: ENVIRONMENT_STAGE.Beta,
41 | env: {
42 | account: EnvConfig.envBeta.account,
43 | region: EnvConfig.envBeta.region
44 | },
45 | runnerConfig: RunnerConfig.runnerBeta
46 | });
47 | const betaStage = pipeline.addStage(betaApp);
48 | // add a post step for unit and integration tests
49 | betaStage.addPost(
50 | new ShellStep('Unit and Integration Test', {
51 | input: source,
52 | commands: ['npm install', 'npm run build', 'npm run test', 'npm run integration'],
53 | envFromCfnOutputs: {
54 | CLOUDFRONT_URL: betaApp.artifactBucketCloudfrontUrlOutput
55 | }
56 | })
57 | );
58 | // Add stages to a wave to deploy them in parallel.
59 | const wave = pipeline.addWave('wave');
60 |
61 | const prodApp = new FinchPipelineAppStage(this, 'Production', {
62 | environmentStage: ENVIRONMENT_STAGE.Prod,
63 | env: {
64 | account: EnvConfig.envProd.account,
65 | region: EnvConfig.envProd.region
66 | },
67 | runnerConfig: RunnerConfig.runnerProd
68 | });
69 | wave.addStage(prodApp);
70 |
71 | const releaseApp = new FinchPipelineAppStage(this, 'Release', {
72 | environmentStage: ENVIRONMENT_STAGE.Release,
73 | env: {
74 | account: EnvConfig.envRelease.account,
75 | region: EnvConfig.envRelease.region
76 | },
77 | runnerConfig: RunnerConfig.runnerRelease
78 | });
79 | wave.addStage(releaseApp);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/image-scanning-notifications-lambda-handler/main.py:
--------------------------------------------------------------------------------
1 | '''
2 | lambda function to read ECR Image Inpsection events from Amazon EventBridge
3 | and send notifications to Finch team regarding security notifications.
4 | '''
5 | import boto3
6 | import os
7 |
8 | def build_message(event):
9 | '''build_message reads an {event} from Inspector image scanning and builds
10 | the body of reporting email with vulnerability findings.
11 |
12 | :param EventBridgeEvent event: The EventBridgeEvent containing an Inspector scan finding.
13 |
14 | Schema: https://docs.aws.amazon.com/inspector/latest/user/eventbridge-integration.html#event-finding
15 | '''
16 | detail = event['detail']
17 | title = detail['title']
18 | description = detail['description']
19 | severity = detail['severity']
20 | source_url = detail['packageVulnerabilityDetails']['sourceUrl']
21 | status = detail['status']
22 | type = detail['type']
23 | finding_arn = detail['findingArn']
24 | first_observed_at = detail['firstObservedAt']
25 |
26 | message = f'''{title} - Severity {severity}
27 |
28 | Severity: {severity}
29 | Type: {type}
30 | Description: {description}
31 | Source URL: {source_url}
32 |
33 | Status: {status}
34 | Observed: {first_observed_at}
35 |
36 | For more info, view the finding via ARN in the AWS Console: {finding_arn}
37 | '''
38 |
39 | return message
40 |
41 | def send_sns(subject: str, message: str):
42 | '''send_sns sends an email with subject and body
43 |
44 | :param str subject: The subject of the email
45 | :param str message: The body of the email
46 | '''
47 | client = boto3.client("sns")
48 | topic_arn = os.environ["SNS_ARN"]
49 | client.publish(TopicArn=topic_arn, Message=message, Subject=subject)
50 |
51 | def lambda_handler(event, context) -> dict:
52 | '''lambda_handler handles EventBridge events, calling send_sns to send an email for security findings.
53 |
54 | :param EventBridgeEvent event: the EventBridge event
55 | :param LambdaContext context: the Lambda execution context
56 | '''
57 | detailType = event["detail-type"]
58 |
59 | if (detailType == "Inspector2 Finding"):
60 | subject = "Rootfs Image Security Finding"
61 | message = build_message(event)
62 | send_sns(subject, message)
63 | else:
64 | print("No findings found, skipping sending email")
65 |
66 | return {'statusCode': 200}
67 |
--------------------------------------------------------------------------------
/lib/pvre-reporting-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import * as cfninc from 'aws-cdk-lib/cloudformation-include';
3 | import { Construct } from 'constructs';
4 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
5 |
6 | export class PVREReportingStack extends cdk.Stack {
7 | constructor(scope: Construct, id: string, props?: cdk.StackProps) {
8 | super(scope, id, props);
9 | applyTerminationProtectionOnStacks([this]);
10 |
11 | new cfninc.CfnInclude(this, 'PVREReportingTemplate', {
12 | templateFile: 'lib/pvre-reporting-template.yml'
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/lib/pvre-reporting-template.yml:
--------------------------------------------------------------------------------
1 | Resources:
2 | InventoryCollection:
3 | Type: "AWS::SSM::Association"
4 | Properties:
5 | Name: {Ref: PvreInventoryCollectionDocument}
6 | AssociationName:
7 | Fn::Sub: "${AWS::AccountId}-InventoryCollection"
8 | ScheduleExpression: "rate(12 hours)"
9 | Targets:
10 | - Key: "tag:PVRE-Reporting"
11 | Values:
12 | - "SSM"
13 | PvreReporting:
14 | Type: "AWS::SSM::ResourceDataSync"
15 | Properties:
16 | BucketName:
17 | Fn::Sub: "pvrev2-prod-${AWS::Region}-ssm-updates"
18 | BucketRegion:
19 | Ref: "AWS::Region"
20 | SyncFormat: "JsonSerDe"
21 | SyncName:
22 | Fn::Sub: "${AWS::AccountId}-PvreReporting"
23 | PvreInventoryCollectionDocument:
24 | Type: "AWS::SSM::Document"
25 | Properties:
26 | Name:
27 | Fn::Sub: "PVREInventoryCollectionDocument"
28 | DocumentType: "Command"
29 | Content:
30 | schemaVersion: "2.2"
31 | description: "Collect software inventory and kernel information"
32 | parameters:
33 | applications:
34 | type: "String"
35 | default: "Enabled"
36 | description: "(Optional) Collect data for installed applications."
37 | allowedValues:
38 | - "Enabled"
39 | - "Disabled"
40 | awsComponents:
41 | type: "String"
42 | default: "Enabled"
43 | description: "(Optional) Collect data for AWS Components like amazon-ssm-agent."
44 | allowedValues:
45 | - "Enabled"
46 | - "Disabled"
47 | files:
48 | type: "String"
49 | default: ""
50 | description: "(Optional, requires SSMAgent version 2.2.64.0 and above)
Linux example:
[{\"Path\":\"/usr/bin\", \"Pattern\":[\"aws*\", \"*ssm*\"],\"Recursive\":false},{\"Path\":\"/var/log\", \"Pattern\":[\"amazon*.*\"], \"Recursive\":true, \"DirScanLimit\":1000}]
Windows example:
[{\"Path\":\"%PROGRAMFILES%\", \"Pattern\":[\"*.exe\"],\"Recursive\":true}]
Learn More: http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-about.html#sysman-inventory-file-and-registry
"
51 | displayType: "textarea"
52 | networkConfig:
53 | type: "String"
54 | default: "Enabled"
55 | description: "(Optional) Collect data for Network configurations."
56 | allowedValues:
57 | - "Enabled"
58 | - "Disabled"
59 | windowsUpdates:
60 | type: "String"
61 | default: "Enabled"
62 | description: "(Optional, Windows OS only) Collect data for all Windows Updates."
63 | allowedValues:
64 | - "Enabled"
65 | - "Disabled"
66 | instanceDetailedInformation:
67 | type: "String"
68 | default: "Enabled"
69 | description: "(Optional) Collect additional information about the instance, including the CPU model, speed, and the number of cores, to name a few."
70 | allowedValues:
71 | - "Enabled"
72 | - "Disabled"
73 | services:
74 | type: "String"
75 | default: "Enabled"
76 | description: "(Optional, Windows OS only, requires SSMAgent version 2.2.64.0 and above) Collect data for service configurations."
77 | allowedValues:
78 | - "Enabled"
79 | - "Disabled"
80 | windowsRegistry:
81 | type: "String"
82 | default: ""
83 | description: "(Optional, Windows OS only, requires SSMAgent version 2.2.64.0 and above)
Example:
[{\"Path\":\"HKEY_CURRENT_CONFIG\\System\",\"Recursive\":true},{\"Path\":\"HKEY_LOCAL_MACHINE\\SOFTWARE\\Amazon\\MachineImage\", \"ValueNames\":[\"AMIName\"]}]
Learn More: http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-about.html#sysman-inventory-file-and-registry
"
84 | displayType: "textarea"
85 | windowsRoles:
86 | type: "String"
87 | default: "Enabled"
88 | description: "(Optional, Windows OS only, requires SSMAgent version 2.2.64.0 and above) Collect data for Microsoft Windows role configurations."
89 | allowedValues:
90 | - "Enabled"
91 | - "Disabled"
92 | customInventory:
93 | type: "String"
94 | default: "Enabled"
95 | description: "(Optional) Collect data for custom inventory."
96 | allowedValues:
97 | - "Enabled"
98 | - "Disabled"
99 | billingInfo:
100 | type: "String"
101 | default: "Enabled"
102 | description: "(Optional) Collect billing info for license included applications."
103 | allowedValues:
104 | - "Enabled"
105 | - "Disabled"
106 | mainSteps:
107 | - action: "aws:runShellScript"
108 | name: "collectCustomInventoryItems"
109 | inputs:
110 | timeoutSeconds: 7200
111 | runCommand:
112 | - "#!/bin/bash"
113 | - "token=$(curl --silent --show-error --retry 3 -X PUT \"http://169.254.169.254/latest/api/token\" -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\")"
114 | - "instance_id=$(curl --silent --show-error --retry 3 -H \"X-aws-ec2-metadata-token: $token\" http://169.254.169.254/latest/meta-data/instance-id)"
115 | - "kernel_version=$(uname -r)"
116 | - "content=\"{\\\"SchemaVersion\\\": \\\"1.0\\\", \\\"TypeName\\\": \\\"Custom:SystemInfo\\\", \\\"Content\\\": {\\\"KernelVersion\\\": \\\"$kernel_version\\\"}}\""
117 | - "dir_path=\"/var/lib/amazon/ssm/$instance_id/inventory/custom\""
118 | - "mkdir -p $dir_path"
119 | - "echo $content > $dir_path/CustomSystemInfo.json"
120 | - action: "aws:softwareInventory"
121 | name: "collectSoftwareInventoryItems"
122 | inputs:
123 | applications: "{{ applications }}"
124 | awsComponents: "{{ awsComponents }}"
125 | networkConfig: "{{ networkConfig }}"
126 | files: "{{ files }}"
127 | services: "{{ services }}"
128 | windowsRoles: "{{ windowsRoles }}"
129 | windowsRegistry: "{{ windowsRegistry}}"
130 | windowsUpdates: "{{ windowsUpdates }}"
131 | instanceDetailedInformation: "{{ instanceDetailedInformation }}"
132 | billingInfo: "{{ billingInfo }}"
133 | customInventory: "{{ customInventory }}"
--------------------------------------------------------------------------------
/lib/ssm-patching-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { CfnMaintenanceWindow, CfnMaintenanceWindowTarget, CfnMaintenanceWindowTask } from 'aws-cdk-lib/aws-ssm';
3 | import { Construct } from 'constructs';
4 | import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection';
5 |
6 | export class SSMPatchingStack extends cdk.Stack {
7 | constructor(scope: Construct, id: string, props?: cdk.StackProps) {
8 | super(scope, id, props);
9 | applyTerminationProtectionOnStacks([this]);
10 |
11 | const maintenanceWindow = new CfnMaintenanceWindow(this, 'MaintenanceWindow', {
12 | name: `Patching-Window`,
13 | allowUnassociatedTargets: false,
14 | cutoff: 0,
15 | duration: 2,
16 | // Every day at 8 AM UTC
17 | schedule: 'cron(0 8 ? * * *)'
18 | });
19 |
20 | const maintenanceTarget = new CfnMaintenanceWindowTarget(this, 'MaintenanceWindowTarget', {
21 | name: 'All-Instances-Patch-Target',
22 | windowId: maintenanceWindow.ref,
23 | resourceType: 'INSTANCE',
24 | targets: [
25 | {
26 | key: 'tag:PVRE-Reporting',
27 | values: ['SSM']
28 | }
29 | ]
30 | });
31 |
32 | new CfnMaintenanceWindowTask(this, 'MaintenanceWindowTask', {
33 | taskArn: 'AWS-RunPatchBaseline',
34 | priority: 1,
35 | taskType: 'RUN_COMMAND',
36 | windowId: maintenanceWindow.ref,
37 | name: 'Patch-Task',
38 | targets: [
39 | {
40 | key: 'WindowTargetIds',
41 | values: [maintenanceTarget.ref]
42 | }
43 | ],
44 | taskInvocationParameters: {
45 | maintenanceWindowRunCommandParameters: {
46 | parameters: {
47 | Operation: ['Install']
48 | },
49 | documentVersion: '$LATEST'
50 | }
51 | },
52 | maxErrors: '100%',
53 | maxConcurrency: '1'
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import * as codebuild from 'aws-cdk-lib/aws-codebuild';
2 |
3 | export enum BuildImageOS {
4 | LINUX = 'linux',
5 | WINDOWS = 'windows',
6 | MAC = 'mac'
7 | }
8 |
9 | export interface CodeBuildStackArgs {
10 | operatingSystem: string;
11 | arch: string;
12 | amiSearchString: string;
13 | environmentType: codebuild.EnvironmentType;
14 | buildImageOS: BuildImageOS;
15 | }
16 |
17 | export const CODEBUILD_STACKS: CodeBuildStackArgs[] = [
18 | {
19 | operatingSystem: 'ubuntu',
20 | arch: 'x86_64',
21 | amiSearchString: 'ubuntu/images/hvm-ssd/ubuntu*22.04*',
22 | environmentType: codebuild.EnvironmentType.LINUX_EC2,
23 | buildImageOS: BuildImageOS.LINUX
24 | },
25 | {
26 | operatingSystem: 'ubuntu',
27 | arch: 'arm64',
28 | amiSearchString: 'ubuntu/images/hvm-ssd/ubuntu*22.04*',
29 | environmentType: codebuild.EnvironmentType.ARM_EC2,
30 | buildImageOS: BuildImageOS.LINUX
31 | }
32 | ];
33 |
34 | // members of the runfinch org (+dependabot)
35 | // curl -s https://api.github.com/users/ | jq '.id'
36 | // TODO: automate fetching account ID's
37 | export const GITHUB_ALLOWLISTED_ACCOUNT_IDS = [
38 | '47769978', // coderbirju
39 | '424987', // pendo324
40 | '55906459', // austinvazquez
41 | '2304727', // Kern--
42 | '2967759', // henry118
43 | '47723536', // Shubhranshu153
44 | '55555210', // sondavidb
45 | '5525370', // swagatbora90
46 | '59450965', // cezar-r
47 | '49699333' // dependabot[bot] github app
48 | ];
49 |
50 | /**
51 | * Replaces all underscores with hyphens in a string.
52 | * Used for making strings compatible with stack names, which don't allow "_" in the name.
53 | *
54 | * @param name The string to process
55 | * @returns A new string with all underscores replaced by hyphens
56 | */
57 | export const toStackName = (name: string) => {
58 | return name.replace(/_/g, '-');
59 | };
60 |
61 | // @ts-expect-error Extending private class
62 | export class LinuxAMIBuildImage extends codebuild.LinuxBuildImage implements codebuild.IBuildImage {
63 | declare type: codebuild.EnvironmentType;
64 |
65 | constructor(imageId: string, environmentType: codebuild.EnvironmentType) {
66 | // @ts-expect-error Extending private class
67 | super({ imageId });
68 | this.type = environmentType;
69 | }
70 | }
71 |
72 | // @ts-expect-error Extending private class
73 | export class WindowsAMIBuildImage extends codebuild.WindowsBuildImage implements codebuild.IBuildImage {
74 | declare type: codebuild.EnvironmentType;
75 |
76 | constructor(imageId: string, environmentType: codebuild.EnvironmentType) {
77 | // @ts-expect-error Extending private class
78 | super({ imageId });
79 | this.type = environmentType;
80 | }
81 | }
82 |
83 | // @ts-expect-error Extending private class
84 | export class MacAMIBuildImage extends codebuild.LinuxBuildImage implements codebuild.IBuildImage {
85 | declare type: codebuild.EnvironmentType;
86 |
87 | constructor(imageId: string, environmentType: codebuild.EnvironmentType) {
88 | // @ts-expect-error Extending private class
89 | super({ imageId });
90 | this.type = environmentType;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "finch-aws-cdk",
3 | "version": "0.1.0",
4 | "bin": {
5 | "finch-aws-cdk": "bin/finch-aws-cdk.js"
6 | },
7 | "scripts": {
8 | "build": "tsc",
9 | "watch": "tsc -w",
10 | "test": "jest -c ./jest.config.unit.js",
11 | "integration": "jest -c ./jest.config.integration.js",
12 | "cdk": "cdk",
13 | "prettier-format": "prettier --config .prettierrc --write 'lib/*.ts' 'bin/*.ts' 'test/*.ts' 'integration_test/*.ts' 'config/*.ts'"
14 | },
15 | "devDependencies": {
16 | "@tsconfig/node18": "^18.2.4",
17 | "@types/jest": "^29.5.14",
18 | "@types/node": "^22.15.30",
19 | "@types/prettier": "3.0.0",
20 | "aws-cdk": "^2.1018.1",
21 | "jest": "^29.7.0",
22 | "prettier": "^3.5.3",
23 | "ts-jest": "^29.4.0",
24 | "ts-node": "^10.9.2",
25 | "typescript": "^5.8.3"
26 | },
27 | "dependencies": {
28 | "aws-cdk-lib": "^2.201.0",
29 | "axios": "^1.10.0",
30 | "constructs": "^10.4.2",
31 | "source-map-support": "^0.5.21"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/scripts/setup-linux-runner.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 | set -ex
3 |
4 | # The user data script is run as root so do not use sudo command
5 |
6 | # Log output
7 | exec &> >(tee /var/log/setup-runner.log)
8 | echo $0
9 |
10 | # load all variables from /etc/os-release prefixed with OS_RELEASE as to not clobber
11 | OS_RELEASE_PREFIX="OS_RELEASE"
12 | source <(sed -Ee "s/^([^#])/${OS_RELEASE_PREFIX}_\1/" "/etc/os-release")
13 |
14 | # set variables to make accessing values in /etc/os-release easier
15 | eval "OS_NAME=\"\${${OS_RELEASE_PREFIX}_NAME}\""
16 | eval "OS_VERSION=\"\${${OS_RELEASE_PREFIX}_VERSION_ID}\""
17 |
18 | # configure download parameters based on architecture
19 | UNAME_MACHINE="$(/usr/bin/uname -m)"
20 | if [ "${UNAME_MACHINE}" = "aarch64" ]; then
21 | GH_RUNNER_ARCH="arm64"
22 | NODE_DOWNLOAD_ARCH="arm64"
23 | GH_RUNNER_DOWNLOAD_HASH="a96b0cec7b0237ca5e4210982368c6f7d8c2ab1e5f6b2604c1ccede9cedcb143"
24 | else
25 | GH_RUNNER_ARCH="x64"
26 | NODE_DOWNLOAD_ARCH="x86_64"
27 | GH_RUNNER_DOWNLOAD_HASH="b13b784808359f31bc79b08a191f5f83757852957dd8fe3dbfcc38202ccf5768"
28 | fi
29 |
30 | if [ "${OS_NAME}" = "Amazon Linux" ]; then
31 | USERNAME="ec2-user"
32 | DISTRO="amazonlinux"
33 | BASE_PACKAGES="golang zlib-static containerd nerdctl cni-plugins iptables"
34 | if [ "${OS_VERSION}" = "2" ]; then
35 | GH_RUNNER_DEPENDENCIES="openssl krb5-libs zlib jq"
36 | ADDITIONAL_PACKAGES="policycoreutils-python systemd-rpm-macros inotify-tools ${GH_RUNNER_DEPENDENCIES}"
37 | NODE_VERSION="21.2.0"
38 | curl -OL "https://d3rnber7ry90et.cloudfront.net/linux-${NODE_DOWNLOAD_ARCH}/node-v${NODE_VERSION}.tar.gz"
39 | tar -xf node-v${NODE_VERSION}.tar.gz
40 | mv node-v${NODE_VERSION}/bin/* /usr/bin
41 | # enable EPEL repos, required to install inotify-tools package
42 | amazon-linux-extras install epel -y
43 | elif [ "${OS_VERSION}" = "2023" ]; then
44 | GH_RUNNER_DEPENDENCIES="lttng-ust openssl-libs krb5-libs zlib libicu"
45 | ADDITIONAL_PACKAGES="policycoreutils-python-utils ${GH_RUNNER_DEPENDENCIES}"
46 | fi
47 | fi
48 |
49 | HOMEDIR="/home/${USERNAME}"
50 | RUNNER_DIR="${HOMEDIR}/ar"
51 | mkdir -p "${RUNNER_DIR}" && cd "${HOMEDIR}"
52 |
53 | # TODO: add check for non-Fedora based systems if needed
54 | yum upgrade -y
55 | yum group install -y "Development Tools"
56 | # build dependencies for packages
57 | # this sometimes fails on Amazon Linux 2023, so retry if necessary
58 | for i in {1..2}; do
59 | yum install -y ${BASE_PACKAGES} ${ADDITIONAL_PACKAGES} && break || sleep 5
60 | done
61 |
62 | # start containerd
63 | systemctl enable --now containerd
64 |
65 | GH_RUNNER_VERSION="2.322.0"
66 | GH_RUNNER_FILENAME="actions-runner-linux-${GH_RUNNER_ARCH}-${GH_RUNNER_VERSION}.tar.gz"
67 | GH_RUNNER_DOWNLOAD_URL="https://github.com/actions/runner/releases/download/v${GH_RUNNER_VERSION}/${GH_RUNNER_FILENAME}"
68 |
69 | curl -OL "${GH_RUNNER_DOWNLOAD_URL}"
70 | echo "${GH_RUNNER_DOWNLOAD_HASH} ${GH_RUNNER_FILENAME}" | sha256sum -c
71 | tar -C "${RUNNER_DIR}" -xzf "./${GH_RUNNER_FILENAME}"
72 | chown -R "${USERNAME}:${USERNAME}" "${RUNNER_DIR}"
73 | rm "${GH_RUNNER_FILENAME}"
74 |
75 | # TODO: install SSM agent on non-AL hosts if needed
76 |
77 | # Get GH API key and fetch a runner registration token
78 | GH_KEY=$(aws secretsmanager get-secret-value --secret-id $REPO-runner-reg-key --region $REGION | jq '.SecretString' -r)
79 | RUNNER_REG_TOKEN=$(curl -L -s \
80 | -X POST \
81 | -H "Accept: application/vnd.github+json" \
82 | -H "Authorization: Bearer $GH_KEY" \
83 | -H "X-GitHub-Api-Version: 2022-11-28" \
84 | "https://api.github.com/repos/runfinch/${REPO}/actions/runners/registration-token" | jq -r '.token')
85 |
86 | if [ -z ${GH_RUNNER_DEPENDENCIES+x} ]; then
87 | echo "Executing installdependencies.sh because GH_RUNNER_DEPENDENCIES is not defined."
88 | "${RUNNER_DIR}/bin/installdependencies.sh"
89 | fi
90 |
91 | # Patch runsvc.sh to use previously downloaded version of Node unless its already patched.
92 | # This needs to be run as part of the systemd service since the runner can auto-update
93 | # and we want to patch every time there's an update, not just the initial installation.
94 | if [ "${OS_NAME}" = "Amazon Linux" ] && [ "${OS_VERSION}" = "2" ]; then
95 | RUNNER_SERVICE_NAME="actions.runner.runfinch-finch.$(hostname | cut -d. -f1).service"
96 | RUNNER_SERVICE_DROPIN_DIR="/etc/systemd/system/${RUNNER_SERVICE_NAME}.d"
97 | RUNNER_SERVICE_DROPIN_FILE="/etc/systemd/system/${RUNNER_SERVICE_NAME}.d/replace-node.conf"
98 |
99 | RUNNER_PATCH_SCRIPT="${HOMEDIR}/replace-node.sh"
100 | RUNSVC_PATH="${RUNNER_DIR}/runsvc.sh"
101 | ORIGINAL_NODE_PATH="./externals/\\\$nodever/bin/node"
102 | SYSTEM_NODE_PATH="/usr/bin/node"
103 |
104 | # Create a systemd service that will fix the node paths whenever a new node version is added.
105 | # This may happen if the actions runner service autoupdates, but does not restart the systemctl service
106 | # (meaning it does not re-trigger the ExecStartPre command which is inserted via our dropin unit).
107 | WATCHER_UNIT_NAME="/etc/systemd/system/fix-actions-runner-node.service"
108 | WATCHER_SCRIPT="${HOMEDIR}/watcher.sh"
109 |
110 | cat > "${RUNNER_PATCH_SCRIPT}" << EOF
111 | #!/usr/bin/bash
112 | if grep -q "${ORIGINAL_NODE_PATH}" "${RUNSVC_PATH}"; then
113 | sed -e "s|${ORIGINAL_NODE_PATH}|${SYSTEM_NODE_PATH}|g" -i "${RUNSVC_PATH}"
114 | fi
115 |
116 | # replace any bundled node binary with a symlink to system node
117 | find "${RUNNER_DIR}" -wholename "${RUNNER_DIR}/externals*/node*/bin/node" | while read line; do
118 | rm -rf \$line
119 | ln -s ${SYSTEM_NODE_PATH} \$line
120 | done
121 |
122 | EOF
123 |
124 | chmod +x "${RUNNER_PATCH_SCRIPT}"
125 |
126 | mkdir -p "${RUNNER_SERVICE_DROPIN_DIR}"
127 | cat > "${RUNNER_SERVICE_DROPIN_FILE}" << EOF
128 | [Service]
129 | ExecStartPre=${RUNNER_PATCH_SCRIPT}
130 | EOF
131 |
132 | # Monitor the $RUNNER_DIR for all file changes. This cannot be scoped down
133 | # because inotifywait does not take a file glob itself, and the externals directory
134 | # can be suffixed with version numbers (e.g. externals.someversion, not just one externals dir).
135 | cat > "${WATCHER_SCRIPT}" << EOF
136 | #!/usr/bin/bash
137 | inotifywait -mr "${RUNNER_DIR}" -e create -e moved_to |
138 | while read -r directory action file; do
139 | path="\${directory}\${file}"
140 | if [[ ! "\$path" =~ .*externals.*\/node.* ]]; then
141 | echo 'no match for file \${path}, skipping' | systemd-cat -t "node-patcher" -p info
142 | continue
143 | fi
144 | if [[ "\$(readlink -e ${SYSTEM_NODE_PATH})" == "\$(readlink -e \$file)" ]]; then
145 | # file is already linked properly, skip
146 | echo "file \${path} already linked properly, skipping" | systemd-cat -t "node-patcher" -p info
147 | continue
148 | fi
149 |
150 | echo 'updating node path, triggered by \${path}' | systemd-cat -t "node-patcher" -p info
151 |
152 | "${RUNNER_PATCH_SCRIPT}"
153 | done
154 | EOF
155 |
156 | chmod +x "${WATCHER_SCRIPT}"
157 |
158 | cat > "${WATCHER_UNIT_NAME}" << EOF
159 | [Service]
160 | ExecStart=${WATCHER_SCRIPT}
161 |
162 | [Unit]
163 | Description=GitHub Actions runner node version watcher
164 |
165 | [Install]
166 | WantedBy=multi-user.target
167 | EOF
168 |
169 | systemctl daemon-reload
170 | systemctl enable --now "${WATCHER_UNIT_NAME}"
171 | fi
172 |
173 | # Configure the runner with the registration token, launch the service
174 | # these commands must NOT be run as root
175 | sudo -i -u "${USERNAME}" bash < /Users/ec2-user/setup-runner.log
4 |
5 | echo $0
6 |
7 | # Set up the environment
8 | HOMEDIR="/Users/ec2-user"
9 | RUNNER_DIR="$HOMEDIR/ar"
10 | mkdir -p $RUNNER_DIR && cd $RUNNER_DIR
11 |
12 | # Configure brew path
13 | # Adding to .zshenv so that it will load even in non-interactive/login shells
14 | UNAME_MACHINE="$(/usr/bin/uname -m)"
15 | if [[ "${UNAME_MACHINE}" == "arm64" ]]
16 | then
17 | HOMEBREW_PREFIX="/opt/homebrew"
18 | else
19 | HOMEBREW_PREFIX="/usr/local"
20 | fi
21 | (echo; echo 'eval "$('"${HOMEBREW_PREFIX}"'/bin/brew shellenv)"') >> $HOMEDIR/.zshenv
22 |
23 | # Setup current shell
24 | PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"
25 |
26 | # Download and setup the runner
27 | if [ $(arch) == "arm64" ]
28 | then
29 | LABEL_ARCH="arm64"
30 | curl -o actions-runner-osx-arm64-2.303.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.303.0/actions-runner-osx-arm64-2.303.0.tar.gz
31 | echo "bbbac8011066b6cec93deb2365132b082b92287baaf34b5d9539e955ffe450ff actions-runner-osx-arm64-2.303.0.tar.gz" | shasum -a 256 -c
32 | # Extract the installer
33 | tar xzf ./actions-runner-osx-arm64-2.303.0.tar.gz
34 | else
35 | LABEL_ARCH="amd64"
36 | curl -o actions-runner-osx-x64-2.303.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.303.0/actions-runner-osx-x64-2.303.0.tar.gz
37 | echo "8bd595568ceee5eb25576972bc8075b47c149b3fac7eb7873deed67944b45739 actions-runner-osx-x64-2.303.0.tar.gz" | shasum -a 256 -c
38 | # Extract the installer
39 | tar xzf ./actions-runner-osx-x64-2.303.0.tar.gz
40 | fi
41 |
42 | # Get GH API key and fetch a runner registration token
43 | su ec2-user -c 'brew install jq'
44 | GH_KEY=$(su ec2-user -c "aws secretsmanager get-secret-value --secret-id $REPO-runner-reg-key --region $REGION" | su ec2-user -c "jq '.SecretString' -r")
45 | LABEL_VER=$(sw_vers -productVersion | cut -d '.' -f 1)
46 | RUNNER_REG_TOKEN=$(curl -L -s \
47 | -X POST \
48 | -H "Accept: application/vnd.github+json" \
49 | -H "Authorization: Bearer $GH_KEY"\
50 | -H "X-GitHub-Api-Version: 2022-11-28" \
51 | https://api.github.com/repos/runfinch/$REPO/actions/runners/registration-token | su ec2-user -c "jq -r '.token'")
52 |
53 | # Configure the runner with the registration token, launch the service
54 | su - ec2-user -c "cd $RUNNER_DIR && ./config.sh --url https://github.com/runfinch/$REPO --unattended --token $RUNNER_REG_TOKEN --work _work --labels '$LABEL_ARCH,$LABEL_VER,$LABEL_STAGE'"
55 | su - ec2-user -c "cd $RUNNER_DIR && ./svc.sh install"
56 | PLIST_NAME="actions.runner.runfinch-$REPO.$(hostname | cut -d '.' -f 1).plist"
57 | cp /Users/ec2-user/Library/LaunchAgents/$PLIST_NAME /Library/LaunchDaemons
58 | /bin/launchctl load /Library/LaunchDaemons/$PLIST_NAME
59 |
--------------------------------------------------------------------------------
/scripts/windows-runner-user-data.yaml:
--------------------------------------------------------------------------------
1 | # windows-runner-user-data.yaml
2 | #
3 | # User Script for launching a Windows EC2 instance that:
4 | # - logs to C:\UserData.log
5 | # - installs git, Make, AWS tools, latest powershell, Nuget, yq and Carbon
6 | # - installs WSL2 and its dependencies
7 | # * reboots the instance to apply the installation
8 | # - periodically updates WSL2
9 | # - registers as a self-hosted GitHub Actions runner on instance startup
10 | version: 1.0
11 | tasks:
12 | - task: executeScript
13 | inputs:
14 | - frequency: once
15 | type: powershell
16 | runAs: admin
17 | content: |-
18 | Start-Transcript -Path "C:\UserData.log" -Append
19 | $progressPreference = 'silentlyContinue'
20 | $RUNNER_DIR="C:\actions-runner"
21 |
22 | # "" values set in lib/asg-runner-stack.ts
23 | $LABEL_ARCH="amd64"
24 | $LABEL_STAGE=""
25 | $REPO=""
26 | $REGION=""
27 |
28 | Write-Information "Installing latest powershell7 version..."
29 | Invoke-Expression "& { $(Invoke-RestMethod 'https://aka.ms/install-powershell.ps1') } -useMSI -EnablePSRemoting -Quiet"
30 |
31 | New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Program Files\PowerShell\7\pwsh.exe" -PropertyType String -Force
32 |
33 | New-Item -Path $Profile -ItemType File -Force
34 | 'Set-PSReadLineOption -EditMode Emacs' | Out-File -Append $Profile
35 |
36 | New-Item -Path $Home\setup -ItemType Directory
37 | New-Item -Path $Home\setup\bin -ItemType Directory
38 | Set-Location $Home\setup
39 |
40 | Write-Information "Install dependencies git, make, go, AWS tools, NuGet and update path..."
41 |
42 | # Install latest NuGet so that Carbon installation works later
43 | Install-PackageProvider -Name NuGet -Force
44 |
45 | $pws7script = @'
46 | Start-Transcript -Path "$Home\setup\setup7.log" -Append
47 | Import-Module -Name Appx -UseWindowsPowerShell
48 |
49 | # Install git and Make
50 | Invoke-WebRequest -Uri 'https://github.com/git-for-windows/git/releases/download/v2.47.0.windows.2/Git-2.47.0.2-64-bit.exe' -OutFile 'git-2.47.0.2.exe'
51 | .\git-2.47.0.2.exe /SILENT
52 | # use curl to follow redirects
53 | & "C:\Windows\System32\curl.exe" -L https://sourceforge.net/projects/gnuwin32/files/make/3.81/make-3.81.exe/download -o make.exe
54 | .\make.exe /SILENT
55 |
56 | # Install AWS tools
57 | Install-Module -Name AWS.Tools.Common -Force
58 | Install-Module -Name AWS.Tools.SecretsManager -Force
59 | Install-Module -Name AWS.Tools.EC2 -Force
60 | Install-Module -Name AWS.Tools.AutoScaling -Force
61 |
62 | # Install AWS CLI
63 | Invoke-WebRequest -Uri https://awscli.amazonaws.com/AWSCLIV2.msi -OutFile AWSCLIV2.msi
64 | Start-Process msiexec.exe -Wait -ArgumentList '/I AWSCLIV2.msi /quiet'
65 | Remove-Item AWSCLIV2.msi
66 |
67 | # Install Go
68 | Invoke-WebRequest -Uri 'https://go.dev/dl/go1.23.3.windows-amd64.msi' -OutFile 'go1.23.3.windows-amd64.msi'
69 | Start-Process msiexec.exe -Wait -ArgumentList '/I C:\Users\Administrator\setup\go1.23.3.windows-amd64.msi /quiet'
70 |
71 | # Install yq
72 | Invoke-WebRequest -Uri 'https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_windows_amd64.exe' -OutFile '.\bin\yq.exe'
73 |
74 | # Configure path; include path to pre release WSL
75 | $newPath = ("C:\Program Files\Git\bin\;" + "C:\Program Files\Git\usr\bin\;" + "C:\Program Files\WSL\;" + "$env:Path" + ";C:\Program Files\Git\bin\;" + "C:\Program Files (x86)\GnuWin32\bin\;" + "C:\Program Files\Go\bin\;" + "$Home\setup\bin\;" + "C:\Program Files\Amazon\AWSCLIV2\;")
76 | $env:Path = $newPath
77 | # Persist the path to the registry for new shells
78 | Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newPath
79 | '@
80 |
81 | # Write PowerShell 7 script to file
82 | $pws7script | Out-File $Home\setup\setup7.ps1
83 |
84 | # Execute script with PowerShell 7
85 | Write-Information "Running setup7.ps1..."
86 | $ConsoleCommand = "$Home\setup\setup7.ps1"
87 | Start-Process "C:\Program Files\PowerShell\7\pwsh" -Wait -NoNewWindow -PassThru -ArgumentList "-Command &{ $ConsoleCommand }"
88 |
89 | Write-Information "Downloading and configuring GitHub Actions Runner..."
90 | mkdir $RUNNER_DIR; cd $RUNNER_DIR
91 | Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v2.316.0/actions-runner-win-x64-2.316.0.zip -OutFile actions-runner-win-x64-2.316.0.zip
92 | if((Get-FileHash -Path actions-runner-win-x64-2.316.0.zip -Algorithm SHA256).Hash.ToUpper() -ne '9b2d0443d11ce5c2c4391d708576dc37b1ecf62edcceec7c0c9c8e6b4472b5a1'.ToUpper()){ throw 'Computed checksum did not match' }
93 | Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD/actions-runner-win-x64-2.316.0.zip", "$PWD")
94 |
95 | $GH_KEY=(Get-SECSecretValue -SecretId $REPO-runner-reg-key -Region $REGION).SecretString
96 | $LABEL_OS_VER=$([System.Environment]::OSVersion.Version.Major)
97 | $LABEL_BUILD_VER=$([System.Environment]::OSVersion.Version.Build)
98 |
99 | $RUNNER_REG_TOKEN=((Invoke-WebRequest -UseBasicParsing -Method POST -Headers @{"Accept" = "application/vnd.github+json"; "Authorization" = "Bearer $GH_KEY"; "X-GitHub-Api-Version" = "2022-11-28"} -Uri https://api.github.com/repos/runfinch/$REPO/actions/runners/registration-token).Content | ConvertFrom-Json).token
100 |
101 | Write-Information "Starting GitHub Actions Runner..."
102 | cd $RUNNER_DIR; ./config.cmd --url https://github.com/runfinch/$REPO --unattended --runasservice --token $RUNNER_REG_TOKEN --work _work --labels $LABEL_ARCH,$LABEL_OS_VER,$LABEL_BUILD_VER,$LABEL_STAGE
103 |
104 | # To install WSL2, the instance must be rebooted: https://learn.microsoft.com/en-us/windows/wsl/install
105 | # To accomplish this while not replacing the instance in the autoscaling group,
106 | #
107 | # 1. Put the instance in "StandBy" mode
108 | # 2. Register a powershell script to be run at instance reboot on login
109 | # 3. Install WSL2 and restart the instance
110 |
111 | Write-Information "Entering StandBy..."
112 | $InstanceId=(Get-EC2InstanceMetadata -Category InstanceId)
113 | $ASGName=(Get-ASAutoScalingInstance -InstanceId $InstanceId).AutoScalingGroupName
114 | Enter-ASStandby -AutoScalingGroupName $ASGName -InstanceId $InstanceId -ShouldDecrementDesiredCapacity $true
115 |
116 | # Script runs on login after reboot to start GitHub Actions service, put the instance InService in the ASG,
117 | # and update wsl version to pre-release verion such that ssh / session 0 connections can call wsl
118 | # Allow inbound connections on vEthernet interface for traffic from wsl to host
119 | $ServiceName=(Get-Service actions.runner.*).name
120 | $startupscript = @'
121 | Start-Transcript -Path "C:\StartupScript.log" -Append
122 | Invoke-WebRequest -Uri 'https://github.com/microsoft/WSL/releases/download/2.4.13/wsl.2.4.13.0.x64.msi' -OutFile 'C:\Users\Administrator\setup\wsl.2.4.13.0.x64.msi'
123 | Start-Process msiexec.exe -Wait -ArgumentList '/i C:\Users\Administrator\setup\wsl.2.4.13.0.x64.msi /L*V C:\WSLInstallation.log /quiet'
124 | # Update the WSL kernel
125 | Invoke-WebRequest -Uri 'https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi' -OutFile 'C:\Users\Administrator\setup\wsl_update_x64.msi'
126 | Start-Process msiexec.exe -Wait -ArgumentList '/i C:\Users\Administrator\setup\wsl_update_x64.msi /L*V C:\WSLInstallation.log /quiet'
127 | Exit-ASStandby -AutoScalingGroupName $ASGName -InstanceId $InstanceId
128 | Add-MpPreference -ExclusionPath "C:\Users\ADMINI~1\AppData\Local\Temp\go-build*" -Force
129 | Start-Job -ScriptBlock { Start-Process -NoNewWindow -FilePath wsl -ArgumentList '--install Ubuntu' }
130 | sleep 30 # sleep to allow Ubuntu VM to start
131 | New-NetFirewallRule -DisplayName "WSL" -Direction Inbound -InterfaceAlias "vEthernet (WSL)" -Action Allow
132 | '@
133 |
134 | # Write startup powershell script to file.
135 | # $ExecutionContext.InvokeCommand.ExpandString will evaluate variables first, so the ASG and instance ID are specified
136 | Set-Content 'C:\startup.ps1' ($ExecutionContext.InvokeCommand.ExpandString($startupscript))
137 |
138 | # Register a scheduled job to run the script to regist the instance as a runner on boot.
139 | $trigger = New-JobTrigger -AtStartup -RandomDelay 00:00:30
140 | Register-ScheduledJob -Trigger $trigger -FilePath C:\startup.ps1 -Name startup-job-inservice
141 |
142 | Write-Information "Changing GHA service ownership to Administrator..."
143 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
144 | Install-Module -Name 'Carbon' -AllowClobber -Force
145 | # Configure the GitHub Actions runner service to log on as Administrator; grant the Administrator
146 | # user "Log on as a Service" privileges via Carbon module:
147 | # https://get-carbon.org/Grant-Privilege.html
148 | Stop-Service "actions.runner.*"
149 | Grant-CPrivilege -Identity "Administrator" "SeServiceLogonRight"
150 |
151 | net user Administrator "$GH_KEY"
152 |
153 | Write-Information "starting GHA service..."
154 | Set-Service -Name $ServiceName -StartupType Automatic
155 | Start-Service "actions.runner.*"
156 |
157 | Invoke-Expression "cmd.exe /c sc config $ServiceName obj= '$(hostname)\Administrator' password= '$GH_KEY' type= own"
158 |
159 | wsl --install
160 |
161 | Write-Information "Restarting instance..."
162 | Restart-Computer
163 |
--------------------------------------------------------------------------------
/test/artifact-bucket-cloudfront.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import { ArtifactBucketCloudfrontStack } from '../lib/artifact-bucket-cloudfront';
4 |
5 | describe('ArtifactBucketCloudfrontStack', () => {
6 | test('synthesizes the way we expect', () => {
7 | const app = new cdk.App();
8 | const cloudfront = new ArtifactBucketCloudfrontStack(app, 'CloudfrontStack', 'test');
9 |
10 | // prepare the ArtifactBucketCloudfrontStack template for assertions
11 | const template = Template.fromStack(cloudfront);
12 |
13 | // assert it creates the s3 bucket
14 | template.resourceCountIs('AWS::S3::Bucket', 1);
15 | template.hasResource('AWS::S3::Bucket', {
16 | Properties: {
17 | BucketName: Match.anyValue(),
18 | PublicAccessBlockConfiguration: {
19 | BlockPublicAcls: true,
20 | BlockPublicPolicy: true,
21 | IgnorePublicAcls: true,
22 | RestrictPublicBuckets: true
23 | }
24 | },
25 | UpdateReplacePolicy: 'Retain',
26 | DeletionPolicy: 'Retain'
27 | });
28 |
29 | // assert it creates the cloudfront distribution
30 | template.resourceCountIs('AWS::CloudFront::Distribution', 1);
31 |
32 | expect(cloudfront.terminationProtection).toBeTruthy();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/asg-runner-stack.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Template } from 'aws-cdk-lib/assertions';
3 | import { PlatformType, RunnerConfig, RunnerType } from '../config/runner-config';
4 | import { ASGRunnerStack } from '../lib/asg-runner-stack';
5 | import { ENVIRONMENT_STAGE } from '../lib/finch-pipeline-app-stage';
6 |
7 | const generateASGStackName = (runnerType: RunnerType) =>
8 | `ASG-${runnerType.platform}-${runnerType.repo}-${runnerType.version.split('.')[0]}-${runnerType.arch}Stack`;
9 |
10 | describe('ASGRunnerStack test', () => {
11 | const app = new cdk.App();
12 | const runnerConfig = RunnerConfig.runnerProd;
13 | const stacks: ASGRunnerStack[] = [];
14 | runnerConfig.runnerTypes.forEach((runnerType) => {
15 | const ASGStackName = generateASGStackName(runnerType);
16 | const licenseArn =
17 | runnerType.platform === PlatformType.WINDOWS ? runnerConfig.windowsLicenseArn : runnerConfig.macLicenseArn;
18 | stacks.push(
19 | new ASGRunnerStack(app, ASGStackName, {
20 | env: {
21 | account: '123456789012',
22 | region: 'us-east-1'
23 | },
24 | stage: ENVIRONMENT_STAGE.Prod,
25 | licenseArn: licenseArn,
26 | type: runnerType
27 | })
28 | );
29 | });
30 | const templates = stacks.map((stack) => Template.fromStack(stack));
31 |
32 | it('should have the correct number of resources', () => {
33 | templates.forEach((template) => {
34 | template.resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 1);
35 | template.resourceCountIs('AWS::EC2::LaunchTemplate', 1);
36 | template.resourceCountIs('AWS::ResourceGroups::Group', 1);
37 | template.resourceCountIs('AWS::EC2::SecurityGroup', 1);
38 | template.resourceCountIs('AWS::IAM::Role', 1);
39 | template.resourceCountIs('AWS::IAM::Policy', 1);
40 | });
41 | });
42 |
43 | it('should match the runner configuration', () => {
44 | expect(stacks.length).toBe(runnerConfig.runnerTypes.length);
45 | runnerConfig.runnerTypes.forEach((type) => {
46 | const stack = stacks.find((stack) => stack.stackName === generateASGStackName(type));
47 | expect(stack).toBeDefined();
48 | const template = Template.fromStack(stack!);
49 | let instanceType = '';
50 | switch (type.platform) {
51 | case PlatformType.WINDOWS: {
52 | instanceType = 'm5zn.metal';
53 | break;
54 | }
55 | case PlatformType.MAC: {
56 | if (type.arch === 'arm') {
57 | instanceType = 'mac2.metal';
58 | } else {
59 | instanceType = 'mac1.metal';
60 | }
61 | break;
62 | }
63 | case PlatformType.AMAZONLINUX: {
64 | if (type.arch === 'arm') {
65 | instanceType = 'c7g.large';
66 | } else {
67 | instanceType = 'c7a.large';
68 | }
69 | break;
70 | }
71 | }
72 | template.hasResourceProperties('AWS::EC2::LaunchTemplate', {
73 | LaunchTemplateData: {
74 | InstanceType: instanceType
75 | }
76 | });
77 | });
78 | });
79 |
80 | it('must have termination protection enabled', () => {
81 | stacks.forEach((stack) => {
82 | expect(stack.terminationProtection).toBeTruthy();
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/test/aspects/stack-termination-protection.test.ts:
--------------------------------------------------------------------------------
1 | // Finch Infrastructure
2 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 |
4 | import { App, Stack } from "aws-cdk-lib";
5 | import { Bucket } from "aws-cdk-lib/aws-s3";
6 | import { applyTerminationProtectionOnStacks } from "../../lib/aspects/stack-termination-protection";
7 |
8 | describe('Stack termination protection aspect', () => {
9 | it('must enable termination protection when applied to a stack and synthesized', () => {
10 | const app = new App();
11 | const stack = new Stack(app, 'FooStack');
12 | // stack needs one resource
13 | new Bucket(stack, 'BarBucket');
14 |
15 | applyTerminationProtectionOnStacks([stack]);
16 | app.synth();
17 |
18 | expect(stack.terminationProtection).toBeTruthy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/cloudfront_cdn.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import * as s3 from 'aws-cdk-lib/aws-s3';
4 | import { CfnBucket } from 'aws-cdk-lib/aws-s3';
5 | import { CloudfrontCdn } from '../lib/cloudfront_cdn';
6 |
7 | describe('CloudfrontCdn', () => {
8 | test('synthesizes the way we expect', () => {
9 | const app = new cdk.App();
10 |
11 | // create a stack for CloudfrontCdn to live in
12 | const cloudfrontCdnStack = new cdk.Stack(app, 'CloudfrontCdnStack');
13 | const bucket = new s3.Bucket(cloudfrontCdnStack, 'TestBucket', {
14 | bucketName: 'test-bucket',
15 | publicReadAccess: false,
16 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL
17 | });
18 |
19 | // create the CloudfrontCdn stack for assertions
20 | const cloudfrontCdn = new CloudfrontCdn(cloudfrontCdnStack, 'CloudfrontCdn', {
21 | bucket: bucket
22 | });
23 |
24 | const template = Template.fromStack(cloudfrontCdnStack);
25 |
26 | // assert it creates the s3 bucket
27 | template.resourceCountIs('AWS::S3::Bucket', 1);
28 | template.hasResource('AWS::S3::Bucket', {
29 | Properties: {
30 | BucketName: 'test-bucket',
31 | PublicAccessBlockConfiguration: {
32 | BlockPublicAcls: true,
33 | BlockPublicPolicy: true,
34 | IgnorePublicAcls: true,
35 | RestrictPublicBuckets: true
36 | }
37 | },
38 | UpdateReplacePolicy: 'Retain',
39 | DeletionPolicy: 'Retain'
40 | });
41 |
42 | const bukcetLogicalId = cloudfrontCdnStack.getLogicalId(bucket.node.defaultChild as CfnBucket);
43 | // assert the bucket policy
44 | template.hasResourceProperties('AWS::S3::BucketPolicy', {
45 | Bucket: {
46 | Ref: bukcetLogicalId
47 | },
48 | PolicyDocument: {
49 | Statement: [
50 | Match.objectLike({
51 | Action: 's3:GetObject',
52 | Effect: 'Allow',
53 | Principal: {
54 | CanonicalUser: {
55 | 'Fn::GetAtt': [Match.anyValue(), 'S3CanonicalUserId']
56 | }
57 | },
58 | Resource: {
59 | 'Fn::Join': [
60 | '',
61 | [
62 | {
63 | 'Fn::GetAtt': [bukcetLogicalId, 'Arn']
64 | },
65 | '/*'
66 | ]
67 | ]
68 | }
69 | })
70 | ],
71 | Version: '2012-10-17'
72 | }
73 | });
74 |
75 | // assert it creates the cloudfront distribution
76 | template.resourceCountIs('AWS::CloudFront::Distribution', 1);
77 | // the cloufront has a s3 origin and OAI to access the s3 bucket
78 | template.hasResourceProperties('AWS::CloudFront::Distribution', {
79 | DistributionConfig: {
80 | DefaultCacheBehavior: {
81 | AllowedMethods: ['GET', 'HEAD']
82 | },
83 | Origins: [
84 | {
85 | DomainName: {
86 | 'Fn::GetAtt': [Match.anyValue(), 'RegionalDomainName']
87 | },
88 | Id: Match.anyValue(),
89 | S3OriginConfig: {
90 | OriginAccessIdentity: {
91 | 'Fn::Join': [
92 | '',
93 | [
94 | 'origin-access-identity/cloudfront/',
95 | {
96 | Ref: Match.anyValue()
97 | }
98 | ]
99 | ]
100 | }
101 | }
102 | }
103 | ]
104 | }
105 | });
106 |
107 | template.hasResourceProperties('AWS::CloudFront::CloudFrontOriginAccessIdentity', {
108 | CloudFrontOriginAccessIdentityConfig: {
109 | Comment: 'OAI for artifact bucket cloudfront'
110 | }
111 | });
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/test/codebuild-stack.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import * as codebuild from 'aws-cdk-lib/aws-codebuild';
4 | import { CodeBuildStack } from '../lib/codebuild-stack';
5 |
6 | import { BuildImageOS, CODEBUILD_STACKS, CodeBuildStackArgs, toStackName } from '../lib/utils';
7 |
8 | describe('CodeBuildStack', () => {
9 | test('synthesizes the way we expect', () => {
10 | const app = new cdk.App();
11 | const codebuildStackArgs: CodeBuildStackArgs = {
12 | operatingSystem: 'ubuntu',
13 | arch: 'x86_64',
14 | amiSearchString: 'ubuntu*22.04*',
15 | environmentType: codebuild.EnvironmentType.LINUX_EC2,
16 | buildImageOS: BuildImageOS.LINUX
17 | };
18 |
19 | const stack = new CodeBuildStack(app, 'TestStack', {
20 | env: {
21 | account: '123456789012',
22 | region: 'us-east-1'
23 | },
24 | projectName: 'test-project',
25 | region: 'us-west-2',
26 | ...codebuildStackArgs
27 | });
28 | const template = Template.fromStack(stack);
29 |
30 | validateTemplate(codebuildStackArgs, template);
31 | });
32 |
33 | test('all codebuild stacks in ./utils synthesize as expected', () => {
34 | for (const codebuildStackArgs of CODEBUILD_STACKS) {
35 | const app = new cdk.App();
36 | const stack = new CodeBuildStack(app, 'TestStack', {
37 | env: {
38 | account: '123456789012',
39 | region: 'us-east-1'
40 | },
41 | projectName: 'test-project',
42 | region: 'us-west-2',
43 | ...codebuildStackArgs
44 | });
45 | const template = Template.fromStack(stack);
46 |
47 | validateTemplate(codebuildStackArgs, template);
48 | }
49 | });
50 | });
51 |
52 | const validateTemplate = (codebuildStack: CodeBuildStackArgs, template: Template) => {
53 | // Assert that the stack creates a Fleet
54 | template.hasResourceProperties('AWS::CodeBuild::Project', {
55 | Name: 'test-project',
56 | Environment: {
57 | Type: codebuildStack.environmentType,
58 | ComputeType: 'BUILD_GENERAL1_MEDIUM',
59 | Image: Match.stringLikeRegexp('ami-1234'),
60 | },
61 | Source: {
62 | Type: 'GITHUB',
63 | Location: 'https://github.com/runfinch/finch.git',
64 | ReportBuildStatus: true
65 | }
66 | });
67 |
68 | // Assert that the stack creates a Fleet
69 | template.hasResourceProperties('AWS::CodeBuild::Fleet', {
70 | BaseCapacity: 1,
71 | ComputeType: 'BUILD_GENERAL1_MEDIUM',
72 | EnvironmentType: codebuildStack.environmentType
73 | });
74 |
75 | // Assert that the stack creates a Fleet service role
76 | template.hasResourceProperties('AWS::IAM::Role', {
77 | ManagedPolicyArns: Match.arrayWith([
78 | {
79 | 'Fn::Join': ['', Match.arrayWith([':iam::aws:policy/AmazonEC2FullAccess'])]
80 | },
81 | {
82 | 'Fn::Join': ['', Match.arrayWith([':iam::aws:policy/AmazonSSMManagedInstanceCore'])]
83 | }
84 | ])
85 | });
86 |
87 | // Assert that the stack creates a KMS key
88 | template.hasResourceProperties('AWS::KMS::Key', {
89 | Description: 'Kms Key to encrypt data-at-rest',
90 | Enabled: true
91 | });
92 |
93 | // Assert that the stack creates a KMS alias
94 | template.hasResourceProperties('AWS::KMS::Alias', {
95 | AliasName: Match.stringLikeRegexp(
96 | `alias/finch-${codebuildStack.operatingSystem}-${toStackName(codebuildStack.arch)}-kms-us-west-2`
97 | )
98 | });
99 |
100 | // Check resource count
101 | template.resourceCountIs('AWS::CodeBuild::Project', 1);
102 | template.resourceCountIs('AWS::CodeBuild::Fleet', 1);
103 | template.resourceCountIs('AWS::KMS::Key', 1);
104 | };
105 |
--------------------------------------------------------------------------------
/test/ecr-repo-stack.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import { ECRRepositoryStack } from '../lib/ecr-repo-stack';
4 |
5 | describe('ECRRepositoryStack', () => {
6 | test('synthesizes the way we expect', () => {
7 | const app = new cdk.App();
8 | const ecrRepo = new ECRRepositoryStack(app, 'ECRRepositoryStack', 'test');
9 |
10 | // prepare the ECRRepositoryStack template for assertions
11 | const template = Template.fromStack(ecrRepo);
12 |
13 | // assert it creates the ecr repo with properties set.
14 | template.resourceCountIs('AWS::ECR::Repository', 1);
15 | template.hasResource('AWS::ECR::Repository', {
16 | Properties: {
17 | RepositoryName: Match.anyValue(),
18 | ImageTagMutability: "IMMUTABLE",
19 | ImageScanningConfiguration: {
20 | ScanOnPush: true,
21 | },
22 | },
23 | });
24 |
25 | expect(ecrRepo.terminationProtection).toBeTruthy();
26 | });
27 | })
28 |
--------------------------------------------------------------------------------
/test/event-bridge-scan-notifs-stack.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import { EventBridgeScanNotifsStack } from '../lib/event-bridge-scan-notifs-stack';
4 |
5 | describe('EventBridgeScanNotifsStack', () => {
6 | test('synthesizes the way we expect', () => {
7 | const app = new cdk.App();
8 | const eventBridgeStack = new EventBridgeScanNotifsStack(app, 'EventBridgeScanNotifsStack', 'test');
9 |
10 | const template = Template.fromStack(eventBridgeStack);
11 |
12 | template.resourceCountIs('AWS::Lambda::Function', 1);
13 | template.hasResource('AWS::Lambda::Function', {
14 | Properties: {
15 | Environment:{
16 | Variables: {
17 | "SNS_ARN": Match.anyValue()
18 | }
19 | },
20 | Runtime: "python3.11",
21 | },
22 | });
23 |
24 | const lambda = template.findResources('AWS::Lambda::Function')
25 | const lambdaLogicalID = Object.keys(lambda)[0]
26 |
27 | template.resourceCountIs('AWS::SNS::Topic', 1);
28 |
29 | template.resourceCountIs('AWS::Events::Rule', 1);
30 | template.hasResource('AWS::Events::Rule', {
31 | Properties: {
32 | EventPattern: {
33 | source: ["aws.inspector2"]
34 | },
35 | State: "ENABLED",
36 | Targets: [
37 | {
38 | "Arn":{
39 | "Fn::GetAtt": [
40 | lambdaLogicalID,
41 | "Arn"
42 | ]
43 | }
44 | }
45 | ],
46 | }
47 | });
48 |
49 | expect(eventBridgeStack.terminationProtection).toBeTruthy();
50 | });
51 | })
52 |
--------------------------------------------------------------------------------
/test/finch-pipeline-stack.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Template } from 'aws-cdk-lib/assertions';
3 | import { EnvConfig } from '../config/env-config';
4 | import { FinchPipelineStack } from '../lib/finch-pipeline-stack';
5 |
6 | describe('FinchPipelineStack', () => {
7 | test('synthesizes the way we expect', () => {
8 | const app = new cdk.App();
9 | const stack = new FinchPipelineStack(app, 'TestPipelineStack', {
10 | env: EnvConfig.envPipeline
11 | });
12 | const template = Template.fromStack(stack);
13 |
14 | // assert that the pipeline has a s3 bucket used to store artifacts, e.g. source code of the infrastructure repo
15 | template.resourceCountIs('AWS::S3::Bucket', 1);
16 | template.hasResource('AWS::S3::Bucket', {
17 | Properties: {
18 | BucketEncryption: {
19 | ServerSideEncryptionConfiguration: [
20 | {
21 | ServerSideEncryptionByDefault: {
22 | SSEAlgorithm: 'aws:kms'
23 | }
24 | }
25 | ]
26 | },
27 | PublicAccessBlockConfiguration: {
28 | BlockPublicAcls: true,
29 | BlockPublicPolicy: true,
30 | IgnorePublicAcls: true,
31 | RestrictPublicBuckets: true
32 | }
33 | },
34 | UpdateReplacePolicy: 'Retain',
35 | DeletionPolicy: 'Retain'
36 | });
37 |
38 | // assert the pipeline role
39 | template.hasResourceProperties('AWS::IAM::Role', {
40 | AssumeRolePolicyDocument: {
41 | Statement: [
42 | {
43 | Action: 'sts:AssumeRole',
44 | Effect: 'Allow',
45 | Principal: {
46 | Service: 'codepipeline.amazonaws.com'
47 | }
48 | }
49 | ],
50 | Version: '2012-10-17'
51 | }
52 | });
53 |
54 | // assert the FinchPipelineStack creates a CodePipeline resource
55 | template.hasResourceProperties('AWS::CodePipeline::Pipeline', {
56 | RoleArn: {
57 | 'Fn::GetAtt': ['FinchPipelineRole198D7E07', 'Arn']
58 | }
59 | });
60 |
61 | expect(stack.terminationProtection).toBeTruthy();
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/test/ssm-patching-stack.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Template } from 'aws-cdk-lib/assertions';
3 | import { SSMPatchingStack } from '../lib/ssm-patching-stack';
4 |
5 | describe('SSMPatchingStack', () => {
6 | test('synthesizes the way we expect', () => {
7 | const app = new cdk.App();
8 | const ssmPatchingStack = new SSMPatchingStack(app, 'SSMPatchingStack');
9 |
10 | const template = Template.fromStack(ssmPatchingStack);
11 |
12 | template.hasResource('AWS::SSM::MaintenanceWindow', {
13 | Properties: {
14 | AllowUnassociatedTargets: false,
15 | Cutoff: 0,
16 | Duration: 2,
17 | Name: 'Patching-Window',
18 | Schedule: 'cron(0 8 ? * * *)'
19 | }
20 | });
21 |
22 | template.hasResource('AWS::SSM::MaintenanceWindowTarget', {
23 | Properties: {
24 | Name: 'All-Instances-Patch-Target',
25 | ResourceType: 'INSTANCE',
26 | Targets: [
27 | {
28 | Key: 'tag:PVRE-Reporting',
29 | Values: ['SSM']
30 | }
31 | ],
32 | WindowId: {
33 | Ref: 'MaintenanceWindow'
34 | }
35 | }
36 | });
37 |
38 | template.hasResource('AWS::SSM::MaintenanceWindowTask', {
39 | Properties: {
40 | MaxConcurrency: '1',
41 | MaxErrors: '100%',
42 | Name: 'Patch-Task',
43 | Priority: 1,
44 | Targets: [
45 | {
46 | Key: 'WindowTargetIds',
47 | Values: [
48 | {
49 | Ref: 'MaintenanceWindowTarget'
50 | }
51 | ]
52 | }
53 | ],
54 | TaskArn: 'AWS-RunPatchBaseline',
55 | TaskInvocationParameters: {
56 | MaintenanceWindowRunCommandParameters: {
57 | Parameters: {
58 | Operation: ['Install']
59 | },
60 | DocumentVersion: '$LATEST'
61 | }
62 | },
63 | TaskType: 'RUN_COMMAND',
64 | WindowId: {
65 | Ref: 'MaintenanceWindow'
66 | }
67 | }
68 | });
69 |
70 | expect(ssmPatchingStack.terminationProtection).toBeTruthy();
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { toStackName } from '../lib/utils';
2 |
3 | describe('toStackName', () => {
4 | test('should return a string with the correct format', () => {
5 | const name = 'CodebuildStack-x86_64_ubuntu';
6 | const expected = 'CodebuildStack-x86-64-ubuntu';
7 | const actual = toStackName(name);
8 | expect(actual).toBe(expected);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "noUnusedLocals": false,
6 | "noUnusedParameters": false,
7 | "noImplicitReturns": true,
8 | "noFallthroughCasesInSwitch": false,
9 | "inlineSourceMap": true,
10 | "inlineSources": true,
11 | "experimentalDecorators": true,
12 | "strictPropertyInitialization": false,
13 | "resolveJsonModule": true,
14 | "outDir": "./dist",
15 | },
16 | "exclude": [
17 | "node_modules",
18 | "cdk.out",
19 | "dist",
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------