diff --git a/.github/workflows/reusable-terraform.yml b/.github/workflows/reusable-terraform.yml index 6df0285..7c77db6 100644 --- a/.github/workflows/reusable-terraform.yml +++ b/.github/workflows/reusable-terraform.yml @@ -14,6 +14,7 @@ on: permissions: id-token: write contents: read + issues: write pull-requests: write security-events: write @@ -23,12 +24,16 @@ env: TF_WORKING_DIR: infra/environments/${{ inputs.environment }} jobs: - terraform: runs-on: ubuntu-latest + # env: + # TF_VAR_grafana_admin_password: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} + # TF_VAR_slack_webhook: ${{ secrets.SLACK_WEBHOOK }} + environment: - name: ${{ inputs.environment }} + #name: ${{ inputs.environment }} + name: ${{ inputs.environment == 'shared' && 'dev' || inputs.environment }} defaults: run: @@ -36,19 +41,188 @@ jobs: steps: + ################################################# + # CONFIGURE TERRAFORM VARIABLES + ################################################# + + - name: Configure Terraform Variables + if: inputs.environment != 'shared' + env: + GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + run: | + { + echo "TF_VAR_grafana_admin_password<> "$GITHUB_ENV" + + ################################################# + # CHECKOUT + ################################################# + - name: Checkout uses: actions/checkout@v4 + ################################################# + # VALIDATE ENVIRONMENT + ################################################# + + - name: Validate Terraform Environment + run: | + case "${{ inputs.environment }}" in + shared|dev|staging|prod) + echo "Valid Terraform environment: ${{ inputs.environment }}" + ;; + *) + echo "Invalid Terraform environment: ${{ inputs.environment }}" + echo "Allowed values: shared, dev, staging, prod" + exit 1 + ;; + esac + ################################################# # OIDC AUTHENTICATION ################################################# + - name: Check AWS role secret + run: | + if [ -z "${{ secrets.AWS_ROLE_ARN }}" ]; then + echo "AWS_ROLE_ARN secret is missing" + exit 1 + fi + + echo "AWS role secret is present" + - name: Configure AWS Credentials via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: ${{ env.AWS_REGION }} + ################################################# + # TERRAFORM BACKEND BOOTSTRAP + ################################################# + + - name: Bootstrap Terraform Backend + env: + TF_STATE_BUCKET: ${{ secrets.TF_STATE_BUCKET }} + TF_LOCK_TABLE: ${{ secrets.TF_LOCK_TABLE }} + + working-directory: ${{ env.TF_WORKING_DIR }} + + run: | + ENVIRONMENT="${{ inputs.environment }}" + BACKEND_KEY="${ENVIRONMENT}/tf.tfstate" + + echo "Terraform environment: ${ENVIRONMENT}" + echo "Terraform working directory: ${TF_WORKING_DIR}" + echo "Terraform backend key: ${BACKEND_KEY}" + + ################################################# + # CREATE S3 BUCKET IF MISSING + ################################################# + + if aws s3api head-bucket \ + --bucket "$TF_STATE_BUCKET" 2>/dev/null; then + + echo "S3 bucket exists: $TF_STATE_BUCKET" + + else + + echo "Creating S3 bucket: $TF_STATE_BUCKET" + + if [ "$AWS_REGION" = "us-east-1" ]; then + + aws s3api create-bucket \ + --bucket "$TF_STATE_BUCKET" \ + --region "$AWS_REGION" + + else + + aws s3api create-bucket \ + --bucket "$TF_STATE_BUCKET" \ + --region "$AWS_REGION" \ + --create-bucket-configuration \ + LocationConstraint="$AWS_REGION" + + fi + + aws s3api put-bucket-versioning \ + --bucket "$TF_STATE_BUCKET" \ + --versioning-configuration Status=Enabled + + aws s3api put-bucket-encryption \ + --bucket "$TF_STATE_BUCKET" \ + --server-side-encryption-configuration '{ + "Rules": [ + { + "ApplyServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }' + + aws s3api put-public-access-block \ + --bucket "$TF_STATE_BUCKET" \ + --public-access-block-configuration '{ + "BlockPublicAcls": true, + "IgnorePublicAcls": true, + "BlockPublicPolicy": true, + "RestrictPublicBuckets": true + }' + + fi + + ################################################# + # CREATE DYNAMODB LOCK TABLE IF MISSING + ################################################# + + if aws dynamodb describe-table \ + --table-name "$TF_LOCK_TABLE" \ + --region "$AWS_REGION" >/dev/null 2>&1; then + + echo "DynamoDB table exists: $TF_LOCK_TABLE" + + else + + echo "Creating DynamoDB table: $TF_LOCK_TABLE" + + aws dynamodb create-table \ + --table-name "$TF_LOCK_TABLE" \ + --attribute-definitions AttributeName=LockID,AttributeType=S \ + --key-schema AttributeName=LockID,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region "$AWS_REGION" + + aws dynamodb wait table-exists \ + --table-name "$TF_LOCK_TABLE" \ + --region "$AWS_REGION" + + echo "DynamoDB table is active" + + fi + + ################################################# + # GENERATE BACKEND CONFIG + ################################################# + + cat > backend.hcl < ~/.terraformrc + echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' \ + > ~/.terraformrc ################################################# # TERRAFORM INIT ################################################# - name: Terraform Init - run: terraform init -input=false working-directory: ${{ env.TF_WORKING_DIR }} + run: | + terraform init \ + -upgrade \ + -backend-config=backend.hcl \ + -input=false ################################################# - # TERRAFORM FMT + # TERRAFORM FORMAT ################################################# - name: Terraform Format Check @@ -93,8 +271,8 @@ jobs: - name: Terraform Validate id: validate - run: terraform validate -no-color working-directory: ${{ env.TF_WORKING_DIR }} + run: terraform validate -no-color ################################################# # TFLINT @@ -134,16 +312,29 @@ jobs: - name: Terraform Plan id: plan + working-directory: ${{ env.TF_WORKING_DIR }} run: | + + set -e + echo "Terraform working directory:" + pwd + echo "Terraform state:" + terraform state list || true + echo "Running Terraform plan..." + terraform plan \ - -var-file=environments/${{ inputs.environment }}/terraform.tfvars \ -out=tfplan \ -input=false \ -no-color + # terraform show -json tfplan > tfplan.json + # terraform show -no-color tfplan > tfplan.txt + + echo "Terraform plan completed successfully." terraform show -json tfplan > tfplan.json + echo "Terraform JSON generated." terraform show -no-color tfplan > tfplan.txt - working-directory: ${{ env.TF_WORKING_DIR }} + echo "Terraform text plan generated." ################################################# # PLAN SUMMARY @@ -151,17 +342,15 @@ jobs: - name: Parse Terraform Plan Summary id: plan-summary + working-directory: ${{ env.TF_WORKING_DIR }} run: | - ADD=$(jq '[.resource_changes[]?.change.actions?] | flatten | map(select(. == "create")) | length' tfplan.json) - CHANGE=$(jq '[.resource_changes[]?.change.actions?] | flatten | map(select(. == "update")) | length' tfplan.json) - DESTROY=$(jq '[.resource_changes[]?.change.actions?] | flatten | map(select(. == "delete")) | length' tfplan.json) - echo "add=$ADD" >> $GITHUB_OUTPUT - echo "change=$CHANGE" >> $GITHUB_OUTPUT - echo "destroy=$DESTROY" >> $GITHUB_OUTPUT + echo "add=$ADD" >> "$GITHUB_OUTPUT" + echo "change=$CHANGE" >> "$GITHUB_OUTPUT" + echo "destroy=$DESTROY" >> "$GITHUB_OUTPUT" ################################################# # CHECKOV SUMMARY @@ -170,24 +359,19 @@ jobs: - name: Parse Checkov Summary id: checkov-summary run: | - if [ -f checkov.json ]; then - PASSED=$(jq '.summary.passed // 0' checkov.json) FAILED=$(jq '.summary.failed // 0' checkov.json) SKIPPED=$(jq '.summary.skipped // 0' checkov.json) - else - PASSED=0 FAILED=0 SKIPPED=0 - fi - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "failed=$FAILED" >> $GITHUB_OUTPUT - echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT + echo "passed=$PASSED" >> "$GITHUB_OUTPUT" + echo "failed=$FAILED" >> "$GITHUB_OUTPUT" + echo "skipped=$SKIPPED" >> "$GITHUB_OUTPUT" ################################################# # UPLOAD PLAN @@ -198,79 +382,10 @@ jobs: with: name: tfplan-${{ inputs.environment }} path: | - ${{ env.TF_WORKING_DIR }}/tfplan - ${{ env.TF_WORKING_DIR }}/tfplan.json + ${{ env.TF_WORKING_DIR }}/tfplan + ${{ env.TF_WORKING_DIR }}/tfplan.json ${{ env.TF_WORKING_DIR }}/tfplan.txt - ################################################# - # PR COMMENT - ################################################# - - - name: Comment Terraform Plan on PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - - const plan = fs.readFileSync('tfplan.txt', 'utf8'); - - const body = ` - ## Terraform Plan - ${{ inputs.environment }} - - ### Terraform Changes - - | Action | Count | - |---|---| - | Add | ${{ steps.plan-summary.outputs.add }} | - | Change | ${{ steps.plan-summary.outputs.change }} | - | Destroy | ${{ steps.plan-summary.outputs.destroy }} | - - ### Checkov Summary - - | Result | Count | - |---|---| - | Passed | ${{ steps.checkov-summary.outputs.passed }} | - | Failed | ${{ steps.checkov-summary.outputs.failed }} | - | Skipped | ${{ steps.checkov-summary.outputs.skipped }} | - -
- Terraform Plan - - \`\`\`terraform - ${plan.substring(0, 60000)} - \`\`\` - -
- `; - - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existing = comments.data.find(comment => - comment.body.includes('## Terraform Plan') - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } - ################################################# # APPLY / DESTROY ################################################# @@ -281,7 +396,9 @@ jobs: github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/download-artifact@v4 + with: name: tfplan-${{ inputs.environment }} path: ${{ env.TF_WORKING_DIR }} @@ -295,13 +412,17 @@ jobs: github.event_name == 'workflow_dispatch' ) && inputs.tf_action == 'apply' + + working-directory: ${{ env.TF_WORKING_DIR }} + run: terraform apply -auto-approve tfplan - name: Terraform Destroy if: | github.event_name == 'workflow_dispatch' && inputs.tf_action == 'destroy' + + working-directory: ${{ env.TF_WORKING_DIR }} + run: | - terraform destroy \ - -auto-approve \ - working-directory: ${{ env.TF_WORKING_DIR }} \ No newline at end of file + terraform destroy -auto-approve \ No newline at end of file diff --git a/.github/workflows/terraform-drift.yml b/.github/workflows/terraform-drift.yml index d5306b7..1925e57 100644 --- a/.github/workflows/terraform-drift.yml +++ b/.github/workflows/terraform-drift.yml @@ -18,6 +18,7 @@ jobs: strategy: matrix: environment: + - shared - dev - staging - prod @@ -30,27 +31,96 @@ jobs: - name: Checkout uses: actions/checkout@v4 + ################################################# + # AWS + ################################################# + - name: Configure AWS Credentials via OIDC uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: arn:aws:iam::123456789012:role/github-terraform-role - aws-region: eu-west-1 + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: us-east-1 + + ################################################# + # SETUP TERRAFORM + ################################################# - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.8.5" + ################################################# + # TERRAFORM BACKEND + ################################################# + + - name: Configure Terraform Backend + env: + TF_STATE_BUCKET: ${{ secrets.TF_STATE_BUCKET }} + TF_LOCK_TABLE: ${{ secrets.TF_LOCK_TABLE }} + AWS_REGION: us-east-1 + + working-directory: infra/environments/${{ matrix.environment }} + + run: | + + BACKEND_KEY="${{ matrix.environment }}/tf.tfstate" + + cat > backend.hcl <> $GITHUB_OUTPUT - echo "tf_action=${{ github.event.inputs.action }}" >> $GITHUB_OUTPUT + + echo "Manual workflow detected" + + echo "environment=${{ inputs.environment }}" >> "$GITHUB_OUTPUT" + echo "tf_action=${{ inputs.action }}" >> "$GITHUB_OUTPUT" + + echo "Environment: ${{ inputs.environment }}" + echo "Action: ${{ inputs.action }}" + exit 0 fi - BRANCH="${GITHUB_REF#refs/heads/}" - if [[ "$BRANCH" == "develop" ]]; then - echo "environment=dev" >> $GITHUB_OUTPUT + ################################################# + # PULL REQUEST + ################################################# + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + + SOURCE_BRANCH="${{ github.head_ref }}" + TARGET_BRANCH="${{ github.base_ref }}" + + echo "Pull request detected" + echo "Source branch: ${SOURCE_BRANCH}" + echo "Target branch: ${TARGET_BRANCH}" + + case "$TARGET_BRANCH" in - elif [[ "$BRANCH" == "staging" ]]; then - echo "environment=staging" >> $GITHUB_OUTPUT + develop) + echo "environment=dev" >> "$GITHUB_OUTPUT" + echo "tf_action=plan" >> "$GITHUB_OUTPUT" + ;; - elif [[ "$BRANCH" == "main" ]]; then - echo "environment=prod" >> $GITHUB_OUTPUT + staging) + echo "environment=staging" >> "$GITHUB_OUTPUT" + echo "tf_action=plan" >> "$GITHUB_OUTPUT" + ;; - else - echo "environment=dev" >> $GITHUB_OUTPUT + main) + echo "environment=prod" >> "$GITHUB_OUTPUT" + echo "tf_action=plan" >> "$GITHUB_OUTPUT" + ;; + + *) + echo "Unsupported PR target branch: ${TARGET_BRANCH}" + exit 1 + ;; + + esac + + exit 0 fi - echo "tf_action=apply" >> $GITHUB_OUTPUT + + ################################################# + # PUSH + ################################################# + + BRANCH="${GITHUB_REF#refs/heads/}" + + echo "Push detected" + echo "Branch: ${BRANCH}" + + case "$BRANCH" in + + develop) + + echo "environment=dev" >> "$GITHUB_OUTPUT" + echo "tf_action=apply" >> "$GITHUB_OUTPUT" + ;; + + staging) + + echo "environment=staging" >> "$GITHUB_OUTPUT" + echo "tf_action=apply" >> "$GITHUB_OUTPUT" + ;; + + main) + + echo "environment=prod" >> "$GITHUB_OUTPUT" + echo "tf_action=apply" >> "$GITHUB_OUTPUT" + ;; + + feature/*) + + echo "environment=dev" >> "$GITHUB_OUTPUT" + echo "tf_action=plan" >> "$GITHUB_OUTPUT" + ;; + + *) + + echo "Unsupported branch: ${BRANCH}" + exit 1 + ;; + + esac + + + ################################################# + # TERRAFORM + ################################################# terraform: needs: determine-env diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/README.md b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/README.md deleted file mode 100644 index 9b7932d..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Install and Run a Kubernetes Cluster on Ubuntu - -You can install and configure Kubernetes in different ways on your personal laptops, physical servers, Virtual machines, and as a cloud service. -
-I will show the Kubernetes setup on AWS cloud using Docker. I am taking 3 Ubuntu instances, 1 Master Node and 2 Worker nodes using terraform. \ No newline at end of file diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/.terraform.lock.hcl b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/.terraform.lock.hcl deleted file mode 100644 index 2b93947..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/.terraform.lock.hcl +++ /dev/null @@ -1,25 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "6.7.0" - constraints = ">= 2.7.0" - hashes = [ - "h1:vISrEI1xUh0w7NXTQ9m6ZEnQ1dv02yy+EJvxW78DAoI=", - "zh:3c0a256f813e5e2c1e1aa137204ad9168ebe487f6cee874af9e9c78eb300568e", - "zh:3c49dd75ea28395b29ba259988826b956c8adf6c0b59dd8874feb4f47bad976a", - "zh:3e6e3e3bfc6594f4f9e2c017ee588c5fcad394b87dd0b68a3f37cd66001f3c8c", - "zh:3f9b55826eeebf9b2ed448fc111d772c703e1edc6678e1bb646e66f3c3f9308f", - "zh:44e4ced936045ddc42d22c653a6427e7eb2b7aee918dff8438da0cb40996beb4", - "zh:474ab4d63918f41e8ea1cef43aeb1c719629dbf289db175c95de1431a8853ae7", - "zh:71b9e1d82c5ccc8d9bf72b3712c2b90722fc1f35a0f0f7a9557b9ee01971e6e2", - "zh:7723256d6ccc55f4000d1df8db202b02b30a7d917f5d31624c717e14ba15ea95", - "zh:82174836faa830aff0e47ea61d4cfbb5c97e1e944b1978f1d933acd37f584c88", - "zh:8e62fdc10206ba7232eec991e5a387378f2fbe47cc717b7f60eeb1df2c974514", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:be24dd2d53b224d7098e75ca432746e3420ce071189eea100aa8cbcd2498d389", - "zh:d27651d0e458933127ddca35a833e1a0f0ff0c131391288b3239763a2fd8f96f", - "zh:d33c181fff1b96bf8366e6c3d92408370b21649291e8f4d1f7e9a3fbb920fc9d", - "zh:edc0a0a84f85036c6d3df29d09557bd43206d9ee57b10542b484050f0f34d242", - ] -} diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/Backend.tf b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/Backend.tf deleted file mode 100644 index 0d8fb3b..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/Backend.tf +++ /dev/null @@ -1,15 +0,0 @@ -terraform { - backend "s3" { - bucket = "my-fullstack-tfstate" - region = "us-east-1" - key = "kubernetes/node.tfstate" - encrypt = true - } - required_version = ">=0.13.0" - required_providers { - aws = { - version = ">= 2.7.0" - source = "hashicorp/aws" - } - } -} \ No newline at end of file diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/ec2.tf b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/ec2.tf deleted file mode 100644 index a6a77be..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/ec2.tf +++ /dev/null @@ -1,73 +0,0 @@ -data "aws_ami" "ubuntu" { - most_recent = true - - filter { - name = "name" - values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - owners = ["099720109477"] # Canonical -} - -# resource "aws_instance" "master" { -# ami = data.aws_ami.ubuntu.id -# instance_type = "t3.micro" -# key_name = var.key-name -# subnet_id = aws_subnet.public-subnet.id -# vpc_security_group_ids = [aws_security_group.security-group.id] -# iam_instance_profile = aws_iam_instance_profile.instance-profile.name - -# tags = { -# Name = var.master -# } -# } - -# resource "aws_instance" "worker1" { -# ami = data.aws_ami.ubuntu.id -# instance_type = "t3.micro" -# key_name = var.key-name -# subnet_id = aws_subnet.public-subnet.id -# vpc_security_group_ids = [aws_security_group.security-group.id] -# iam_instance_profile = aws_iam_instance_profile.instance-profile.name - -# tags = { -# Name = var.worker1 -# } -# } - -# resource "aws_instance" "worker2" { -# ami = data.aws_ami.ubuntu.id -# instance_type = "t3.micro" -# key_name = var.key-name -# subnet_id = aws_subnet.public-subnet.id -# vpc_security_group_ids = [aws_security_group.security-group.id] -# iam_instance_profile = aws_iam_instance_profile.instance-profile.name - -# tags = { -# Name = var.worker2 -# } -# } - -resource "aws_instance" "ec2" { - for_each = local.ec2_instances - ami = data.aws_ami.ubuntu.id - instance_type = "t3.micro" - key_name = var.key-name - subnet_id = aws_subnet.public-subnet.id - vpc_security_group_ids = [aws_security_group.security-group.id] - iam_instance_profile = aws_iam_instance_profile.instance-profile.name - user_data = templatefile("scripts/setup.sh", { - hostname = each.value.hostname - role = each.key - }) - - tags = { - Name = each.value - Role = each.key - } -} \ No newline at end of file diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/iam.tf b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/iam.tf deleted file mode 100644 index 90f5654..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/iam.tf +++ /dev/null @@ -1,26 +0,0 @@ -resource "aws_iam_instance_profile" "instance-profile" { - name = "cluster-node-instance-profile" -} - -resource "aws_iam_role" "iam-role" { - name = var.iam-role - assume_role_policy = <> /etc/hosts - -# Disable the swap settings -sudo swapoff -a -sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab - -# Install Containerd -cat < /dev/null -echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list -sudo apt update - -# Install kubeadm, kubelet, and kubectl -sudo apt-get update -sudo apt-get install -y kubelet kubeadm kubectl -sudo apt-mark hold kubelet kubeadm kubectl - -# Set Containerd as Default -sudo mkdir -p /etc/containerd -sudo apt install -y containerd -containerd config default | sudo tee /etc/containerd/config.toml > /dev/null -sudo systemctl restart containerd -systemctl status containerd - - - -# For only master node -if [ "${role}" = "master" ]; then - # Intializing kubernetes Master Node - echo "Intializing kubernetes Master Node..." - # For testing purpose only - sudo kubeadm init --pod-network-cidr=10.244.0.0/16 \ - --ignore-preflight-errors=Mem,FileContent--proc-sys-net-ipv4-ip_forward - - # Setuping kubeconfig for the master node - echo "Setuping kubeconfig for the master node..." - sudo mkdir -p $HOME/.kube - sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config - sudo chown $(id -u):$(id -g) $HOME/.kube/config - - # setup the CNI as flannel for the kubernetes cluster - echo "setup the CNI as flannel for the kubernetes cluster..." - kubectl apply -f https://github.com/coreos/flannel/releases/download/v0.20.2/kube-flannel.yml - - # Join Worker Nodes - echo "Join Worker Nodes..." - # kubeadm join :6443 --token --discovery-token-ca-cert-hash sha256: - # How to get the real value - kubeadm token create --print-join-command - kubeadm join 172.31.39.8:6443 --token gkhryh.k55atakut4z7vnd1 --discovery-token-ca-cert-hash sha256:17c3413311de0a6a28ad5c7216b7ae3ce776a560f1d77eefabf899b8e19f1f84 - # Verify the Kubernetes Cluster - echo "Verify the Kubernetes Cluster..." - kubectl get nodes - - # Check the K8s Resources - echo "Checking the K8s Resources" - kubectl get all -n kube-system - - # Create Pod Application - echo "Creating Pod Application..." - - # Create a service for this pod - echo "Creating a service for this pod..." - kubectl expose pod mypod1 --type=NodePort --port=80 --target-port=8080 -fi \ No newline at end of file diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/security_group.tf b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/security_group.tf deleted file mode 100644 index dd5bd0b..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/security_group.tf +++ /dev/null @@ -1,33 +0,0 @@ -resource "aws_security_group" "security-group" { - vpc_id = aws_vpc.vpc.id - description = "Allowing SSH Access" - ingress = [ - for port in [22, 80]: { - description = "TLS from VPC" - from_port = port - to_port = port - protocol = "tcp" - ipv6_cidr_blocks = ["::/0"] - self = false - prefix_list_ids = [] - security_groups = [] - cidr_blocks = ["0.0.0.0/0"] - } - ] - egress = [ - { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - self = false - prefix_list_ids = [] - security_groups = [] - description = "Allow all outbound traffic" - } - ] - tags = { - Name = var.sg-name - } -} \ No newline at end of file diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/variables.tf b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/variables.tf deleted file mode 100644 index 03b7cbd..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "vpc-name" {} -variable "igw-name" {} -variable "subnet-name" {} -variable "rt-name" {} -variable "sg-name" {} -variable "key-name" {} -# variable "instance-name" {} -variable "iam-role" {} -variable "master" {} -variable "worker1" {} -variable "worker2" {} \ No newline at end of file diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/variables.tfvars b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/variables.tfvars deleted file mode 100644 index 2cc8e82..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/variables.tfvars +++ /dev/null @@ -1,10 +0,0 @@ -vpc-name = "kubernetes-vpc" -igw-name = "kubernetes-igw" -subnet-name = "kubernetes-public-subnet" -rt-name = "kubernetes-rt" -sg-name = "kubernetes-sg" -key-name = "devops" -master = "master-node" -worker1 = "worker Node 1" -worker2 = "worker Node 2" -iam-role = "kubernetes-cluster-role" diff --git a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/vpc.tf b/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/vpc.tf deleted file mode 100644 index 5b2a560..0000000 --- a/1-Install-and-Run-a-Kubernetes-Cluster-on-Ubuntu/nodes/vpc.tf +++ /dev/null @@ -1,39 +0,0 @@ -resource "aws_vpc" "vpc" { - cidr_block = "10.0.0.0/16" - tags = { - Name = var.vpc-name - } -} - -resource "aws_internet_gateway" "igw" { - vpc_id = aws_vpc.vpc.id - tags = { - Name = var.igw-name - } -} - -resource "aws_subnet" "public-subnet" { - vpc_id = aws_vpc.vpc.id - cidr_block = "10.0.0.0/24" - availability_zone = "us-east-1a" - map_public_ip_on_launch = true - tags = { - Name = var.subnet-name - } -} - -resource "aws_route_table" "rt" { - vpc_id = aws_vpc.vpc.id - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.igw.id - } - tags = { - Name: var.rt-name - } -} - -resource "aws_route_table_association" "rt-association" { - route_table_id = aws_route_table.rt.id - subnet_id = aws_subnet.public-subnet.id -} \ No newline at end of file diff --git a/GITHUB_ACTION.md b/GITHUB_ACTION.md index 9fa9f48..7fa0742 100644 --- a/GITHUB_ACTION.md +++ b/GITHUB_ACTION.md @@ -31,10 +31,18 @@ This prevents old runs from stacking up when a new change is pushed. ### 2. Reusable Terraform workflow: .github/workflows/reusable-terraform.yml This is the main implementation path used by the CI/CD pipeline. +It now includes a backend bootstrap phase that: +- generates a per-environment S3 state key +- creates the Terraform state bucket if missing +- enables S3 versioning and encryption +- creates the DynamoDB lock table if missing +- writes a generated `backend.hcl` for `terraform init` + It currently includes: - OIDC-based AWS authentication (no static AWS keys) +- Terraform backend bootstrap and backend.hcl generation - Terraform setup and plugin cache -- terraform init +- terraform init with backend config - terraform fmt -check - terraform validate - TFLint diff --git a/README.md b/README.md new file mode 100644 index 0000000..d64fbef --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# Enterprise Kubernetes Platform on AWS EKS + +[![Build Status](https://github.com/Emmy-github-webdev/Kubernetes/actions/workflows/terraform.yml/badge.svg?branch=main)](https://github.com/Emmy-github-webdev/Kubernetes/actions/workflows/terraform.yml) +[![Infrastructure Repo](https://img.shields.io/badge/GitHub-Kubernetes-181717?logo=github)](https://github.com/Emmy-github-webdev/Kubernetes/tree/main) +[![Terraform](https://img.shields.io/badge/Terraform-1.6%2B-623CE4?logo=terraform)](https://www.terraform.io/) +[![AWS EKS](https://img.shields.io/badge/AWS-EKS-FF9900?logo=amazonaws)](https://aws.amazon.com/eks/) +[![Coverage](https://img.shields.io/badge/coverage-ongoing-lightgrey)](https://github.com/Emmy-github-webdev/Kubernetes) + +## Description + +This repository is the infrastructure backbone for an enterprise-grade, cloud-native platform running containerized applications on Amazon EKS. It forms the foundation of a multi-repository delivery model that spans platform provisioning, application delivery, and GitOps-based deployment operations. + +### Repository ecosystem + +- Infrastructure repository: [Kubernetes](https://github.com/Emmy-github-webdev/Kubernetes/tree/main) +- Application source repository: [ja-mics-ap](https://github.com/Emmy-github-webdev/ja-mics-ap) +- GitOps repository: [kubernetes-argocd](https://github.com/Emmy-github-webdev/Kubernetes-argocd) + +The platform combines Infrastructure as Code, secure networking, Kubernetes-native delivery, observability, and CI/CD automation to support development, staging, and production environments with strong governance, traceability, and operational consistency. + +## Why this platform exists + +This platform was designed to support enterprise application delivery with a strong focus on security, reliability, and repeatability: + +- Secure and scalable deployment of containerized workloads on AWS +- Consistent environment provisioning across development, staging, and production +- Strong separation between public and private application layers +- GitOps-driven deployment and environment promotion +- Production-grade monitoring, alerting, and operational visibility +- A reusable foundation for future services, platform expansion, and team enablement + +## Drawbacks and trade-offs + +While this architecture is powerful and enterprise-friendly, it also introduces some trade-offs: + +- Higher initial setup and platform complexity +- Greater operational responsibility for Kubernetes and cloud networking +- Additional cost for multi-AZ, private networking, monitoring, and managed services +- A steeper learning curve for teams adopting GitOps and platform engineering practices + +## Architecture + +![Architecture overview](pub_priv_eks_pg.png) + +The platform is organized around three repositories and a clear delivery flow: + +1. [Infrastructure repository](https://github.com/Emmy-github-webdev/Kubernetes) + - Provisions networking, EKS, IAM, security groups, load balancers, databases, cache, and shared platform services using Terraform. + - Implements environment-specific modules for dev, staging, and prod. + +2. [Application repository](https://github.com/Emmy-github-webdev/ja-mics-ap) + - Contains the application source code, container build logic, and application-level CI/CD automation. + - Produces container images and publishes them to the configured registry. + +3. [GitOps repository](https://github.com/Emmy-github-webdev/Kubernetes-argocd) + - Stores Kubernetes manifests, Argo CD application definitions, overlays, and monitoring configuration. + - Synchronizes application deployment state from Git into the cluster. + +### High-level deployment flow + +- Developers commit changes to the application repository. +- CI pipelines build and validate the application. +- Container images are published to the appropriate registry. +- The GitOps repository is updated with the new image or manifest state. +- Argo CD detects and applies the changes to the EKS cluster. +- Kubernetes services run behind the platform networking and ingress layer. + +## Prerequisites + +Before using this platform, ensure the following tools are installed and configured: + +- AWS CLI +- Terraform +- kubectl +- Helm +- Git +- Docker (recommended for application image builds) + +Example verification commands: + +```bash +aws sts get-caller-identity +terraform version +kubectl version --client +helm version +``` + +## Quick start guide + +1. Clone the infrastructure repository. +2. Configure your AWS credentials and preferred region. +3. Review the environment modules under the infra/environments directory. +4. Initialize and plan the desired environment. +5. Apply the Terraform configuration to provision the platform. + +Example: + +```bash +git clone +cd Kubernetes +terraform -chdir=infra/environments/dev init +terraform -chdir=infra/environments/dev plan +terraform -chdir=infra/environments/dev apply +``` + +> Replace the environment path and variables according to your deployment target and organizational conventions. + +## Installation instructions + +This repository is structured to support repeatable infrastructure deployment through Terraform modules. + +### Environment setup + +- Review the variables defined in each environment folder. +- Provide the required values through a tfvars file or environment-specific input configuration. +- Validate the configuration before applying changes. + +### Recommended workflow + +```bash +terraform -chdir=infra/environments/dev fmt -check +terraform -chdir=infra/environments/dev validate +terraform -chdir=infra/environments/dev plan +terraform -chdir=infra/environments/dev apply +``` + +For staging and production, follow the same process with the appropriate environment folder and approvals. + +## Basic usage examples + +### Validate infrastructure + +```bash +terraform -chdir=infra/environments/dev validate +``` + +### Review planned changes + +```bash +terraform -chdir=infra/environments/dev plan +``` + +### Apply changes + +```bash +terraform -chdir=infra/environments/dev apply +``` + +### Remove deployed resources + +```bash +terraform -chdir=infra/environments/dev destroy +``` + +## Comprehensive documentation + +Additional project documentation is available here: + +- [GITHUB_ACTION.md](GITHUB_ACTION.md) +- [README1.md](README1.md) + +These documents provide deeper context on CI/CD workflows, Terraform automation, and platform operations. + +## Contributing + +Contributions are welcome. Please follow standard engineering practices: + +- Create a feature branch for your work. +- Open a pull request with a clear description of the change. +- Ensure validation and review steps are completed before merging. +- Align your changes with the repository’s CI/CD and infrastructure standards described in [GITHUB_ACTION.md](GITHUB_ACTION.md). + +## License + +This repository does not currently include a license file. For enterprise adoption, align the repository with your organization’s approved licensing model before broader internal or public reuse. + +## Technologies used + +- Terraform +- AWS EKS +- Amazon VPC and networking services +- AWS IAM and security groups +- Application Load Balancer and ingress management +- Amazon RDS and Redis-compatible services +- Kubernetes and Helm +- Argo CD for GitOps delivery +- GitHub Actions for automation and validation +- Prometheus, Grafana, and Alertmanager for observability + +--- + +This repository represents the infrastructure backbone for a secure, scalable, and enterprise-ready Kubernetes platform built for modern application delivery. diff --git a/README1.md b/README1.md new file mode 100644 index 0000000..1affbb4 --- /dev/null +++ b/README1.md @@ -0,0 +1,639 @@ +# EKS Microservices Platform + +The EKS Microservices Platform is a production-ready, cloud-native architecture built on Amazon EKS (Elastic Kubernetes Service) for deploying and managing containerized microservices at scale. The platform is designed with a secure networking model that leverages both Public and Private EKS API Endpoints, enabling controlled external access while ensuring worker nodes and workloads remain isolated within private subnets. + +The solution incorporates AWS Load Balancer Controller for dynamic ingress management, allowing secure exposure of services through Application Load Balancers (ALBs). To provide comprehensive observability, the platform integrates Prometheus, Grafana, Alertmanager, Node Exporter, and kube-state-metrics, delivering real-time monitoring, alerting, and visualization of cluster, node, pod, and application metrics. + +The architecture follows AWS best practices for security, scalability, high availability, and operational excellence, utilizing multi-AZ deployments, private worker nodes, VPC endpoints, IAM Roles for Service Accounts (IRSA), and centralized monitoring. This platform serves as a reference implementation for running enterprise-grade microservices on Kubernetes with end-to-end networking, security, and observability capabilities. + +## Key Features + +Amazon EKS with Public and Private API Endpoints Multi-AZ deployment for high availability Private worker nodes and workloads AWS Load Balancer Controller for ingress management Prometheus-based metrics collection Grafana dashboards and visualization Alertmanager for proactive alerting Node Exporter and kube-state-metrics integration VPC Endpoints for secure AWS service access IAM Roles for Service Accounts (IRSA) Production-grade monitoring, security, and scalability Infrastructure-as-Code ready (Terraform/CloudFormation compatible) Architecture Goals + +- Security: Isolate workloads in private subnets while maintaining controlled administrative access. +- Scalability: Support horizontal scaling of microservices and worker nodes. +- Observability: Provide full-stack monitoring, alerting, and operational visibility. +- Reliability: Ensure high availability through multi-AZ deployment and resilient networking. +- Operational Excellence: Simplify deployment, monitoring, troubleshooting, and maintenance of Kubernetes workloads. + +## Prerequisites + +_Install_: + - AWS CLI + - kubectl + - eksctl + - Helm 3 +_Verify_: + +``` +awsstsget-caller-identity +kubectlversion--client +helmversion +eksctlversion +``` + +## Architecture + +[](./pub_priv_eks_pg.png) + +### High level guide + +infra-repo +│ +├── Terraform +| ├── environments +| | ├── dev +| | ├── prod +| | ├── staging +├── Terraform +│ ├── VPC +│ ├── EKS +│ ├── ECR +│ ├── RDS +│ ├── alb-ingress +│ ├── GitHub OIDC +│ └── ArgoCD +│ +└── ArgoCD Application + │ + ▼ +Kubenetes-argocd Repo +├── apps +│ ├── user-service +│ │ ├── base +| │ | ├── deployment.yaml +| │ | ├── kustomization.yaml +| │ | └── poddistruption.yaml +| │ | └── service.yaml +│ │ └── overlays +| │ | ├── dev +| | | | ├── external-secret-patch.yaml +| | | | ├── Image-patch.yaml +| | | | ├── kustomization.yaml +| │ | ├── staging +| │ | └── prod +│ ├── order-service +│ ├── payment-service +│ └── product-service +| +| +│ +├── monitoring +│ ├── base +│ │ ├── namespace.yaml +│ │ ├── kustomization.yaml +│ │ ├── prometheus-values.yaml +│ │ ├── grafana-dashboards/ +│ │ ├── kubernetes.json +│ │ ├── jvm.json +│ │ ├── postgres.json +│ │ ├── predis.json +│ │ └── springboot.json +│ │ ├── servicemonitors/ +│ │ │ ├── order.yaml +│ │ │ ├── user.yaml +│ │ │ ├── payment.yaml +│ │ │ └── product.yaml +│ │ │ +│ │ └── prometheusrules/ +│ │ ├── high-cpu.yaml +│ │ ├── high-memory.yaml +│ │ ├── pod-restarts.yaml +│ │ └── database-down.yaml +│ │ +│ └── overlays +│ ├── dev +│ │ ├── kustomization.yaml +│ │ └── values-patch.yaml +│ │ +│ ├── staging +│ └── prod +│ +| +|___argocd +│ ├── dev +│ | ├── applicationset-apps.yaml +| | ├── applicationset-infra.yaml +| | ├── applicationset-monitoring.yaml +│ | └── root-app.yaml +| | +│ ├── prod +| | +│ ├── staging +| +|___infrastructure +│ | ├── dev +│ | | ├── postgres-master-secret.yaml +│ | | ├── order-db-secret.yaml +│ | | ├── user-db-secret.yaml +│ | | ├── payment-db-secret.yaml +│ | | ├── product-db-secret.yaml +│ | | └── postgres-bootstrap-job.yaml +│ | | ├── cluster-secret-store.yaml +│ | | ├── ingress.yaml +│ | | └── namespace-database.yaml +│ | ├── prod +│ | | +│ | ├── staging +└── README.md + │ + ▼ +app-repo GitHub Actions +│ +├── Build Docker image +├── Push to ECR +└── Update gitops-repo image tag + │ + ▼ +ArgoCD syncs + │ + ▼ +EKS pulls image from ECR + │ + ▼ +Application running + +### The flow + +app-repo + ↓ +Build Image + ↓ +Push to ECR + ↓ +Update gitops-repo image tag + ↓ +ArgoCD detects change + ↓ +Deploy to EKS + +### Developer Github architecture + +Developer + ↓ +GitHub + ↓ +Pull Request + ↓ +CI Pipeline + ├── SonarQube + ├── Dependency Check + ├── Trivy File Scan + ├── Unit Tests + ├── Build + ├── Trivy Image Scan + └── Push to ECR + ↓ + ArgoCD + ↓ + EKS + ↓ +Prometheus + Grafana + +### Target architecture +Internet + | + v +ALB (AWS) + | + v +Kubernetes Ingress + | + v +ClusterIP Service + | + v +Pods + +## Step By Step Creation of Resources + +### Phase 1 - Network + +1. _Create VPC_ + +``` +# CIDR: + +10.0.0.0/16 +``` +2. _Create Public Subnets_ + +``` +10.0.1.0/24 AZ-A +10.0.2.0/24 AZ-B + +Purpose: + - NAT Gateways + - Public ALBs +``` + +3. _Create Private Subnets_ + +``` +10.0.11.0/24 AZ-A +10.0.12.0/24 AZ-B + +Purpose: + - EKS worker nodes + - Pods + - Monitoring stack +``` + +4. _Create Internet Gateway_ +Attach to VPC. + +5. _Create NAT Gateways_ + - One NAT Gateway per AZ. + - Assign Elastic IPs. + + +6. _Configure Route Tables_ + +``` +Public Route Table: +0.0.0.0/0 -> Internet Gateway + +Private Route Table: +0.0.0.0/0 -> NAT Gateway + +``` + +7. _Create Security Groups_ + +- Cluster security group + +``` +Inbound: +TCP 443 +Source: WorkerNodeSG + +Outbound: +All +``` + +- Worker Node Security group + +``` +Outbound: +TCP 443 → Cluster SG + +Inbound: +WorkerNodeSG → WorkerNodeSG +``` + +### Phase 2 - EKS Cluster + +8. _Create EKS Cluster_ + +``` +Enable: + +endpointPublicAccess:false +endpointPrivateAccess:true + +#--------------------------------------- + +Restrict: + +publicAccessCidrs: + -YOUR_OFFICE_IP/32 +``` + +9. _Create Managed Node Group_ + +``` +Place nodes only in: + +PrivateSubnetA +PrivateSubnetB + +#---------------------- +Disable public IP assignment. + +Verify: + +``` +kubectl get nodes +``` + +Expected: + +STATUS Ready +``` + +### Phase 3 - VPC Endpoints + +10. _Create Interface Endpoints_ +Create: + - ECR API + - ECR DKR + - STS + - CloudWatch Logs +Create Gateway Endpoint: + - S3 +Validation: + +``` +kubectlruncurlpod--image=curlimages/curl-it--rm--sh +``` + +Inside pod: + +``` +curlhttps://sts.amazonaws.com +``` + +Expected: + +``` +HTTP 403 + +# 403 confirms connectivity +``` + +### Phase 4 - IAM Roles for Service Accounts + +11. _Enable OIDC Provider_ + +``` +# I use terraform + +eksctlutilsassociate-iam-oidc-provider + --clustereks-prod + --approve + +``` +Verify: + +``` +awseksdescribe-cluster + --nameeks-prod + --querycluster.identity.oidc.issuer +``` + +### Phase 5 - AWS Load Balancer Controller + +12. _Install Controller_ + - Create IAM policy. + - Create service account. + - Install Helm chart. + +Verify: +``` +kubectl get pods-nkube-system +``` + +Expected: +``` +aws-load-balancer-controller +Running +``` + +### Phase 6 - Monitoring Stack + +13. _Create Monitoring Namespace_ +create name space monitoring + +Verify: + +``` +kubectl get ns monitoring +``` + +14. _Add Helm Repository_ + +``` +helm repo add prometheus-community +https://prometheus-community.github.io/helm-charts +helmrepoupdate +``` +15. _Install kube-prometheus-stack_ + +``` +helm install monitoring +prometheus-community/kube-prometheus-stack +-nmonitoring +``` + +This deploys: + - Prometheus + - Alertmanager + - Grafana + - Node Exporter + - kube-state-metrics + +Verify: +``` +kubectlgetpods-nmonitoring +``` + +Expected: + +``` +prometheus-* +grafana-* +alertmanager-* +Running +``` + +### Phase 7 - Expose Grafana + +16. Create Ingress + +Create: + +``` +kind: Ingress +``` +Annotations: + +``` +alb.ingress.kubernetes.io/scheme:internet-facing +alb.ingress.kubernetes.io/target-type:ip +``` + +Apply (Using terraform): + +``` +kubectlapply-fgrafana-ingress.yaml +``` + +Verify: + +``` +kubectlgetingress-nmonitoring +``` + +Expected: + +``` +ADDRESS: +xxxxxxxx.elb.amazonaws.com +``` + +### Phase 8 - GitOps Platform +- Create namespace +- Install ArgoCD + +Verify + +``` +kubectl get pods -n argocd + +# Expected + +argocd-server +argocd-repo-server +argocd-application-controller +Running +``` +- Install SonarQube Server + +### Phase 9 - DevSecOps Platform +- Namespace: sonarqube +- Deploy + - SonarQube + - PostgreSQL + - Persistent Volume + - Ingress + +### Phase 10 - Functional Testing + +- Test 1 - Public EKS Endpoint + +From workstation: + +``` +kubectlgetnodes +``` + +Expected: + +``` +Node list returned +``` + +- Test 2 - Private Endpoint Usage + +SSH into node or exec into pod. + +Run: + +``` +curlhttps:// +``` + +Expected: + +``` +403 Forbidden + +# Connectivity confirmed +``` + +- Test 3 - Node Registration + +``` +kubectl get nodes -o wide +``` + +Expected: + +``` +All nodes Ready +``` + +- Test 4 - Prometheus Targets + +portforward: + +``` +kubectlport-forwardsvc/monitoring-kube-prometheus-prometheus +9090:9090-nmonitoring +``` + +Navigate: + +``` +http://localhost:9090 +``` +Verify: + +Status -> Targets + +Expected: + +``` +All critical targets UP +``` + +- Test 5 - Grafana Access +Open: + +``` +https://grafana.company.com +``` +Verify: + - Login successful + - Dashboards load + +- Test 6 - Kubernetes Metrics + +Open dashboard: + +``` +Kubernetes / Compute Resources / Cluster +``` + +Verify: + - CPU metrics + - Memory metrics + - Pod metrics + - Node metrics + +- Test 7 - Alertmanager + +Port forward: + +``` +kubectlport-forwardsvc/monitoring-kube-prometheus-alertmanager +9093:9093-nmonitoring +``` + +open: + +``` +http://localhost:9093 +``` +Verify: + - Alertmanager UI loads. + +- Final Validation + +Confirm: + + - Public EKS Endpoint reachable from approved CIDRs + - Worker nodes only in private subnets + - Private Endpoint enabled + - ECR access working + - STS access working + - Prometheus collecting metrics + - Grafana displaying dashboards + - Alertmanager operational + - ALB exposing Grafana + - No worker nodes have public IPs + + +## Resources + +- [Checkov - AWS General Policies](https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/aws-general-policies) + + +Notes + +Sonarcube +trivy +Argocd +code quality analysis +Dependency check +File scan diff --git a/infra/environments/dev/main.tf b/infra/environments/dev/main.tf new file mode 100644 index 0000000..f9924f3 --- /dev/null +++ b/infra/environments/dev/main.tf @@ -0,0 +1,107 @@ +module "tags" { + source = "../../modules/terraform-tags" + env_name = var.environment + project_name = var.project + region_name = var.region +} + +module "vpc" { + source = "../../modules/network" + tags = module.tags.common_tags + vpc_cidr = "10.0.0.0/16" + azs = ["us-east-1a", "us-east-1b"] + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"] + cloudwatch_log_group_arn = module.cloudwatch.vpc_flow_logs_cloudwatch_loggroup_arn + vpc_flow_logs_iam_role_arn = module.iam.vpc_flow_logs_iam_role_arn + cluster_name = module.eks.cluster_name + sg_eks_nodes_id = module.eks.cluster_security_group_id + oidc_issuer_url = module.eks.oidc_issuer_url +} + +module "security_groups" { + source = "../../modules/security_group" + tags = module.tags.common_tags + eks_vpc_id = module.vpc.vpc_id +} + +module "cloudwatch" { + source = "../../modules/cloudwatch" + tags = module.tags.common_tags +} + +module "iam" { + source = "../../modules/IAM" + tags = module.tags.common_tags + vpc_flow_logs_arn = module.cloudwatch.vpc_flow_logs_cloudwatch_loggroup_arn +} + +module "eks" { + source = "../../modules/eks" + tags = module.tags.common_tags + kms_key_arn = module.cloudwatch.kms_key_arn + private_subnet_ids = module.vpc.private_subnet_ids + eks_vpc_id = module.vpc.vpc_id +} + +module "argocd" { + source = "../../modules/argocd" + tags = module.tags.common_tags + cluster_name = module.eks.cluster_name + oidc_issuer_url = module.eks.oidc_issuer_url + oidc_provider_arn = module.eks.oidc_provider_arn + providers = { + kubernetes = kubernetes + helm = helm + } + depends_on = [module.eks] +} + +module "alb_ingress" { + source = "../../modules/alb-ingress" + tags = module.tags.common_tags + cluster_name = module.eks.cluster_name + oidc_issuer_url = module.eks.oidc_issuer_url + oidc_provider_arn = module.eks.oidc_provider_arn + vpc_id = module.vpc.vpc_id + providers = { + kubernetes = kubernetes + helm = helm + } + depends_on = [module.eks] +} + +module "alb" { + source = "../../modules/alb" + domain_name = "api.dev.emmanuelogah.com" +} + +module "rds" { + source = "../../modules/rds" + tags = module.tags.common_tags + eks_node_security_group_id = module.eks.cluster_security_group_id + eks_oidc_provider_url = module.eks.oidc_issuer_url + private_subnet_ids = module.vpc.private_subnet_ids + eks_oidc_provider_arn = module.eks.oidc_provider_arn + vpc_id = module.vpc.vpc_id +} + +module "grafana" { + source = "../../modules/grafana" + tags = module.tags.common_tags + grafana_admin_password = var.grafana_admin_password +} + +module "alertmanager" { + source = "../../modules/alertmanager" + tags = module.tags.common_tags + slack_webhook = var.slack_webhook +} + +module "redis" { + source = "../../modules/redis" + tags = module.tags.common_tags + eks_node_security_group_id = module.eks.cluster_security_group_id + private_subnet_ids = module.vpc.private_subnet_ids + vpc_id = module.vpc.vpc_id +} \ No newline at end of file diff --git a/infra/environments/dev/outputs.tf b/infra/environments/dev/outputs.tf new file mode 100644 index 0000000..d71fe8a --- /dev/null +++ b/infra/environments/dev/outputs.tf @@ -0,0 +1,15 @@ +output "db_endpoint" { + value = module.rds.endpoint +} + +output "db_port" { + value = module.rds.port +} + +output "master_secret_arn" { + value = module.rds.master_secret_arn +} + +output "service_secret_arns" { + value = module.rds.service_secret_arns +} \ No newline at end of file diff --git a/infra/environments/dev/provider.tf b/infra/environments/dev/provider.tf new file mode 100644 index 0000000..4b603c1 --- /dev/null +++ b/infra/environments/dev/provider.tf @@ -0,0 +1,61 @@ +terraform { + required_version = ">= 1.8.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + + helm = { + source = "hashicorp/helm" + version = "~> 2.14" + } + + } + + backend "s3" {} +} + +provider "aws" { + region = "us-east-1" +} + +provider "kubernetes" { + host = module.eks.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.eks_cluster_certificate) + + exec { + api_version = "client.authentication.k8s.io/v1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_name + ] + } +} + +provider "helm" { + kubernetes { + host = module.eks.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.eks_cluster_certificate) + + exec { + api_version = "client.authentication.k8s.io/v1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_name + ] + } + } +} \ No newline at end of file diff --git a/infra/environments/dev/variables.tf b/infra/environments/dev/variables.tf new file mode 100644 index 0000000..1ef2625 --- /dev/null +++ b/infra/environments/dev/variables.tf @@ -0,0 +1,39 @@ +variable "environment" { + description = "Defines the environment to provision the resurces" + type = string + default = "dev" +} + +variable "project" { + description = "Defines the project name" + type = string + default = "eks" +} + +variable "region" { + type = string + description = "Defines the region where the resources are created" + default = "us-east-1" +} + +variable "grafana_admin_password" { + type = string + sensitive = true +} +variable "slack_webhook" { + description = "Slack webhook URL for Alertmanager notifications" + type = string + sensitive = true +} + +variable "terraform_state_bucket" { + description = "S3 bucket name for storing Terraform state" + type = string + default = "emmy-github-webdev-kubernetes" +} + +variable "terraform_lock_table" { + description = "DynamoDB table name for Terraform locking" + type = string + default = "Emmy-github-webdev_Kubernetes_dynamo_tbl" +} diff --git a/infra/environments/prod/main.tf b/infra/environments/prod/main.tf new file mode 100644 index 0000000..5c06098 --- /dev/null +++ b/infra/environments/prod/main.tf @@ -0,0 +1,107 @@ +module "tags" { + source = "../../modules/terraform-tags" + env_name = var.environment + project_name = var.project + region_name = var.region +} + +module "vpc" { + source = "../../modules/network" + tags = module.tags.common_tags + vpc_cidr = "10.0.0.0/16" + azs = ["us-east-1a", "us-east-1b"] + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"] + cloudwatch_log_group_arn = module.cloudwatch.vpc_flow_logs_cloudwatch_loggroup_arn + vpc_flow_logs_iam_role_arn = module.iam.vpc_flow_logs_iam_role_arn + cluster_name = module.eks.cluster_name + sg_eks_nodes_id = module.eks.cluster_security_group_id + oidc_issuer_url = module.eks.oidc_issuer_url +} + +module "security_groups" { + source = "../../modules/security_group" + tags = module.tags.common_tags + eks_vpc_id = module.vpc.vpc_id +} + +module "cloudwatch" { + source = "../../modules/cloudwatch" + tags = module.tags.common_tags +} + +module "iam" { + source = "../../modules/IAM" + tags = module.tags.common_tags + vpc_flow_logs_arn = module.cloudwatch.vpc_flow_logs_cloudwatch_loggroup_arn +} + +module "eks" { + source = "../../modules/eks" + tags = module.tags.common_tags + kms_key_arn = module.cloudwatch.kms_key_arn + private_subnet_ids = module.vpc.private_subnet_ids + eks_vpc_id = module.vpc.vpc_id +} + +module "argocd" { + source = "../../modules/argocd" + tags = module.tags.common_tags + cluster_name = module.eks.cluster_name + oidc_issuer_url = module.eks.oidc_issuer_url + oidc_provider_arn = module.eks.oidc_provider_arn + providers = { + kubernetes = kubernetes + helm = helm + } + depends_on = [module.eks] +} + +module "alb_ingress" { + source = "../../modules/alb-ingress" + tags = module.tags.common_tags + cluster_name = module.eks.cluster_name + oidc_issuer_url = module.eks.oidc_issuer_url + oidc_provider_arn = module.eks.oidc_provider_arn + vpc_id = module.vpc.vpc_id + providers = { + kubernetes = kubernetes + helm = helm + } + depends_on = [module.eks] +} + +module "alb" { + source = "../../modules/alb" + domain_name = "api.prod.emmanuelogah.com" +} + +module "rds" { + source = "../../modules/rds" + tags = module.tags.common_tags + eks_node_security_group_id = module.eks.cluster_security_group_id + eks_oidc_provider_url = module.eks.oidc_issuer_url + private_subnet_ids = module.vpc.private_subnet_ids + eks_oidc_provider_arn = module.eks.oidc_provider_arn + vpc_id = module.vpc.vpc_id +} + +module "grafana" { + source = "../../modules/grafana" + tags = module.tags.common_tags + grafana_admin_password = var.grafana_admin_password +} + +module "alertmanager" { + source = "../../modules/alertmanager" + tags = module.tags.common_tags + slack_webhook = var.slack_webhook +} + +module "redis" { + source = "../../modules/redis" + tags = module.tags.common_tags + eks_node_security_group_id = module.eks.cluster_security_group_id + private_subnet_ids = module.vpc.private_subnet_ids + vpc_id = module.vpc.vpc_id +} \ No newline at end of file diff --git a/infra/environments/prod/outputs.tf b/infra/environments/prod/outputs.tf new file mode 100644 index 0000000..d71fe8a --- /dev/null +++ b/infra/environments/prod/outputs.tf @@ -0,0 +1,15 @@ +output "db_endpoint" { + value = module.rds.endpoint +} + +output "db_port" { + value = module.rds.port +} + +output "master_secret_arn" { + value = module.rds.master_secret_arn +} + +output "service_secret_arns" { + value = module.rds.service_secret_arns +} \ No newline at end of file diff --git a/infra/environments/prod/provider.tf b/infra/environments/prod/provider.tf new file mode 100644 index 0000000..4b603c1 --- /dev/null +++ b/infra/environments/prod/provider.tf @@ -0,0 +1,61 @@ +terraform { + required_version = ">= 1.8.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + + helm = { + source = "hashicorp/helm" + version = "~> 2.14" + } + + } + + backend "s3" {} +} + +provider "aws" { + region = "us-east-1" +} + +provider "kubernetes" { + host = module.eks.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.eks_cluster_certificate) + + exec { + api_version = "client.authentication.k8s.io/v1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_name + ] + } +} + +provider "helm" { + kubernetes { + host = module.eks.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.eks_cluster_certificate) + + exec { + api_version = "client.authentication.k8s.io/v1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_name + ] + } + } +} \ No newline at end of file diff --git a/infra/environments/prod/variables.tf b/infra/environments/prod/variables.tf new file mode 100644 index 0000000..e0fe372 --- /dev/null +++ b/infra/environments/prod/variables.tf @@ -0,0 +1,39 @@ +variable "environment" { + description = "Defines the environment to provision the resurces" + type = string + default = "prod" +} + +variable "project" { + description = "Defines the project name" + type = string + default = "eks" +} + +variable "region" { + type = string + description = "Defines the region where the resources are created" + default = "us-east-1" +} + +variable "grafana_admin_password" { + type = string + sensitive = true +} +variable "slack_webhook" { + description = "Slack webhook URL for Alertmanager notifications" + type = string + sensitive = true +} + +variable "terraform_state_bucket" { + description = "S3 bucket name for storing Terraform state" + type = string + default = "emmy-github-webdev-kubernetes" +} + +variable "terraform_lock_table" { + description = "DynamoDB table name for Terraform locking" + type = string + default = "Emmy-github-webdev_Kubernetes_dynamo_tbl" +} \ No newline at end of file diff --git a/infra/environments/shared/main.tf b/infra/environments/shared/main.tf new file mode 100644 index 0000000..f3992ea --- /dev/null +++ b/infra/environments/shared/main.tf @@ -0,0 +1,20 @@ +terraform { + required_version = ">= 1.8.5" + + backend "s3" {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = "us-east-1" +} + +module "ecr" { + source = "../../modules/ecr" +} \ No newline at end of file diff --git a/infra/environments/shared/outputs.tf b/infra/environments/shared/outputs.tf new file mode 100644 index 0000000..39caa8c --- /dev/null +++ b/infra/environments/shared/outputs.tf @@ -0,0 +1,3 @@ +output "ecr_repository_urls" { + value = module.ecr.repository_urls +} \ No newline at end of file diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf new file mode 100644 index 0000000..a9087c2 --- /dev/null +++ b/infra/environments/staging/main.tf @@ -0,0 +1,107 @@ +module "tags" { + source = "../../modules/terraform-tags" + env_name = var.environment + project_name = var.project + region_name = var.region +} + +module "vpc" { + source = "../../modules/network" + tags = module.tags.common_tags + vpc_cidr = "10.0.0.0/16" + azs = ["us-east-1a", "us-east-1b"] + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"] + cloudwatch_log_group_arn = module.cloudwatch.vpc_flow_logs_cloudwatch_loggroup_arn + vpc_flow_logs_iam_role_arn = module.iam.vpc_flow_logs_iam_role_arn + cluster_name = module.eks.cluster_name + sg_eks_nodes_id = module.eks.cluster_security_group_id + oidc_issuer_url = module.eks.oidc_issuer_url +} + +module "security_groups" { + source = "../../modules/security_group" + tags = module.tags.common_tags + eks_vpc_id = module.vpc.vpc_id +} + +module "cloudwatch" { + source = "../../modules/cloudwatch" + tags = module.tags.common_tags +} + +module "iam" { + source = "../../modules/IAM" + tags = module.tags.common_tags + vpc_flow_logs_arn = module.cloudwatch.vpc_flow_logs_cloudwatch_loggroup_arn +} + +module "eks" { + source = "../../modules/eks" + tags = module.tags.common_tags + kms_key_arn = module.cloudwatch.kms_key_arn + private_subnet_ids = module.vpc.private_subnet_ids + eks_vpc_id = module.vpc.vpc_id +} + +module "argocd" { + source = "../../modules/argocd" + tags = module.tags.common_tags + cluster_name = module.eks.cluster_name + oidc_issuer_url = module.eks.oidc_issuer_url + oidc_provider_arn = module.eks.oidc_provider_arn + providers = { + kubernetes = kubernetes + helm = helm + } + depends_on = [module.eks] +} + +module "alb_ingress" { + source = "../../modules/alb-ingress" + tags = module.tags.common_tags + cluster_name = module.eks.cluster_name + oidc_issuer_url = module.eks.oidc_issuer_url + oidc_provider_arn = module.eks.oidc_provider_arn + vpc_id = module.vpc.vpc_id + providers = { + kubernetes = kubernetes + helm = helm + } + depends_on = [module.eks] +} + +module "alb" { + source = "../../modules/alb" + domain_name = "api.staging.emmanuelogah.com" +} + +module "rds" { + source = "../../modules/rds" + tags = module.tags.common_tags + eks_node_security_group_id = module.eks.cluster_security_group_id + eks_oidc_provider_url = module.eks.oidc_issuer_url + private_subnet_ids = module.vpc.private_subnet_ids + eks_oidc_provider_arn = module.eks.oidc_provider_arn + vpc_id = module.vpc.vpc_id +} + +module "grafana" { + source = "../../modules/grafana" + tags = module.tags.common_tags + grafana_admin_password = var.grafana_admin_password +} + +module "alertmanager" { + source = "../../modules/alertmanager" + tags = module.tags.common_tags + slack_webhook = var.slack_webhook +} + +module "redis" { + source = "../../modules/redis" + tags = module.tags.common_tags + eks_node_security_group_id = module.eks.cluster_security_group_id + private_subnet_ids = module.vpc.private_subnet_ids + vpc_id = module.vpc.vpc_id +} \ No newline at end of file diff --git a/infra/environments/staging/outputs.tf b/infra/environments/staging/outputs.tf new file mode 100644 index 0000000..d71fe8a --- /dev/null +++ b/infra/environments/staging/outputs.tf @@ -0,0 +1,15 @@ +output "db_endpoint" { + value = module.rds.endpoint +} + +output "db_port" { + value = module.rds.port +} + +output "master_secret_arn" { + value = module.rds.master_secret_arn +} + +output "service_secret_arns" { + value = module.rds.service_secret_arns +} \ No newline at end of file diff --git a/infra/environments/staging/provider.tf b/infra/environments/staging/provider.tf new file mode 100644 index 0000000..4b603c1 --- /dev/null +++ b/infra/environments/staging/provider.tf @@ -0,0 +1,61 @@ +terraform { + required_version = ">= 1.8.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + + helm = { + source = "hashicorp/helm" + version = "~> 2.14" + } + + } + + backend "s3" {} +} + +provider "aws" { + region = "us-east-1" +} + +provider "kubernetes" { + host = module.eks.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.eks_cluster_certificate) + + exec { + api_version = "client.authentication.k8s.io/v1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_name + ] + } +} + +provider "helm" { + kubernetes { + host = module.eks.eks_cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.eks_cluster_certificate) + + exec { + api_version = "client.authentication.k8s.io/v1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_name + ] + } + } +} \ No newline at end of file diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf new file mode 100644 index 0000000..f897b35 --- /dev/null +++ b/infra/environments/staging/variables.tf @@ -0,0 +1,39 @@ +variable "environment" { + description = "Defines the environment to provision the resurces" + type = string + default = "staging" +} + +variable "project" { + description = "Defines the project name" + type = string + default = "eks" +} + +variable "region" { + type = string + description = "Defines the region where the resources are created" + default = "us-east-1" +} + +variable "grafana_admin_password" { + type = string + sensitive = true +} +variable "slack_webhook" { + description = "Slack webhook URL for Alertmanager notifications" + type = string + sensitive = true +} + +variable "terraform_state_bucket" { + description = "S3 bucket name for storing Terraform state" + type = string + default = "emmy-github-webdev-kubernetes" +} + +variable "terraform_lock_table" { + description = "DynamoDB table name for Terraform locking" + type = string + default = "Emmy-github-webdev_Kubernetes_dynamo_tbl" +} \ No newline at end of file diff --git a/infra/modules/IAM/iam.tf b/infra/modules/IAM/iam.tf new file mode 100644 index 0000000..8760a97 --- /dev/null +++ b/infra/modules/IAM/iam.tf @@ -0,0 +1,37 @@ +# IAM Role for Flow Logs + +resource "aws_iam_role" "vpc_flow_logs_role" { + name = "${var.tags.project}-${var.tags.environment}-vpc-flowlogs-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + Service = "vpc-flow-logs.amazonaws.com" + } + Action = "sts:AssumeRole" + }] + }) +} + +resource "aws_iam_role_policy" "vpc_flow_logs_policy" { + name = "vpc-flowlogs-policy" + role = aws_iam_role.vpc_flow_logs_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams" + ] + Resource = "${var.vpc_flow_logs_arn}:*" + } + ] + }) +} \ No newline at end of file diff --git a/infra/modules/IAM/outputs.tf b/infra/modules/IAM/outputs.tf new file mode 100644 index 0000000..87b5d75 --- /dev/null +++ b/infra/modules/IAM/outputs.tf @@ -0,0 +1,4 @@ +output "vpc_flow_logs_iam_role_arn" { + description = "IAM Role ARN for VPC flow logs" + value = aws_iam_role.vpc_flow_logs_role.arn +} \ No newline at end of file diff --git a/infra/modules/IAM/variables.tf b/infra/modules/IAM/variables.tf new file mode 100644 index 0000000..6f10e46 --- /dev/null +++ b/infra/modules/IAM/variables.tf @@ -0,0 +1,10 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "vpc_flow_logs_arn" { + description = "IAM Role ARN for VPC flow logs" + type = string +} diff --git a/infra/modules/alb-ingress/iam_policy.json b/infra/modules/alb-ingress/iam_policy.json new file mode 100644 index 0000000..761d0e7 --- /dev/null +++ b/infra/modules/alb-ingress/iam_policy.json @@ -0,0 +1,251 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:CreateServiceLinkedRole" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "iam:AWSServiceName": "elasticloadbalancing.amazonaws.com" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInternetGateways", + "ec2:DescribeVpcs", + "ec2:DescribeVpcPeeringConnections", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeInstances", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeTags", + "ec2:GetCoipPoolUsage", + "ec2:DescribeCoipPools", + "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", + "ec2:DescribeRouteTables", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeListenerCertificates", + "elasticloadbalancing:DescribeSSLPolicies", + "elasticloadbalancing:DescribeRules", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeTags", + "elasticloadbalancing:DescribeTrustStores", + "elasticloadbalancing:DescribeListenerAttributes", + "elasticloadbalancing:DescribeCapacityReservation" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "cognito-idp:DescribeUserPoolClient", + "acm:ListCertificates", + "acm:DescribeCertificate", + "iam:ListServerCertificates", + "iam:GetServerCertificate", + "waf-regional:GetWebACL", + "waf-regional:GetWebACLForResource", + "waf-regional:AssociateWebACL", + "waf-regional:DisassociateWebACL", + "wafv2:GetWebACL", + "wafv2:GetWebACLForResource", + "wafv2:AssociateWebACL", + "wafv2:DisassociateWebACL", + "shield:GetSubscriptionState", + "shield:DescribeProtection", + "shield:CreateProtection", + "shield:DeleteProtection" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateSecurityGroup" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags" + ], + "Resource": "arn:aws:ec2:*:*:security-group/*", + "Condition": { + "StringEquals": { + "ec2:CreateAction": "CreateSecurityGroup" + }, + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags", + "ec2:DeleteTags" + ], + "Resource": "arn:aws:ec2:*:*:security-group/*", + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "true", + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:DeleteSecurityGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:CreateLoadBalancer", + "elasticloadbalancing:CreateTargetGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:CreateListener", + "elasticloadbalancing:DeleteListener", + "elasticloadbalancing:CreateRule", + "elasticloadbalancing:DeleteRule" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags", + "elasticloadbalancing:RemoveTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" + ], + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "true", + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags", + "elasticloadbalancing:RemoveTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:SetIpAddressType", + "elasticloadbalancing:SetSecurityGroups", + "elasticloadbalancing:SetSubnets", + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:ModifyTargetGroup", + "elasticloadbalancing:ModifyTargetGroupAttributes", + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:ModifyListenerAttributes", + "elasticloadbalancing:ModifyCapacityReservation", + "elasticloadbalancing:ModifyIpPools" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" + ], + "Condition": { + "StringEquals": { + "elasticloadbalancing:CreateAction": [ + "CreateTargetGroup", + "CreateLoadBalancer" + ] + }, + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:RegisterTargets", + "elasticloadbalancing:DeregisterTargets" + ], + "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:SetWebAcl", + "elasticloadbalancing:ModifyListener", + "elasticloadbalancing:AddListenerCertificates", + "elasticloadbalancing:RemoveListenerCertificates", + "elasticloadbalancing:ModifyRule", + "elasticloadbalancing:SetRulePriorities" + ], + "Resource": "*" + } + ] +} diff --git a/infra/modules/alb-ingress/lb-controller.tf b/infra/modules/alb-ingress/lb-controller.tf new file mode 100644 index 0000000..e84e3c6 --- /dev/null +++ b/infra/modules/alb-ingress/lb-controller.tf @@ -0,0 +1,78 @@ +resource "aws_iam_policy" "alb_controller" { + #name = "AWSLoadBalancerControllerIAMPolicy" + name = "${var.tags.environment}-AWSLoadBalancerControllerIAMPolicy" + policy = file("${path.module}/iam_policy.json") +} + +data "aws_iam_policy_document" "alb_assume_role" { + statement { + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = [var.oidc_provider_arn] + } + + condition { + test = "StringEquals" + variable = "${replace(var.oidc_issuer_url, "https://", "")}:sub" + + values = [ + "system:serviceaccount:kube-system:aws-load-balancer-controller" + ] + } + } +} + +resource "aws_iam_role" "alb_controller" { + name = "${var.tags.project}-${var.tags.environment}-alb-controller-role" + assume_role_policy = data.aws_iam_policy_document.alb_assume_role.json +} + +resource "aws_iam_role_policy_attachment" "alb_controller" { + role = aws_iam_role.alb_controller.name + policy_arn = aws_iam_policy.alb_controller.arn +} + +resource "kubernetes_service_account" "alb_controller" { + metadata { + name = "aws-load-balancer-controller" + namespace = "kube-system" + + annotations = { + "eks.amazonaws.com/role-arn" = aws_iam_role.alb_controller.arn + } + } +} + +resource "helm_release" "aws_load_balancer_controller" { + name = "aws-load-balancer-controller" + repository = "https://aws.github.io/eks-charts" + chart = "aws-load-balancer-controller" + namespace = "kube-system" + + create_namespace = false + + values = [ + yamlencode({ + clusterName = var.cluster_name + + region = "${var.tags.region}" + + vpcId = var.vpc_id + + serviceAccount = { + create = false + name = kubernetes_service_account.alb_controller.metadata[0].name + } + }) + ] + + depends_on = [ + kubernetes_service_account.alb_controller + ] + + wait = true + timeout = 600 +} + diff --git a/infra/modules/alb-ingress/outputs.tf b/infra/modules/alb-ingress/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/infra/modules/alb-ingress/variables.tf b/infra/modules/alb-ingress/variables.tf new file mode 100644 index 0000000..6d41759 --- /dev/null +++ b/infra/modules/alb-ingress/variables.tf @@ -0,0 +1,23 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "cluster_name" { + description = "EKS cluster name" + type = string +} + +variable "vpc_id" { + description = "VPC ID where the ALB Ingress Controller will be deployed" + type = string +} + +variable "oidc_issuer_url" { + type = string +} + +variable "oidc_provider_arn" { + type = string +} \ No newline at end of file diff --git a/infra/modules/alb-ingress/version.tf b/infra/modules/alb-ingress/version.tf new file mode 100644 index 0000000..f58e2d8 --- /dev/null +++ b/infra/modules/alb-ingress/version.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + + kubernetes = { + source = "hashicorp/kubernetes" + } + + helm = { + source = "hashicorp/helm" + } + } +} \ No newline at end of file diff --git a/infra/modules/alb/acm.tf b/infra/modules/alb/acm.tf new file mode 100644 index 0000000..0977394 --- /dev/null +++ b/infra/modules/alb/acm.tf @@ -0,0 +1,15 @@ +resource "aws_acm_certificate" "api" { + domain_name = var.domain_name + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate_validation" "api" { + certificate_arn = aws_acm_certificate.api.arn + validation_record_fqdns = [ + for record in aws_route53_record.api_validation : record.fqdn + ] +} \ No newline at end of file diff --git a/infra/modules/alb/outputs.tf b/infra/modules/alb/outputs.tf new file mode 100644 index 0000000..fea99a8 --- /dev/null +++ b/infra/modules/alb/outputs.tf @@ -0,0 +1,4 @@ +output "api_nameservers" { + description = "The Api nameservers" + value = aws_route53_zone.api.name_servers +} \ No newline at end of file diff --git a/infra/modules/alb/route53.tf b/infra/modules/alb/route53.tf new file mode 100644 index 0000000..c731bb0 --- /dev/null +++ b/infra/modules/alb/route53.tf @@ -0,0 +1,16 @@ +resource "aws_route53_zone" "api" { + name = var.domain_name +} + +resource "aws_route53_record" "api_validation" { + for_each = { + for dvo in aws_acm_certificate.api.domain_validation_options : + dvo.domain_name => dvo + } + + zone_id = aws_route53_zone.api.zone_id + type = each.value.resource_record_type + name = each.value.resource_record_name + records = [each.value.resource_record_value] + ttl = 60 +} \ No newline at end of file diff --git a/infra/modules/alb/variables.tf b/infra/modules/alb/variables.tf new file mode 100644 index 0000000..9b6e4e7 --- /dev/null +++ b/infra/modules/alb/variables.tf @@ -0,0 +1,4 @@ +variable "domain_name" { + description = "DNS name" + type = string +} \ No newline at end of file diff --git a/infra/modules/alertmanager/alertmanager.tf b/infra/modules/alertmanager/alertmanager.tf new file mode 100644 index 0000000..cddbb70 --- /dev/null +++ b/infra/modules/alertmanager/alertmanager.tf @@ -0,0 +1,12 @@ +resource "aws_secretsmanager_secret" "alertmanager" { + name = "${var.tags.environment}-alertmanager/notifications" + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "alertmanager" { + secret_id = aws_secretsmanager_secret.alertmanager.id + + secret_string = jsonencode({ + slack-webhook = var.slack_webhook + }) +} \ No newline at end of file diff --git a/infra/modules/alertmanager/variable.tf b/infra/modules/alertmanager/variable.tf new file mode 100644 index 0000000..4303da9 --- /dev/null +++ b/infra/modules/alertmanager/variable.tf @@ -0,0 +1,11 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "slack_webhook" { + description = "Slack webhook URL for Alertmanager notifications" + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/modules/argocd/argocd.tf b/infra/modules/argocd/argocd.tf new file mode 100644 index 0000000..7385a6b --- /dev/null +++ b/infra/modules/argocd/argocd.tf @@ -0,0 +1,385 @@ +resource "helm_release" "argocd" { + name = "${var.tags.project}-${var.tags.environment}-argocd" + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + namespace = "argocd" + create_namespace = true + + values = [ + file("${path.module}/values.yaml") + ] +} + +resource "helm_release" "external_secrets" { + name = "external-secrets" + repository = "https://charts.external-secrets.io" + chart = "external-secrets" + namespace = "external-secrets" + create_namespace = true + +} + +resource "helm_release" "external_dns" { + name = "external-dns" + repository = "https://kubernetes-sigs.github.io/external-dns/" + chart = "external-dns" + namespace = "external-dns" + create_namespace = true + + version = "1.18.0" + + values = [ + yamlencode({ + provider = "aws" + + policy = "upsert-only" + registry = "txt" + txtOwnerId = var.cluster_name + + serviceAccount = { + create = true + name = "external-dns" + annotations = { + "eks.amazonaws.com/role-arn" = aws_iam_role.external_dns.arn + } + } + + sources = [ + "service", + "ingress", + ] + }) + ] +} + +# External secrets IAM role and policy +data "aws_eks_cluster" "cluster" { + name = var.cluster_name +} + +data "tls_certificate" "eks" { + url = data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer +} + +data "aws_iam_openid_connect_provider" "eks" { + url = data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer +} + +resource "aws_iam_role" "external_secrets" { + + name = "external-secrets-${var.tags.environment}-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + + Statement = [ + { + Effect = "Allow" + + Principal = { + Federated = data.aws_iam_openid_connect_provider.eks.arn + } + + Action = "sts:AssumeRoleWithWebIdentity" + + Condition = { + + StringEquals = { + + "${replace( + data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, + "https://", + "" + )}:aud" = "sts.amazonaws.com" + + "${replace( + data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, + "https://", + "" + )}:sub" = "system:serviceaccount:external-secrets:external-secrets-sa" + } + } + } + ] + }) +} + +resource "aws_iam_role_policy" "external_secrets" { + + role = aws_iam_role.external_secrets.id + + policy = jsonencode({ + + Version = "2012-10-17" + + Statement = [ + { + Effect = "Allow" + + Action = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + + Resource = "*" + } + ] + }) +} + +resource "aws_iam_role" "external_dns" { + name = "external-dns-${var.tags.environment}-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + + Statement = [ + { + Effect = "Allow" + + Principal = { + Federated = data.aws_iam_openid_connect_provider.eks.arn + } + + Action = "sts:AssumeRoleWithWebIdentity" + + Condition = { + StringEquals = { + "${replace( + data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, + "https://", + "" + )}:aud" = "sts.amazonaws.com" + + "${replace( + data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, + "https://", + "" + )}:sub" = "system:serviceaccount:external-dns:external-dns" + } + } + } + ] + }) +} + +resource "aws_iam_role_policy" "external_dns" { + role = aws_iam_role.external_dns.id + + policy = jsonencode({ + Version = "2012-10-17" + + Statement = [ + { + Effect = "Allow" + + Action = [ + "route53:ChangeResourceRecordSets" + ] + + Resource = [ + "arn:aws:route53:::hostedzone/" + ] + }, + { + Effect = "Allow" + + Action = [ + "route53:ListHostedZones", + "route53:ListResourceRecordSets", + "route53:ListTagsForResource" + ] + + Resource = "*" + } + ] + }) +} + +resource "helm_release" "kube_prometheus_stack" { + name = "kube-prometheus-stack" + namespace = "monitoring" + create_namespace = true + + repository = "https://prometheus-community.github.io/helm-charts" + chart = "kube-prometheus-stack" + version = "88.0.1" +} + +resource "helm_release" "cert_manager" { + name = "cert-manager" + namespace = "cert-manager" + create_namespace = true + + repository = "https://charts.jetstack.io" + chart = "cert-manager" + version = "v1.18.2" + + set { + name = "crds.enabled" + value = "true" + } +} + +resource "helm_release" "kyverno" { + name = "kyverno" + namespace = "kyverno" + create_namespace = true + + repository = "https://kyverno.github.io/kyverno/" + chart = "kyverno" +} + +resource "helm_release" "velero" { + name = "velero" + namespace = "velero" + create_namespace = true + + repository = "https://vmware-tanzu.github.io/helm-charts" + chart = "velero" + version = "11.1.1" + + set { + name = "serviceAccount.server.name" + value = "velero" + } + + set { + name = "serviceAccount.server.annotations.eks\\.amazonaws\\.com/role-arn" + value = aws_iam_role.velero.arn + } + + set { + name = "serviceAccount.server.create" + value = "true" + } + + set { + name = "configuration.backupStorageLocation[0].name" + value = "default" + } + + set { + name = "configuration.backupStorageLocation[0].provider" + value = "aws" + } + + set { + name = "configuration.volumeSnapshotLocation[0].name" + value = "default" + } + + set { + name = "configuration.volumeSnapshotLocation[0].provider" + value = "aws" + } + + set { + name = "configuration.backupStorageLocation[0].config.region" + value = var.tags.region + } + + set { + name = "configuration.backupStorageLocation[0].bucket" + value = aws_s3_bucket.velero.id + } + + set { + name = "configuration.volumeSnapshotLocation[0].config.region" + value = var.tags.region + } + + + depends_on = [ + aws_iam_role_policy_attachment.velero, + helm_release.snapshot_controller + ] + +} + +resource "aws_iam_policy" "velero" { + name = "${var.tags.project}-${var.tags.environment}-velero-backup-policy" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:*", + "ec2:DescribeVolumes", + "ec2:DescribeSnapshots", + "ec2:CreateSnapshot", + "ec2:DeleteSnapshot" + ] + Resource = "*" + } + ] + }) +} + +data "aws_iam_policy_document" "velero_assume_role" { + + statement { + effect = "Allow" + + principals { + type = "Federated" + identifiers = [ + var.oidc_provider_arn + ] + } + + actions = [ + "sts:AssumeRoleWithWebIdentity" + ] + + condition { + test = "StringEquals" + + variable = "${replace(var.oidc_issuer_url, "https://", "")}:sub" + + values = [ + "system:serviceaccount:velero:velero" + ] + } + } +} + +data "aws_caller_identity" "current" {} + +resource "aws_s3_bucket" "velero" { + bucket = "${var.tags.project}-${var.tags.environment}-${data.aws_caller_identity.current.account_id}-velero-backups" +} + +resource "aws_iam_role" "velero" { + name = "${var.tags.project}-${var.tags.environment}-velero" + + assume_role_policy = data.aws_iam_policy_document.velero_assume_role.json +} + +resource "aws_iam_role_policy_attachment" "velero" { + + role = aws_iam_role.velero.name + + policy_arn = aws_iam_policy.velero.arn +} + +resource "helm_release" "metrics_server" { + name = "metrics-server" + namespace = "kube-system" + + repository = "https://kubernetes-sigs.github.io/metrics-server" + chart = "metrics-server" +} + +resource "helm_release" "snapshot_controller" { + name = "${var.tags.project}-${var.tags.environment}-snapshot-controller" + namespace = "kube-system" + create_namespace = false + + repository = "https://piraeus.io/helm-charts/" + chart = "snapshot-controller" + + version = "5.1.1" +} \ No newline at end of file diff --git a/infra/modules/argocd/outputs.tf b/infra/modules/argocd/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/infra/modules/argocd/values.yaml b/infra/modules/argocd/values.yaml new file mode 100644 index 0000000..212d9d2 --- /dev/null +++ b/infra/modules/argocd/values.yaml @@ -0,0 +1,7 @@ +server: + service: + type: ClusterIP + +configs: + params: + server.insecure: true \ No newline at end of file diff --git a/infra/modules/argocd/variables.tf b/infra/modules/argocd/variables.tf new file mode 100644 index 0000000..d47f9de --- /dev/null +++ b/infra/modules/argocd/variables.tf @@ -0,0 +1,18 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "cluster_name" { + description = "EKS cluster name" + type = string +} + +variable "oidc_provider_arn" { + type = string +} + +variable "oidc_issuer_url" { + type = string +} \ No newline at end of file diff --git a/infra/modules/argocd/version.tf b/infra/modules/argocd/version.tf new file mode 100644 index 0000000..f58e2d8 --- /dev/null +++ b/infra/modules/argocd/version.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + + kubernetes = { + source = "hashicorp/kubernetes" + } + + helm = { + source = "hashicorp/helm" + } + } +} \ No newline at end of file diff --git a/infra/modules/cloudwatch/cloudwatch.tf b/infra/modules/cloudwatch/cloudwatch.tf new file mode 100644 index 0000000..edbc66d --- /dev/null +++ b/infra/modules/cloudwatch/cloudwatch.tf @@ -0,0 +1,56 @@ +data "aws_caller_identity" "current" {} + +# VPC Flow Logs CloudWatch log group +resource "aws_cloudwatch_log_group" "eks_vpc_flow_logs" { + name = "/aws/vpc/flowlogs/${var.tags.project}-${var.tags.environment}" + retention_in_days = 365 + kms_key_id = aws_kms_key.cloudwatch_logs.arn + tags = { + Name = "/aws/vpc/flowlogs/${var.tags.project}-${var.tags.environment}" + } +} + +resource "aws_kms_key" "cloudwatch_logs" { + description = "KMS key for CloudWatch Log Groups" + enable_key_rotation = true + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "EnableRootPermissions" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + + { + Sid = "AllowCloudWatchLogsUseOfKey" + Effect = "Allow" + Principal = { + Service = "logs.amazonaws.com" + } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + } + ] + }) + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-kms-cloudwatch-logs" + } +} + +resource "aws_kms_alias" "cloudwatch_logs" { + name = "alias/cloudwatch-logs-${var.tags.environment}" + target_key_id = aws_kms_key.cloudwatch_logs.key_id +} \ No newline at end of file diff --git a/infra/modules/cloudwatch/outputs.tf b/infra/modules/cloudwatch/outputs.tf new file mode 100644 index 0000000..50981c6 --- /dev/null +++ b/infra/modules/cloudwatch/outputs.tf @@ -0,0 +1,10 @@ + +output "vpc_flow_logs_cloudwatch_loggroup_arn" { + description = "CloudWatch log group for VPC flow logs" + value = aws_cloudwatch_log_group.eks_vpc_flow_logs.arn +} + +output "kms_key_arn" { + description = "ARN of the KMS key for CloudWatch Logs" + value = aws_kms_key.cloudwatch_logs.arn +} \ No newline at end of file diff --git a/infra/modules/cloudwatch/variables.tf b/infra/modules/cloudwatch/variables.tf new file mode 100644 index 0000000..39c748b --- /dev/null +++ b/infra/modules/cloudwatch/variables.tf @@ -0,0 +1,5 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} \ No newline at end of file diff --git a/infra/modules/ecr/ecr.tf b/infra/modules/ecr/ecr.tf new file mode 100644 index 0000000..130333d --- /dev/null +++ b/infra/modules/ecr/ecr.tf @@ -0,0 +1,50 @@ +locals { + services = [ + "user-service", + "order-service", + "payment-service", + "product-service" + ] + repository_prefix = var.app_name +} + +resource "aws_ecr_repository" "eks_ecr_repository" { + for_each = toset(local.services) + name = "${local.repository_prefix}/${each.value}" + image_tag_mutability = "IMMUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + encryption_configuration { + encryption_type = "KMS" + } + + force_delete = false + tags = merge(var.tags, { + Name = "${var.app_name}-shared-ecr" + }) +} + +resource "aws_ecr_lifecycle_policy" "eks_ecr_lifecycle_policy" { + for_each = aws_ecr_repository.eks_ecr_repository + + repository = each.value.name + + policy = jsonencode({ + rules = [{ + rulePriority = 1 + + selection = { + tagStatus = "any" + countType = "imageCountMoreThan" + countNumber = 50 + } + + action = { + type = "expire" + } + }] + }) +} \ No newline at end of file diff --git a/infra/modules/ecr/outputs.tf b/infra/modules/ecr/outputs.tf new file mode 100644 index 0000000..10d2f8c --- /dev/null +++ b/infra/modules/ecr/outputs.tf @@ -0,0 +1,18 @@ +output "ecr_repositories" { + value = { + for name, repo in aws_ecr_repository.eks_ecr_repository : + name => { + name = repo.name + url = repo.repository_url + arn = repo.arn + } + } +} + +output "repository_urls" { + description = "Shared ECR repository URLs keyed by service" + value = { + for name, repo in aws_ecr_repository.eks_ecr_repository : + name => repo.repository_url + } +} \ No newline at end of file diff --git a/infra/modules/ecr/variable.tf b/infra/modules/ecr/variable.tf new file mode 100644 index 0000000..7796e81 --- /dev/null +++ b/infra/modules/ecr/variable.tf @@ -0,0 +1,11 @@ +variable "app_name" { + description = "The name of the microservices application" + type = string + default = "ja-mics-ap" +} + +variable "tags" { + description = "Tags applied to the shared ECR repositories" + type = map(string) + default = {} +} \ No newline at end of file diff --git a/infra/modules/eks/eks_cluster.tf b/infra/modules/eks/eks_cluster.tf new file mode 100644 index 0000000..fac6a3a --- /dev/null +++ b/infra/modules/eks/eks_cluster.tf @@ -0,0 +1,122 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "eks_cluster_role" { + name = "${var.tags.project}-${var.tags.environment}-cluster-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "sts:AssumeRole", + "sts:TagSession" + ] + Effect = "Allow" + Principal = { + Service = "eks.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_iam_role_policy_attachment" "eks_cluster_policy_attachment" { + role = aws_iam_role.eks_cluster_role.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" +} + +resource "aws_iam_role" "eks_admin" { + name = "${var.tags.project}-${var.tags.environment}-admin-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "sts:AssumeRole" + }] + }) +} + +locals { + github_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/github-Kubernetes-${var.tags.environment}-role" + + eks_admin_role_arn = aws_iam_role.eks_admin.arn + + emmy = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/emmy" # for testing purposes only + + eks_admin_principals = { + eks_admin = local.eks_admin_role_arn + github = local.github_role_arn + emmy = local.emmy + } +} + +resource "aws_eks_access_entry" "eks_access_entry" { + for_each = local.eks_admin_principals + cluster_name = aws_eks_cluster.eks_cluster.name + principal_arn = each.value + type = "STANDARD" +} + +resource "aws_eks_access_policy_association" "eks_access_policy_association" { + for_each = local.eks_admin_principals + cluster_name = aws_eks_cluster.eks_cluster.name + principal_arn = each.value + + policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + + access_scope { + type = "cluster" + } +} + +resource "aws_eks_cluster" "eks_cluster" { + name = "${var.tags.project}-${var.tags.environment}-cluster" + role_arn = aws_iam_role.eks_cluster_role.arn + version = "1.33" + + access_config { + authentication_mode = "API" + } + + vpc_config { + endpoint_private_access = true + endpoint_public_access = true + subnet_ids = var.private_subnet_ids + } + + encryption_config { + resources = ["secrets"] + provider { + key_arn = var.kms_key_arn + } + } + enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-cluster" + } + + depends_on = [ + aws_iam_role_policy_attachment.eks_cluster_policy_attachment + ] +} + + +data "tls_certificate" "eks" { + url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer +} + +resource "aws_iam_openid_connect_provider" "eks" { + url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer + + client_id_list = [ + "sts.amazonaws.com" + ] + + thumbprint_list = [ + data.tls_certificate.eks.certificates[0].sha1_fingerprint + ] +} \ No newline at end of file diff --git a/infra/modules/eks/node_group.tf b/infra/modules/eks/node_group.tf new file mode 100644 index 0000000..d98c9f8 --- /dev/null +++ b/infra/modules/eks/node_group.tf @@ -0,0 +1,121 @@ +resource "aws_eks_node_group" "eks_managed_node_group" { + cluster_name = aws_eks_cluster.eks_cluster.name + node_group_name = "${var.tags.project}-${var.tags.environment}-node-group" + node_role_arn = aws_iam_role.eks_nodegroup_role.arn + subnet_ids = var.private_subnet_ids + + instance_types = ["t3.large"] + + ami_type = "AL2023_x86_64_STANDARD" + + scaling_config { + desired_size = 2 + min_size = 1 + max_size = 5 + } + + update_config { + max_unavailable_percentage = 25 + } + + labels = { + workload = "eks-microservices-application" + } + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-node-group" + Environment = var.tags.environment + Project = var.tags.project + } + + depends_on = [ + aws_iam_role_policy_attachment.eks-AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.eks-AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.eks-AmazonEC2ContainerRegistryReadOnly, + ] +} + +resource "aws_eks_addon" "pod_identity" { + cluster_name = aws_eks_cluster.eks_cluster.name + addon_name = "eks-pod-identity-agent" +} + +resource "aws_iam_role" "eks_nodegroup_role" { + name = "${var.tags.project}-${var.tags.environment}-nodegroup-role" + + assume_role_policy = jsonencode({ + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + }] + Version = "2012-10-17" + }) +} + +resource "aws_iam_role_policy_attachment" "eks-AmazonEKSWorkerNodePolicy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" + role = aws_iam_role.eks_nodegroup_role.name +} + +resource "aws_iam_role_policy_attachment" "eks-AmazonEKS_CNI_Policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + role = aws_iam_role.eks_nodegroup_role.name +} + +resource "aws_iam_role_policy_attachment" "eks-AmazonEC2ContainerRegistryReadOnly" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + role = aws_iam_role.eks_nodegroup_role.name +} + + +# Worker Node Security Group Used by the EC2 instances running Kubernetes workloads +#resource "aws_security_group" "eks_worker_nodes" { +# name = "${var.tags.project}-${var.tags.environment}-worker-sg" +# description = "EKS Worker Node Security Group Used by the EC2 instances running Kubernetes workloads" +# vpc_id = var.eks_vpc_id + +# tags = { +# Name = "${var.tags.project}-${var.tags.environment}-worker-sg" +# } +#} + +# Nodes → Cluster API Server +#resource "aws_vpc_security_group_egress_rule" "eks_worker_to_cluster_https" { +# description = "Allow worker nodes to communicate with the EKS control plane on HTTPS (port 443)" +# security_group_id = aws_security_group.eks_worker_nodes.id +# referenced_security_group_id = aws_security_group.eks_cluster.id + +# ip_protocol = "tcp" +# from_port = 443 +# to_port = 443 +#} + +# Allow control plane → worker node on kubelet port +#resource "aws_vpc_security_group_ingress_rule" "eks_cluster_to_worker_kubelet" { +# description = "Allow EKS control plane to communicate with worker nodes on kubelet port" +# security_group_id = aws_security_group.eks_worker_nodes.id +# referenced_security_group_id = aws_security_group.eks_cluster.id +# ip_protocol = "tcp" +# from_port = 1025 +# to_port = 65535 +#} + +# Node-to-Node Communication +#resource "aws_vpc_security_group_ingress_rule" "eks_worker_self" { +# description = "Allow worker nodes to communicate with each other" +# security_group_id = aws_security_group.eks_worker_nodes.id +# referenced_security_group_id = aws_security_group.eks_worker_nodes.id +# ip_protocol = "-1" +#} + +# worker nodes also need outbound internet/AWS access for image pull from ECR, AWS API, Update downloads +#resource "aws_vpc_security_group_egress_rule" "eks_worker_all_out" { +# description = "Allow all outbound traffic from EKS worker nodes" +# security_group_id = aws_security_group.eks_worker_nodes.id + +# ip_protocol = "-1" +# cidr_ipv4 = "0.0.0.0/0" +#} diff --git a/infra/modules/eks/outputs.tf b/infra/modules/eks/outputs.tf new file mode 100644 index 0000000..05d92ca --- /dev/null +++ b/infra/modules/eks/outputs.tf @@ -0,0 +1,53 @@ +output "cluster_name" { + value = aws_eks_cluster.eks_cluster.name + description = "EKS Cluster Name" +} + +output "cluster_security_group_id" { + description = "EKS managed cluster security group" + value = aws_eks_cluster.eks_cluster.vpc_config[0].cluster_security_group_id +} + +output "eks_cluster_endpoint" { + value = aws_eks_cluster.eks_cluster.endpoint + description = "EKS Cluster API endpoint" +} + +output "eks_cluster_certificate" { + value = aws_eks_cluster.eks_cluster.certificate_authority[0].data + description = "Base64 encoded certificate data required to communicate with the cluster" +} + +output "oidc_issuer_url" { + value = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer + description = "EKS Cluster OIDC Issuer URL" +} + +output "eks_nodes_role_id" { + value = aws_iam_role.eks_nodegroup_role.id + description = "IAM Role ID for EKS worker nodes" +} + +#output "eks_worker_nodes_sg_id" { +# value = aws_security_group.eks_worker_nodes.id +# description = "Security group ID for EKS worker nodes" +#} + +output "eks_nodegroup_role_arn" { + value = aws_iam_role.eks_nodegroup_role.arn + description = "ARN of the EKS node group IAM role" +} + +output "eks_cluster_name" { + value = aws_eks_cluster.eks_cluster.name + description = "EKS Cluster Name" +} + +output "eks_cluster_arn" { + value = aws_eks_cluster.eks_cluster.arn + description = "EKS Cluster ARN" +} + +output "oidc_provider_arn" { + value = aws_iam_openid_connect_provider.eks.arn +} \ No newline at end of file diff --git a/infra/modules/eks/variables.tf b/infra/modules/eks/variables.tf new file mode 100644 index 0000000..798a786 --- /dev/null +++ b/infra/modules/eks/variables.tf @@ -0,0 +1,20 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "private_subnet_ids" { + description = "List of private subnet IDs for the EKS cluster" + type = list(string) +} + +variable "kms_key_arn" { + description = "ARN of the KMS key for encrypting EKS secrets" + type = string +} + +variable "eks_vpc_id" { + description = "VPC ID for the EKS cluster" + type = string +} \ No newline at end of file diff --git a/infra/modules/grafana/grafana.tf b/infra/modules/grafana/grafana.tf new file mode 100644 index 0000000..9b38f52 --- /dev/null +++ b/infra/modules/grafana/grafana.tf @@ -0,0 +1,15 @@ + +resource "aws_secretsmanager_secret" "grafana_admin" { + name = "${var.tags.environment}-grafana/admin" + + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "grafana_admin" { + secret_id = aws_secretsmanager_secret.grafana_admin.id + + secret_string = jsonencode({ + username = "admin" + password = var.grafana_admin_password + }) +} \ No newline at end of file diff --git a/infra/modules/grafana/variable.tf b/infra/modules/grafana/variable.tf new file mode 100644 index 0000000..0ec42b6 --- /dev/null +++ b/infra/modules/grafana/variable.tf @@ -0,0 +1,10 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "grafana_admin_password" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/modules/network/eip.tf b/infra/modules/network/eip.tf new file mode 100644 index 0000000..2e8c4f9 --- /dev/null +++ b/infra/modules/network/eip.tf @@ -0,0 +1,8 @@ +resource "aws_eip" "eks_eip" { + for_each = local.public_subnets + domain = "vpc" + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-eip-${each.key}" + } +} \ No newline at end of file diff --git a/infra/modules/network/igw.tf b/infra/modules/network/igw.tf new file mode 100644 index 0000000..556129d --- /dev/null +++ b/infra/modules/network/igw.tf @@ -0,0 +1,7 @@ +resource "aws_internet_gateway" "eks_igw" { + vpc_id = aws_vpc.eks_vpc.id + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-igw" + } +} \ No newline at end of file diff --git a/infra/modules/network/nat-gateway.tf b/infra/modules/network/nat-gateway.tf new file mode 100644 index 0000000..ff8a217 --- /dev/null +++ b/infra/modules/network/nat-gateway.tf @@ -0,0 +1,12 @@ +resource "aws_nat_gateway" "eks_nat" { + for_each = local.public_subnets + + allocation_id = aws_eip.eks_eip[each.key].id + subnet_id = aws_subnet.eks_public_subnets[each.key].id + + depends_on = [aws_internet_gateway.eks_igw] + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-nat-${each.key}" + } +} \ No newline at end of file diff --git a/infra/modules/network/outputs.tf b/infra/modules/network/outputs.tf new file mode 100644 index 0000000..2145661 --- /dev/null +++ b/infra/modules/network/outputs.tf @@ -0,0 +1,22 @@ +output "vpc_id" { + description = "The ID of the VPC" + value = aws_vpc.eks_vpc.id +} + +output "public_subnet_ids" { + description = "List of public subnet IDs" + value = [for subnet in values(aws_subnet.eks_public_subnets) : subnet.id] +} + +output "private_subnet_ids" { + description = "List of private subnet IDs" + value = [for subnet in values(aws_subnet.eks_private_subnets) : subnet.id] +} + +# output "oidc_provider_arn" { +# value = aws_iam_openid_connect_provider.eks_oidc_provider.arn +# } + +# output "oidc_provider_url" { +# value = aws_iam_openid_connect_provider.eks_oidc_provider.url +# } \ No newline at end of file diff --git a/infra/modules/network/routes.tf b/infra/modules/network/routes.tf new file mode 100644 index 0000000..f7a015b --- /dev/null +++ b/infra/modules/network/routes.tf @@ -0,0 +1,40 @@ +resource "aws_route_table" "eks_public_rt" { + vpc_id = aws_vpc.eks_vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.eks_igw.id + } +} + +resource "aws_route_table_association" "eks_public_rt_assoc" { + for_each = local.public_subnets + + subnet_id = aws_subnet.eks_public_subnets[each.key].id + route_table_id = aws_route_table.eks_public_rt.id +} + +resource "aws_route_table" "eks_private_rt" { + for_each = local.private_subnets + vpc_id = aws_vpc.eks_vpc.id + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-private-rt-${each.key}" + } +} + +# Default Routes to NAT Gateways +resource "aws_route" "eks_private_nat" { + for_each = local.private_subnets + + route_table_id = aws_route_table.eks_private_rt[each.key].id + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.eks_nat[each.key].id +} + +resource "aws_route_table_association" "eks_private_rt_assoc" { + for_each = local.private_subnets + + subnet_id = aws_subnet.eks_private_subnets[each.key].id + route_table_id = aws_route_table.eks_private_rt[each.key].id +} \ No newline at end of file diff --git a/infra/modules/network/subnets.tf b/infra/modules/network/subnets.tf new file mode 100644 index 0000000..92dd57c --- /dev/null +++ b/infra/modules/network/subnets.tf @@ -0,0 +1,36 @@ +locals { + public_subnets = { + for i, az in var.azs : az => var.public_subnet_cidrs[i] + } + + private_subnets = { + for i, az in var.azs : az => var.private_subnet_cidrs[i] + } +} + +resource "aws_subnet" "eks_public_subnets" { + for_each = local.public_subnets + vpc_id = aws_vpc.eks_vpc.id + cidr_block = each.value + availability_zone = each.key + map_public_ip_on_launch = false + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-public-subnet-${each.key}" + } +} + + +resource "aws_subnet" "eks_private_subnets" { + for_each = local.private_subnets + vpc_id = aws_vpc.eks_vpc.id + cidr_block = each.value + availability_zone = each.key + map_public_ip_on_launch = false + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-private-subnet-${each.key}" + "kubernetes.io/cluster/${var.tags.project}-${var.tags.environment}-eks-cluster" = "shared" + + } +} \ No newline at end of file diff --git a/infra/modules/network/variables.tf b/infra/modules/network/variables.tf new file mode 100644 index 0000000..94834e6 --- /dev/null +++ b/infra/modules/network/variables.tf @@ -0,0 +1,52 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "vpc_cidr" { + description = "CIDR block for the VPC" + type = string +} + +variable "azs" { + description = "List of availability zones for subnets" + type = list(string) +} + +# Public Subnet +variable "public_subnet_cidrs" { + description = "List of CIDR blocks for public subnets" + type = list(string) +} + +# Private Subnet +variable "private_subnet_cidrs" { + description = "List of CIDR blocks for private subnets" + type = list(string) +} + +variable "cloudwatch_log_group_arn" { + description = "ARN of the CloudWatch log group for VPC flow logs" + type = string +} + +variable "vpc_flow_logs_iam_role_arn" { + description = "IAM Role ARN for VPC flow logs" + type = string +} + +variable "cluster_name" { + description = "EKS Cluster Name for tagging and resource naming" + type = string +} + +variable "sg_eks_nodes_id" { + description = "Security Group ID for EKS worker nodes, used for VPC endpoint security group rules" + type = string +} + +variable "oidc_issuer_url" { + description = "EKS Cluster OIDC Issuer URL for creating OIDC provider" + type = string +} \ No newline at end of file diff --git a/infra/modules/network/vpc-cni.tf b/infra/modules/network/vpc-cni.tf new file mode 100644 index 0000000..3d1f4ec --- /dev/null +++ b/infra/modules/network/vpc-cni.tf @@ -0,0 +1,25 @@ +resource "aws_eks_addon" "vpc_cni" { + cluster_name = var.cluster_name + addon_name = "vpc-cni" +} + +resource "aws_eks_addon" "coredns" { + cluster_name = var.cluster_name + addon_name = "coredns" + + configuration_values = jsonencode({ + replicaCount = 2 + + resources = { + requests = { + cpu = "100m" + memory = "70Mi" + } + + limits = { + cpu = "250m" + memory = "170Mi" + } + } + }) +} \ No newline at end of file diff --git a/infra/modules/network/vpc-flow-logs.tf b/infra/modules/network/vpc-flow-logs.tf new file mode 100644 index 0000000..f1f20e4 --- /dev/null +++ b/infra/modules/network/vpc-flow-logs.tf @@ -0,0 +1,14 @@ +resource "aws_flow_log" "vpc_flow_logs" { + log_destination = var.cloudwatch_log_group_arn + log_destination_type = "cloud-watch-logs" + traffic_type = "ALL" + + vpc_id = aws_vpc.eks_vpc.id + iam_role_arn = var.vpc_flow_logs_iam_role_arn + + max_aggregation_interval = 60 + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-vpc-flowlogs" + } +} \ No newline at end of file diff --git a/infra/modules/network/vpc.tf b/infra/modules/network/vpc.tf new file mode 100644 index 0000000..4dc1874 --- /dev/null +++ b/infra/modules/network/vpc.tf @@ -0,0 +1,10 @@ +# VPC +resource "aws_vpc" "eks_vpc" { + cidr_block = var.vpc_cidr + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-vpc" + } +} \ No newline at end of file diff --git a/infra/modules/rds/outputs.tf b/infra/modules/rds/outputs.tf new file mode 100644 index 0000000..1fa79b0 --- /dev/null +++ b/infra/modules/rds/outputs.tf @@ -0,0 +1,26 @@ +output "endpoint" { + value = aws_db_instance.postgres.address +} + +output "port" { + value = aws_db_instance.postgres.port +} + +output "master_secret_arn" { + value = aws_secretsmanager_secret.postgres_master.arn +} + +output "service_secret_arns" { + value = { + for k, v in aws_secretsmanager_secret.service : + k => v.arn + } +} + +output "security_group_id" { + value = aws_security_group.postgres.id +} + +output "db_identifier" { + value = aws_db_instance.postgres.identifier +} \ No newline at end of file diff --git a/infra/modules/rds/rds.tf b/infra/modules/rds/rds.tf new file mode 100644 index 0000000..b04715b --- /dev/null +++ b/infra/modules/rds/rds.tf @@ -0,0 +1,120 @@ +# create PostgreSQL users + +locals { + services = { + order = { db = "orderdb" } + user = { db = "userdb" } + payment = { db = "paymentdb" } + product = { db = "productdb" } + } +} + +resource "random_password" "service" { + for_each = local.services + + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "random_password" "master" { + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +# Store credentials +resource "aws_secretsmanager_secret" "service" { + for_each = local.services + + name = "/${var.tags.environment}/${each.key}/db" + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret" "postgres_master" { + name = "/${var.tags.environment}/postgres/master" + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "postgres_master" { + + secret_id = aws_secretsmanager_secret.postgres_master.id + + secret_string = jsonencode({ + host = aws_db_instance.postgres.address + port = 5432 + username = var.master_username + password = random_password.master.result + }) +} + +resource "aws_secretsmanager_secret_version" "service" { + for_each = local.services + + secret_id = aws_secretsmanager_secret.service[each.key].id + + secret_string = jsonencode({ + database = each.value.db + host = aws_db_instance.postgres.address + port = 5432 + username = "${each.key}_user" + password = random_password.service[each.key].result + }) +} + +resource "aws_security_group" "postgres" { + name = "${var.tags.environment}-postgres" + vpc_id = var.vpc_id +} + +resource "aws_vpc_security_group_ingress_rule" "postgres_from_eks" { + + security_group_id = aws_security_group.postgres.id + + referenced_security_group_id = var.eks_node_security_group_id + + from_port = 5432 + to_port = 5432 + ip_protocol = "tcp" +} + +resource "aws_vpc_security_group_egress_rule" "postgres_all" { + + security_group_id = aws_security_group.postgres.id + + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" +} + +resource "aws_db_subnet_group" "postgres" { + + name = "${var.tags.environment}-postgres" + + subnet_ids = var.private_subnet_ids +} + +resource "aws_db_instance" "postgres" { + + identifier = "${var.tags.environment}-postgres" + + engine = "postgres" + engine_version = "16.4" + + instance_class = "db.r6g.large" + + allocated_storage = 20 + max_allocated_storage = 50 + storage_type = "gp3" + + username = var.master_username + password = random_password.master.result + + publicly_accessible = false + multi_az = true + deletion_protection = false + + db_subnet_group_name = aws_db_subnet_group.postgres.name + vpc_security_group_ids = [aws_security_group.postgres.id] + + skip_final_snapshot = true +} \ No newline at end of file diff --git a/infra/modules/rds/variables.tf b/infra/modules/rds/variables.tf new file mode 100644 index 0000000..a3e964a --- /dev/null +++ b/infra/modules/rds/variables.tf @@ -0,0 +1,36 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "private_subnet_ids" { + description = "The private subnet ID" + type = list(string) +} + +variable "vpc_id" { + description = "VPC ID" + type = string +} + +variable "eks_node_security_group_id" { + description = "EKS node security group ID" + type = string +} + +variable "eks_oidc_provider_arn" { + description = "EKS OIDC provider arn" + type = string +} + +variable "eks_oidc_provider_url" { + description = "EKS OIDC Provider URL" + type = string +} + +variable "master_username" { + description = "Master username for the RDS instance" + type = string + default = "masteradmin" +} \ No newline at end of file diff --git a/infra/modules/redis/outputs.tf b/infra/modules/redis/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/infra/modules/redis/redis.tf b/infra/modules/redis/redis.tf new file mode 100644 index 0000000..946b78d --- /dev/null +++ b/infra/modules/redis/redis.tf @@ -0,0 +1,54 @@ +# Redis security group +resource "aws_security_group" "redis" { + name = "${var.tags.environment}-redis" + vpc_id = var.vpc_id +} + +resource "aws_vpc_security_group_ingress_rule" "redis_from_eks" { + security_group_id = aws_security_group.redis.id + referenced_security_group_id = var.eks_node_security_group_id + + from_port = 6379 + to_port = 6379 + ip_protocol = "tcp" +} + +resource "aws_vpc_security_group_egress_rule" "redis_all" { + security_group_id = aws_security_group.redis.id + + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" +} + +# Elasticache subnet group +resource "aws_elasticache_subnet_group" "redis" { + name = "${var.tags.environment}-redis" + subnet_ids = var.private_subnet_ids +} + +# Redis cluster +resource "aws_elasticache_cluster" "redis" { + cluster_id = "${var.tags.environment}-redis" + engine = "redis" + node_type = "cache.t4g.micro" + num_cache_nodes = 1 + parameter_group_name = "default.redis7" + + subnet_group_name = aws_elasticache_subnet_group.redis.name + security_group_ids = [aws_security_group.redis.id] +} + +# Store Redis connection details in AWS Secrets Manager +resource "aws_secretsmanager_secret" "redis" { + name = "/${var.tags.environment}/redis/cache" + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "redis" { + secret_id = aws_secretsmanager_secret.redis.id + + secret_string = jsonencode({ + host = aws_elasticache_cluster.redis.cache_nodes[0].address + port = 6379 + }) +} \ No newline at end of file diff --git a/infra/modules/redis/variables.tf b/infra/modules/redis/variables.tf new file mode 100644 index 0000000..c62ef97 --- /dev/null +++ b/infra/modules/redis/variables.tf @@ -0,0 +1,20 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "private_subnet_ids" { + description = "The private subnet ID" + type = list(string) +} + +variable "vpc_id" { + description = "VPC ID" + type = string +} + +variable "eks_node_security_group_id" { + description = "EKS node security group ID" + type = string +} \ No newline at end of file diff --git a/infra/modules/security_group/outputs.tf b/infra/modules/security_group/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/infra/modules/security_group/sg.tf b/infra/modules/security_group/sg.tf new file mode 100644 index 0000000..6f916f2 --- /dev/null +++ b/infra/modules/security_group/sg.tf @@ -0,0 +1,14 @@ +# Restrict default security group to deny all traffic +resource "aws_default_security_group" "default" { + vpc_id = var.eks_vpc_id + + # Completely restrict all inbound traffic + ingress = [] + + # Completely restrict all outbound traffic + egress = [] + + tags = { + Name = "${var.tags.project}-${var.tags.environment}-default-sg" + } +} \ No newline at end of file diff --git a/infra/modules/security_group/variables.tf b/infra/modules/security_group/variables.tf new file mode 100644 index 0000000..e4aeb32 --- /dev/null +++ b/infra/modules/security_group/variables.tf @@ -0,0 +1,10 @@ +variable "tags" { + description = "Global resources" + type = map(string) + default = {} +} + +variable "eks_vpc_id" { + description = "VPC ID for the EKS cluster" + type = string +} \ No newline at end of file diff --git a/infra/modules/terraform-tags/locals.tf b/infra/modules/terraform-tags/locals.tf new file mode 100644 index 0000000..be9cc58 --- /dev/null +++ b/infra/modules/terraform-tags/locals.tf @@ -0,0 +1,13 @@ +locals { + env_name = var.env_name + project_name = var.project_name + region_name = var.region_name +} + +locals { + common_tags = { + environment = local.env_name + project = local.project_name + region = local.region_name + } +} \ No newline at end of file diff --git a/infra/modules/terraform-tags/outputs.tf b/infra/modules/terraform-tags/outputs.tf new file mode 100644 index 0000000..800a24f --- /dev/null +++ b/infra/modules/terraform-tags/outputs.tf @@ -0,0 +1,3 @@ +output "common_tags" { + value = local.common_tags +} \ No newline at end of file diff --git a/infra/modules/terraform-tags/variables.tf b/infra/modules/terraform-tags/variables.tf new file mode 100644 index 0000000..d183ebf --- /dev/null +++ b/infra/modules/terraform-tags/variables.tf @@ -0,0 +1,14 @@ +variable "env_name" { + description = "Defines the environment to provision the resurces" + type = string +} + +variable "project_name" { + description = "Defines the project name" + type = string +} + +variable "region_name" { + type = string + description = "Defines the region where the resources are created" +} \ No newline at end of file diff --git a/pub_priv_eks_pg.png b/pub_priv_eks_pg.png new file mode 100644 index 0000000..cce029b Binary files /dev/null and b/pub_priv_eks_pg.png differ