diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..10a0032 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,41 @@ +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ~1.5 + + - name: Terraform Version + run: terraform -version + + - name: Install terraform-docs + run: | + WORK_DIR=$(mktemp -d) + curl -Lo ${WORK_DIR}/terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/download/v0.16.0/terraform-docs-v0.16.0-$(uname)-amd64.tar.gz + cd ${WORK_DIR} + tar -xzf terraform-docs.tar.gz + chmod +x terraform-docs + mv terraform-docs /usr/local/bin/terraform-docs + - name: Generate docs + run: make docs + + - name: Check git diff is clean (all files generated should be committed) + run: git diff --exit-code + + - name: Terraform Format Check + run: make fmt-check + + - name: Stub Github App credentials (required for validation) + run: cd ./examples/with-backstage && STUB_FILE=1 node create-gh-app/index.js + + - name: Terraform Validate + run: make validate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9686a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +.terraform +.terraform.lock.hcl +*.tfstate +*.tfstate.* +*.tfplan +*.terraformrc +*.tfvars +terraform.rc +github-app-credentials.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ccca688 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +TF_DIRS = $(patsubst %/main.tf, %, $(shell find . -type d -name .terraform -prune -o -name 'main.tf' -print)) +VALIDATE_TF_DIRS = $(addprefix validate-,$(TF_DIRS)) + +# Generate docs +.PHONY: docs +docs: + terraform-docs --lockfile=false ./modules/base + terraform-docs --config docs/.terraform-docs.yaml . + terraform-docs --config docs/.terraform-docs-example.yaml . + terraform-docs --config docs/.terraform-docs.yaml ./examples/with-backstage + terraform-docs --config docs/.terraform-docs-example.yaml ./examples/with-backstage + +# Format all terraform files +fmt: + terraform fmt -recursive + +# Check if all terraform files are formatted +fmt-check: + terraform fmt -recursive -check + +# Validate a terraform directories +$(VALIDATE_TF_DIRS): validate-%: + @echo "Validate $*" + terraform -chdir="$*" init -upgrade + terraform -chdir="$*" validate + +# Validate all terraform directories +validate: $(VALIDATE_TF_DIRS) + @echo "All validated" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7139df6 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# Humanitec AWS Reference Architecture + +This repo contains an implementation of part of the Humanitec Reference Architecture for an Internal Developer Platform. + +To install an implementation containing add-ons, follow the separate README. We currently feature these add-ons: + +* [Base layer plus Backstage](examples/with-backstage/) + +![AWS reference architecture Humanitec](docs/images/AWS-reference-architecture-Humanitec.png) + +This repo covers the base layer of the implementation for AWS. + +By default, the following will be provisioned: + +- VPC +- EKS Cluster +- IAM User to access the cluster +- Ingress NGINX in the cluster +- Resource Definitions in Humanitec for: + - Kubernetes Cluster + - Logging + +## Prerequisites + +* A Humanitec account with the `Administrator` role in an Organization. Get a [free trial](https://humanitec.com/free-trial?utm_source=github&utm_medium=referral&utm_campaign=aws_refarch_repo) if you are just starting. +* An AWS account +* [AWS CLI](https://aws.amazon.com/cli/) installed locally +* [terraform](https://www.terraform.io/) installed locally + +## Usage + +**Note: Using this Reference Architecture Implementation will incur costs for your AWS project.** + +It is recommended that you fully review the code before you run it to ensure you understand the impact of provisioning this infrastructure. +Humanitec does not take responsibility for any costs incurred or damage caused when using the Reference Architecture Implementation. + +This reference architecture implementation uses Terraform. You will need to do the following: + +1. [Fork this GitHub repo](https://github.com/humanitec-architecture/reference-architecture-aws/fork), clone it to your local machine and navigate to the root of the repository. + +2. Set the required input variables. (see [Required input variables](#required-input-variables)) + +3. Ensure you are logged in with `aws`. (Follow the [quickstart](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html) if you aren't) + +4. Set the `HUMANITEC_TOKEN` environment variable to an appropriate Humanitec API token with the `Administrator` role on the Humanitec Organization. + + For example: + + ``` + export HUMANITEC_TOKEN="my-humanitec-api-token" + ``` + +5. Run terraform: + + ``` + terraform init + terraform plan + terraform apply + ``` + + `terraform plan` and `apply` might output this message: + ``` + │ Warning: Argument is deprecated + │ + │ with module.base.module.aws_eks.aws_eks_addon.this["aws-ebs-csi-driver"], + │ [...] + ``` + This is due to an upstream issue with the Terraform AWS modules, and can be ignored. + +### Required input variables + +Terraform reads variables by default from a file called `terraform.tfvars`. You can create your own file by renaming the `terraform.tfvars.example` file in the root of the repo and then filling in the missing values. + +You can see find a details about each of those variables and additional supported variables under [Inputs](#inputs). + + +## Verify your result + +Check for the existence of key elements of the reference architecture. This is a subset of all elements only. For a complete list of what was installed, review the Terraform code. + +1. Set the `HUMANITEC_ORG` environment variable to the ID of your Humanitec Organization (must be all lowercase): + + ``` + export HUMANITEC_ORG="my-humanitec-org" + ``` + +2. Verify the existence of the Resource Definition for the EKS cluster in your Humanitec Organization: + + ``` + curl -s https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs/ref-arch \ + --header "Authorization: Bearer ${HUMANITEC_TOKEN}" \ + | jq .id,.type + ``` + + This should output: + ``` + "ref-arch" + "k8s-cluster" + ``` + +3. Verify the existence of the newly created EKS cluster: + + ``` + aws eks list-clusters --region + ``` + + This should output: + + ``` + { + "clusters": [ + "ref-arch", + "[more previously existing clusters here]" + ] + } + ``` + +## Cleaning up + +Once you are finished with the reference architecture, you can remove all provisioned infrastrcuture and the resource definitions created in Humanitec with the following: + +1. Ensure you are (still) logged in with `aws`. + +2. Ensure you still have the `HUMANITEC_TOKEN` environment variable set to an appropriate Humanitec API token with the `Administrator` role on the Humanitec Organization. + +3. Run terraform: + + ``` + terraform destroy + ``` + +## Terraform docs + + +### Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.3.0 | +| aws | ~> 5.17 | +| humanitec | ~> 0.13 | + +### Modules + +| Name | Source | Version | +|------|--------|---------| +| base | ./modules/base | n/a | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| aws\_account\_id | AWS Account (ID) to use | `string` | n/a | yes | +| aws\_region | AWS Region to deploy into | `string` | n/a | yes | +| humanitec\_org\_id | Humanitec Organization ID | `string` | n/a | yes | + diff --git a/docs/.terraform-docs-example.yaml b/docs/.terraform-docs-example.yaml new file mode 100644 index 0000000..4bf5908 --- /dev/null +++ b/docs/.terraform-docs-example.yaml @@ -0,0 +1,9 @@ +formatter: "tfvars hcl" + +output: + file: "./terraform.tfvars.example" + mode: replace + template: "{{ .Content }}" + +settings: + description: true diff --git a/docs/.terraform-docs.yaml b/docs/.terraform-docs.yaml new file mode 100644 index 0000000..f1f7132 --- /dev/null +++ b/docs/.terraform-docs.yaml @@ -0,0 +1,15 @@ +formatter: "markdown table" + +output: + file: "./README.md" + +sort: + enabled: true + by: required + + +settings: + anchor: false + indent: 3 + hide-empty: true + lockfile: false diff --git a/docs/images/AWS-reference-architecture-Humanitec.png b/docs/images/AWS-reference-architecture-Humanitec.png new file mode 100644 index 0000000..7cfbd4b Binary files /dev/null and b/docs/images/AWS-reference-architecture-Humanitec.png differ diff --git a/examples/with-backstage/README.md b/examples/with-backstage/README.md new file mode 100644 index 0000000..8736c3d --- /dev/null +++ b/examples/with-backstage/README.md @@ -0,0 +1,140 @@ +# AWS reference architecture with Backstage + +Provisions the AWS reference architecture connected to Humanitec and installs Backstage. + +## Prerequisites + +* The same prerequisites as the [base reference architecture](../../README.md#prerequisites), plus the following items. +* A GitHub organization and permission to create new repositories in it. Go to https://github.com/account/organizations/new to create a new org (the "Free" option is fine). Note: is has to be an organization, a free account is not sufficient. +* Create a classic github personal access token with `repo`, `workflow`, `delete_repo` and `admin:org` scope [here](https://github.com/settings/tokens). +* Set the `GITHUB_TOKEN` environment variable to your token. + ``` + export GITHUB_TOKEN="my-github-token" + ``` +* Set the `GITHUB_ORG_ID` environment variable to your GitHub organization ID. + ``` + export GITHUB_ORG_ID="my-github-org-id" + ``` +* [Node.js](https://nodejs.org) installed locally. +* Install the GitHub App for Backstage into your GitHub organization using `node create-gh-app/index.js`. Follow the instructions. + * “All repositories” ~> Install + * “Okay, […] was installed on the […] account.” ~> You can close the window and server. + +## Usage + +Follow the same steps as for the [base layer](../../README.md#usage), applying these modifications: +* Execute `cd ./examples/with-backstage` after cloning the repo. Execute all subsequent commands in this directory. +* In particular, use the `./examples/with-backstage/terraform.tfvars.example` file as the basis for your `terraform.tfvars` file. It defines additional variables needed to setup and configure Backstage. + +## Verify your result + +Check for the existence of key elements of the backstage module. This is a subset of all elements only. For a complete list of what was installed, review the Terraform code. + +1. Perform the [verification steps of the base installation](../../README.md) if you have not already done so. +2. Verify the existence of the Backstage Application in your Humanitec Organization: + ``` + curl -s https://api.humanitec.io/orgs/${HUMANITEC_ORG}/apps/backstage \ + --header "Authorization: Bearer ${HUMANITEC_TOKEN}" + ``` + This should output a JSON formatted representation of the Application like so: + ``` + {"id":"backstage","name":"backstage","created_at":"2023-10-02T13:44:27Z","created_by":"s-d3e94a0e-8b53-29f9-b666-27548b7e06e0","envs":[{"id":"development","name":"Development","type":"development"}]} + ``` + You can also check for the Application in the [Humanitec Platform Orchestrator UI](https://app.humanitec.io). + +3. Connect to your EKS cluster via `kubectl`. See the [AWS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html) or use this command: + ``` + aws eks update-kubeconfig --region --name ref-arch + ``` +4. Get the elements in the newly created Kubernetes namespace: + ``` + kubectl get all -n backstage-development + ``` + You should see + - a `deployment`, `replicaset`, running `pod`, and `service` for Backstage + - a `statefulset`, running `pod`, and `service` for PostgreSQL database used by Backstage. + + Note: it may take up to ten minutes after the `terraform apply` completed until you actually see those resources. The Backstage application needs to built and deployed via a GitHub action out of the newly created repository in your GitHub organization. + + +## Cleaning up + +Once you are finished with the reference architecture, you can remove all provisioned infrastrcuture and the resource definitions created in Humanitec with the following: + +1. Delete all Humanitec applications scaffolded using Backstage, but not the `backstage` app itself. + +2. Follow the [base reference architecture cleanup instructions](../../README.md#cleaning-up). + +## Terraform docs + + +### Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.3.0 | +| aws | ~> 5.17 | +| github | ~> 5.38 | +| humanitec | ~> 0.13 | + +### Providers + +| Name | Version | +|------|---------| +| aws | ~> 5.17 | +| github | ~> 5.38 | +| humanitec | ~> 0.13 | + +### Modules + +| Name | Source | Version | +|------|--------|---------| +| backstage\_ecr | terraform-aws-modules/ecr/aws | ~> 1.6 | +| backstage\_iam\_policy\_ecr\_create\_repository | git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/iam-policy/ecr-create-repository | n/a | +| backstage\_iam\_role\_service\_account | git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/iam-role/service-account | n/a | +| backstage\_k8s\_service\_account | git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/k8s/service-account | n/a | +| backstage\_mysql | git::https://github.com/humanitec-architecture/resource-packs-in-cluster.git//humanitec-resource-defs/mysql/basic | n/a | +| backstage\_postgres | git::https://github.com/humanitec-architecture/resource-packs-in-cluster.git//humanitec-resource-defs/postgres/basic | n/a | +| backstage\_workload | git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/workload/service-account | n/a | +| base | ../../modules/base | n/a | +| iam\_github\_oidc\_provider | terraform-aws-modules/iam/aws//modules/iam-github-oidc-provider | ~> 5.30 | +| iam\_github\_oidc\_role | terraform-aws-modules/iam/aws//modules/iam-github-oidc-role | ~> 5.30 | + +### Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.ecr_push_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [github_actions_organization_secret.backstage_humanitec_token](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_organization_secret) | resource | +| [github_actions_organization_variable.backstage_aws_region](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_organization_variable) | resource | +| [github_actions_organization_variable.backstage_aws_role_arn](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_organization_variable) | resource | +| [github_actions_organization_variable.backstage_humanitec_org_id](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_organization_variable) | resource | +| [github_repository.backstage](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository) | resource | +| [humanitec_application.backstage](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/application) | resource | +| [humanitec_resource_definition_criteria.backstage_iam_policy_ecr_create_repository](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.backstage_iam_role_service_account](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.backstage_k8s_service_account](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.backstage_mysql](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.backstage_postgres](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.backstage_workload](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_value.aws_default_region](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_github_app_client_id](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_github_app_client_secret](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_github_app_id](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_github_app_private_key](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_github_app_webhook_secret](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_github_org_id](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_humanitec_org](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | +| [humanitec_value.backstage_humanitec_token](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/value) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| aws\_account\_id | AWS Account (ID) to use | `string` | n/a | yes | +| aws\_region | AWS region | `string` | n/a | yes | +| github\_org\_id | GitHub org id | `string` | n/a | yes | +| humanitec\_ci\_service\_user\_token | Humanitec CI Service User Token | `string` | n/a | yes | +| humanitec\_org\_id | Humanitec Organization ID | `string` | n/a | yes | +| resource\_packs\_aws\_rev | Revision of the resource-packs-aws repository to use | `string` | `"refs/heads/main"` | no | + diff --git a/examples/with-backstage/aws-github.tf b/examples/with-backstage/aws-github.tf new file mode 100644 index 0000000..bf6f6f1 --- /dev/null +++ b/examples/with-backstage/aws-github.tf @@ -0,0 +1,51 @@ +locals { + name = "gha-ecr-push" +} + +# Create a role for GitHub Actions to push to ECR using OpenID Connect (OIDC) so we don't need to store AWS credentials in GitHub +# Reference https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services + +# Source https://github.com/terraform-aws-modules/terraform-aws-iam +module "iam_github_oidc_provider" { + source = "terraform-aws-modules/iam/aws//modules/iam-github-oidc-provider" + version = "~> 5.30" +} + +module "iam_github_oidc_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-github-oidc-role" + version = "~> 5.30" + + name = local.name + + subjects = [ + "${var.github_org_id}/*", + ] + + policies = { + ecr_push_policy = aws_iam_policy.ecr_push_policy.arn + } +} + +# Reference https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-push.html#image-push-iam +resource "aws_iam_policy" "ecr_push_policy" { + name = local.name + description = "GitHub Actions ECR Push Policy" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "ecr:CompleteLayerUpload", + "ecr:GetAuthorizationToken", + "ecr:UploadLayerPart", + "ecr:InitiateLayerUpload", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage" + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} diff --git a/examples/with-backstage/backstage-aws.tf b/examples/with-backstage/backstage-aws.tf new file mode 100644 index 0000000..c0b157c --- /dev/null +++ b/examples/with-backstage/backstage-aws.tf @@ -0,0 +1,14 @@ + +# Create ECR repository for the backstage image +# Source https://github.com/terraform-aws-modules/terraform-aws-ecr +module "backstage_ecr" { + source = "terraform-aws-modules/ecr/aws" + version = "~> 1.6" + + repository_name = "backstage" + repository_image_scan_on_push = false + repository_image_tag_mutability = "MUTABLE" + create_lifecycle_policy = false + + repository_force_delete = true +} diff --git a/examples/with-backstage/backstage-github.tf b/examples/with-backstage/backstage-github.tf new file mode 100644 index 0000000..8e10ea0 --- /dev/null +++ b/examples/with-backstage/backstage-github.tf @@ -0,0 +1,62 @@ +# Configure GitHub variables & secrets for Backstage itself and for all scaffolded apps + +locals { + github_app_credentials_file = "github-app-credentials.json" + github_app_credentials = jsondecode(file("${path.module}/${local.github_app_credentials_file}")) + github_app_id = local.github_app_credentials["appId"] + github_app_client_id = local.github_app_credentials["clientId"] + github_app_client_secret = local.github_app_credentials["clientSecret"] + github_app_private_key = local.github_app_credentials["privateKey"] + github_webhook_secret = local.github_app_credentials["webhookSecret"] +} + +locals { + backstage_repo = "backstage" +} + +resource "github_actions_organization_variable" "backstage_aws_region" { + variable_name = "AWS_REGION" + visibility = "all" + value = var.aws_region +} + +resource "github_actions_organization_variable" "backstage_aws_role_arn" { + variable_name = "AWS_ROLE_ARN" + visibility = "all" + value = module.iam_github_oidc_role.arn +} + +resource "github_actions_organization_variable" "backstage_humanitec_org_id" { + variable_name = "HUMANITEC_ORG_ID" + visibility = "all" + value = var.humanitec_org_id +} + +resource "github_actions_organization_secret" "backstage_humanitec_token" { + secret_name = "HUMANITEC_TOKEN" + visibility = "all" + plaintext_value = var.humanitec_ci_service_user_token +} + +# Backstage repository itself + +resource "github_repository" "backstage" { + name = local.backstage_repo + description = "Backstage" + + visibility = "public" + + template { + owner = "humanitec-architecture" + repository = "backstage" + } + + depends_on = [ + module.base, + module.backstage_ecr, + module.iam_github_oidc_role, + humanitec_application.backstage, + humanitec_resource_definition_criteria.backstage_postgres, + github_actions_organization_secret.backstage_humanitec_token, + ] +} diff --git a/examples/with-backstage/backstage-humanitec.tf b/examples/with-backstage/backstage-humanitec.tf new file mode 100644 index 0000000..1172370 --- /dev/null +++ b/examples/with-backstage/backstage-humanitec.tf @@ -0,0 +1,191 @@ +resource "humanitec_application" "backstage" { + id = "backstage" + name = "backstage" +} + +# Configure required values for backstage + +resource "humanitec_value" "backstage_github_org_id" { + app_id = humanitec_application.backstage.id + key = "GITHUB_ORG_ID" + description = "" + value = var.github_org_id + is_secret = false +} + +resource "humanitec_value" "backstage_github_app_id" { + app_id = humanitec_application.backstage.id + key = "GITHUB_APP_ID" + description = "" + value = local.github_app_id + is_secret = false +} + +resource "humanitec_value" "backstage_github_app_client_id" { + app_id = humanitec_application.backstage.id + key = "GITHUB_APP_CLIENT_ID" + description = "" + value = local.github_app_client_id + is_secret = true +} + +resource "humanitec_value" "backstage_github_app_client_secret" { + app_id = humanitec_application.backstage.id + key = "GITHUB_APP_CLIENT_SECRET" + description = "" + value = local.github_app_client_secret + is_secret = true +} + +resource "humanitec_value" "backstage_github_app_private_key" { + app_id = humanitec_application.backstage.id + key = "GITHUB_APP_PRIVATE_KEY" + description = "" + value = indent(2, local.github_app_private_key) + is_secret = true +} + +resource "humanitec_value" "backstage_github_app_webhook_secret" { + app_id = humanitec_application.backstage.id + key = "GITHUB_APP_WEBHOOK_SECRET" + description = "" + value = local.github_webhook_secret + is_secret = true +} + +resource "humanitec_value" "backstage_humanitec_org" { + app_id = humanitec_application.backstage.id + key = "HUMANITEC_ORG_ID" + description = "" + value = var.humanitec_org_id + is_secret = false +} + +resource "humanitec_value" "backstage_humanitec_token" { + app_id = humanitec_application.backstage.id + key = "HUMANITEC_TOKEN" + description = "" + value = var.humanitec_ci_service_user_token + is_secret = true +} + +resource "humanitec_value" "aws_default_region" { + app_id = humanitec_application.backstage.id + key = "AWS_DEFAULT_REGION" + description = "" + value = var.aws_region + is_secret = false +} + +# Configure required resources for backstage + +locals { + res_def_prefix = "backstage-" +} + +# in-cluster postgres + +module "backstage_postgres" { + source = "git::https://github.com/humanitec-architecture/resource-packs-in-cluster.git//humanitec-resource-defs/postgres/basic" + + prefix = local.res_def_prefix +} + +resource "humanitec_resource_definition_criteria" "backstage_postgres" { + resource_definition_id = module.backstage_postgres.id + app_id = humanitec_application.backstage.id + + force_delete = true +} + +# k8s service account (to assume an AWS role) + +module "backstage_k8s_service_account" { + source = "git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/k8s/service-account" + + prefix = local.res_def_prefix +} + +resource "humanitec_resource_definition_criteria" "backstage_k8s_service_account" { + resource_definition_id = module.backstage_k8s_service_account.id + app_id = humanitec_application.backstage.id + + force_delete = true +} + +# AWS policy to create ECR repositories (required to scaffold apps) + +module "backstage_iam_policy_ecr_create_repository" { + source = "git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/iam-policy/ecr-create-repository" + + access_key = module.base.aws_access_key_id + secret_key = module.base.aws_secret_access_key + resource_packs_aws_rev = var.resource_packs_aws_rev + humanitec_organization = var.humanitec_org_id + region = var.aws_region + + prefix = local.res_def_prefix +} + +resource "humanitec_resource_definition_criteria" "backstage_iam_policy_ecr_create_repository" { + resource_definition_id = module.backstage_iam_policy_ecr_create_repository.id + app_id = humanitec_application.backstage.id + + force_delete = true +} + +# AWS role assumable by the k8s service account + +module "backstage_iam_role_service_account" { + source = "git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/iam-role/service-account" + + access_key = module.base.aws_access_key_id + secret_key = module.base.aws_secret_access_key + resource_packs_aws_rev = var.resource_packs_aws_rev + humanitec_organization = var.humanitec_org_id + region = var.aws_region + + policy_classes = ["default"] + + oidc_provider = module.base.eks_oidc_provider + oidc_provider_arn = module.base.eks_oidc_provider_arn + prefix = local.res_def_prefix +} + +resource "humanitec_resource_definition_criteria" "backstage_iam_role_service_account" { + resource_definition_id = module.backstage_iam_role_service_account.id + app_id = humanitec_application.backstage.id + + force_delete = true +} + +# Workload resource that sets the service account + +module "backstage_workload" { + source = "git::https://github.com/humanitec-architecture/resource-packs-aws.git//humanitec-resource-defs/workload/service-account" + + prefix = local.res_def_prefix +} + +resource "humanitec_resource_definition_criteria" "backstage_workload" { + resource_definition_id = module.backstage_workload.id + app_id = humanitec_application.backstage.id + + force_delete = true +} + + +# Configure required resources for scaffolded apps + +# in-cluster mysql + +module "backstage_mysql" { + source = "git::https://github.com/humanitec-architecture/resource-packs-in-cluster.git//humanitec-resource-defs/mysql/basic" + + prefix = local.res_def_prefix +} + +resource "humanitec_resource_definition_criteria" "backstage_mysql" { + resource_definition_id = module.backstage_mysql.id + env_type = module.base.environment +} diff --git a/examples/with-backstage/create-gh-app/index.js b/examples/with-backstage/create-gh-app/index.js new file mode 100644 index 0000000..9f264d6 --- /dev/null +++ b/examples/with-backstage/create-gh-app/index.js @@ -0,0 +1,153 @@ +// Small CLI tool to create a GitHub App for Backstage +// +// Heavily inspired by https://github.com/backstage/backstage/blob/master/packages/cli/src/commands/create-github-app/ + +const http = require('http'); +const crypto = require('crypto'); +const fs = require('fs/promises') + +const hostname = '127.0.0.1'; +const port = 3000; + +const FORM_PAGE = ` + + +
+ + +
+ + + +`; + + +let baseUrl; + + +const webhookId = crypto +.randomBytes(15) +.toString('base64') +.replace(/[\+\/]/g, ''); + +const webhookUrl = `https://smee.io/${webhookId}`; + +const handleIndex = (req, res, GITHUB_ORG_ID) => { + const encodedOrg = encodeURIComponent(GITHUB_ORG_ID); + const actionUrl = `https://github.com/organizations/${encodedOrg}/settings/apps/new`; + + + res.statusCode = 200; + const manifest = { + default_events: ['create', 'delete', 'push', 'repository'], + default_permissions: { + members: 'read', + administration: 'write', + contents: 'write', + metadata: 'read', + pull_requests: 'write', + issues: 'write', + workflows: 'write', + checks: 'read', + actions_variables: 'write', + secrets: 'write', + environments: 'write', + }, + name: `backstage-${GITHUB_ORG_ID}`, + url: 'https://backstage.io', + description: 'GitHub App for Backstage', + public: false, + redirect_url: `${baseUrl}/callback`, + hook_attributes: { + url: webhookUrl, + active: false, + }, + }; + + const manifestJson = JSON.stringify(manifest).replace(/\"/g, '"'); + + let body = FORM_PAGE; + body = body.replace('MANIFEST_JSON', manifestJson); + body = body.replace('ACTION_URL', actionUrl); + + res.setHeader('content-type', 'text/html'); + res.end(body); +} + + +const writeConfigFile = async (data, webhookUrl) => { + const fileName = `github-app-credentials.json`; + const content = JSON.stringify({ + name: data.name, + slug: data.slug, + appId: data.id, + webhookUrl: webhookUrl, + clientId: data.client_id, + clientSecret: data.client_secret, + webhookSecret: data.webhook_secret, + privateKey: data.pem, + }, null, 2) + + await fs.writeFile(fileName, content); +} + +const handleCallback = async (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const conversionRes = await fetch(`https://api.github.com/app-manifests/${encodeURIComponent(url.searchParams.get('code'))}/conversions`, { + method: 'POST', + }); + + if (conversionRes.status !== 201) { + const body = await conversionRes.text(); + res.statusCode = conversionRes.status; + res.end(body); + } + + const data = await conversionRes.json(); + + await writeConfigFile(data, webhookUrl); + + console.log(`Created ${fileName}, you can close the server now.`) + + res.writeHead(302, { Location: `${data.html_url}/installations/new` }); + res.end(); +} + +if (process.env.STUB_FILE === '1') { + writeConfigFile({ + name: 'stub', + slug: 'stub', + id: 'stub', + client_id: 'stub', + client_secret: 'stub', + webhook_secret: 'stub', + pem: 'stub', + }, 'https://smee.io/stub'); + + return; +} + +const GITHUB_ORG_ID = process.env.GITHUB_ORG_ID; +if (!GITHUB_ORG_ID) { + console.error('Please export GITHUB_ORG_ID'); + process.exit(1); +} + +const server = http.createServer((req, res) => { + if (req.url === '/') { + handleIndex(req, res, GITHUB_ORG_ID); + } else if (req.url.startsWith('/callback?')) { + handleCallback(req, res); + } else { + res.statusCode = 404; + res.end('Not found, url: ' + req.url); + } +}); + +server.listen(port, hostname, () => { + baseUrl = `http://${hostname}:${port}`; + + console.log(`Open ${baseUrl}`); +}); diff --git a/examples/with-backstage/main.tf b/examples/with-backstage/main.tf new file mode 100644 index 0000000..fc5e4b2 --- /dev/null +++ b/examples/with-backstage/main.tf @@ -0,0 +1,33 @@ +# AWS reference architecture + +module "base" { + source = "../../modules/base" + + region = var.aws_region +} + +provider "kubernetes" { + host = module.base.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.base.eks_cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + # This requires the awscli to be installed locally where Terraform is executed + args = ["eks", "get-token", "--cluster-name", module.base.eks_cluster_name] + } +} + +provider "helm" { + kubernetes { + host = module.base.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.base.eks_cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + # This requires the awscli to be installed locally where Terraform is executed + args = ["eks", "get-token", "--cluster-name", module.base.eks_cluster_name] + } + } +} diff --git a/examples/with-backstage/provider.tf b/examples/with-backstage/provider.tf new file mode 100644 index 0000000..1cab49e --- /dev/null +++ b/examples/with-backstage/provider.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.17" + } + humanitec = { + source = "humanitec/humanitec" + version = "~> 0.13" + } + github = { + source = "integrations/github" + version = "~> 5.38" + } + } + required_version = ">= 1.3.0" +} + +provider "humanitec" { + org_id = var.humanitec_org_id +} + +provider "github" { + owner = var.github_org_id +} + +provider "aws" { + region = var.aws_region + allowed_account_ids = [var.aws_account_id] +} diff --git a/examples/with-backstage/terraform.tfvars.example b/examples/with-backstage/terraform.tfvars.example new file mode 100644 index 0000000..4c407d3 --- /dev/null +++ b/examples/with-backstage/terraform.tfvars.example @@ -0,0 +1,18 @@ + +# AWS Account (ID) to use +aws_account_id = "" + +# AWS region +aws_region = "" + +# GitHub org id +github_org_id = "" + +# Humanitec CI Service User Token +humanitec_ci_service_user_token = "" + +# Humanitec Organization ID +humanitec_org_id = "" + +# Revision of the resource-packs-aws repository to use +resource_packs_aws_rev = "refs/heads/main" \ No newline at end of file diff --git a/examples/with-backstage/variables.tf b/examples/with-backstage/variables.tf new file mode 100644 index 0000000..dd71328 --- /dev/null +++ b/examples/with-backstage/variables.tf @@ -0,0 +1,31 @@ +variable "aws_account_id" { + description = "AWS Account (ID) to use" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string +} + +variable "github_org_id" { + description = "GitHub org id" + type = string +} + +variable "humanitec_org_id" { + description = "Humanitec Organization ID" + type = string +} + +variable "humanitec_ci_service_user_token" { + description = "Humanitec CI Service User Token" + type = string + sensitive = true +} + +variable "resource_packs_aws_rev" { + description = "Revision of the resource-packs-aws repository to use" + type = string + default = "refs/heads/main" +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..d37a2f5 --- /dev/null +++ b/main.tf @@ -0,0 +1,56 @@ + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.17" + } + humanitec = { + source = "humanitec/humanitec" + version = "~> 0.13" + } + } + required_version = ">= 1.3.0" +} + +provider "humanitec" { + org_id = var.humanitec_org_id +} + +provider "aws" { + region = var.aws_region + allowed_account_ids = [var.aws_account_id] +} + + +module "base" { + source = "./modules/base" + + region = var.aws_region +} + +provider "kubernetes" { + host = module.base.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.base.eks_cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + # This requires the awscli to be installed locally where Terraform is executed + args = ["eks", "get-token", "--cluster-name", module.base.eks_cluster_name] + } +} + +provider "helm" { + kubernetes { + host = module.base.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.base.eks_cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + # This requires the awscli to be installed locally where Terraform is executed + args = ["eks", "get-token", "--cluster-name", module.base.eks_cluster_name] + } + } +} diff --git a/modules/base/.terraform-docs.yaml b/modules/base/.terraform-docs.yaml new file mode 100644 index 0000000..844071f --- /dev/null +++ b/modules/base/.terraform-docs.yaml @@ -0,0 +1,11 @@ +formatter: "markdown table" + +output: + file: "./README.md" + +sort: + enabled: true + by: required + +settings: + hide-empty: true diff --git a/modules/base/README.md b/modules/base/README.md new file mode 100644 index 0000000..59df3b0 --- /dev/null +++ b/modules/base/README.md @@ -0,0 +1,83 @@ +# base + +Module that provides the reference architecture. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.50 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0.3 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.50 | +| [helm](#provider\_helm) | n/a | +| [humanitec](#provider\_humanitec) | n/a | +| [kubernetes](#provider\_kubernetes) | >= 2.0.3 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [aws\_eks](#module\_aws\_eks) | terraform-aws-modules/eks/aws | ~> 19.16 | +| [aws\_vpc](#module\_aws\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.1 | +| [ebs\_csi\_irsa\_role](#module\_ebs\_csi\_irsa\_role) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks | ~> 5.30 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_access_key.humanitec_svc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource | +| [aws_iam_user.humanitec_svc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | +| [aws_iam_user_policy_attachment.humanitec_svc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource | +| [helm_release.ingress_nginx](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [humanitec_resource_definition.k8s_cluster_driver](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition) | resource | +| [humanitec_resource_definition.k8s_logging](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition) | resource | +| [humanitec_resource_definition.k8s_namespace](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition) | resource | +| [humanitec_resource_definition_criteria.k8s_cluster_driver](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.k8s_logging](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [humanitec_resource_definition_criteria.k8s_namespace](https://registry.terraform.io/providers/humanitec/humanitec/latest/docs/resources/resource_definition_criteria) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [kubernetes_service.ingress_nginx_controller](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/service) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_aws\_auth\_users](#input\_additional\_aws\_auth\_users) | Additional users add to the k8s aws-auth configmap | `list(any)` | `[]` | no | +| [capacity\_type](#input\_capacity\_type) | Defines whether to use ON\_DEMAND or SPOT EC2 instances for EKS nodes | `string` | `"ON_DEMAND"` | no | +| [cluster\_name](#input\_cluster\_name) | Name for the EKS cluster | `string` | `"ref-arch"` | no | +| [cluster\_version](#input\_cluster\_version) | Version of the EKS cluster to deploy | `string` | `null` | no | +| [eks\_public\_access\_cidrs](#input\_eks\_public\_access\_cidrs) | List of CIDRs that can access the EKS cluster's public endpoint | `list(string)` |
[
"0.0.0.0/0"
]
| no | +| [environment](#input\_environment) | Name of the environment to be deployed into | `string` | `"development"` | no | +| [iam\_user\_name](#input\_iam\_user\_name) | Name of the IAM user to create for Humanitec EKS access | `string` | `"svc-humanitec"` | no | +| [ingress\_nginx\_min\_unavailable](#input\_ingress\_nginx\_min\_unavailable) | Number of allowed unavaiable replicas for the ingress-nginx controller | `number` | `1` | no | +| [ingress\_nginx\_replica\_count](#input\_ingress\_nginx\_replica\_count) | Number of replicas for the ingress-nginx controller | `number` | `2` | no | +| [instance\_types](#input\_instance\_types) | List of EC2 instances types to use for EKS nodes | `list(string)` |
[
"t3.large"
]
| no | +| [node\_group\_desired\_size](#input\_node\_group\_desired\_size) | Desired number of nodes for the EKS node group | `number` | `3` | no | +| [node\_group\_max\_size](#input\_node\_group\_max\_size) | Maximum number of nodes for the EKS node group | `number` | `3` | no | +| [node\_group\_min\_size](#input\_node\_group\_min\_size) | Minimum number of nodes for the EKS node group | `number` | `2` | no | +| [region](#input\_region) | AWS Region to deploy into | `string` | `"us-east-1"` | no | +| [vpc\_name](#input\_vpc\_name) | AWS VPC name | `string` | `"ref-arch"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [aws\_access\_key\_id](#output\_aws\_access\_key\_id) | n/a | +| [aws\_secret\_access\_key](#output\_aws\_secret\_access\_key) | n/a | +| [eks\_cluster\_certificate\_authority\_data](#output\_eks\_cluster\_certificate\_authority\_data) | Base64 encoded certificate data required to communicate with the cluster | +| [eks\_cluster\_endpoint](#output\_eks\_cluster\_endpoint) | Endpoint for your Kubernetes API server | +| [eks\_cluster\_name](#output\_eks\_cluster\_name) | The name of the EKS cluster | +| [eks\_oidc\_provider](#output\_eks\_oidc\_provider) | The OpenID Connect identity provider (issuer URL without leading `https://`) | +| [eks\_oidc\_provider\_arn](#output\_eks\_oidc\_provider\_arn) | The ARN of the OIDC Provider | +| [environment](#output\_environment) | Name of the environment to be deployed into | +| [ingress\_nginx\_external\_dns](#output\_ingress\_nginx\_external\_dns) | External DNS entry for the Nginx ingress controller | +| [vpc\_id](#output\_vpc\_id) | VPC id | + diff --git a/modules/base/humanitec.tf b/modules/base/humanitec.tf new file mode 100644 index 0000000..e8ba08f --- /dev/null +++ b/modules/base/humanitec.tf @@ -0,0 +1,63 @@ +# Humanitec resource definition to connect the cluster to Humanitec + +locals { + ingress_address = data.kubernetes_service.ingress_nginx_controller.status.0.load_balancer.0.ingress.0.hostname +} + +resource "humanitec_resource_definition" "k8s_cluster_driver" { + driver_type = "humanitec/k8s-cluster-eks" + id = var.cluster_name + name = var.cluster_name + type = "k8s-cluster" + + driver_inputs = { + values_string = jsonencode({ + "name" = module.aws_eks.cluster_name + "loadbalancer" = local.ingress_address + "region" = var.region + }), + secrets_string = jsonencode({ + "credentials" = { + "aws_access_key_id" = aws_iam_access_key.humanitec_svc.id + "aws_secret_access_key" = aws_iam_access_key.humanitec_svc.secret + } + }) + } +} + +resource "humanitec_resource_definition_criteria" "k8s_cluster_driver" { + resource_definition_id = humanitec_resource_definition.k8s_cluster_driver.id + env_type = var.environment +} + + +resource "humanitec_resource_definition" "k8s_logging" { + driver_type = "humanitec/logging-k8s" + id = "default-logging" + name = "default-logging" + type = "logging" + + driver_inputs = {} +} + +resource "humanitec_resource_definition_criteria" "k8s_logging" { + resource_definition_id = humanitec_resource_definition.k8s_logging.id +} + + +resource "humanitec_resource_definition" "k8s_namespace" { + driver_type = "humanitec/static" + id = "default-namespace" + name = "default-namespace" + type = "k8s-namespace" + + driver_inputs = { + values_string = jsonencode({ + "namespace" = "$${context.app.id}-$${context.env.id}" + }) + } +} + +resource "humanitec_resource_definition_criteria" "k8s_namespace" { + resource_definition_id = humanitec_resource_definition.k8s_namespace.id +} diff --git a/modules/base/main.tf b/modules/base/main.tf new file mode 100644 index 0000000..706a68e --- /dev/null +++ b/modules/base/main.tf @@ -0,0 +1,158 @@ +locals { + admin_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" + tags = { + Terraform = "true" + Environment = var.environment + } +} + +# User for Humanitec to access the EKS cluster + +resource "aws_iam_user" "humanitec_svc" { + name = var.iam_user_name +} + +resource "aws_iam_user_policy_attachment" "humanitec_svc" { + user = aws_iam_user.humanitec_svc.name + policy_arn = local.admin_policy_arn +} + +resource "aws_iam_access_key" "humanitec_svc" { + user = aws_iam_user.humanitec_svc.name + + # Ensure that the policy is not deleted before the access key + depends_on = [aws_iam_user_policy_attachment.humanitec_svc] +} + +# VPC and EKS cluster + +data "aws_region" "current" {} + +module "aws_vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.1" + + name = var.vpc_name + cidr = "10.0.0.0/16" + + azs = formatlist("${data.aws_region.current.name}%s", ["a", "b", "c"]) + private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] + + enable_nat_gateway = true + + tags = local.tags +} + +data "aws_caller_identity" "current" {} + +locals { + default_aws_auth_users = [ + { + userarn = data.aws_caller_identity.current.arn + username = "creator" + groups = ["system:masters"] + }, + { + userarn = aws_iam_user.humanitec_svc.arn + username = aws_iam_user.humanitec_svc.name + groups = ["system:masters"] + } + ] + aws_auth_users = concat(local.default_aws_auth_users, var.additional_aws_auth_users) +} + +module "ebs_csi_irsa_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "~> 5.30" + + role_name = "ebs-csi" + attach_ebs_csi_policy = true + + oidc_providers = { + ex = { + provider_arn = module.aws_eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"] + } + } + + tags = local.tags +} + +module "aws_eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 19.16" + + cluster_name = var.cluster_name + cluster_version = var.cluster_version + + cluster_endpoint_public_access = true + cluster_endpoint_public_access_cidrs = var.eks_public_access_cidrs + + vpc_id = module.aws_vpc.vpc_id + subnet_ids = module.aws_vpc.private_subnets + + eks_managed_node_groups = { + green = { + min_size = var.node_group_min_size + max_size = var.node_group_max_size + desired_size = var.node_group_desired_size + + instance_types = var.instance_types + capacity_type = var.capacity_type + } + } + + cluster_addons = { + aws-ebs-csi-driver = { + most_recent = true + service_account_role_arn = module.ebs_csi_irsa_role.iam_role_arn + } + } + + manage_aws_auth_configmap = true + aws_auth_users = local.aws_auth_users + + # required for ingress-nginx see https://github.com/terraform-aws-modules/terraform-aws-eks/issues/2513 + node_security_group_additional_rules = { + ingress_self_all = { + description = "Node to node all ports/protocols" + protocol = "-1" + from_port = 0 + to_port = 0 + type = "ingress" + self = true + } + } + + tags = local.tags +} + + +# Ingress controller + +resource "helm_release" "ingress_nginx" { + name = "ingress-nginx" + namespace = "ingress-nginx" + create_namespace = true + repository = "https://kubernetes.github.io/ingress-nginx" + + chart = "ingress-nginx" + version = "4.5.0" + wait = true + timeout = 600 + + set { + type = "string" + name = "controller.replicaCount" + value = var.ingress_nginx_replica_count + } + + set { + type = "string" + name = "controller.minAvailable" + value = var.ingress_nginx_min_unavailable + } + + depends_on = [module.aws_eks.eks_managed_node_groups] +} diff --git a/modules/base/meta.tf b/modules/base/meta.tf new file mode 100644 index 0000000..fbfc111 --- /dev/null +++ b/modules/base/meta.tf @@ -0,0 +1,7 @@ +data "kubernetes_service" "ingress_nginx_controller" { + metadata { + name = "ingress-nginx-controller" + namespace = "ingress-nginx" + } + depends_on = [helm_release.ingress_nginx] +} diff --git a/modules/base/outputs.tf b/modules/base/outputs.tf new file mode 100644 index 0000000..7b99cb8 --- /dev/null +++ b/modules/base/outputs.tf @@ -0,0 +1,58 @@ +# General outputs + +output "environment" { + description = "Name of the environment to be deployed into" + value = var.environment +} + +# VPC outputs + +output "vpc_id" { + description = "VPC id" + value = module.aws_vpc.vpc_id +} + +# EKS outputs + +output "eks_oidc_provider" { + description = "The OpenID Connect identity provider (issuer URL without leading `https://`)" + value = module.aws_eks.oidc_provider +} + +output "eks_oidc_provider_arn" { + description = "The ARN of the OIDC Provider" + value = module.aws_eks.oidc_provider_arn +} + +output "eks_cluster_endpoint" { + description = "Endpoint for your Kubernetes API server" + value = module.aws_eks.cluster_endpoint +} + +output "eks_cluster_certificate_authority_data" { + description = "Base64 encoded certificate data required to communicate with the cluster" + value = module.aws_eks.cluster_certificate_authority_data +} + +output "eks_cluster_name" { + description = "The name of the EKS cluster" + value = module.aws_eks.cluster_name +} + +# Ingress outputs + +output "ingress_nginx_external_dns" { + description = "External DNS entry for the Nginx ingress controller" + value = local.ingress_address +} + + +# Key + +output "aws_access_key_id" { + value = aws_iam_access_key.humanitec_svc.id +} + +output "aws_secret_access_key" { + value = aws_iam_access_key.humanitec_svc.secret +} diff --git a/modules/base/providers.tf b/modules/base/providers.tf new file mode 100644 index 0000000..76fb784 --- /dev/null +++ b/modules/base/providers.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.50" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.0.3" + } + helm = { + source = "hashicorp/helm" + } + humanitec = { + source = "humanitec/humanitec" + } + } + required_version = ">= 1.3.0" +} diff --git a/modules/base/variables.tf b/modules/base/variables.tf new file mode 100644 index 0000000..3bf079b --- /dev/null +++ b/modules/base/variables.tf @@ -0,0 +1,91 @@ +variable "eks_public_access_cidrs" { + description = "List of CIDRs that can access the EKS cluster's public endpoint" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "region" { + description = "AWS Region to deploy into" + type = string + default = "us-east-1" +} + +variable "vpc_name" { + description = "AWS VPC name" + type = string + default = "ref-arch" +} + +variable "environment" { + description = "Name of the environment to be deployed into" + type = string + default = "development" +} + +variable "cluster_name" { + description = "Name for the EKS cluster" + type = string + default = "ref-arch" +} + +variable "cluster_version" { + description = "Version of the EKS cluster to deploy" + type = string + default = null +} + +variable "node_group_min_size" { + description = "Minimum number of nodes for the EKS node group" + type = number + default = 2 +} + +variable "node_group_max_size" { + description = "Maximum number of nodes for the EKS node group" + type = number + default = 3 +} + +variable "node_group_desired_size" { + description = "Desired number of nodes for the EKS node group" + type = number + default = 3 +} + +variable "instance_types" { + description = "List of EC2 instances types to use for EKS nodes" + type = list(string) + default = [ + "t3.large" + ] +} + +variable "capacity_type" { + description = "Defines whether to use ON_DEMAND or SPOT EC2 instances for EKS nodes" + type = string + default = "ON_DEMAND" +} + +variable "iam_user_name" { + description = "Name of the IAM user to create for Humanitec EKS access" + type = string + default = "svc-humanitec" +} + +variable "additional_aws_auth_users" { + description = "Additional users add to the k8s aws-auth configmap" + type = list(any) + default = [] +} + +variable "ingress_nginx_replica_count" { + description = "Number of replicas for the ingress-nginx controller" + type = number + default = 2 +} + +variable "ingress_nginx_min_unavailable" { + description = "Number of allowed unavaiable replicas for the ingress-nginx controller" + type = number + default = 1 +} diff --git a/terraform.tfvars.example b/terraform.tfvars.example new file mode 100644 index 0000000..46fe5c1 --- /dev/null +++ b/terraform.tfvars.example @@ -0,0 +1,9 @@ + +# AWS Account (ID) to use +aws_account_id = "" + +# AWS Region to deploy into +aws_region = "" + +# Humanitec Organization ID +humanitec_org_id = "" \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..2dd9f8c --- /dev/null +++ b/variables.tf @@ -0,0 +1,15 @@ + +variable "aws_region" { + description = "AWS Region to deploy into" + type = string +} + +variable "aws_account_id" { + description = "AWS Account (ID) to use" + type = string +} + +variable "humanitec_org_id" { + description = "Humanitec Organization ID" + type = string +}