diff --git a/.github/workflows/apply-configmap-and-restart.yaml b/.github/workflows/apply-configmap-and-restart.yaml new file mode 100644 index 00000000..df4445d0 --- /dev/null +++ b/.github/workflows/apply-configmap-and-restart.yaml @@ -0,0 +1,57 @@ +# syntax=docker/dockerfile:1.4 +name: Apply configmap and restart + +on: + workflow_dispatch: + inputs: + namespace: + description: "Kubernetes namespace" + default: "credreg-staging" + kube_cluster: + description: "EKS cluster name" + default: "ce-registry-eks" + +permissions: + id-token: write + contents: read + +env: + AWS_REGION: us-east-1 + +jobs: + apply-and-restart: + if: ${{ github.repository_owner == 'CredentialEngine' }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT }}:role/github-oidc-widget + aws-region: ${{ env.AWS_REGION }} + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + with: + version: v1.29.6 + + - name: Update kubeconfig + run: | + CLUSTER="${{ inputs.kube_cluster }}" + aws eks update-kubeconfig --name "$CLUSTER" --region "${{ env.AWS_REGION }}" + + - name: Apply ConfigMap + working-directory: terraform/environments/eks + run: | + NS="${{ inputs.namespace }}" + kubectl -n "$NS" apply -f k8s-manifests-staging/app-configmap.yaml + + - name: Restart Deployments + run: | + NS="${{ inputs.namespace }}" + kubectl -n "$NS" rollout restart deploy/worker-app + kubectl -n "$NS" rollout restart deploy/main-app + kubectl -n "$NS" rollout status deploy/worker-app --timeout=5m + kubectl -n "$NS" rollout status deploy/main-app --timeout=5m diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..92333794 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,74 @@ +# syntax=docker/dockerfile:1.4 +name: Build and push + +on: + push: + branches: ["eks-infrastructure","staging","main","production"] + + workflow_dispatch: + inputs: + environment: + description: 'Build & Push' + +permissions: + id-token: write + contents: read + +env: + AWS_REGION: us-east-1 + ECR_REPOSITORY: registry + EKS_CLUSTER: ce-registry-eks + +concurrency: + group: eks-cluster-image-build + cancel-in-progress: true + +jobs: + build-and-push: + if: ${{ github.repository_owner == 'CredentialEngine' }} + runs-on: ubuntu-latest + outputs: + image: ${{ steps.img.outputs.image }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT }}:role/github-oidc-widget + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Compute image tag (date.build) + id: tag + run: | + DATE_TAG=$(date -u +%Y.%m.%d) + BUILD_NUM=$(printf "%04d" $(( GITHUB_RUN_NUMBER % 10000 )) ) + TAG="$DATE_TAG.$BUILD_NUM" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Build Docker image (multi-stage) + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ steps.tag.outputs.tag }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:staging + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Export image URI + id: img + run: | + echo "image=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ steps.tag.outputs.tag }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 79fda324..592d9c05 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,15 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: ruby/setup-ruby@v1.256.0 + + - name: Pre-cache grape-middleware-logger gem + run: | + mkdir -p vendor/cache + if [ -f local_packages/grape-middleware-logger-2.4.0.gem ]; then + cp -v local_packages/grape-middleware-logger-2.4.0.gem vendor/cache/ + fi + + - uses: ruby/setup-ruby@v1 with: bundler-cache: true - run: RACK_ENV=test bundle exec rake db:migrate diff --git a/.gitignore b/.gitignore index 46916250..e12a8205 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,19 @@ config/authorized_keys/learning_registry/public_key.txt .DS_Store *~ + +# Terraform +**/builds +# Ignore only .terraform directories (keep .terraform.lock.hcl tracked) +**/.terraform/ +terraform*backup +*.zip + +# Allow vendored artifacts in local_packages for Docker build +!local_packages/ +!local_packages/*.gem +!local_packages/*.zip +terraform.tfstate +terraform.tfstate.backup +terraform/development/builds/ +terraform/builds diff --git a/Dockerfile b/Dockerfile index 00f33da6..67f955af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,6 @@ RUN set -eux; \ findutils diffutils procps-ng \ ca-certificates \ libpq libpq-devel \ - postgresql \ krb5-libs \ openldap \ cyrus-sasl-lib \ @@ -45,6 +44,17 @@ RUN set -eux; \ pkgconf-pkg-config \ && microdnf clean all +# Install PostgreSQL 17 client from PGDG and expose binaries on PATH +RUN set -eux; \ + curl -fsSL https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-x86_64/pgdg-redhat-repo-latest.noarch.rpm -o /tmp/pgdg.rpm; \ + rpm -Uvh /tmp/pgdg.rpm; \ + microdnf -y module disable postgresql || true; \ + microdnf -y install --setopt=install_weak_deps=0 --setopt=tsflags=nodocs postgresql17; \ + ln -sf /usr/pgsql-17/bin/psql /usr/bin/psql; \ + ln -sf /usr/pgsql-17/bin/pg_dump /usr/bin/pg_dump; \ + ln -sf /usr/pgsql-17/bin/pg_restore /usr/bin/pg_restore; \ + microdnf clean all + # Install local RPMs shipped in repo (EL10 builds) COPY rpms/ /tmp/rpms/ RUN if ls /tmp/rpms/*.rpm >/dev/null 2>&1; then rpm -Uvh --nosignature /tmp/rpms/*.rpm; fi @@ -108,8 +118,12 @@ RUN mkdir -p /runtime/usr/local /runtime/etc /runtime/usr/bin /runtime/usr/lib64 cp -a /usr/share/crypto-policies/back-ends/opensslcnf.config /runtime/etc/crypto-policies/back-ends/; \ fi && \ cp -a /usr/bin/openssl /runtime/usr/bin/ && \ - for b in /usr/bin/psql /usr/bin/pg_dump /usr/bin/pg_restore; do \ - cp -a "$b" /runtime/usr/bin/ 2>/dev/null || true; \ + # Copy PostgreSQL client binaries, dereferencing symlinks if present + for b in \ + /usr/bin/psql /usr/bin/pg_dump /usr/bin/pg_restore \ + /usr/pgsql-17/bin/psql /usr/pgsql-17/bin/pg_dump /usr/pgsql-17/bin/pg_restore; do \ + [ -f "$b" ] || continue; \ + cp -aL "$b" /runtime/usr/bin/ 2>/dev/null || true; \ done && \ mkdir -p /runtime/usr/lib64/ossl-modules && \ cp -a /usr/lib64/ossl-modules/* /runtime/usr/lib64/ossl-modules/ 2>/dev/null || true diff --git a/docker-compose.runtime.yml b/docker-compose.runtime.yml deleted file mode 100644 index a681ab58..00000000 --- a/docker-compose.runtime.yml +++ /dev/null @@ -1,97 +0,0 @@ -version: "3.9" - -services: - db: - image: postgres:16-alpine - environment: - - POSTGRES_PASSWORD=postgres - ports: - - 5432:5432 - volumes: - - postgres:/var/lib/postgresql/data - - redis: - image: redis:7.4.1 - expose: - - 6379 - - app_ubi10: - image: registry:ubi10 - command: bash -c "bin/rake db:create db:migrate && bin/rackup -o 0.0.0.0" - environment: - - POSTGRESQL_ADDRESS=db - - POSTGRESQL_DATABASE=cr_development - - POSTGRESQL_USERNAME=postgres - - POSTGRESQL_PASSWORD=postgres - - REDIS_URL=redis://redis:6379/1 - - SECRET_KEY_BASE=${SECRET_KEY_BASE} - - RACK_ENV=production - - DOCKER_ENV=true - ports: - - 9292:9292 - depends_on: - - db - - redis - security_opt: - - seccomp:unconfined - - worker_ubi10: - image: registry:ubi10 - command: bash -c "bin/sidekiq -r ./config/application.rb" - environment: - - POSTGRESQL_ADDRESS=db - - POSTGRESQL_DATABASE=cr_development - - POSTGRESQL_USERNAME=postgres - - POSTGRESQL_PASSWORD=postgres - - REDIS_URL=redis://redis:6379/1 - - SECRET_KEY_BASE=${SECRET_KEY_BASE} - - RACK_ENV=production - - DOCKER_ENV=true - - STATEMENT_TIMEOUT=900000 - depends_on: - - db - - redis - security_opt: - - seccomp:unconfined - - app_navy: - image: registry:ubi10-navy - command: bash -c "bin/rake db:create db:migrate && bin/rackup -o 0.0.0.0" - environment: - - POSTGRESQL_ADDRESS=db - - POSTGRESQL_DATABASE=cr_development - - POSTGRESQL_USERNAME=postgres - - POSTGRESQL_PASSWORD=postgres - - REDIS_URL=redis://redis:6379/1 - - SECRET_KEY_BASE=${SECRET_KEY_BASE} - - RACK_ENV=production - - DOCKER_ENV=true - ports: - - 9293:9292 - depends_on: - - db - - redis - security_opt: - - seccomp:unconfined - - worker_navy: - image: registry:ubi10-navy - command: bash -c "bin/sidekiq -r ./config/application.rb" - environment: - - POSTGRESQL_ADDRESS=db - - POSTGRESQL_DATABASE=cr_development - - POSTGRESQL_USERNAME=postgres - - POSTGRESQL_PASSWORD=postgres - - REDIS_URL=redis://redis:6379/1 - - SECRET_KEY_BASE=${SECRET_KEY_BASE} - - RACK_ENV=production - - DOCKER_ENV=true - - STATEMENT_TIMEOUT=900000 - depends_on: - - db - - redis - security_opt: - - seccomp:unconfined - -volumes: - postgres: diff --git a/docker-compose.yml b/docker-compose.yml index ff18cf42..b67e42fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: - SECRET_KEY_BASE=${SECRET_KEY_BASE} - RACK_ENV=production - DOCKER_ENV=true + - ENVELOPE_GRAPHS_BUCKET=ce-registry-envelopes-staging ports: - 9292:9292 depends_on: @@ -49,6 +50,7 @@ services: - RACK_ENV=production - DOCKER_ENV=true - STATEMENT_TIMEOUT=900000 + - ENVELOPE_GRAPHS_BUCKET=ce-registry-envelopes-staging # no ports needed for worker depends_on: - db diff --git a/docs/run-db-migration-job.md b/docs/run-db-migration-job.md new file mode 100644 index 00000000..4e7fdb13 --- /dev/null +++ b/docs/run-db-migration-job.md @@ -0,0 +1,74 @@ +# Running DB Migration Job Manually (staging) + +This guide shows how to run and observe the one‑shot database migration Job in the `credreg-staging` namespace using `kubectl`. + +Prerequisites +- `kubectl` configured to point at the EKS cluster (e.g., `aws eks update-kubeconfig --name ce-registry-eks --region us-east-1`) +- Permissions to create Jobs and read logs in `credreg-staging` + +Paths +- Job manifest: `terraform/environments/eks/k8s-manifests-staging/db-migrate-job.yaml` + - Uses image: `996810415034.dkr.ecr.us-east-1.amazonaws.com/registry:staging` + - Runs: `bundle exec rake db:migrate RACK_ENV=production` + +## 1) Create a Job +The manifest uses `generateName`, so Kubernetes assigns a unique name per run. Capture it into a shell variable: + +```bash +NAMESPACE=credreg-staging +MANIFEST=terraform/environments/eks/k8s-manifests-staging/db-migrate-job.yaml +JOB_NAME=$(kubectl -n "$NAMESPACE" create -f "$MANIFEST" -o name | sed 's|job.batch/||') +echo "Created job: $JOB_NAME" +``` + +## 2) Wait for completion +```bash +kubectl -n "$NAMESPACE" wait --for=condition=complete "job/$JOB_NAME" --timeout=10m +``` + +## 3) View logs +```bash +kubectl -n "$NAMESPACE" logs "job/$JOB_NAME" --all-containers=true +``` +If there are multiple pods (retries), view the most recent pod: +```bash +POD=$(kubectl -n "$NAMESPACE" get pods -l job-name="$JOB_NAME" -o jsonpath='{.items[-1:].0.metadata.name}') +kubectl -n "$NAMESPACE" logs "$POD" --all-containers=true +``` + +## 4) Troubleshooting +- Describe the Job and Pod for events: +```bash +kubectl -n "$NAMESPACE" describe job "$JOB_NAME" +kubectl -n "$NAMESPACE" get pods -l job-name="$JOB_NAME" +POD=$(kubectl -n "$NAMESPACE" get pods -l job-name="$JOB_NAME" -o jsonpath='{.items[-1:].0.metadata.name}') +kubectl -n "$NAMESPACE" describe pod "$POD" +``` +- If the Job failed and you want to retry, delete it and re‑create: +```bash +kubectl -n "$NAMESPACE" delete job "$JOB_NAME" +# Then go back to step 1 to create a fresh job +``` + +## 5) Use a specific image tag (optional) +By default the manifest uses the `staging` tag. To run with a specific tag produced by CI (e.g., `2025.09.25.7391`), create from a modified manifest on the fly: +```bash +TAG=2025.09.25.7391 +NAMESPACE=credreg-staging +MANIFEST=terraform/environments/eks/k8s-manifests-staging/db-migrate-job.yaml +JOB_NAME=$(sed "s#:staging#:$TAG#g" "$MANIFEST" \ + | kubectl -n "$NAMESPACE" create -f - -o name | sed 's|job.batch/||') +echo "Created job: $JOB_NAME (image tag: $TAG)" +``` +Note: The Job template includes `imagePullPolicy: Always`, so the node pulls the exact tag specified. + +## 6) Cleanup +The Job has `ttlSecondsAfterFinished: 600`, so Kubernetes will garbage‑collect it ~10 minutes after completion. To delete immediately: +```bash +kubectl -n "$NAMESPACE" delete job "$JOB_NAME" +``` + +## 7) Common errors +- `CrashLoopBackOff` or `ImagePullBackOff`: Verify the image and tag exist in ECR and the node can pull (IRSA/ECR auth). +- `rake`/migration errors: Check DB connectivity env (from `app-secrets` and `main-app-config`), and inspect logs. +- `pg_dump` version mismatch (SQL schema dumps): Ensure the image contains the correct PostgreSQL client version. diff --git a/openssl.cnf b/openssl.cnf index 480c1583..adfa225f 100644 --- a/openssl.cnf +++ b/openssl.cnf @@ -12,4 +12,3 @@ activate = 1 [legacy_sect] activate = 1 - diff --git a/scripts/stress_get_root.sh b/scripts/stress_get_root.sh new file mode 100644 index 00000000..004e66e0 --- /dev/null +++ b/scripts/stress_get_root.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple HTTP stress script for GET / +# - Sends N total requests with C concurrent workers to the provided base URL +# - Prints HTTP status code distribution and average latency + +usage() { + cat <&2; usage; exit 1 ;; + \?) echo "Unknown option -$OPTARG" >&2; usage; exit 1 ;; + esac +done + +if ! command -v curl >/dev/null 2>&1; then + echo "curl is required" >&2 + exit 1 +fi + +TMP_OUT=$(mktemp) +trap 'rm -f "$TMP_OUT"' EXIT + +echo "Hitting: ${BASE_URL}/ Total: ${TOTAL} Concurrency: ${CONCURRENCY}" >&2 + +# Fire requests in parallel; record http_code and total_time per request +seq 1 "$TOTAL" | \ + xargs -P "$CONCURRENCY" -n 1 -I {} \ + curl -sS -o /dev/null -w "%{http_code} %{time_total}\n" "${BASE_URL}/" \ + | tee "$TMP_OUT" >/dev/null + +# Summarize results +TOTAL_DONE=$(wc -l < "$TMP_OUT" | awk '{print $1}') +SUCCESS=$(awk '$1 ~ /^2/ {count++} END {print count+0}' "$TMP_OUT") +REDIRECT=$(awk '$1 ~ /^3/ {count++} END {print count+0}' "$TMP_OUT") +CLIENT_ERR=$(awk '$1 ~ /^4/ {count++} END {print count+0}' "$TMP_OUT") +SERVER_ERR=$(awk '$1 ~ /^5/ {count++} END {print count+0}' "$TMP_OUT") +AVG_LAT=$(awk '{sum+=$2} END { if (NR>0) printf "%.3f", sum/NR; else print "0" }' "$TMP_OUT") + +echo "--- Summary ---" +echo "Total: $TOTAL_DONE" +echo "2xx: $SUCCESS" +echo "3xx: $REDIRECT" +echo "4xx: $CLIENT_ERR" +echo "5xx: $SERVER_ERR" +echo "Avg (s): $AVG_LAT" + +exit 0 + diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 00000000..d10c4d04 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,249 @@ +# AWS INFRASTRUCTURE + +## Prerequisites + +### Install Metrics Server (for HPA) + +Horizontal Pod Autoscalers rely on the Kubernetes Metrics API to obtain CPU / memory +usage. The API is provided by the *metrics-server* add-on, which you must +install once in every cluster **before** deploying any HPA manifests. + +The project keeps the cluster self-contained and does **not** use Helm here to +avoid an extra tool dependency; a single `kubectl apply` is enough: + +```bash +# Install the latest released version +kubectl apply -f \ + https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml + +# (Optional) if your cluster uses self-signed certificates for kubelet +# endpoints add the insecure-TLS flag: +# kubectl patch deployment metrics-server -n kube-system \ +# --type='json' \ +# -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' + +# Verify that metrics are available +kubectl -n kube-system get deployment metrics-server +kubectl top nodes +kubectl top pods -A +``` + +When `kubectl top …` starts returning numbers the Metrics API is working and +Horizontal Pod Autoscalers (e.g. for the Laravel deployment) will function +without the *FailedGetResourceMetric* errors. + +### NGINX Ingress Controller (production cluster) + +The staging and production EKS cluster needs a Kubernetes Ingress controller so that +`Ingress` resources can expose services through a single AWS +Network-Load-Balancer (NLB). We use the community **ingress-nginx** +project and install it with Helm. + +#### A. Using Helm manually (quick, imperative) + +Prerequisites + +* `kubectl` is configured to talk to the **production** cluster. +* Helm ≥ v3 is installed on your workstation or CI runner. + +#### 1 – Add the chart repo & create the namespace + +```bash +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +helm repo update + +# namespace is only created if it does not already exist +kubectl create namespace ingress-nginx +``` + +#### 2 – Install / upgrade the controller + +```bash +helm upgrade --install nginx-ingress ingress-nginx/ingress-nginx \ + --namespace ingress-nginx \ + --set controller.replicaCount=2 \ + --set controller.ingressClassResource.name=nginx \ + --set controller.ingressClass=nginx \ + --set controller.service.externalTrafficPolicy=Local \ + --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-type"="nlb" +``` + +The annotation above tells EKS to provision an NLB. Adjust or remove it +if you are on a different cloud provider. + +#### 3 – Verify + +```bash +# all controller pods / services should be running +kubectl --namespace ingress-nginx get all + +# the IngressClass object should exist +kubectl get ingressclass + +# check that the Service has an external hostname / IP +kubectl get svc -n ingress-nginx +``` + +When the `nginx-ingress-controller` `Service` shows an **EXTERNAL-IP** +(or AWS hostname) you can create `Ingress` manifests that reference + +```yaml +ingressClassName: nginx +``` + +and they will be routed through the newly created load balancer. + + +### EBS CSI + +eksctl create iamserviceaccount \ + --region us-east-1 \ + --name ebs-csi-controller-sa \ + --namespace kube-system \ + --cluster [CLUSTER NAME] \ + --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \ + --approve \ + --role-only \ + --role-name AmazonEKS_EBS_CSI_DriverRole_prod + + eksctl create addon --name aws-ebs-csi-driver --cluster [CLUSTER NAME] --service-account-role-arn arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/AmazonEKS_EBS_CSI_DriverRole_prod --force + +### External Secrets Operator (ESO) + +#### Helm values template + +The chart values are version-controlled at + +``` +environments/eks/k8s-manifests/external-secrets-values.yaml +``` + +It contains two placeholders: + +* `` – the ARN above. +* `` – e.g. `us-east-1`. + +### Install or upgrade (staging example) + +``` +# render the values file with envsubst +export ROLE_ARN=$(terraform output -raw external_secrets_irsa_role_arn) +export AWS_REGION=us-east-1 + +envsubst < environments/eks/k8s-manifests/external-secrets-values.yaml \ + > /tmp/eso-values.yaml + +# install / upgrade +helm repo add external-secrets https://charts.external-secrets.io +helm repo update + +helm upgrade --install eso external-secrets/external-secrets \ + --namespace external-secrets --create-namespace \ + --values /tmp/eso-values.yaml + +rm /tmp/eso-values.yaml + +kubectl apply -f external-secrets-operator.yaml +``` + +The values set the IRSA annotation and pass `AWS_REGION` / +`AWS_DEFAULT_REGION` as environment variables. Without those the AWS +SDK fails to call `AssumeRoleWithWebIdentity` and ESO reports *Missing Region*. + +#### Verifying + +``` +# Store should be ready +kubectl get clustersecretstore aws-secret-manager -o=jsonpath='{.status.conditions[?(@.type=="Ready")].status}' + +# ExternalSecret objects should sync and create Kubernetes Secrets +kubectl get externalsecret -A +``` + +If everything is configured correctly the commands above will return +`True` and your Kubernetes Secret will appear in the target namespace. + +### Cluster Autoscaler + +The repository includes the upstream **Cluster Autoscaler** manifest so that +the managed node group can grow or shrink automatically based on pending +pods. + +#### 1 – IRSA role + +`terraform apply` creates an IAM role that the autoscaler assumes via +OIDC. Grab its ARN: + +```bash +terraform output -raw cluster_autoscaler_irsa_role_arn +``` + +#### 2 – Annotate the ServiceAccount + +Edit `environments/[environment]/k8s-manifests/cluster-autoscaler.yaml` and set the +annotation to the ARN from step 1: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cluster-autoscaler + namespace: kube-system + annotations: + eks.amazonaws.com/role-arn: +``` + +#### 3 – Deploy / upgrade + +```bash +kubectl apply -f environments/[environment]/k8s-manifests/cluster-autoscaler.yaml +``` + +Tail the logs: + +```bash +kubectl -n kube-system logs deploy/cluster-autoscaler -f | grep -iE '(scale|node group)' +``` + +#### 4 – Load test + +Use the provided load generator to create Pending pods and trigger a +scale-up: + +```bash +kubectl apply -f environments/[environment]/k8s-manifests/load-generator.yaml +kubectl get nodes -w +``` + +Delete when finished: + +```bash +kubectl delete -f environments/[environment]/k8s-manifests/load-generator.yaml +``` + +#### 5 – Troubleshooting checklist + +* Autoscaler log explains every decision—always read it first. +* ClusterRole includes `patch` / `update` on `nodes` and `nodes/status` + (already in the manifest). +* Node group tags must include + `k8s.io/cluster-autoscaler/enabled=true` and + `k8s.io/cluster-autoscaler/=owned` (added by Terraform). + +With the annotation set and the tags in place the autoscaler will scale +out when pods are Unschedulable and scale in when nodes are idle. + + +### Cert-Manager + +### Install using Helm + +helm install \ + cert-manager oci://quay.io/jetstack/charts/cert-manager \ + --version v1.18.2 \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true + +#### Make sure to add the annotation to the cert-manager SA + helm upgrade cert-manager oci://quay.io/jetstack/charts/cert-manager -n cert-manager --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::996810415034:role/ce-registry-eks-cert-manager-irsa-role --version v1.18.2 \ No newline at end of file diff --git a/terraform/environments/eks/.terraform.lock.hcl b/terraform/environments/eks/.terraform.lock.hcl new file mode 100644 index 00000000..a1ff1b26 --- /dev/null +++ b/terraform/environments/eks/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = ">= 5.50.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/environments/eks/backend.tf b/terraform/environments/eks/backend.tf new file mode 100644 index 00000000..37313cac --- /dev/null +++ b/terraform/environments/eks/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "terraform-state-o1r8" + key = "eks-registry/tfstate" + region = "us-east-1" + encrypt = true + dynamodb_table = "terraform-state-locks" + } +} diff --git a/terraform/environments/eks/k8s-manifests-staging/app-configmap.yaml b/terraform/environments/eks/k8s-manifests-staging/app-configmap.yaml new file mode 100644 index 00000000..73aff2aa --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: main-app-config +data: + POSTGRESQL_DATABASE: credential_registry_production + POSTGRESQL_USERNAME: credential_registry_production + RACK_ENV: production + DOCKER_ENV: "true" + ENVELOPE_GRAPHS_BUCKET: ce-registry-envelopes-staging + IAM_COMMUNITY_ROLE_ADMIN: ROLE_ADMINISTRATOR + IAM_COMMUNITY_ROLE_READEE: ROLE_READER + IAM_COMMUNITY_ROLE_PUBLISHER: ROLE_PUBLISHER + IAM_COMMUNITY_CLAIM_NAME: community_name + IAM_CLIENT_ID: RegistryAPI + IAM_URL: https://test-ce-kc-002.credentialengine.org/realms/CE-Test + IAM_CLIENT: TestStagingRegistryAPI + AIRBRAKE_PROJECT_ID: '270205' + SIDEKIQ_CONCURRENCY: '10' \ No newline at end of file diff --git a/terraform/environments/eks/k8s-manifests-staging/app-deployment.yaml b/terraform/environments/eks/k8s-manifests-staging/app-deployment.yaml new file mode 100644 index 00000000..651c4d3b --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: main-app + labels: + app: main +spec: + replicas: 1 # Adjust based on traffic + selector: + matchLabels: + app: main-app + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: main-app + spec: + serviceAccountName: main-app-service-account + # DB migrations are handled via a dedicated Job + containers: + - name: main-app + image: 996810415034.dkr.ecr.us-east-1.amazonaws.com/registry:staging + imagePullPolicy: Always + command: ["/bin/bash", "-c", "bin/rackup -o 0.0.0.0"] + ports: + - containerPort: 9292 + envFrom: + - secretRef: + name: app-secrets # DB credentials, APP_KEY, etc. + - configMapRef: + name: main-app-config + resources: + requests: + cpu: "500m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "1024Mi" + +--- +apiVersion: v1 +kind: Service +metadata: + name: main-app +spec: + type: ClusterIP + selector: + app: main-app + ports: + - protocol: TCP + port: 9292 + targetPort: 9292 diff --git a/terraform/environments/eks/k8s-manifests-staging/app-hpa.yaml b/terraform/environments/eks/k8s-manifests-staging/app-hpa.yaml new file mode 100644 index 00000000..261531df --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-hpa.yaml @@ -0,0 +1,27 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: main-app-hpa + namespace: credreg-staging + labels: + app: laravel +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: main-app + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 70 diff --git a/terraform/environments/eks/k8s-manifests-staging/app-ingress.yaml b/terraform/environments/eks/k8s-manifests-staging/app-ingress.yaml new file mode 100644 index 00000000..55efaee5 --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-ingress.yaml @@ -0,0 +1,33 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: main-app-ingress + annotations: + # NGINX Ingress Controller annotations + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" # HTTP → HTTPS + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/proxy-body-size: "10m" +spec: + ingressClassName: nginx + tls: + - hosts: + - staging.credentialengineregistry.org + secretName: staging-credentialengineregistry-org-tls + rules: + - host: staging.credentialengineregistry.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: main-app + port: + number: 9292 + - path: /skooner + pathType: Prefix + backend: + service: + name: skooner + port: + number: 80 \ No newline at end of file diff --git a/terraform/environments/eks/k8s-manifests-staging/app-namespace.yaml b/terraform/environments/eks/k8s-manifests-staging/app-namespace.yaml new file mode 100644 index 00000000..6a69da5b --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: credreg-staging + labels: + name: main-app \ No newline at end of file diff --git a/terraform/environments/eks/k8s-manifests-staging/app-secrets.yaml b/terraform/environments/eks/k8s-manifests-staging/app-secrets.yaml new file mode 100644 index 00000000..31653072 --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-secrets.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: main-app-secrets +type: Opaque +stringData: + POSTGRESQL_PASSWORD=[secret value] + SECRET_KEY_BASE=[openssl rand -hex 32] + POSTGRESQL_ADDRESS=[POSTGRESQL_ADDRESS] + SIDEKIQ_USERNAME=[SIDEKIQ_USERNAME] + SIDEKIQ_PASSWORD=[SIDEKIQ_PASSWORD] + REDIS_URL=[REDIS_URL] + AIRBRAKE_PROJECT_KEY=[AIRBRAKE_PROJECT_KEY] \ No newline at end of file diff --git a/terraform/environments/eks/k8s-manifests-staging/app-service-account.yaml b/terraform/environments/eks/k8s-manifests-staging/app-service-account.yaml new file mode 100644 index 00000000..286c697e --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/app-service-account.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: main-app-service-account + annotations: + eks.amazonaws.com/role-arn: "arn:aws:iam::996810415034:role/ce-registry-eks-application-irsa-role" diff --git a/terraform/environments/eks/k8s-manifests-staging/certificate.yaml b/terraform/environments/eks/k8s-manifests-staging/certificate.yaml new file mode 100644 index 00000000..7cf9fb0d --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/certificate.yaml @@ -0,0 +1,12 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: staging-credentialengineregistry-org-tls + namespace: credreg-staging +spec: + secretName: staging-credentialengineregistry-org-tls + dnsNames: + - staging.credentialengineregistry.org + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer \ No newline at end of file diff --git a/terraform/environments/eks/k8s-manifests-staging/cluster-autoscaler.yaml b/terraform/environments/eks/k8s-manifests-staging/cluster-autoscaler.yaml new file mode 100644 index 00000000..fb8aba78 --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/cluster-autoscaler.yaml @@ -0,0 +1,141 @@ +# Cluster Autoscaler manifest for the ${local.project_name}-${var.env} EKS cluster +# +# IMPORTANT: Replace with the actual IAM role +# ARN that Terraform outputs (cluster_autoscaler_irsa_role_arn) before applying +# this manifest. +# +# You can get the role ARN after running `terraform apply`: +# terraform -chdir=../../environments/dev output -raw cluster_autoscaler_irsa_role_arn +# and then update the annotation below accordingly. + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cluster-autoscaler + namespace: kube-system + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::996810415034:role/ce-registry-eks-cluster-autoscaler-irsa-role + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-autoscaler +rules: + - apiGroups: [""] + resources: ["events", "endpoints"] + verbs: ["create", "patch"] + - apiGroups: [""] + resources: ["pods/eviction"] + verbs: ["create"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods", "nodes", "services"] + verbs: ["watch", "list", "get", "patch", "update"] + # Allow autoscaler to patch node status (to add deletion candidate taints, etc.) + - apiGroups: [""] + resources: ["nodes/status"] + verbs: ["patch", "update"] + - apiGroups: ["autoscaling.k8s.io"] + resources: ["*"] + verbs: ["*"] + - apiGroups: ["apps"] + resources: ["replicasets", "statefulsets", "daemonsets"] + verbs: ["get", "list", "watch"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["list", "watch", "get", "patch"] + # Additional resources required by autoscaler for status and discovery + - apiGroups: [""] + resources: ["replicationcontrollers"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + # Added to support leader election using Leases in coordination.k8s.io + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["policy"] + resources: ["poddisruptionbudgets"] + verbs: ["get", "list", "watch"] + # Storage resources + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"] + verbs: ["get", "list", "watch"] + # Namespaces list is needed for TopologySpreadConstraints & PDBs + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + app: cluster-autoscaler +spec: + replicas: 1 + selector: + matchLabels: + app: cluster-autoscaler + template: + metadata: + labels: + app: cluster-autoscaler + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: "false" + spec: + serviceAccountName: cluster-autoscaler + containers: + - name: cluster-autoscaler + image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.26.4 + command: + - ./cluster-autoscaler + - --cluster-name=ce-registry-eks + - --cloud-provider=aws + - --scan-interval=10s + - --balance-similar-node-groups + - --skip-nodes-with-system-pods=false + - --skip-nodes-with-local-storage=false + - --aws-use-static-instance-list=true + - --expander=least-waste + # The line below ensures the autoscaler understands the min/max for the managed node group. + - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/ce-registry-eks + resources: + limits: + cpu: 200m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + volumeMounts: + - name: ssl-certs + mountPath: /etc/ssl/certs/ca-certificates.crt + readOnly: true + volumes: + - name: ssl-certs + hostPath: + path: /etc/ssl/certs/ca-bundle.crt + type: FileOrCreate diff --git a/terraform/environments/eks/k8s-manifests-staging/clusterissuer.yaml b/terraform/environments/eks/k8s-manifests-staging/clusterissuer.yaml new file mode 100644 index 00000000..0fad230e --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/clusterissuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + email: ariel@learningtapestry.com + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - dns01: + route53: + hostedZoneID: Z1N75467P1FUL5 diff --git a/terraform/environments/eks/k8s-manifests-staging/db-migrate-job.yaml b/terraform/environments/eks/k8s-manifests-staging/db-migrate-job.yaml new file mode 100644 index 00000000..1ff2d6fb --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/db-migrate-job.yaml @@ -0,0 +1,28 @@ +apiVersion: batch/v1 +kind: Job +metadata: + generateName: db-migrate- + namespace: credreg-staging + labels: + app: main-app +spec: + backoffLimit: 1 + activeDeadlineSeconds: 900 + ttlSecondsAfterFinished: 600 + template: + metadata: + labels: + app: main-app + spec: + serviceAccountName: main-app-service-account + restartPolicy: Never + containers: + - name: db-migrate + image: 996810415034.dkr.ecr.us-east-1.amazonaws.com/registry:staging + imagePullPolicy: Always + command: ["/bin/bash","-lc","bundle exec rake db:migrate RACK_ENV=production"] + envFrom: + - secretRef: + name: app-secrets + - configMapRef: + name: main-app-config diff --git a/terraform/environments/eks/k8s-manifests-staging/external-secrets-operator.yaml b/terraform/environments/eks/k8s-manifests-staging/external-secrets-operator.yaml new file mode 100644 index 00000000..4f186d8b --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/external-secrets-operator.yaml @@ -0,0 +1,52 @@ + +--- +# 1. Namespace +apiVersion: v1 +kind: Namespace +metadata: + name: external-secrets + +--- +# 2. ServiceAccount with IRSA annotation +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-secrets + namespace: external-secrets + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::996810415034:role/ce-registry-eks-external-secrets-irsa-role + + +--- +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: aws-secret-manager +spec: + provider: + aws: + service: SecretsManager + region: us-east-1 + auth: + jwt: + serviceAccountRef: + name: external-secrets + namespace: external-secrets + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: app-secret + namespace: credreg-staging +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secret-manager + kind: ClusterSecretStore + target: + name: app-secrets + creationPolicy: Owner + dataFrom: + - extract: + key: credreg-secrets-eks-staging diff --git a/terraform/environments/eks/k8s-manifests-staging/external-secrets-values.yaml b/terraform/environments/eks/k8s-manifests-staging/external-secrets-values.yaml new file mode 100644 index 00000000..a1dbd08c --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/external-secrets-values.yaml @@ -0,0 +1,35 @@ +# Helm values for External Secrets Operator (ESO) +# ============================================== +# These values are consumed by the official Helm chart hosted at +# https://charts.external-secrets.io +# +# The file is parameterised with two placeholders that **must** be +# substituted before you run `helm upgrade --install`: +# +# – the IAM Role ARN that Terraform +# outputs as `external_secrets_irsa_role_arn`. +# – the AWS region where your cluster and +# Secrets Manager live (e.g. us-east-1). +# +# Either replace those strings manually or use `envsubst` as shown in +# the README. + +installCRDs: true + +serviceAccount: + name: external-secrets + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::996810415034:role/ce-registry-eks-external-secrets-irsa-role + +env: + AWS_REGION: us-east-1 + AWS_DEFAULT_REGION: us-east-1 + +# Default resource requests/limits are conservative but can be tuned. +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi diff --git a/terraform/environments/eks/k8s-manifests-staging/img-inspect.yaml b/terraform/environments/eks/k8s-manifests-staging/img-inspect.yaml new file mode 100644 index 00000000..734bff2a --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/img-inspect.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: img-inspect + namespace: credreg-staging + labels: + app: img-inspect +spec: + restartPolicy: Never + serviceAccountName: main-app-service-account + containers: + - name: inspector + image: 996810415034.dkr.ecr.us-east-1.amazonaws.com/registry:staging + imagePullPolicy: Always + command: ["/bin/bash","-lc","sleep infinity"] + envFrom: + - secretRef: + name: app-secrets + - configMapRef: + name: main-app-config + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" diff --git a/terraform/environments/eks/k8s-manifests-staging/redis-configmap.yaml b/terraform/environments/eks/k8s-manifests-staging/redis-configmap.yaml new file mode 100644 index 00000000..c852bb70 --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/redis-configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: redis-config +data: + redis.conf: | + bind 0.0.0.0 + port 6379 + requirepass your_secure_password + appendonly yes + maxmemory 500mb + maxmemory-policy allkeys-lru + tcp-keepalive 300 + protected-mode yes diff --git a/terraform/environments/eks/k8s-manifests-staging/redis-deployment.yaml b/terraform/environments/eks/k8s-manifests-staging/redis-deployment.yaml new file mode 100644 index 00000000..81dfbef0 --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/redis-deployment.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + labels: + app: redis +spec: + serviceName: redis-service + replicas: 1 # For production, use 3 with Redis Sentinel + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7.2-alpine # Official Redis image + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] + ports: + - containerPort: 6379 + volumeMounts: + - name: redis-data + mountPath: /data + - name: redis-config + mountPath: /usr/local/etc/redis + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "1Gi" + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: redis-config + configMap: + name: redis-config + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: "gp2" # AWS EBS gp3 (adjust if needed) + resources: + requests: + storage: 10Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + clusterIP: None # Headless service for direct pod access + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis diff --git a/terraform/environments/eks/k8s-manifests-staging/worker-deployment.yaml b/terraform/environments/eks/k8s-manifests-staging/worker-deployment.yaml new file mode 100644 index 00000000..3b367ab0 --- /dev/null +++ b/terraform/environments/eks/k8s-manifests-staging/worker-deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker-app + labels: + app: worker-app +spec: + replicas: 1 + selector: + matchLabels: + app: worker-app + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: worker-app + spec: + serviceAccountName: main-app-service-account + containers: + - name: worker + image: 996810415034.dkr.ecr.us-east-1.amazonaws.com/registry:staging + imagePullPolicy: Always + env: + - name: PATH + value: "/app/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + command: ["/bin/bash","-lc"] + args: + - | + if [ -x ./bin/sidekiq ]; then + ./bin/sidekiq -r ./config/application.rb + else + bundle exec sidekiq -r ./config/application.rb + fi + envFrom: + - secretRef: + name: app-secrets + - configMapRef: + name: main-app-config + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "1024Mi" diff --git a/terraform/environments/eks/main.tf b/terraform/environments/eks/main.tf new file mode 100644 index 00000000..a82303a2 --- /dev/null +++ b/terraform/environments/eks/main.tf @@ -0,0 +1,143 @@ +locals { + project_name = "ce-registry" + common_tags = { + "project" = local.project_name + "environment" = var.env + } +} + + +module "vpc" { + source = "../../modules/vpc" + project_name = local.project_name + env = var.env + vpc_cidr = var.vpc_cidr + public_subnet_cidrs = var.public_subnet_cidrs + private_subnet_cidrs = var.private_subnet_cidrs + azs = var.azs + common_tags = local.common_tags +} + +## Staging RDS instance +module "rds-staging" { + source = "../../modules/rds" + project_name = local.project_name + security_group_description = "Allow inbound traffic from bastion" + env = var.env + vpc_id = module.vpc.vpc_id + vpc_cidr = var.vpc_cidr + subnet_ids = module.vpc.public_subnet_ids + db_name = var.db_name_staging + db_username = var.db_username_staging + instance_class = var.instance_class + common_tags = local.common_tags + ssm_db_password_arn = var.ssm_db_password_arn + rds_engine_version = var.rds_engine_version + allocated_storage = var.allocated_storage + # Allow destroying without snapshot for staging + skip_final_snapshot = true + deletion_protection = false + name_suffix = "-staging" +} + +## Production RDS instance +module "rds-production" { + source = "../../modules/rds" + project_name = local.project_name + security_group_description = "Allow inbound traffic from bastion" + env = var.env + vpc_id = module.vpc.vpc_id + vpc_cidr = var.vpc_cidr + subnet_ids = module.vpc.public_subnet_ids + db_name = var.db_name_prod + db_username = var.db_username_prod + instance_class = var.instance_class + common_tags = local.common_tags + ssm_db_password_arn = var.ssm_db_password_arn + rds_engine_version = var.rds_engine_version + allocated_storage = var.allocated_storage + # Leave production safer by default; override if needed during teardown + skip_final_snapshot = false + deletion_protection = true + name_suffix = "-prod" +} + +module "ecr" { + source = "../../modules/ecr" + project_name = var.ecr_repository_name + env = var.env +} + +output "ecr_repository_url" { + value = module.ecr.repository_url +} + +output "cluster_autoscaler_irsa_role_arn" { + description = "IAM role ARN that the Cluster Autoscaler service account should assume via IRSA" + value = module.eks.cluster_autoscaler_irsa_role_arn +} + +module "eks" { + source = "../../modules/eks" + environment = var.env + cluster_name = "${local.project_name}-${var.env}" + cluster_version = var.cluster_version + private_subnets = module.vpc.private_subnet_ids + common_tags = local.common_tags + priv_ng_max_size = var.priv_ng_max_size + priv_ng_min_size = var.priv_ng_min_size + priv_ng_des_size = var.priv_ng_des_size + priv_ng_instance_type = var.priv_ng_instance_type + route53_hosted_zone_id = var.route53_hosted_zone_id ## For IRSA role and cert-manager issuance + app_namespace = var.app_namespace_staging + app_service_account = var.app_service_account +} + +module "application_secret" { + source = "../../modules/secrets" + + secret_name = "credreg-secrets-${var.env}-staging" + description = "credreg application secrets for the ${var.env} staging environment" + + secret_values = { + POSTGRESQL_PASSWORD = var.db_password_staging + SECRET_KEY_BASE = var.secret_key_base_staging + POSTGRESQL_ADDRESS = var.db_host_staging + REDIS_URL = var.redis_url_staging + SIDEKIQ_USERNAME = var.sidekiq_username_staging + SIDEKIQ_PASSWORD = var.sidekiq_password_staging + } + + tags = local.common_tags +} + +module "application_secret_prod" { + source = "../../modules/secrets" + + secret_name = "credreg-secrets-${var.env}-production" + description = "credreg application secrets for the ${var.env} production environment" + + secret_values = { + POSTGRESQL_PASSWORD = var.db_password_prod + SECRET_KEY_BASE = var.secret_key_base_prod + POSTGRESQL_ADDRESS = var.db_host_prod + REDIS_URL = var.redis_url_prod + SIDEKIQ_USERNAME = var.sidekiq_username_prod + SIDEKIQ_PASSWORD = var.sidekiq_password_prod + } + + tags = local.common_tags +} + +## Staging S3: Envelope Graphs (module) +module "envelope_graphs_s3_staging" { + source = "../../modules/envelope_graphs_s3" + bucket_name = var.envelope_graphs_bucket_name_staging + environment = "staging" + common_tags = local.common_tags +} + +output "cer_envelope_graphs_bucket_name" { + value = module.envelope_graphs_s3_staging.bucket_name + description = "Staging S3 bucket name for envelope graphs" +} diff --git a/terraform/environments/eks/providers.tf b/terraform/environments/eks/providers.tf new file mode 100644 index 00000000..cdbfe290 --- /dev/null +++ b/terraform/environments/eks/providers.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.50" + } + } +} + +# Configure the AWS provider. Region can also be set via the AWS_REGION env var. +provider "aws" { + region = "us-east-1" +} diff --git a/terraform/environments/eks/skooner/ingress.yaml b/terraform/environments/eks/skooner/ingress.yaml new file mode 100644 index 00000000..23641c23 --- /dev/null +++ b/terraform/environments/eks/skooner/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: skooner-ingress + namespace: kube-system + # # If you use cert-manager for TLS, uncomment the next line and set your issuer + # # cert-manager.io/cluster-issuer: letsencrypt +spec: + ingressClassName: nginx +tls: + - hosts: + - status-staging.credentialengineregistry.org + secretName: skooner-tls + rules: + - host: status-staging.credentialengineregistry.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: skooner + port: + number: 80 \ No newline at end of file diff --git a/terraform/environments/eks/skooner/roles-bindings.yaml b/terraform/environments/eks/skooner/roles-bindings.yaml new file mode 100644 index 00000000..016fb5ea --- /dev/null +++ b/terraform/environments/eks/skooner/roles-bindings.yaml @@ -0,0 +1,29 @@ +# cluster-view-access.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: skooner-cluster-view +subjects: +- kind: ServiceAccount + name: skooner-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: view + apiGroup: rbac.authorization.k8s.io + +--- +# olc-namespace-admin.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: skooner-olc-admin + namespace: olc +subjects: +- kind: ServiceAccount + name: skooner-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: admin + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/terraform/environments/eks/terraform.tfvars b/terraform/environments/eks/terraform.tfvars new file mode 100644 index 00000000..fe8a287e --- /dev/null +++ b/terraform/environments/eks/terraform.tfvars @@ -0,0 +1,52 @@ +public_subnet_cidrs = ["10.19.1.0/24", "10.19.2.0/24"] +private_subnet_cidrs = ["10.19.3.0/24", "10.19.4.0/24"] +azs = ["us-east-1a", "us-east-1b"] +vpc_cidr = "10.19.0.0/16" +env = "eks" +instance_class = "db.t4g.medium" ## DB instance +db_name_staging = "ceregistrystaging" +db_name_prod = "ceregistryprod" + +ssm_db_password_arn = "arn:aws:ssm:us-east-1:996810415034:parameter/ce-registry/rds/rds_db_password" +image_tag_prod = "production" +image_tag_staging = "staging" + +rds_engine_version = "17.5" +allocated_storage = 40 +cluster_version = 1.33 +db_username_staging = "ceregistrystaging" +db_username_prod = "ceregistryprod" + +priv_ng_max_size = 10 +priv_ng_min_size = 1 +priv_ng_des_size = 2 ## this is irrelevant since the cluster uses the autoscaler to determine the appropriate value for it +priv_ng_instance_type = "t3.large" +route53_hosted_zone_id = "Z1N75467P1FUL5" + +ecr_repository_name = "registry" +# --------------------------------------------------------------------------- +# Sensitive values for the Laravel application secret. Provide real values via +# secure means (e.g. CI secrets, SSM Parameter Store) before running +# `terraform apply`. +# --------------------------------------------------------------------------- + +db_password_staging = "CHANGEME-db-pass" +secret_key_base_staging = "CHANGEME" +db_host_staging = "CHANGEME" +redis_url_staging = "CHANGEME" +sidekiq_username_staging = "CHANGEME" +sidekiq_password_staging = "CHANGEME" + +db_password_prod = "CHANGEME-db-pass" +secret_key_base_prod = "CHANGEME" +db_host_prod = "CHANGEME" +redis_url_prod = "CHANGEME" +sidekiq_username_prod = "CHANGEME" +sidekiq_password_prod = "CHANGEME" + +app_namespace_staging = "credreg-staging" +app_namespace_prod = "credreg-prod" +app_service_account = "ce_staging_sa" + +# Staging S3 bucket for envelope graphs +envelope_graphs_bucket_name_staging = "cer-envelope-graphs-staging" diff --git a/terraform/environments/eks/variables.tf b/terraform/environments/eks/variables.tf new file mode 100644 index 00000000..76256825 --- /dev/null +++ b/terraform/environments/eks/variables.tf @@ -0,0 +1,197 @@ +variable "env" { + description = "Environment name" + type = string +} + +# VPC Variables +variable "vpc_cidr" { + description = "VPC Cidr" +} + +variable "public_subnet_cidrs" { + description = "public_subnet_cidrs" +} + +variable "private_subnet_cidrs" { + description = "private_subnet_cidrs" +} + +variable "azs" { + type = list(string) + description = "Availability zones" +} + +variable "instance_class" { + type = string + description = "RDS instance class" +} + +variable "db_name_staging" { + type = string + description = "Staging DB instance name" +} +variable "db_name_prod" { + type = string + description = "Production DB instance name" +} +variable "db_username_staging" { + type = string + description = "Staging Database master username" +} +variable "db_username_prod" { + type = string + description = "Production Database master username" +} + +variable "ssm_db_password_arn" { + type = string + description = "ssm_db_password_arn" +} + +variable "image_tag_staging" { + type = string + description = "Staging Image tag to deploy from ECR" +} +variable "image_tag_prod" { + type = string + description = "Production Image tag to deploy from ECR" +} +variable "rds_engine_version" { + type = string + description = "rds_engine_version" +} + +variable "allocated_storage" { + type = number + description = "RDS Allocated storage" +} + +variable "cluster_version" { + type = string + description = "Kubernetes version" +} + +variable "priv_ng_max_size" { + type = number + description = "EKS node group max size" +} + +variable "priv_ng_min_size" { + type = number + description = "EKS node group min size" +} + +variable "priv_ng_des_size" { + type = number + description = "EKS node group desired size" +} + +variable "priv_ng_instance_type" { + type = string + description = "EKS node group instance type" +} + +# --------------------------------------------------------------------------- +# Secret values for Laravel application (stored in AWS Secrets Manager) +# --------------------------------------------------------------------------- + + +variable "db_password_staging" { + type = string + description = "Primary database password (sensitive)" + sensitive = true +} + +variable "db_password_prod" { + type = string + description = "Primary database password (sensitive)" + sensitive = true +} + +variable "secret_key_base_staging" { + type = string + description = "secret key base (sensitive)" + sensitive = true +} +variable "secret_key_base_prod" { + type = string + description = "secret key base (sensitive)" + sensitive = true +} + +variable "db_host_staging" { + type = string + description = "DB host url (sensitive)" + sensitive = true +} + +variable "db_host_prod" { + type = string + description = "DB host url (sensitive)" + sensitive = true +} + +variable "redis_url_staging" { + type = string + description = "Redis host url (sensitive)" + sensitive = true +} + +variable "redis_url_prod" { + type = string + description = "Redis host url (sensitive)" + sensitive = true +} + +variable "sidekiq_username_staging" { + type = string + description = "Sidekiq UI username (sensitive)" + sensitive = true +} + +variable "sidekiq_username_prod" { + type = string + description = "Sidekiq UI username (sensitive)" + sensitive = true +} + +variable "sidekiq_password_staging" { + type = string + description = "Sidekiq UI password (sensitive)" + sensitive = true +} + +variable "sidekiq_password_prod" { + type = string + description = "Sidekiq UI password (sensitive)" + sensitive = true +} + +variable "route53_hosted_zone_id" { + description = "route53_hosted_zone_id" + type = string +} + +variable "app_namespace_staging" { + description = "Staging K8s application namespace" + type = string +} + +variable "app_namespace_prod" { + description = "Production K8s application namespace" + type = string +} +variable "app_service_account" { + description = "K8s application service account name" + type = string +} + +variable "ecr_repository_name" { + description = "Name of the AWS ECR repository" + type = string +} + +variable "envelope_graphs_bucket_name_staging" { + description = "S3 bucket name for envelope graphs (staging)" + type = string +} diff --git a/terraform/environments/github-ci-oidc/README.md b/terraform/environments/github-ci-oidc/README.md new file mode 100644 index 00000000..13e7e78b --- /dev/null +++ b/terraform/environments/github-ci-oidc/README.md @@ -0,0 +1,229 @@ +## Github Actions authentication to AWS using OIDC + +The following steps is a guidance to set up **GitHub Actions** to authenticate to **AWS** using the built-in OIDC provider (short-lived tokens) instead of relying on static **AWS access keys** (long-lived). + +The instructions below will let you _re-create the whole mechanism from scratch_ if the OIDC provider or IAM role ever need to be rebuilt. + +-------------------------------------------------------------------- +1. Prerequisites +-------------------------------------------------------------------- + +• AWS CLI v2 configured for the target AWS account (you need **Administrator** or equivalent permissions only _while_ creating the resources). +• GitHub repository **owner/repo** where the workflow will run (replace with yours in the examples). + +-------------------------------------------------------------------- +2. Create / verify the OIDC identity provider in AWS +-------------------------------------------------------------------- + +AWS now automatically creates the GitHub OIDC provider (`token.actions.githubusercontent.com`) the first time it is referenced. If your account does **not** have one yet you can create it explicitly: + +```bash +aws iam create-open-id-connect-provider \ + --url https://token.actions.githubusercontent.com \ + --thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1" \ + --client-id-list sts.amazonaws.com +``` + + +You should see the provider listed afterwards: + +```bash +aws iam list-open-id-connect-providers +``` + +-------------------------------------------------------------------- +3. Write the **trust policy** (trust.json) +-------------------------------------------------------------------- + +This policy tells AWS STS *which* GitHub runs are allowed to assume the role. The most common pattern is to scope it to a repository (and optionally branch / environment). + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam:::oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", + "token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:*" + } + } + } + ] +} +``` + +Replace **OWNER/REPO** with your GitHub repository (e.g. `learningtapestry/lt-devops`). + +You may further tighten the `sub` condition, e.g. `repo:OWNER/REPO:ref:refs/heads/main` to allow only the **main** branch. + +-------------------------------------------------------------------- +4. Create the IAM role and attach permissions +-------------------------------------------------------------------- + +```bash +# 4.1 Create the role with the trust relationship from step 3 +aws iam create-role \ + --role-name github-oidc-widget \ + --assume-role-policy-document file://trust.json + +# 4.2 Attach an inline policy (replace the actions/resources with your needs) +aws iam put-role-policy \ + --role-name github-oidc-widget \ + --policy-name ecr+eks \ + --policy-document file://role-permissions.json +``` + +Example **role-permissions.json** granting ECR push/pull, EKS access and S3 uploads: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECR", + "Effect": "Allow", + "Action": [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:UploadLayerPart", + "ecr:InitiateLayerUpload", + "ecr:PutImage" + ], + "Resource": "*" + }, + { + "Sid": "EKS", + "Effect": "Allow", + "Action": [ + "eks:DescribeCluster" + ], + "Resource": "arn:aws:eks:::cluster/" + } + ] +} +``` + +-------------------------------------------------------------------- +5. Reference the role in your GitHub Actions workflow +-------------------------------------------------------------------- + +Add / update the **aws-actions/configure-aws-credentials** step (already present in our workflows): + +```yaml +steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam:::role/github-oidc-widget + aws-region: us-east-1 +``` + +No AWS secrets are needed – GitHub issues an OIDC token that the action exchanges for temporary STS credentials under the hood. + +-------------------------------------------------------------------- +6. (Optional) Test the setup locally +-------------------------------------------------------------------- + +```bash +aws sts assume-role-with-web-identity \ + --role-arn arn:aws:iam:::role/github-oidc-widget \ + --role-session-name "TestSession" \ + --web-identity-token "$(gh auth token)" # requires GitHub CLI logged-in under the repo identity +``` + +-------------------------------------------------------------------- +7. Troubleshooting tips +-------------------------------------------------------------------- + +• `AccessDenied` – check the **aud** and **sub** entries in the trust policy exactly match what the GitHub token contains. You can print the token info inside a workflow using `echo "${{ steps.[id].outputs.id_token }}" | jq -R 'split(".") | .[1] | @base64d | fromjson'`. +• `NoSuchEntity` – the role name or provider ARN is misspelled. +• Make sure the workflow actually runs in the repository / branch you allowed. + +-------------------------------------------------------------------- +8. Clean-up +-------------------------------------------------------------------- + +```bash +aws iam delete-role-policy --role-name github-oidc-widget --policy-name ecr+eks +aws iam delete-role --role-name github-oidc-widget +# Do **not** delete the OIDC provider if other roles depend on it +``` + +--- + +Once the steps above are complete, the `github-oidc-widget` role will be ready for your GitHub Actions workflows and no static AWS keys will be stored anywhere. + +-------------------------------------------------------------------- +9. Granting the role access to EKS clusters (aws-auth / access entries) +-------------------------------------------------------------------- + +Assuming EKS requires two parts: + +1) AWS-side: the role must be allowed to call `eks:DescribeCluster` (already in `role-permissions.json`). +2) Kubernetes-side: the role must be mapped to a Kubernetes user/group in each cluster you want to access. + +Staging example (works): + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: aws-auth + namespace: kube-system +data: + mapRoles: | + - groups: + - system:bootstrappers + - system:nodes + rolearn: arn:aws:iam::996810415034:role/[CLUSTER NAME]-eks-nodegroup-role + username: system:node:{{EC2PrivateDNSName}} + - groups: + - system:masters + rolearn: arn:aws:iam::996810415034:role/github-oidc-widget + username: github-actions +``` + +Production example (missing mapping): only nodegroup role is present – the GitHub OIDC role is not mapped yet. You must add a similar entry for production. + +Ways to add the mapping (pick one): + +- eksctl CLI + ```bash + eksctl create iamidentitymapping \ + --cluster [CLUSTER NAME] \ + --region us-east-1 \ + --arn arn:aws:iam::996810415034:role/github-oidc-widget \ + --username github-actions \ + --group system:masters + ``` + +- Edit aws-auth ConfigMap directly (admin context required) + ```bash + kubectl -n kube-system get configmap aws-auth -o yaml > aws-auth.yaml + # Add the mapRoles item for the GitHub role (similar to the staging example), then: + kubectl -n kube-system apply -f aws-auth.yaml + ``` + +- EKS Access Entries (newer model via console / API) + - Console: EKS → Cluster → Access → Add access entry → Principal ARN of the role → Kubernetes group `system:masters` (or a read-only group with a proper ClusterRoleBinding). + +Verification steps (in CI or locally after `aws eks update-kubeconfig`): + +```bash +aws sts get-caller-identity +aws eks describe-cluster --name --region +kubectl config current-context +kubectl auth can-i list pods -n +kubectl get pods -n +``` + +If these commands succeed, the role is correctly mapped and kubectl is authorized against the cluster. diff --git a/terraform/environments/github-ci-oidc/role-permissions.json b/terraform/environments/github-ci-oidc/role-permissions.json new file mode 100644 index 00000000..7bbd978e --- /dev/null +++ b/terraform/environments/github-ci-oidc/role-permissions.json @@ -0,0 +1,27 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ecr:GetAuthorizationToken", + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:GetRepositoryPolicy", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:DescribeImages", + "ecr:BatchGetImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:PutImage" + ], + "Resource": "arn:aws:ecr:us-east-1:996810415034:*" + } + ] +} \ No newline at end of file diff --git a/terraform/environments/github-ci-oidc/trust.json b/terraform/environments/github-ci-oidc/trust.json new file mode 100644 index 00000000..5393ed7e --- /dev/null +++ b/terraform/environments/github-ci-oidc/trust.json @@ -0,0 +1,20 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::996810415034:oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:[organization/repository]:*" + } + } + } + ] +} \ No newline at end of file diff --git a/terraform/modules/ecr/main.tf b/terraform/modules/ecr/main.tf new file mode 100644 index 00000000..799a89e8 --- /dev/null +++ b/terraform/modules/ecr/main.tf @@ -0,0 +1,47 @@ +resource "aws_ecr_repository" "app_repo" { + name = var.project_name + image_tag_mutability = "MUTABLE" # Prevent tag overwrites + + image_scanning_configuration { + scan_on_push = true # Enable automatic vulnerability scanning + } + + tags = var.common_tags +} + +# Lifecycle policy to clean up old images +resource "aws_ecr_lifecycle_policy" "app_repo" { + repository = aws_ecr_repository.app_repo.name + policy = <