From 6437c73242b02d8f6e00525d46ca0432e46f9f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Mon, 8 Jul 2024 15:52:06 +0200 Subject: [PATCH] feat: migrate to talos linux (#252) This PR migrates to Talos Linux for all environments. * The Terraform configuration is simplified to a single base module in modules/base * It uses three t3a.small instances for sandbox and t3a.medium instances for staging and production. * It adds a GitHub OIDC provider to allow GitHub to request session tokens and removes the need to store AWS credentials in GitHub secrets. * IAM roles are attached to an EC2 instance profile to allow EC2 instances to pull images from ECR and use S3 static storage without AWS access credentials. It achieves this by using an ECR credential helper in Talos. S3 access is not fully working yet and requires connection of the cluster OIDC config to AWS as per https://nikogura.com/TalosAWSOIDC.html * Finally, it installs ArgoCD in the cluster and bootstraps the cluster using the app of apps pattern. Note that even though the base module installs Talos Linux, it has been structured to make it easy to drop an alternative AMI image. --- cookiecutter.json | 2 + scaf | 2 +- .../terraform/.gitignore | 3 + .../terraform/README.md | 135 ++++++++++++--- .../terraform/ec2-cluster/Makefile | 62 ------- .../terraform/ec2-cluster/README.md | 113 ------------- .../terraform/ec2-cluster/backend.tf | 25 --- .../terraform/ec2-cluster/bin/command | 3 - .../terraform/ec2-cluster/bin/generate-tfvars | 6 - .../ec2-cluster/bin/get-my-global-ip | 3 - .../terraform/ec2-cluster/bin/get-node-port | 3 - .../terraform/ec2-cluster/bin/ip | 3 - .../terraform/ec2-cluster/config.yml | 22 --- .../terraform/ec2-cluster/ec2_cluster.tf | 30 ---- .../terraform/ec2-cluster/outputs.tf | 8 - .../terraform/ec2-cluster/security_groups.tf | 75 --------- .../ec2-cluster/terraform.tfvars.template | 2 - .../terraform/ec2-cluster/variables.tf | 75 --------- .../terraform/github/oidc.tf | 60 +++++++ .../terraform/management/backend.tf | 25 --- .../terraform/management/iam.tf | 57 ------- .../terraform/management/locals.tf | 7 - .../terraform/management/outputs.tf | 7 - .../terraform/management/route53_zone.tf | 4 - .../terraform/management/s3.tf | 11 -- .../terraform/modules/application/acm.tf | 21 --- .../terraform/modules/application/data.tf | 7 - .../terraform/modules/application/iam.tf | 60 ------- .../terraform/modules/application/outputs.tf | 12 -- .../terraform/modules/application/route53.tf | 44 ----- .../modules/application/variables.tf | 51 ------ .../terraform/modules/base/Makefile | 63 +++++++ .../terraform/modules/base/acm.tf | 22 +++ .../terraform/modules/base/backend.tf | 9 + .../{application => base}/cloudfront.tf | 44 +++-- .../terraform/modules/base/ec2-iam-role.tf | 82 +++++++++ .../terraform/modules/base/ec2.tf | 25 +++ .../{management => modules/base}/ecr.tf | 10 +- .../terraform/modules/base/elb.tf | 44 +++++ .../terraform/modules/base/github-iam-role.tf | 43 +++++ .../terraform/modules/base/helm.tf | 87 ++++++++++ .../terraform/modules/base/iam.tf | 14 ++ .../terraform/modules/base/kms.tf | 39 +++++ .../modules/{application => base}/locals.tf | 4 +- .../terraform/modules/base/outputs.tf | 38 +++++ .../modules/base/repocreds.template.yaml | 12 ++ .../terraform/modules/base/route53.tf | 62 +++++++ .../modules/{application => base}/s3.tf | 20 ++- .../terraform/modules/base/security_groups.tf | 52 ++++++ .../terraform/modules/base/talos.tf | 156 ++++++++++++++++++ .../terraform/modules/base/variables.tf | 133 +++++++++++++++ .../terraform/modules/base/versions.tf | 37 +++++ .../terraform/modules/base/vpc.tf | 22 +++ .../terraform/prod/application.tf | 15 -- .../terraform/prod/backend.tf | 14 -- .../terraform/prod/cluster.tf | 23 +++ .../terraform/prod/data.tf | 3 - .../terraform/prod/locals.tf | 8 - .../terraform/prod/outputs.tf | 39 +++-- .../terraform/prod/variables.tf | 28 ---- .../terraform/sandbox/application.tf | 15 -- .../terraform/sandbox/backend.tf | 10 -- .../terraform/sandbox/cluster.tf | 24 +++ .../terraform/sandbox/data.tf | 3 - .../terraform/sandbox/locals.tf | 8 - .../terraform/sandbox/outputs.tf | 39 +++-- .../terraform/sandbox/variables.tf | 28 ---- .../terraform/staging/backend.tf | 10 ++ .../terraform/staging/cluster.tf | 23 +++ .../terraform/staging/outputs.tf | 37 +++++ 70 files changed, 1333 insertions(+), 950 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/terraform/.gitignore delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/Makefile delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/README.md delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/backend.tf delete mode 100755 {{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/command delete mode 100755 {{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/generate-tfvars delete mode 100755 {{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-my-global-ip delete mode 100755 {{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-node-port delete mode 100755 {{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/ip delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/config.yml delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/ec2_cluster.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/outputs.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/security_groups.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/terraform.tfvars.template delete mode 100644 {{cookiecutter.project_slug}}/terraform/ec2-cluster/variables.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/github/oidc.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/management/backend.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/management/iam.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/management/locals.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/management/outputs.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/management/route53_zone.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/management/s3.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/modules/application/acm.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/modules/application/data.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/modules/application/iam.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/modules/application/outputs.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/modules/application/route53.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/modules/application/variables.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/Makefile create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/acm.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/backend.tf rename {{cookiecutter.project_slug}}/terraform/modules/{application => base}/cloudfront.tf (71%) create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/ec2-iam-role.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/ec2.tf rename {{cookiecutter.project_slug}}/terraform/{management => modules/base}/ecr.tf (80%) create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/elb.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/github-iam-role.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/helm.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/iam.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/kms.tf rename {{cookiecutter.project_slug}}/terraform/modules/{application => base}/locals.tf (53%) create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/outputs.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/repocreds.template.yaml create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/route53.tf rename {{cookiecutter.project_slug}}/terraform/modules/{application => base}/s3.tf (78%) create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/security_groups.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/talos.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/variables.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/versions.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/modules/base/vpc.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/prod/application.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/prod/cluster.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/prod/data.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/prod/locals.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/prod/variables.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/sandbox/application.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/sandbox/cluster.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/sandbox/data.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/sandbox/locals.tf delete mode 100644 {{cookiecutter.project_slug}}/terraform/sandbox/variables.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/staging/backend.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/staging/cluster.tf create mode 100644 {{cookiecutter.project_slug}}/terraform/staging/outputs.tf diff --git a/cookiecutter.json b/cookiecutter.json index 265b073c..d0702dae 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -5,6 +5,8 @@ "description": "Behold My Awesome Project!", "author_name": "Joe Sixie", "domain_name": "sixfeetup.com", + "repo_name": "{{ cookiecutter.project_name }}", + "repo_url": "git@github.com:sixfeetup/{{ cookiecutter.project_slug }}.git", "email": "{{ cookiecutter.author_name.lower()|replace(' ', '-') }}@example.com", "version": "0.1.0", "timezone": "US/Eastern", diff --git a/scaf b/scaf index 23be4da2..fe8d3800 100755 --- a/scaf +++ b/scaf @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Default repository URL if none is provided DEFAULT_REPO_URL="https://github.com/sixfeetup/scaf/" diff --git a/{{cookiecutter.project_slug}}/terraform/.gitignore b/{{cookiecutter.project_slug}}/terraform/.gitignore new file mode 100644 index 00000000..1d6a5946 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/.gitignore @@ -0,0 +1,3 @@ +talosconfig +kubeconfig +repocreds.yaml diff --git a/{{cookiecutter.project_slug}}/terraform/README.md b/{{cookiecutter.project_slug}}/terraform/README.md index ef69f887..40688637 100644 --- a/{{cookiecutter.project_slug}}/terraform/README.md +++ b/{{cookiecutter.project_slug}}/terraform/README.md @@ -1,31 +1,118 @@ -### Terraform is an infrastructure as code tool that manages provisioning AWS resources. -https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli +# Terraform -The terraform directory handles all infrastructure provisioning using terraform. +This directory contains the Terraform configurations for the Scaf project. The +configurations are organized into several directories, each serving a specific +purpose. Below is a brief overview of each directory and instructions on how to +run the Terraform configurations. -### Commands: -* `terraform init`: needs to be run first for every directory, installs the terraform providers -* `terraform plan`: shows changes that will be done by the manifests, no changes will be applied yet -* `terraform apply`: applies the changes shown by the plan output +## Directory Structure -### First step: -* `./bootstrap` -Run apply in the bootstrap directory first to set up the terraform remote state used in all other manifests. -* If your account is not an organisation account you will need to remove or adjust the assume_role block in the bootstrap/init.tf file. +- **bootstrap**: Bootstraps the Terraform state in an S3 bucket and a DynamoDB + table. This configuration contains the states for all environments and only + needs to be run once. -### Next steps: -* `./management` -Set up the ECR repositories for the docker images, as well as IAM users and route 53 zone, this should be run after bootstrap. +- **github**: Sets up a GitHub OIDC provider to allow GitHub to push container + images to ECR repositories. -* `./ec2-cluster` -Sets up an EC2 instance and deploys a k3s cluster on it. For more information follow ./ec2_cluster/README.md -Note this will create a t2.medium instance that does not fall under the free tier. -This should be set up before attempting to deploy prod/sandbox. +- **modules**: Contains a base module that is used by all environments. -* `./prod` and `./sandbox` -Sets up route53 for prod and sandbox respectively. +- **prod**: Contains the configuration for the production environment. -### After terraform has initialised the deployment process will need to be updated with its outputs: -* update CI/CD with the AWS access keys of the IAM `cicd_user`. -* update kubernetes manifests and any CI/CD making calls to the ECR images with the ECR url. -* update CloudNativePG manifest to set the backup with S3 `cloudnative_pg` bucket url. +- **sandbox**: Contains the configuration for the sandbox environment. + +- **staging**: Contains the configuration for the staging environment. + +## Setup Instructions + +### Step 1: Bootstrap + +The first step is to bootstrap the Terraform state. This involves creating an S3 +bucket and a DynamoDB table to manage the state and locking. + +1. Navigate to the `bootstrap` directory: + ```bash + cd bootstrap + ``` + +2. Initialize the Terraform configuration: + ```bash + terraform init + ``` + +3. Plan the Terraform configuration: + ```bash + terraform plan -out="tfplan.out" + ``` + +4. Apply the Terraform configuration: + ```bash + terraform apply tfplan.out + ``` + +### Step 2: GitHub OIDC Provider + +After bootstrapping the state, the next step is to set up the GitHub OIDC +provider. + +1. Navigate to the `github` directory: + ```bash + cd ../github + ``` + +2. Initialize the Terraform configuration: + ```bash + terraform init + ``` + +3. Plan the Terraform configuration: + ```bash + terraform plan -out="tfplan.out" + ``` + +4. Apply the Terraform configuration: + ```bash + terraform apply tfplan.out + ``` + +### Step 3: Environment Configurations + +The final step is to set up the respective environment configurations (prod, +sandbox, staging). + +1. Navigate to the desired environment directory (e.g., `prod`, `sandbox`, + `staging`): + + ```bash + cd ../ + ``` + +2. Initialize the Terraform configuration: + ```bash + terraform init + ``` + +3. Plan the Terraform configuration: + ```bash + terraform plan -out="tfplan.out" + ``` + +4. Apply the Terraform configuration: + ```bash + terraform apply tfplan.out + ``` + +## Summary + +The order of operations is critical for the correct setup of the Terraform +configurations: + +1. Bootstrap the Terraform state (`bootstrap` directory). +2. Set up the GitHub OIDC provider (`github` directory). +3. Configure the desired environment (`prod`, `sandbox`, or `staging` directory). + + +Each step involves running `terraform init`, `terraform plan -out="tfplan.out"`, +and `terraform apply tfplan.out`. + +Following these steps ensures that your infrastructure is set up correctly and +efficiently. diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/Makefile b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/Makefile deleted file mode 100644 index 22c548ab..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/Makefile +++ /dev/null @@ -1,62 +0,0 @@ -generate-tfvars: - terraform apply -refresh-only -auto-approve - bin/generate-tfvars - -key-pair: - ssh-keygen -t ED25519 -f ~/.ssh/{{ cookiecutter.project_slug }}_default_key -N "" - -plan: - terraform plan - -deploy: - terraform apply -auto-approve - -INSTANCE_IP := $(shell ./bin/ip | sed 's/"//g') - -config: - ssh -oStrictHostKeyChecking=no -i ~/.ssh/{{ cookiecutter.project_slug }}_default_key \ - ubuntu@$(INSTANCE_IP) \ - 'curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--tls-san $(INSTANCE_IP)" K3S_KUBECONFIG_MODE="644" sh -s -' - ssh -oStrictHostKeyChecking=no -i ~/.ssh/{{ cookiecutter.project_slug }}_default_key \ - ubuntu@$(INSTANCE_IP) cat /etc/rancher/k3s/k3s.yaml \ - > ~/.kube/{{ cookiecutter.project_slug }}.ec2.config - sed -ie 's/127.0.0.1/$(INSTANCE_IP)/' ~/.kube/{{ cookiecutter.project_slug }}.ec2.config - sed -ie 's/default/{{ cookiecutter.project_slug }}-ec2-cluster/' ~/.kube/{{ cookiecutter.project_slug }}.ec2.config - export KUBECONFIG=~/.kube/{{ cookiecutter.project_slug }}.ec2.config - kubectl config get-contexts - kubectl config use-context {{ cookiecutter.project_slug }}-ec2-cluster - # do not apply cloudconfig in sandbox - # kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.20/releases/cnpg-1.20.0.yaml - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.0/cert-manager.yaml - kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.22/releases/cnpg-1.22.2.yaml - helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets - helm upgrade --install sealed-secrets -n kube-system --set-string fullnameOverride=sealed-secrets-controller sealed-secrets/sealed-secrets - -cluster-uninstall: - ssh -oStrictHostKeyChecking=no -i ~/.ssh/{{ cookiecutter.project_slug }}_default_key \ - ubuntu@$(INSTANCE_IP) \ - 'sudo /usr/local/bin/k3s-uninstall.sh' - -ssh: - ssh -i ~/.ssh/{{ cookiecutter.project_slug }}_default_key ubuntu@$(INSTANCE_IP) - -show-ip: - @./bin/ip - -destroy: - terraform destroy - -kubecreds: - aws sso login --profile={{ cookiecutter.project_slug }} - aws ecr get-login-password --region {{ cookiecutter.aws_region }} | docker login --username AWS \ - --password-stdin {{ cookiecutter.aws_account_id }}.dkr.ecr.{{ cookiecutter.aws_region }}.amazonaws.com - kubectl delete secret regcred -n {{ cookiecutter.project_dash }}-prod --ignore-not-found - kubectl delete secret regcred -n {{ cookiecutter.project_dash }}-sandbox --ignore-not-found - kubectl create secret docker-registry regcred -n {{ cookiecutter.project_dash }}-prod \ - --docker-server={{ cookiecutter.aws_account_id }}.dkr.ecr.{{ cookiecutter.aws_region }}.amazonaws.com \ - --docker-username=AWS \ - --docker-password=$(shell aws ecr get-login-password) - kubectl create secret docker-registry regcred -n {{ cookiecutter.project_dash }}-sandbox \ - --docker-server={{ cookiecutter.aws_account_id }}.dkr.ecr.{{ cookiecutter.aws_region }}.amazonaws.com \ - --docker-username=AWS \ - --docker-password=$(shell aws ecr get-login-password) diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/README.md b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/README.md deleted file mode 100644 index 387bd875..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# k3s on AWS ec2 - -Deploy an AWS ec2 instance with a k3s cluster installed (this only needs to be set up once). - -## Prerequisites - -aws cli and terraform - -## Login to AWS - -Create a SFU profile for your AWS environment and add it to `~/.aws/config` eg: - -``` -[profile {{ cookiecutter.project_slug }}] -region = {{ cookiecutter.aws_region }} -output = json -``` - -Switch to your profile and log in: - -``` -export AWS_PROFILE={{ cookiecutter.project_slug }} -aws sso login -``` - -## Terraform init - -``` -terraform init -``` - -## Generate terraform.tfvars - -``` -make generate-tfvars -``` - -## Create an SSH key (or use your own) - -``` -make key-pair -``` - -## Deploy microk8s instance on AWS - -``` -make deploy -``` - -## Add k3s cluster config - -Once your ec2 instance is up and running, you can run -``` -make config -``` -to add the cluster to your local kubernetes configuration. Add the new cluster to your `KUBECONFIG` environment variable: - -``` -export KUBECONFIG=~/.kube/{{ cookiecutter.project_slug }}.ec2.config -``` - -Update KUBECONFIG in your `.bashrc` file to ensure it is set automatically in future: - -``` -export KUBECONFIG=~/.kube/config:~/.kube/{{ cookiecutter.project_slug }}.ec2.config -``` - -Check that the new cluster is listed: - -``` -kubectl config get-contexts -``` - -Switch to the `{{ cookiecutter.project_slug }}-ec2-cluster`: - -``` -kubectl config use-context {{ cookiecutter.project_slug }}-ec2-cluster -``` - -## ECR Credentials - -The frontend and backend ECR repo's are defined in ./terraform/management - -In order to push and pull images, we need -to authenticate against the ECR repository. - -Switch to the AWS profile and log in: - -``` -export AWS_PROFILE={{ cookiecutter.project_slug }} -aws sso login -``` - -Create namespaces - -``` -kubectl create namespace {{ cookiecutter.project_dash }}-prod -kubectl create namespace {{ cookiecutter.project_dash }}-sandbox -``` - -You need to add credentials to the kubernetes cluster so that it can pull images from the ECR repository. - -``` -kubectl create secret docker-registry regcred \ - --docker-server={{ cookiecutter.aws_account_id }}.dkr.ecr.{{ cookiecutter.aws_region }}.amazonaws.com \ - --docker-username=AWS \ - --docker-password=$(aws ecr get-login-password) \ - --namespace {{ cookiecutter.project_dash }}-sandbox -``` - -NB: AWS credentials will expire after 4 hours. If you are unable to push or pull images to ECR, you will need to reauthenticate. - -To simplify this, you can run `AWS_PROFILE={{ cookiecutter.project_slug }} make kubecreds` diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/backend.tf b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/backend.tf deleted file mode 100644 index c0cc6929..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/backend.tf +++ /dev/null @@ -1,25 +0,0 @@ -provider "aws" { - region = module.global_variables.aws_region -} - -# Storing the state file in an encrypted s3 bucket -terraform { - required_version = ">= 1.4" - required_providers { - aws = { - source = "hashicorp/aws" - } - } - - backend "s3" { - region = "{{ cookiecutter.aws_region }}" - bucket = "{{ cookiecutter.project_dash }}-terraform-state" - key = "{{ cookiecutter.project_dash }}.cluster.json" - encrypt = true - dynamodb_table = "{{ cookiecutter.project_dash }}-terraform-state" - } -} - -module "global_variables" { - source = "../modules/global_variables" -} diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/command b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/command deleted file mode 100755 index 2f11244d..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/command +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -ssh -i ~/.ssh/{{ cookiecutter.project_slug }}_default_key ubuntu@$(./bin/ip) $@ diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/generate-tfvars b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/generate-tfvars deleted file mode 100755 index bd801da6..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/generate-tfvars +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -MY_IP=$(./bin/get-my-global-ip) -MY_AMI_ID=$(terraform output ami_id | tail -n1) - -cat terraform.tfvars.template | sed "s/{admin_ip}/$MY_IP/;s/{ami_id}/$MY_AMI_ID/" > terraform.tfvars \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-my-global-ip b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-my-global-ip deleted file mode 100755 index 17358ac1..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-my-global-ip +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -curl inet-ip.info \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-node-port b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-node-port deleted file mode 100755 index 0fea7828..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/get-node-port +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./bin/command kubectl get svc | grep $1 | awk '{print $5}' | grep -o '3[0-9]*' \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/ip b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/ip deleted file mode 100755 index e22bc711..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/bin/ip +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -terraform output -raw instance_ip | tail -n1 \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/config.yml b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/config.yml deleted file mode 100644 index f6a0ebf7..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/config.yml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: {{ cookiecutter.project_dash }} ---- -apiVersion: v1 -kind: Namespace -metadata: - name: {{ cookiecutter.project_dash }}-sandbox ---- -apiVersion: helm.cattle.io/v1 -kind: HelmChartConfig -metadata: - name: traefik - namespace: kube-system -spec: - valuesContent: |- - additionalArguments: - - "--log.level=DEBUG" - - "--certificatesresolvers.letsencrypt.acme.email={{ cookiecutter.email }}" - - "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json" - - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/ec2_cluster.tf b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/ec2_cluster.tf deleted file mode 100644 index 0e1a37e4..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/ec2_cluster.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "aws_key_pair" "default_key" { - key_name = "default_key" - public_key = file(var.path_to_public_key) -} - -resource "aws_eip" "k8s-ip" { - instance = aws_instance.k8s.id - domain = "vpc" -} - -resource "aws_instance" "k8s" { - ami = var.ami_id - instance_type = var.instance_type - - root_block_device { - volume_size = 30 - } - - associate_public_ip_address = true - key_name = aws_key_pair.default_key.key_name - - vpc_security_group_ids = concat([aws_security_group.admin.id], [for o in aws_security_group.bitbucket : o.id]) - - tags = merge( - var.tags, - { - "Name" = "${module.global_variables.application}-ec2-cluster" - }, - ) -} diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/outputs.tf b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/outputs.tf deleted file mode 100644 index 4b26d828..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/outputs.tf +++ /dev/null @@ -1,8 +0,0 @@ -output "instance_ip" { - value = aws_eip.k8s-ip.public_ip -} - -output "ami_id" { - description = "AMI id to use in the EC2 instance, warning - will update when AMI updates" - value = data.aws_ami.latest_ubuntu.id -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/security_groups.tf b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/security_groups.tf deleted file mode 100644 index 7c3ac45e..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/security_groups.tf +++ /dev/null @@ -1,75 +0,0 @@ -resource "aws_security_group" "admin" { - name = "admin" - description = "admin-security-group" - - ingress { - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - - ingress { - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - - ingress { - from_port = 8080 - to_port = 8080 - protocol = "tcp" - cidr_blocks = [var.admin_ip] - } - - ingress { - from_port = 6443 - to_port = 6443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - from_port = 30000 - to_port = 40000 - protocol = "tcp" - cidr_blocks = [var.admin_ip] - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } -} - -resource "aws_security_group" "bitbucket" { - for_each = local.chunks_map - name = "bitbucket_${each.key}" - description = "bitbucket pipeline" - - ingress { - from_port = 6443 - to_port = 6443 - protocol = "tcp" - cidr_blocks = each.value - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } -} diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/terraform.tfvars.template b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/terraform.tfvars.template deleted file mode 100644 index 76fa26bd..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/terraform.tfvars.template +++ /dev/null @@ -1,2 +0,0 @@ -admin_ip = "{admin_ip}/32" -ami_id = {ami_id} diff --git a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/variables.tf b/{{cookiecutter.project_slug}}/terraform/ec2-cluster/variables.tf deleted file mode 100644 index 40fd57b3..00000000 --- a/{{cookiecutter.project_slug}}/terraform/ec2-cluster/variables.tf +++ /dev/null @@ -1,75 +0,0 @@ -variable "admin_ip" { - type = string - default = "admin_id" -} - -variable "ami_id" { - type = string - description = "AMI id to use in the EC2 instance, warning - will update when AMI updates" - default = "ami-053b0d53c279acc90" -} - -# will fetch the latest ubuntu ami and store in terraform.tfvars -# change ami_id to be constant if you dont want it to change on the next release -data "aws_ami" "latest_ubuntu" { - most_recent = true - owners = ["099720109477"] # Canonical - filter { - name = "name" - values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } -} - -variable "instance_type" { - type = string - default = "t2.medium" -} - -variable "app_instance" { - type = string - description = "App instance" - default = "instance" -} - -variable "path_to_public_key" { - type = string - default = "~/.ssh/{{ cookiecutter.project_slug }}_default_key.pub" -} - -variable "tags" { - type = map(string) - - default = { - automation = "terraform" - "automation.config" = "{{ cookiecutter.project_dash }}" - application = "{{ cookiecutter.project_dash }}" - } -} - -provider "http" {} - -data "http" "bitbucket_ips" { - url = "https://ip-ranges.atlassian.com/" - - request_headers = { - Accept = "application/json" - } -} - -locals { - bitbucket_ipv4_cidrs = [for c in jsondecode(data.http.bitbucket_ips.response_body).items : c.cidr if length(regexall(":", c.cidr)) == 0] -} - -variable "max_egress_rules" { - default = 60 -} - -locals { - chunks = chunklist(local.bitbucket_ipv4_cidrs, var.max_egress_rules) - chunks_map = { for i in range(length(local.chunks)) : i => local.chunks[i] } -} diff --git a/{{cookiecutter.project_slug}}/terraform/github/oidc.tf b/{{cookiecutter.project_slug}}/terraform/github/oidc.tf new file mode 100644 index 00000000..22b8cb6f --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/github/oidc.tf @@ -0,0 +1,60 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.51.1" + } + } + backend "s3" { + region = "{{ cookiecutter.aws_region }}" + bucket = "{{ cookiecutter.project_dash }}-terraform-state" + key = "{{ cookiecutter.project_dash }}.github.json" + encrypt = true + dynamodb_table = "{{ cookiecutter.project_dash }}-terraform-state" + } +} + +provider "aws" { + region = "us-east-1" +} + +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + + client_id_list = [ + "sts.amazonaws.com" + ] + + # https://stackoverflow.com/questions/69247498/how-can-i-calculate-the-thumbprint-of-an-openid-connect-server + # Thumbprints for GitHub + thumbprint_list = [ + "6938fd4d98bab03faadb97b34396831e3780aea1", + "1c58a3a8518e8759bf075b76b750d4f2df264fcd" + ] +} + +# Define the IAM role +resource "aws_iam_role" "github_oidc_role" { + name = "github-oidc-role" + + assume_role_policy = <> .terraform/tfplan-$$(date +%Y%m%d-%H%M%S).log + +tfplan.out: plan + +apply: tfplan.out + # TODO: Add check to see if you are on the VPN + tofu apply "tfplan.out" + +kubeconfig: + tofu output -raw kubeconfig > ./kubeconfig + +remove-kube-state: + rm -f kubeconfig + tofu state rm helm_release.argocd \ + kubernetes_namespace.monitoring || true + +# TODO: add cookiecutter.use_talos check for talos targets +talosconfig: + tofu output -raw talosconfig > ./talosconfig + +remove-talos-state: + rm -f talosconfig + tofu state rm data.talos_client_configuration.this \ + data.talos_cluster_kubeconfig.this || true + +talos-health: + @echo "Fetching Talos node public IPs..." + @ips=$$(tofu output talos_node_public_ips | tr -d '"') ; \ + first_ip=$$(echo $$ips | cut -d',' -f1) ; \ + echo "Running health check on Talos node at IP: $$first_ip" ; \ + talosctl health --nodes $$first_ip + +talos-version: + @echo "Fetching Talos node public IPs..." + @ips=$$(tofu output talos_node_public_ips | tr -d '"') ; \ + IFS=',' read -r -a ip_array <<< "$$ips" ; \ + for ip in $${ip_array[@]} ; do \ + talosctl --nodes $$ip version --short ; \ + done + +upgrade-talos: + @echo "Fetching Talos node public IPs..." + @ips=$$(tofu output talos_node_public_ips | tr -d '"') ; \ + IFS=',' read -r -a ip_array <<< "$$ips" ; \ + for ip in $${ip_array[@]} ; do \ + echo "Upgrading Talos node at IP: $$ip" ; \ + talosctl upgrade --nodes $$ip \ + --image factory.talos.dev/installer/10e276a06c1f86b182757a962258ac00655d3425e5957f617bdc82f06894e39b:v1.7.4 ; \ + done + +destroy: remove-kube-state remove-talos-state + tofu destroy + diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/acm.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/acm.tf new file mode 100644 index 00000000..0be398c4 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/acm.tf @@ -0,0 +1,22 @@ +resource "aws_acm_certificate" "cert" { + domain_name = var.domain_name + validation_method = "DNS" + provider = aws.us_east_1 + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "cert" { + name = tolist(aws_acm_certificate.cert.domain_validation_options)[0].resource_record_name + type = tolist(aws_acm_certificate.cert.domain_validation_options)[0].resource_record_type + zone_id = aws_route53_zone.route_zone.id + records = [tolist(aws_acm_certificate.cert.domain_validation_options)[0].resource_record_value] + ttl = 300 +} + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = aws_acm_certificate.cert.arn + validation_record_fqdns = aws_route53_record.cert.*.fqdn +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/backend.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/backend.tf new file mode 100644 index 00000000..4d73c43b --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/backend.tf @@ -0,0 +1,9 @@ +provider "aws" { + region = var.aws_region +} + +# us-east-1 is the only region that supports ACM certificates +provider "aws" { + region = "us-east-1" + alias = "us_east_1" +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/application/cloudfront.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/cloudfront.tf similarity index 71% rename from {{cookiecutter.project_slug}}/terraform/modules/application/cloudfront.tf rename to {{cookiecutter.project_slug}}/terraform/modules/base/cloudfront.tf index 5da494d5..00fdc612 100644 --- a/{{cookiecutter.project_slug}}/terraform/modules/application/cloudfront.tf +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/cloudfront.tf @@ -1,5 +1,13 @@ -resource "aws_cloudfront_origin_access_identity" "cluster_origin_access_identity" { - comment = "${var.application}-${var.environment}" +data "aws_cloudfront_cache_policy" "caching_optimized" { + name = "Managed-CachingOptimized" +} + +data "aws_cloudfront_origin_request_policy" "all_viewer_except_host" { + name = "Managed-AllViewerExceptHostHeader" +} + +resource "aws_cloudfront_origin_access_identity" "s3_access_identity" { + comment = "${var.app_name}-${var.environment}" } resource "aws_cloudfront_origin_access_control" "static_storage" { @@ -10,9 +18,9 @@ resource "aws_cloudfront_origin_access_control" "static_storage" { signing_protocol = "sigv4" } -resource "aws_cloudfront_distribution" "ec2_cluster" { +resource "aws_cloudfront_distribution" "cloudfront" { enabled = true - aliases = [var.domain] + aliases = [var.domain_name] is_ipv6_enabled = true price_class = "PriceClass_100" default_root_object = "" @@ -24,10 +32,11 @@ resource "aws_cloudfront_distribution" "ec2_cluster" { origin_access_control_id = aws_cloudfront_origin_access_control.static_storage.id } + // Kubernetes cluster origin { - domain_name = var.cluster_domain - origin_id = var.cluster_id + domain_name = var.api_domain_name + origin_id = var.cluster_name custom_origin_config { http_port = 80 @@ -46,21 +55,7 @@ resource "aws_cloudfront_distribution" "ec2_cluster" { allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] cached_methods = ["GET", "HEAD"] viewer_protocol_policy = "redirect-to-https" - target_origin_id = var.cluster_id - } - - ordered_cache_behavior { - path_pattern = "/static/*" - allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"] - cached_methods = ["GET", "HEAD"] - target_origin_id = aws_s3_bucket.static_storage.id - viewer_protocol_policy = "redirect-to-https" - forwarded_values { - query_string = false - cookies { - forward = "none" - } - } + target_origin_id = var.cluster_name } viewer_certificate { @@ -79,11 +74,11 @@ resource "aws_cloudfront_distribution" "ec2_cluster" { } resource "aws_iam_user" "cloudfront_invalidator" { - name = "${var.environment}-cloudfront-invalidator" + name = "${var.app_name}-${var.environment}-cloudfront-invalidator" } resource "aws_iam_user_policy" "cloudfront_invalidator" { - name = "${var.environment}-cloudfront-invalidator" + name = "${var.app_name}-${var.environment}-cloudfront-invalidator" user = aws_iam_user.cloudfront_invalidator.name policy = data.aws_iam_policy_document.cloudfront_invalidator.json } @@ -93,10 +88,11 @@ data "aws_iam_policy_document" "cloudfront_invalidator" { sid = "CloudfrontInvalidation" actions = ["cloudfront:CreateInvalidation"] effect = "Allow" - resources = [var.cluster_arn] + resources = [aws_cloudfront_distribution.cloudfront.arn] } } resource "aws_iam_access_key" "cloudfront_invalidator" { user = aws_iam_user.cloudfront_invalidator.name } + diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/ec2-iam-role.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/ec2-iam-role.tf new file mode 100644 index 00000000..0f59ee43 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/ec2-iam-role.tf @@ -0,0 +1,82 @@ +# Create IAM policy for S3 access +resource "aws_iam_policy" "s3_rw_policy" { + name = "S3ReadWritePolicy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket" + ] + Resource = [ + "${aws_s3_bucket.backups.arn}", + "${aws_s3_bucket.backups.arn}/*" + ] + } + ] + }) +} + +# Create AssumeRole IAM role for EC2 instances +resource "aws_iam_role" "ec2_role" { + name = "EC2S3ReadWriteRole" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) +} + +# Attach the S3 policy to the role +resource "aws_iam_role_policy_attachment" "attach_s3_rw_policy" { + role = aws_iam_role.ec2_role.name + policy_arn = aws_iam_policy.s3_rw_policy.arn +} + +# Create an instance profile for the role +resource "aws_iam_instance_profile" "ec2_instance_profile" { + name = "EC2InstanceProfile" + role = aws_iam_role.ec2_role.name +} + +# Create ECR read policy for EC2 instances +resource "aws_iam_policy" "ecr_read_policy" { + name = "ECRReadPolicy" + description = "Policy to allow read access to an ECR repository" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecr:GetAuthorizationToken", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:GetRepositoryPolicy", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:DescribeImages", + "ecr:DescribeImageScanFindings" + ] + Resource = "*" + } + ] + }) +} + +# Attach the ECR policy to the ec2 role +resource "aws_iam_role_policy_attachment" "ecr_read_policy_attachment" { + role = aws_iam_role.ec2_role.name + policy_arn = aws_iam_policy.ecr_read_policy.arn +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/ec2.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/ec2.tf new file mode 100644 index 00000000..9c51c468 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/ec2.tf @@ -0,0 +1,25 @@ +module "control_plane_nodes" { + source = "terraform-aws-modules/ec2-instance/aws" + version = "~> 5.6.1" + + count = var.control_plane.num_instances + + name = "${var.cluster_name}-${count.index}" + ami = var.control_plane.ami_id == null ? data.aws_ami.talos.id : var.control_plane.ami_id + monitoring = true + instance_type = var.control_plane.instance_type + iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name + subnet_id = element(module.vpc.public_subnets, count.index) + iam_role_use_name_prefix = false + create_iam_instance_profile = false + tags = merge(local.common_tags, local.cluster_required_tags) + + vpc_security_group_ids = [module.cluster_sg.security_group_id] + + root_block_device = [ + { + volume_size = 100 + } + ] +} + diff --git a/{{cookiecutter.project_slug}}/terraform/management/ecr.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/ecr.tf similarity index 80% rename from {{cookiecutter.project_slug}}/terraform/management/ecr.tf rename to {{cookiecutter.project_slug}}/terraform/modules/base/ecr.tf index 4b7a39f7..29100b92 100644 --- a/{{cookiecutter.project_slug}}/terraform/management/ecr.tf +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/ecr.tf @@ -1,11 +1,10 @@ -module "ecr_backend" { +module "ecr_frontend" { source = "terraform-aws-modules/ecr/aws" version = "1.6.0" - repository_name = "${module.global_variables.application}-backend" + repository_name = var.frontend_ecr_repo repository_image_tag_mutability = "MUTABLE" - repository_read_write_access_arns = [aws_iam_user.cicd_user.arn] repository_lifecycle_policy = jsonencode({ rules = [ { @@ -27,14 +26,13 @@ module "ecr_backend" { tags = local.common_tags } -module "ecr_frontend" { +module "ecr_backend" { source = "terraform-aws-modules/ecr/aws" version = "1.6.0" - repository_name = "${module.global_variables.application}-frontend" + repository_name = var.backend_ecr_repo repository_image_tag_mutability = "MUTABLE" - repository_read_write_access_arns = [aws_iam_user.cicd_user.arn] repository_lifecycle_policy = jsonencode({ rules = [ { diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/elb.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/elb.tf new file mode 100644 index 00000000..7b92d1de --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/elb.tf @@ -0,0 +1,44 @@ +module "elb_k8s_elb" { + source = "terraform-aws-modules/elb/aws" + version = "~> 4.0" + + name = "${var.cluster_name}-k8s-api" + subnets = module.vpc.public_subnets + tags = merge(local.common_tags, local.cluster_required_tags) + + security_groups = [module.cluster_sg.security_group_id] + + listener = [ + { + lb_port = 80 + lb_protocol = "tcp" + instance_port = 30080 + instance_protocol = "tcp" + }, + { + lb_port = 443 + lb_protocol = "tcp" + instance_port = 30443 + instance_protocol = "tcp" + }, + { + lb_port = 6443 + lb_protocol = "tcp" + instance_port = 6443 + instance_protocol = "tcp" + }, + ] + + health_check = { + target = "tcp:6443" + interval = 30 + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + } + + number_of_instances = var.control_plane.num_instances + instances = module.control_plane_nodes.*.id +} + + diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/github-iam-role.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/github-iam-role.tf new file mode 100644 index 00000000..a4666989 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/github-iam-role.tf @@ -0,0 +1,43 @@ +data "aws_iam_role" "github_oidc_role" { + name = "github-oidc-role" +} + +# Define the IAM policy for ECR +resource "aws_iam_policy" "ecr_push_policy" { + name = "ecr-push-policy" + description = "Policy to allow pushing images to ECR" + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload" + ], + Resource = [ + "arn:aws:ecr:${var.aws_region}:${var.account_id}:repository/${var.frontend_ecr_repo}", + "arn:aws:ecr:${var.aws_region}:${var.account_id}:repository/${var.backend_ecr_repo}", + ] + }, + { + Effect = "Allow", + Action = "ecr:GetAuthorizationToken", + Resource = "*" + } + ] + }) +} + +# Attach the policy to the role +resource "aws_iam_role_policy_attachment" "ecr_push_policy_attachment" { + role = data.aws_iam_role.github_oidc_role.name + policy_arn = aws_iam_policy.ecr_push_policy.arn +} + diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/helm.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/helm.tf new file mode 100644 index 00000000..f7aaf129 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/helm.tf @@ -0,0 +1,87 @@ +provider "helm" { + kubernetes { + config_path = "${path.module}/kubeconfig" + } +} + +provider "kubernetes" { + config_path = "${path.module}/kubeconfig" +} + +resource "kubernetes_namespace" "monitoring" { + metadata { + name = "monitoring" + labels = { + "release" = "kube-prometheus-stack" + "pod-security.kubernetes.io/audit" = "privileged" + "pod-security.kubernetes.io/enforce" = "privileged" + "pod-security.kubernetes.io/warn" = "privileged" + } + } +} + +resource "helm_release" "argocd" { + name = "argocd" + namespace = "argocd" + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + version = "6.9.2" + create_namespace = true + + # SSL termination done by Traefik + set { + name = "configs.params.server.insecure" + value = "true" + } + + depends_on = [resource.null_resource.kubeconfig_file] +} + +resource "local_file" "repo_creds" { + content = data.template_file.repo_creds.rendered + filename = "${path.module}/repocreds.yaml" +} + +resource "kubectl_manifest" "apply_secret" { + yaml_body = data.template_file.repo_creds.rendered + depends_on = [local_file.repo_creds, helm_release.argocd] +} + +resource "kubectl_manifest" "argocd_root_app" { + yaml_body = yamlencode({ + apiVersion = "argoproj.io/v1alpha1" + kind = "Application" + metadata = { + name = "root" + namespace = "argocd" + finalizers = [ + "resources-finalizer.argocd.argoproj.io" + ] + labels = { + "app.kubernetes.io/name" = "root" + } + } + spec = { + destination = { + namespace = "argocd" + server = "https://kubernetes.default.svc" + } + project = "default" + source = { + path = "argocd/${var.environment}/apps" + repoURL = "{{ cookiecutter.repo_url }}" + targetRevision = "HEAD" + } + syncPolicy = { + automated = { + allowEmpty = true + prune = true + selfHeal = true + } + syncOptions = [ + "allowEmpty=true" + ] + } + } + }) +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/iam.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/iam.tf new file mode 100644 index 00000000..8825f36f --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/iam.tf @@ -0,0 +1,14 @@ +resource "aws_iam_user" "cnpg_user" { + name = "cnpg-user-prod" + + tags = local.common_tags +} + +resource "aws_iam_access_key" "cnpg_user_key" { + user = aws_iam_user.cnpg_user.name +} + +resource "aws_iam_user_policy_attachment" "cnpg_user_policy_attachment" { + user = aws_iam_user.cnpg_user.name + policy_arn = aws_iam_policy.s3_rw_policy.arn +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/kms.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/kms.tf new file mode 100644 index 00000000..7840361b --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/kms.tf @@ -0,0 +1,39 @@ +# generate key pair +resource "tls_private_key" "repo_key" { + algorithm = "ED25519" +} + +resource "aws_secretsmanager_secret" "repo_private_key" { + name = "${var.app_name}-argocd-private-key" + description = "ArgoCD private key for ${var.app_name}" +} + +resource "aws_secretsmanager_secret_version" "repo_private_key_version" { + secret_id = aws_secretsmanager_secret.repo_private_key.id + secret_string = tls_private_key.repo_key.private_key_openssh +} + +# output the public key +output "github_public_deploy_key" { + value = tls_private_key.repo_key.public_key_openssh +} + +# Update the Manifest with Private Key +locals { + repo_name = var.repo_name + repo_url = var.repo_url + type_b64 = base64encode("git") + repo_url_b64 = base64encode(local.repo_url) + private_key = tls_private_key.repo_key.private_key_openssh +} + +data "template_file" "repo_creds" { + template = file("${path.module}/repocreds.template.yaml") + + vars = { + repo_name = local.repo_name + type_b64 = local.type_b64 + repo_url_b64 = local.repo_url_b64 + github_deploy_key_b64 = base64encode(local.private_key) + } +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/application/locals.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/locals.tf similarity index 53% rename from {{cookiecutter.project_slug}}/terraform/modules/application/locals.tf rename to {{cookiecutter.project_slug}}/terraform/modules/base/locals.tf index a5449ce9..398ab29a 100644 --- a/{{cookiecutter.project_slug}}/terraform/modules/application/locals.tf +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/locals.tf @@ -1,8 +1,8 @@ locals { common_tags = merge(var.tags, { automation = "terraform" - "automation.config" = join(".", [var.application, var.environment]) - application = var.application + "automation.config" = join(".", [var.app_name, var.environment]) + application = var.app_name environment = var.environment }) } diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/outputs.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/outputs.tf new file mode 100644 index 00000000..5af201e4 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/outputs.tf @@ -0,0 +1,38 @@ +# TODO: add cookiecutter.use_talos check for talos outputs +output "talosconfig" { + description = "The generated talosconfig." + value = data.talos_client_configuration.this.talos_config + sensitive = true +} + +output "kubeconfig" { + description = "The generated kubeconfig." + value = data.talos_cluster_kubeconfig.this.kubeconfig_raw + sensitive = true +} + +output "machineconfig" { + description = "The generated machineconfig." + value = data.talos_machine_configuration.controlplane.machine_configuration + sensitive = true +} + +output "cnpg-iam-role-arn" { + description = "CloudNativePG iam role arn" + value = aws_iam_role.ec2_role.arn + sensitive = false +} + +output "cnpg_user_access_key" { + value = aws_iam_access_key.cnpg_user_key.id +} + +output "cnpg_user_secret_key" { + sensitive = true + value = aws_iam_access_key.cnpg_user_key.secret +} + +output "control_plane_nodes_public_ips" { + description = "The public ip addresses of the talos control plane nodes." + value = join(",", module.control_plane_nodes.*.public_ip) +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/repocreds.template.yaml b/{{cookiecutter.project_slug}}/terraform/modules/base/repocreds.template.yaml new file mode 100644 index 00000000..c9c9ce6d --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/repocreds.template.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: "${repo_name}-github-deploy-key" + namespace: argocd + labels: + argocd.argoproj.io/secret-type: repository +data: + type: "${type_b64}" + url: ${repo_url_b64} + sshPrivateKey: | + ${github_deploy_key_b64} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/route53.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/route53.tf new file mode 100644 index 00000000..d446527e --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/route53.tf @@ -0,0 +1,62 @@ +resource "aws_route53_zone" "route_zone" { + name = var.domain_name + tags = local.common_tags +} + +resource "aws_route53_record" "api" { + zone_id = aws_route53_zone.route_zone.zone_id + name = var.api_domain_name + type = "CNAME" + records = [module.elb_k8s_elb.elb_dns_name] + ttl = 600 +} + +resource "aws_route53_record" "k8s" { + zone_id = aws_route53_zone.route_zone.zone_id + name = var.cluster_domain_name + type = "CNAME" + records = [module.elb_k8s_elb.elb_dns_name] + ttl = 600 +} + +resource "aws_route53_record" "frontend" { + zone_id = aws_route53_zone.route_zone.zone_id + name = var.domain_name + type = "A" + + alias { + name = aws_cloudfront_distribution.cloudfront.domain_name + zone_id = aws_cloudfront_distribution.cloudfront.hosted_zone_id + evaluate_target_health = false + } +} + +resource "aws_route53_record" "frontend-v6" { + zone_id = aws_route53_zone.route_zone.zone_id + name = var.domain_name + type = "AAAA" + + alias { + name = aws_cloudfront_distribution.cloudfront.domain_name + zone_id = aws_cloudfront_distribution.cloudfront.hosted_zone_id + evaluate_target_health = false + } +} + +# record for argocd call +resource "aws_route53_record" "argocd" { + zone_id = aws_route53_zone.route_zone.zone_id + name = var.argocd_domain_name + type = "CNAME" + records = [module.elb_k8s_elb.elb_dns_name] + ttl = 600 +} + +# record for prometheus call +resource "aws_route53_record" "prometheus" { + zone_id = aws_route53_zone.route_zone.zone_id + name = var.prometheus_domain_name + type = "CNAME" + records = [module.elb_k8s_elb.elb_dns_name] + ttl = 600 +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/application/s3.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/s3.tf similarity index 78% rename from {{cookiecutter.project_slug}}/terraform/modules/application/s3.tf rename to {{cookiecutter.project_slug}}/terraform/modules/base/s3.tf index 18cc3687..66897d81 100644 --- a/{{cookiecutter.project_slug}}/terraform/modules/application/s3.tf +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/s3.tf @@ -1,6 +1,20 @@ +resource "aws_s3_bucket" "backups" { + bucket = "${var.app_name}-${var.environment}-backups" + tags = local.common_tags +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "backups" { + bucket = aws_s3_bucket.backups.bucket + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + resource "aws_s3_bucket" "static_storage" { - bucket_prefix = "${var.application}-${var.environment}-" - tags = local.common_tags + bucket = "${var.app_name}-${var.environment}-static-storage" + tags = local.common_tags } resource "aws_s3_bucket_server_side_encryption_configuration" "static_storage" { @@ -65,7 +79,7 @@ resource "aws_s3_bucket_policy" "static_storage" { "Resource": "${aws_s3_bucket.static_storage.arn}/*", "Condition": { "StringEquals": { - "AWS:SourceArn": "${aws_cloudfront_distribution.ec2_cluster.arn}" + "AWS:SourceArn": "${aws_cloudfront_distribution.cloudfront.arn}" } } } diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/security_groups.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/security_groups.tf new file mode 100644 index 00000000..a5d33f82 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/security_groups.tf @@ -0,0 +1,52 @@ +module "cluster_sg" { + source = "terraform-aws-modules/security-group/aws" + version = "~> 4.0" + + name = var.cluster_name + description = "Allow all intra-cluster and egress traffic" + vpc_id = module.vpc.vpc_id + tags = local.common_tags + + ingress_with_self = [ + { + rule = "all-all" + }, + ] + + ingress_with_cidr_blocks = [ + { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = "0.0.0.0/0" + }, + { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = "0.0.0.0/0" + }, + { + from_port = 6443 + to_port = 6443 + protocol = "tcp" + cidr_blocks = var.kubectl_allowed_ips + description = "Kubernetes API Access" + }, + # TODO: add cookiecutter.use_talos check + { + from_port = 50000 + to_port = 50000 + protocol = "tcp" + cidr_blocks = var.talosctl_allowed_ips + description = "Talos API Access" + }, + ] + + egress_with_cidr_blocks = [ + { + rule = "all-all" + cidr_blocks = "0.0.0.0/0" + }, + ] +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/talos.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/talos.tf new file mode 100644 index 00000000..23a50af1 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/talos.tf @@ -0,0 +1,156 @@ +locals { + common_machine_config_patch = { + machine = { + install = { + image : "factory.talos.dev/installer/10e276a06c1f86b182757a962258ac00655d3425e5957f617bdc82f06894e39b:v1.7.4" + } + kubelet = { + # The registerWithFQDN field is used to force kubelet to use the node + # FQDN for registration. This is required in clouds like AWS. + registerWithFQDN = true + + # # Required for Metrics Server + extraArgs = { + rotate-server-certificates : true + } + + credentialProviderConfig : { + apiVersion : "kubelet.config.k8s.io/v1", + kind : "CredentialProviderConfig", + providers : [ + { + name : "ecr-credential-provider", + matchImages : [ + "*.dkr.ecr.*.amazonaws.com", + "*.dkr.ecr.*.amazonaws.com.cn", + "*.dkr.ecr-fips.*.amazonaws.com", + "*.dkr.ecr.us-iso-east-1.c2s.ic.gov", + "*.dkr.ecr.us-isob-east-1.sc2s.sgov.gov" + ], + defaultCacheDuration : "12h", + apiVersion : "credentialprovider.kubelet.k8s.io/v1" + } + ] + } + } + } + } + + cluster_patches_cp = { + cluster = { + # Allow scheduling work loads on the control plane since we don't have a + # separate control plane + allowSchedulingOnControlPlanes = true + + # Install Kubelet Serving Certificate Approver and metrics-server during + # bootstrap + extraManifests = [ + "https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/main/deploy/standalone-install.yaml", + "https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml" + ] + } + } + + config_patches_common = [ + for path in var.config_patch_files : file(path) + ] + + config_patches_controlplane = [yamlencode(local.cluster_patches_cp)] + + cluster_required_tags = { + "kubernetes.io/cluster/${var.cluster_name}" = "owned" + } +} + +resource "talos_machine_secrets" "this" {} + +data "aws_ami" "talos" { + owners = ["540036508848"] # Sidero Labs + most_recent = true + name_regex = "^talos-v\\d+\\.\\d+\\.\\d+-${data.aws_availability_zones.available.id}-amd64$" +} + +data "talos_machine_configuration" "controlplane" { + cluster_name = var.cluster_name + cluster_endpoint = "https://${module.elb_k8s_elb.elb_dns_name}:6443" + machine_type = "controlplane" + machine_secrets = talos_machine_secrets.this.machine_secrets + kubernetes_version = var.kubernetes_version + talos_version = "v1.7.4" + docs = false + examples = false + config_patches = concat( + local.config_patches_common, + local.config_patches_controlplane, + [yamlencode(local.common_machine_config_patch)], + [for path in var.control_plane.config_patch_files : file(path)] + ) +} + +resource "talos_machine_configuration_apply" "controlplane" { + count = var.control_plane.num_instances + + client_configuration = talos_machine_secrets.this.client_configuration + machine_configuration_input = data.talos_machine_configuration.controlplane.machine_configuration + endpoint = module.control_plane_nodes[count.index].public_ip + node = module.control_plane_nodes[count.index].private_ip +} + +resource "talos_machine_bootstrap" "this" { + depends_on = [talos_machine_configuration_apply.controlplane] + + client_configuration = talos_machine_secrets.this.client_configuration + endpoint = module.control_plane_nodes.0.public_ip + node = module.control_plane_nodes.0.private_ip +} + +resource "null_resource" "check_talosconfig_exists" { + provisioner "local-exec" { + command = "test -f ./talosconfig" + } +} + +data "talos_client_configuration" "this" { + cluster_name = var.cluster_name + client_configuration = talos_machine_secrets.this.client_configuration + endpoints = module.control_plane_nodes.*.public_ip +} + +data "talos_cluster_kubeconfig" "this" { + depends_on = [talos_machine_bootstrap.this] + + client_configuration = talos_machine_secrets.this.client_configuration + endpoint = module.control_plane_nodes.0.public_ip + node = module.control_plane_nodes.0.private_ip +} + +# disable this check by running `make remove-talos-state` +data "talos_cluster_health" "this" { + count = fileexists("./talosconfig") ? 1 : 0 + + depends_on = [ + data.talos_client_configuration.this, + data.talos_cluster_kubeconfig.this, + null_resource.check_talosconfig_exists + ] + + client_configuration = talos_machine_secrets.this.client_configuration + endpoints = module.control_plane_nodes.*.public_ip + control_plane_nodes = module.control_plane_nodes.*.private_ip +} + +resource "null_resource" "talosconfig_file" { + depends_on = [data.talos_client_configuration.this] + + provisioner "local-exec" { + command = "echo '${data.talos_client_configuration.this.talos_config}' > ./talosconfig" + } +} + +resource "null_resource" "kubeconfig_file" { + depends_on = [talos_machine_bootstrap.this] + + provisioner "local-exec" { + command = "echo '${data.talos_cluster_kubeconfig.this.kubeconfig_raw}' > ./kubeconfig" + } +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/variables.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/variables.tf new file mode 100644 index 00000000..c0a87902 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/variables.tf @@ -0,0 +1,133 @@ +variable "account_id" { + description = "The AWS account ID" + type = string + default = "{{ cookiecutter.aws_account_id }}" +} + +variable "aws_region" { + type = string + description = "AWS Region" + default = "{{ cookiecutter.aws_region }}" +} + +variable "app_name" { + description = "Application Name" + type = string + default = "{{ cookiecutter.project_dash }}" +} + +variable "environment" { + description = "Environment Name" + type = string + default = "sandbox" +} + +variable "cluster_name" { + description = "Name of cluster" + type = string + default = "{{ cookiecutter.project_dash }}-sandbox" +} + +variable "domain_name" { + type = string + default = "sandbox.{{ cookiecutter.domain_name }}" +} + +variable "api_domain_name" { + type = string + default = "api.{{ cookiecutter.domain_name }}" +} + +variable "cluster_domain_name" { + type = string + default = "k8s.{{ cookiecutter.domain_name }}" +} + +variable "argocd_domain_name" { + type = string + default = "argocd.{{ cookiecutter.domain_name }}" +} + + +variable "prometheus_domain_name" { + type = string + default = "prometheus.{{ cookiecutter.domain_name }}" +} + +variable "kubernetes_version" { + + description = "Kubernetes version to use for the cluster, if not set the k8s version shipped with the talos sdk or k3s version will be used" + type = string + default = null +} + +variable "control_plane" { + description = "Info for control plane that will be created" + type = object({ + instance_type = optional(string, "t3a.medium") + ami_id = optional(string, null) + num_instances = optional(number, 3) + config_patch_files = optional(list(string), []) + tags = optional(map(string), {}) + }) + + validation { + condition = var.control_plane.ami_id != null ? (length(var.control_plane.ami_id) > 4 && substr(var.control_plane.ami_id, 0, 4) == "ami-") : true + error_message = "The ami_id value must be a valid AMI id, starting with \"ami-\"." + } + + default = {} +} + +variable "cluster_vpc_cidr" { + description = "The IPv4 CIDR block for the VPC." + type = string + default = "172.16.0.0/16" +} + +# TODO: add cookiecutter.use_talos check +variable "config_patch_files" { + description = "Path to talos config path files that applies to all nodes" + type = list(string) + default = [] +} + +variable "repo_name" { + type = string + default = "{{ cookiecutter.repo_name }}" +} + +variable "repo_url" { + type = string + default = "{{ cookiecutter.repo_url }}" +} + +variable "frontend_ecr_repo" { + description = "The Frontend ECR repository name" + type = string + default = "{{ cookiecutter.project_dash }}-sandbox-frontend" +} + +variable "backend_ecr_repo" { + description = "The backend ECR repository name" + type = string + default = "{{ cookiecutter.project_dash }}-sandbox-backend" +} + +variable "kubectl_allowed_ips" { + description = "A list of CIDR blocks that are allowed to access the kubernetes api" + type = string + default = "0.0.0.0/0" +} + +# TODO: add cookiecutter.use_talos check +variable "talosctl_allowed_ips" { + description = "A list of CIDR blocks that are allowed to access the talos api" + type = string + default = "0.0.0.0/0" +} + +variable "tags" { + type = map(string) + default = {} +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/versions.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/versions.tf new file mode 100644 index 00000000..6b2e482b --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/versions.tf @@ -0,0 +1,37 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.51" + } + # TODO: add cookiecutter.use_talos check + talos = { + source = "siderolabs/talos" + version = "0.5.0" + } + helm = { + source = "hashicorp/helm" + version = "2.13.2" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.30.0" + } + null = { + source = "hashicorp/null" + version = "3.2.2" + } + local = { + source = "hashicorp/local" + version = ">= 2.5.1" + } + template = { + source = "hashicorp/template" + version = ">= 2.2.0" + } + kubectl = { + source = "gavinbunney/kubectl" + version = ">= 1.14.0" + } + } +} diff --git a/{{cookiecutter.project_slug}}/terraform/modules/base/vpc.tf b/{{cookiecutter.project_slug}}/terraform/modules/base/vpc.tf new file mode 100644 index 00000000..dd3e59d1 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/modules/base/vpc.tf @@ -0,0 +1,22 @@ +data "aws_availability_zones" "available" { + state = "available" +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.1" + + name = var.cluster_name + cidr = var.cluster_vpc_cidr + tags = local.common_tags + + enable_nat_gateway = false + + map_public_ip_on_launch = true + + # lets pick utmost three AZ's since the CIDR bit is 2 + azs = slice(data.aws_availability_zones.available.names, 0, 3) + public_subnets = [for i, v in slice(data.aws_availability_zones.available.names, 0, 3) : cidrsubnet(var.cluster_vpc_cidr, 2, i)] +} + + diff --git a/{{cookiecutter.project_slug}}/terraform/prod/application.tf b/{{cookiecutter.project_slug}}/terraform/prod/application.tf deleted file mode 100644 index 0e558283..00000000 --- a/{{cookiecutter.project_slug}}/terraform/prod/application.tf +++ /dev/null @@ -1,15 +0,0 @@ -# the application module sets up Route53 records to the EC2 cluster and S3 static storage -module "application" { - source = "../modules/application" - - application = module.global_variables.application - environment = var.environment - domain = var.domain - domain_zone = var.domain_zone - api_domain = var.api_domain - cluster_domain = var.cluster_domain - argocd_domain = var.argocd_domain - cluster_public_id = data.aws_instance.ec2_cluster.public_ip - cluster_id = data.aws_instance.ec2_cluster.id - cluster_arn = data.aws_instance.ec2_cluster.arn -} diff --git a/{{cookiecutter.project_slug}}/terraform/prod/backend.tf b/{{cookiecutter.project_slug}}/terraform/prod/backend.tf index 08eee305..09cd0a2f 100644 --- a/{{cookiecutter.project_slug}}/terraform/prod/backend.tf +++ b/{{cookiecutter.project_slug}}/terraform/prod/backend.tf @@ -1,15 +1,5 @@ -provider "aws" { - region = module.global_variables.aws_region -} - -# Storing the state file in an encrypted s3 bucket terraform { required_version = ">= 1.4" - required_providers { - aws = { - source = "hashicorp/aws" - } - } backend "s3" { region = "{{ cookiecutter.aws_region }}" bucket = "{{ cookiecutter.project_dash }}-terraform-state" @@ -18,7 +8,3 @@ terraform { dynamodb_table = "{{ cookiecutter.project_dash }}-terraform-state" } } - -module "global_variables" { - source = "../modules/global_variables" -} diff --git a/{{cookiecutter.project_slug}}/terraform/prod/cluster.tf b/{{cookiecutter.project_slug}}/terraform/prod/cluster.tf new file mode 100644 index 00000000..06f7fef5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/prod/cluster.tf @@ -0,0 +1,23 @@ +module "cluster" { + source = "../modules/base" + environment = "prod" + cluster_name = "{{ cookiecutter.project_dash }}-prod" + domain_name = "prod.{{ cookiecutter.domain_name }}" + api_domain_name = "api.prod.{{ cookiecutter.domain_name }}" + cluster_domain_name = "k8s.prod.{{ cookiecutter.domain_name }}" + argocd_domain_name = "argocd.prod.{{ cookiecutter.domain_name }}" + prometheus_domain_name = "prometheus.prod.{{ cookiecutter.domain_name }}" + control_plane = { + # 2 vCPUs, 4 GiB RAM, $0.0376 per Hour + instance_type = "t3a.medium" + num_instances = 3 + # NB!: set ami_id to prevent instance recreation when the latest ami + # changes, eg: + # ami_id = "ami-09d22b42af049d453" + } + + # NB!: limit kubectl_allowed_ips and talos_allowed_ips to a set of trusted + # public ip addresses. Both variables are comma separated lists of ips. + # kubectl_allowed_ips = "10.0.0.1/32,10.0.0.2/32" + # talos_allowed_ips = "10.0.0.1/32,10.0.0.2/32" +} diff --git a/{{cookiecutter.project_slug}}/terraform/prod/data.tf b/{{cookiecutter.project_slug}}/terraform/prod/data.tf deleted file mode 100644 index 07b54b76..00000000 --- a/{{cookiecutter.project_slug}}/terraform/prod/data.tf +++ /dev/null @@ -1,3 +0,0 @@ -data "aws_instance" "ec2_cluster" { - instance_tags = { "Name" : "${module.global_variables.application}-ec2-cluster", "automation.config" : module.global_variables.application } -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/prod/locals.tf b/{{cookiecutter.project_slug}}/terraform/prod/locals.tf deleted file mode 100644 index b53786ce..00000000 --- a/{{cookiecutter.project_slug}}/terraform/prod/locals.tf +++ /dev/null @@ -1,8 +0,0 @@ -locals { - common_tags = { - automation = "terraform" - "automation.config" = join(".", [module.global_variables.application, var.environment]) - application = module.global_variables.application - environment = var.environment - } -} diff --git a/{{cookiecutter.project_slug}}/terraform/prod/outputs.tf b/{{cookiecutter.project_slug}}/terraform/prod/outputs.tf index 17051d76..0d2f5805 100644 --- a/{{cookiecutter.project_slug}}/terraform/prod/outputs.tf +++ b/{{cookiecutter.project_slug}}/terraform/prod/outputs.tf @@ -1,20 +1,37 @@ -output "domains" { - value = [var.domain, var.api_domain] +output "talosconfig" { + description = "The generated talosconfig" + value = module.cluster.talosconfig + sensitive = true } -output "ec2_cluster_public_dns" { - value = data.aws_instance.ec2_cluster.public_dns +output "kubeconfig" { + description = "The generated kubeconfig" + value = module.cluster.kubeconfig + sensitive = true } -output "static_storage_domain" { - value = module.application.static_storage_bucket +output "machineconfig" { + description = "The generated machineconfig" + value = module.cluster.machineconfig + sensitive = true } -output "application_user_access_key" { - value = module.application.application_user_access_key +output "cnpg-iam-role-arn" { + description = "CloudNativePG iam role arn" + value = module.cluster.cnpg-iam-role-arn + sensitive = false } -output "application_user_secret_key" { +output "cnpg_user_access_key" { + value = module.cluster.cnpg_user_access_key +} + +output "cnpg_user_secret_key" { sensitive = true - value = module.application.application_user_secret_key -} \ No newline at end of file + value = module.cluster.cnpg_user_secret_key +} + +output "control_plane_nodes_public_ips" { + description = "The public ip addresses of the talos control plane nodes" + value = module.cluster.control_plane_nodes_public_ips +} diff --git a/{{cookiecutter.project_slug}}/terraform/prod/variables.tf b/{{cookiecutter.project_slug}}/terraform/prod/variables.tf deleted file mode 100644 index e2183cc6..00000000 --- a/{{cookiecutter.project_slug}}/terraform/prod/variables.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "domain_zone" { - type = string - default = "{{ cookiecutter.domain_name }}" -} - -variable "domain" { - type = string - default = "{{ cookiecutter.domain_name }}" -} - -variable "api_domain" { - type = string - default = "api.{{ cookiecutter.domain_name }}" -} - -variable "cluster_domain" { - type = string - default = "k8s.{{ cookiecutter.domain_name }}" -} - -variable "argocd_domain" { - type = string - default = "argocd.{{ cookiecutter.domain_name }}" -} - -variable "environment" { - default = "prod" -} diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/application.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/application.tf deleted file mode 100644 index 0e558283..00000000 --- a/{{cookiecutter.project_slug}}/terraform/sandbox/application.tf +++ /dev/null @@ -1,15 +0,0 @@ -# the application module sets up Route53 records to the EC2 cluster and S3 static storage -module "application" { - source = "../modules/application" - - application = module.global_variables.application - environment = var.environment - domain = var.domain - domain_zone = var.domain_zone - api_domain = var.api_domain - cluster_domain = var.cluster_domain - argocd_domain = var.argocd_domain - cluster_public_id = data.aws_instance.ec2_cluster.public_ip - cluster_id = data.aws_instance.ec2_cluster.id - cluster_arn = data.aws_instance.ec2_cluster.arn -} diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/backend.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/backend.tf index 791f7044..20477331 100644 --- a/{{cookiecutter.project_slug}}/terraform/sandbox/backend.tf +++ b/{{cookiecutter.project_slug}}/terraform/sandbox/backend.tf @@ -1,15 +1,5 @@ -provider "aws" { - region = module.global_variables.aws_region -} - -# Storing the state file in an encrypted s3 bucket terraform { required_version = ">= 1.4" - required_providers { - aws = { - source = "hashicorp/aws" - } - } backend "s3" { region = "{{ cookiecutter.aws_region }}" bucket = "{{ cookiecutter.project_dash }}-terraform-state" diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/cluster.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/cluster.tf new file mode 100644 index 00000000..d80be35a --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/sandbox/cluster.tf @@ -0,0 +1,24 @@ +module "cluster" { + source = "../modules/base" + environment = "sandbox" + cluster_name = "{{ cookiecutter.project_dash }}-sandbox" + domain_name = "sandbox.{{ cookiecutter.domain_name }}" + api_domain_name = "api.sandbox.{{ cookiecutter.domain_name }}" + cluster_domain_name = "k8s.sandbox.{{ cookiecutter.domain_name }}" + argocd_domain_name = "argocd.sandbox.{{ cookiecutter.domain_name }}" + prometheus_domain_name = "prometheus.sandbox.{{ cookiecutter.domain_name }}" + control_plane = { + # 2 vCPUs, 2 GiB RAM, $0.0188 per Hour + instance_type = "t3a.small" + num_instances = 3 + # NB!: set ami_id to prevent instance recreation when the latest ami + # changes, eg: + # ami_id = "ami-09d22b42af049d453" + + } + + # NB!: limit kubectl_allowed_ips and talos_allowed_ips to a set of trusted + # public ip addresses. Both variables are comma separated lists of ips. + # kubectl_allowed_ips = "10.0.0.1/32,10.0.0.2/32" + # talos_allowed_ips = "10.0.0.1/32,10.0.0.2/32" +} diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/data.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/data.tf deleted file mode 100644 index 07b54b76..00000000 --- a/{{cookiecutter.project_slug}}/terraform/sandbox/data.tf +++ /dev/null @@ -1,3 +0,0 @@ -data "aws_instance" "ec2_cluster" { - instance_tags = { "Name" : "${module.global_variables.application}-ec2-cluster", "automation.config" : module.global_variables.application } -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/locals.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/locals.tf deleted file mode 100644 index b53786ce..00000000 --- a/{{cookiecutter.project_slug}}/terraform/sandbox/locals.tf +++ /dev/null @@ -1,8 +0,0 @@ -locals { - common_tags = { - automation = "terraform" - "automation.config" = join(".", [module.global_variables.application, var.environment]) - application = module.global_variables.application - environment = var.environment - } -} diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/outputs.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/outputs.tf index 17051d76..0d2f5805 100644 --- a/{{cookiecutter.project_slug}}/terraform/sandbox/outputs.tf +++ b/{{cookiecutter.project_slug}}/terraform/sandbox/outputs.tf @@ -1,20 +1,37 @@ -output "domains" { - value = [var.domain, var.api_domain] +output "talosconfig" { + description = "The generated talosconfig" + value = module.cluster.talosconfig + sensitive = true } -output "ec2_cluster_public_dns" { - value = data.aws_instance.ec2_cluster.public_dns +output "kubeconfig" { + description = "The generated kubeconfig" + value = module.cluster.kubeconfig + sensitive = true } -output "static_storage_domain" { - value = module.application.static_storage_bucket +output "machineconfig" { + description = "The generated machineconfig" + value = module.cluster.machineconfig + sensitive = true } -output "application_user_access_key" { - value = module.application.application_user_access_key +output "cnpg-iam-role-arn" { + description = "CloudNativePG iam role arn" + value = module.cluster.cnpg-iam-role-arn + sensitive = false } -output "application_user_secret_key" { +output "cnpg_user_access_key" { + value = module.cluster.cnpg_user_access_key +} + +output "cnpg_user_secret_key" { sensitive = true - value = module.application.application_user_secret_key -} \ No newline at end of file + value = module.cluster.cnpg_user_secret_key +} + +output "control_plane_nodes_public_ips" { + description = "The public ip addresses of the talos control plane nodes" + value = module.cluster.control_plane_nodes_public_ips +} diff --git a/{{cookiecutter.project_slug}}/terraform/sandbox/variables.tf b/{{cookiecutter.project_slug}}/terraform/sandbox/variables.tf deleted file mode 100644 index d66ef15c..00000000 --- a/{{cookiecutter.project_slug}}/terraform/sandbox/variables.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "domain_zone" { - type = string - default = "{{ cookiecutter.domain_name }}" -} - -variable "domain" { - type = string - default = "sandbox.{{ cookiecutter.domain_name }}" -} - -variable "api_domain" { - type = string - default = "api.sandbox.{{ cookiecutter.domain_name }}" -} - -variable "cluster_domain" { - type = string - default = "k8s.sandbox.{{ cookiecutter.domain_name }}" -} - -variable "argocd_domain" { - type = string - default = "argocd.sandbox.{{ cookiecutter.domain_name }}" -} - -variable "environment" { - default = "sandbox" -} diff --git a/{{cookiecutter.project_slug}}/terraform/staging/backend.tf b/{{cookiecutter.project_slug}}/terraform/staging/backend.tf new file mode 100644 index 00000000..3f898c2c --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/staging/backend.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.4" + backend "s3" { + region = "{{ cookiecutter.aws_region }}" + bucket = "{{ cookiecutter.project_dash }}-terraform-state" + key = "{{ cookiecutter.project_dash }}.staging.json" + encrypt = true + dynamodb_table = "{{ cookiecutter.project_dash }}-terraform-state" + } +} diff --git a/{{cookiecutter.project_slug}}/terraform/staging/cluster.tf b/{{cookiecutter.project_slug}}/terraform/staging/cluster.tf new file mode 100644 index 00000000..9efa34d0 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/staging/cluster.tf @@ -0,0 +1,23 @@ +module "cluster" { + source = "../modules/base" + environment = "staging" + cluster_name = "{{ cookiecutter.project_dash }}-staging" + domain_name = "staging.{{ cookiecutter.domain_name }}" + api_domain_name = "api.staging.{{ cookiecutter.domain_name }}" + cluster_domain_name = "k8s.staging.{{ cookiecutter.domain_name }}" + argocd_domain_name = "argocd.staging.{{ cookiecutter.domain_name }}" + prometheus_domain_name = "prometheus.staging.{{ cookiecutter.domain_name }}" + control_plane = { + # 2 vCPUs, 4 GiB RAM, $0.0376 per Hour + instance_type = "t3a.medium" + num_instances = 3 + # NB!: set ami_id to prevent instance recreation when the latest ami + # changes, eg: + # ami_id = "ami-09d22b42af049d453" + } + + # NB!: limit kubectl_allowed_ips and talos_allowed_ips to a set of trusted + # public ip addresses. Both variables are comma separated lists of ips. + # kubectl_allowed_ips = "10.0.0.1/32,10.0.0.2/32" + # talos_allowed_ips = "10.0.0.1/32,10.0.0.2/32" +} diff --git a/{{cookiecutter.project_slug}}/terraform/staging/outputs.tf b/{{cookiecutter.project_slug}}/terraform/staging/outputs.tf new file mode 100644 index 00000000..0d2f5805 --- /dev/null +++ b/{{cookiecutter.project_slug}}/terraform/staging/outputs.tf @@ -0,0 +1,37 @@ +output "talosconfig" { + description = "The generated talosconfig" + value = module.cluster.talosconfig + sensitive = true +} + +output "kubeconfig" { + description = "The generated kubeconfig" + value = module.cluster.kubeconfig + sensitive = true +} + +output "machineconfig" { + description = "The generated machineconfig" + value = module.cluster.machineconfig + sensitive = true +} + +output "cnpg-iam-role-arn" { + description = "CloudNativePG iam role arn" + value = module.cluster.cnpg-iam-role-arn + sensitive = false +} + +output "cnpg_user_access_key" { + value = module.cluster.cnpg_user_access_key +} + +output "cnpg_user_secret_key" { + sensitive = true + value = module.cluster.cnpg_user_secret_key +} + +output "control_plane_nodes_public_ips" { + description = "The public ip addresses of the talos control plane nodes" + value = module.cluster.control_plane_nodes_public_ips +}