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 +}