diff --git a/.github/workflows/backup_scaleway.yml b/.github/workflows/backup_scaleway.yml new file mode 100644 index 0000000..043e17c --- /dev/null +++ b/.github/workflows/backup_scaleway.yml @@ -0,0 +1,125 @@ +name: Terraform — Scaleway backup bucket + +# Drives 03-backup/scaleway through the .github/actions/terraform composite action. +# +# Domain restriction — NO DESTROY in CI (FR-013): +# The backup domain is intentionally permanent infrastructure. Destroying the +# bucket from CI would silently erase production backups. Destruction must be +# a deliberate admin action with elevated credentials outside of this pipeline. +# Enforcement is layered: +# 1. No `destroy` option in workflow_dispatch (this file). +# 2. No `schedule → destroy` mapping in the command-resolution step. +# 3. `prevent_destroy = true` in 03-backup/scaleway/main.tf blocks +# `terraform destroy` even if someone runs it manually against this root. +# +# Trigger → command mapping: +# pull_request → plan (safety check, never mutates state) +# push to main → apply (the only automated mutation path) +# workflow_dispatch → plan | apply (operator override, explicit choice) +# +# State R/W uses the org state role (vars.AWS_TF_STATE_ROLE_ARN). +# Scaleway creds come from the `scaleway` environment. + +on: + pull_request: + paths: + - '03-backup/scaleway/**' + - '.github/actions/terraform/**' + - '.github/workflows/backup_scaleway.yml' + push: + branches: [main] + paths: + - '03-backup/scaleway/**' + - '.github/actions/terraform/**' + - '.github/workflows/backup_scaleway.yml' + workflow_dispatch: + inputs: + command: + description: 'Terraform command. destroy is not available — see domain restriction above.' + type: choice + options: [plan, apply] + default: plan + +# Concurrency is keyed on the Terraform workspace, not the branch. +# cancel-in-progress is false: Terraform state corruption from mid-run +# interrupts is far worse than waiting in queue. +concurrency: + group: backup-scaleway-03-backup-dev-bucket + cancel-in-progress: false + +permissions: + contents: read + +jobs: + terraform: + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: scaleway + permissions: + id-token: write # mint OIDC tokens for AWS + Infisical + contents: read + env: + SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }} + SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }} + SCW_DEFAULT_ORGANIZATION_ID: ${{ vars.SCW_DEFAULT_ORGANIZATION_ID }} + SCW_DEFAULT_PROJECT_ID: ${{ vars.SCW_DEFAULT_PROJECT_ID }} + INFISICAL_MACHINE_IDENTITY_ID: ${{ vars.INFISICAL_MACHINE_IDENTITY_ID }} + steps: + - name: Assert state role ARN is present + env: + ROLE_ARN: ${{ vars.AWS_TF_STATE_ROLE_ARN }} + run: | + if [ -z "$ROLE_ARN" ]; then + echo "::error::vars.AWS_TF_STATE_ROLE_ARN is not set (provisioned by 01-iam/ci-managed/aws-state-access)." + exit 1 + fi + + # Maps the GitHub event to a Terraform command. + # push → apply is the only automated mutation path (main branch only, per `on.push.branches`). + # workflow_dispatch lets an operator force plan or apply without a code change. + # Everything else (pull_request, etc.) stays read-only with plan. + - name: Resolve terraform command + id: cmd + env: + EVENT: ${{ github.event_name }} + DISPATCH_CMD: ${{ github.event.inputs.command }} + run: | + case "$EVENT" in + push) cmd=apply ;; + workflow_dispatch) cmd="${DISPATCH_CMD:-plan}" ;; + *) cmd=plan ;; + esac + echo "command=$cmd" >> "$GITHUB_OUTPUT" + echo "Resolved terraform command: $cmd (event: $EVENT)" + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 1 + + - uses: ./.github/actions/terraform + with: + root: 03-backup/scaleway + tfvars-file: 03-backup-dev-bucket.tfvars + command: ${{ steps.cmd.outputs.command }} + aws-role-arn: ${{ vars.AWS_TF_STATE_ROLE_ARN }} + + # The backup bucket is permanent infrastructure — state always exists regardless + # of the terraform command. Read outputs unconditionally so downstream steps + # always have the live values from state rather than hardcoded strings. + - name: Read Terraform outputs + id: tf-outputs + run: | + outputs=$(terraform -chdir=03-backup/scaleway output -json) + echo "bucket_name=$(echo "$outputs" | jq -r '.bucket_name.value')" >> "$GITHUB_OUTPUT" + echo "bucket_region=$(echo "$outputs" | jq -r '.bucket_region.value')" >> "$GITHUB_OUTPUT" + + - name: Verify backup bucket exists + if: steps.cmd.outputs.command == 'apply' + env: + BUCKET_NAME: ${{ steps.tf-outputs.outputs.bucket_name }} + BUCKET_REGION: ${{ steps.tf-outputs.outputs.bucket_region }} + run: | + curl -fsSL https://raw.githubusercontent.com/scaleway/scaleway-cli/master/scripts/get.sh | sh + export PATH="$HOME/.local/bin:$PATH" + scw object bucket get "$BUCKET_NAME" --region "$BUCKET_REGION" diff --git a/.gitignore b/.gitignore index 8eae8cb..f30d2f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Spec Kit planning artifacts — local design docs, not part of the reviewed diff +specs/ + # Local .terraform directories .terraform/ diff --git a/.specify/feature.json b/.specify/feature.json new file mode 100644 index 0000000..759c4f1 --- /dev/null +++ b/.specify/feature.json @@ -0,0 +1,3 @@ +{ + "feature_directory": "specs/001-backup-s3-foundation" +} diff --git a/01-iam/bootstrap/scaleway/main.tf b/01-iam/bootstrap/scaleway/main.tf index d78075f..c367af7 100644 --- a/01-iam/bootstrap/scaleway/main.tf +++ b/01-iam/bootstrap/scaleway/main.tf @@ -16,6 +16,39 @@ resource "scaleway_iam_policy" "this" { } } +# Added for 03-backup/scaleway: the backup CI workflow runs under the same +# github-ci identity and needs Object Storage management + IAM application/policy/ +# API key management to provision the bucket and scoped workload credentials. +# Bucket deletion is blocked via prevent_destroy + absence of a destroy trigger +# in the CI workflow (Scaleway bucket policies do not support s3:DeleteBucket). +resource "scaleway_iam_policy" "backup_ci" { + name = "github-ci-backup-management" + description = "Object Storage bucket + IAM workload identity management for the backup domain CI workflow (03-backup/scaleway/)." + application_id = scaleway_iam_application.this.id + + rule { + project_ids = [var.project_id] + permission_set_names = [ + "ObjectStorageBucketsRead", + "ObjectStorageBucketsWrite", + "ObjectStorageObjectsRead", + "ObjectStorageObjectsWrite", + ] + } + + # IAM permission sets are organization-scoped — they cannot be combined + # with project_ids in the same rule. + rule { + organization_id = var.organization_id + permission_set_names = [ + # Required to create/manage the scoped workload IAM application, policy, + # and API key in 03-backup/scaleway/iam.tf. + "IAMApplicationManager", + "IAMPolicyManager", + ] + } +} + # The org enforces an expiry on every API key, and `expires_at` is ForceNew, so # the key inherently rotates when the expiry moves. time_rotating makes that # concrete and self-renewing: the timestamp holds steady until the window diff --git a/01-iam/bootstrap/scaleway/variables.tf b/01-iam/bootstrap/scaleway/variables.tf index e14d310..204631a 100644 --- a/01-iam/bootstrap/scaleway/variables.tf +++ b/01-iam/bootstrap/scaleway/variables.tf @@ -24,6 +24,12 @@ variable "project_id" { default = "6283c05b-a4c7-4f83-a75f-83adad236d54" } +variable "organization_id" { + description = "Scaleway organization ID. Used for org-scoped IAM permission sets (IAMApplicationManager, IAMPolicyManager)." + type = string + default = "6283c05b-a4c7-4f83-a75f-83adad236d54" +} + # Scaleway's org policy requires every API key to carry an expiry. This drives # the key's expires_at; once the window elapses, the next apply rotates the key. variable "api_key_rotation_days" { diff --git a/03-backup/scaleway/.terraform.lock.hcl b/03-backup/scaleway/.terraform.lock.hcl new file mode 100644 index 0000000..501b695 --- /dev/null +++ b/03-backup/scaleway/.terraform.lock.hcl @@ -0,0 +1,93 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.51.0" + hashes = [ + "h1:QWxF+1ePJ4qFCHEc6PyHNeXc865wLvrWVl71d/nABa8=", + "zh:03fcea0a1ea2ca81d62d4d2e2961181bef9068b1c701f2cddc4aa5fac105818a", + "zh:1213944cd623143974ea5c9b70b22ae1ccca33d743924c149ed089d34b8e08b4", + "zh:190a46da0c69082b74da48238ce134d2fc9893e09122ac249c5689f88eab7e13", + "zh:1b312a4b53fa3cf731f95e674c033865feea5455f163b86136f2614424637293", + "zh:2b319814806222c5aba196b1a78756a6b36dc5c91f85edda349234d8a2f20a6a", + "zh:2bddf92c8efc6ad445a2eb8a0e5f88742a0596392c3a4ebc350ebb4105a4a96d", + "zh:3bef0c4f675c09034ff017cf899977b1765b2c0b3d1e489bcb06a5fcac316e2d", + "zh:47c46b5aa22199638fed5c93b195bbfd1182a1408edad4e5c39d4a73a04493f6", + "zh:5f808699650f6db961964466c77f5a581eab142a91c2e54810bb09b6f2fcd3f2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ada97e6be10164f452e278c23412b8597698a9c95ffb68fe83629d63d85906f3", + "zh:c4d73a91810d8dbcf9abbd431d41fcceebb48f8b6fd3c28a84bb3c6ed08be2e9", + "zh:c63ec875d38fc557b16b0b2b0ab1c7635852799453113240e21a52409de94a71", + "zh:cdd0209a755fc3aa14855aa013dae4b166a2fc7f6d3cbb673f7ff2142f5b63a2", + "zh:e5e665a27290391fd1bffc093ab68b596f6c507785be2e3f0949fab4fd6aec1b", + "zh:f6c42046a31d65eff2793737656b38931f90318b53661046bb84326cd4cb558f", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.14.0" + constraints = "~> 0.12" + hashes = [ + "h1:/hlxsUpuN/lvPTNL9+NyVGsOyRsK5NsxwFMsj5CdOp4=", + "h1:4EThC3ocCFiFPMZQSUvSGSxoJqBcGWxMcFYmL67uS7Y=", + "zh:12abfd6b800e4d7fa6db7310dec8ffd440b31993861ef188c7ed5260b3073937", + "zh:23005521e800bb19e1597bf755c5f70d675d30b685d4255001ed5fa47d9df3f1", + "zh:2fea249b582ae97cd1cc10385187ea50993bb47c28cc5df0305e57ceaabf0a10", + "zh:322018d3b987b7aad08697178029a2bb667bed699e88328f0c89c52a2fd41341", + "zh:32a08e98fce2d273cb9b2c89d6c54727cc9f0a32e15bfd896be4e02cc6b48f95", + "zh:3db89aabd0e619616bd4b0f8b373a7586dfe60feffcea12a84a0bdbc445714b3", + "zh:7488f56c81d742dc020f29063626c8f07ca188aa97be61e7307e8d62397020a2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7cb4067f2e7559b13f7562ef722f948950901eb37834873e98360ab28f66e9d7", + "zh:9d552c8345f61e1b7db8e725144981345f18ac1014d58d6f5ddf0928a195fffb", + "zh:a8e69fb6b97fc9d86fb19a9f4d42abe33c4a68e700b15387ce2e17d2b9934bed", + "zh:aeeb900eb8dd0f790c60ea5c0e0c8d42bd6e4a54f391681d4decca15b544394b", + "zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79", + ] +} + +provider "registry.terraform.io/infisical/infisical" { + version = "0.16.32" + constraints = "~> 0.16" + hashes = [ + "h1:IYFOE05ITZi1HVGggR5sQM5Y96e606AWbN17am0mqdo=", + "h1:KLog2xD5jntJk7Yt9ePBex+KJ8rVkpsga4hiJrmU+9Y=", + "zh:05a2d64651d01cb0e99cc914e9795d6c20a671ea54c011ed1e9be57cebae037a", + "zh:3075619c6a3c298aa0d3e6c977afa0e6b18a7c23283c36af92ca3e2bfec0dc90", + "zh:3198d5219ce5c3d2fedcc0096a6b1889bd9caac6acaa8fcac29a6f45835b1e26", + "zh:335ad3f71ada2c43b3e64d37437d2163903815d8509689ae8d0ba66c86833913", + "zh:410a5cfa062a5c61f026a1419f8bdead0bfc46fc194c801f7d55e3fe45717e82", + "zh:534cbb32ff5cc329058666c49b2148834b5e57abaa4f360d518370554f1d0543", + "zh:68c0d3eca17f23df5e60578e480eb41e3d84cc3479b9ec6ea6cd5ef7df1d3d52", + "zh:867b1f91cf34685f55f69e847224f64dff5d101e77f888d9598c334688025233", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:8a68a03db0484b4c4dd1a41abcb767a3d9e05508a13a1018b11b180e3ea68fc1", + "zh:8deb26470ae6db94df3b2198a04ce4432c5be3cac4cf51bcea57f0968ca46ca5", + "zh:cb6f956f777e56a205bf55ef84220cf11b11ae9bee17eb560f5a3661ac3b1a9f", + "zh:dc3881aa9ade9257b7893969b08a5b84c3360588c4061f5c4a20b3e0aef5202f", + "zh:e48ee43b6ff928a46123a83f5409588b8e98c5ad7a844987d7e5f3b768d8cc4e", + "zh:ebbaae992d0b2f0c33cb4e9dac2bb636a6d26389cc9ee07254098c10b6fb0f6f", + ] +} + +provider "registry.terraform.io/scaleway/scaleway" { + version = "2.76.0" + constraints = "~> 2.0" + hashes = [ + "h1:3RImo3Jf88dXcIqmBRuO/A85jAxrAQsvYa4zhVqW1tI=", + "h1:ktRogBsJlCf3JTOTYPm9hKaPUzaps0aLwxsNfP155ns=", + "zh:13b6790dca2c91c7478d6e9cd03d84713e0b0ec001c9923d3c93bd12a1f4152f", + "zh:219582ef77ec6f27d2928684b95603b5a921001631f9eec68c14819fdbc1efea", + "zh:26bc09c7aadc49fe83a9d6af9c1d0951f93de129f90d04e5e65e1d82c8ac1914", + "zh:4015a970be3669ee009344afb269a09eaf9fe1363a12a10d3311326ba769463a", + "zh:4c1c5997ef4182e49e46ff6563581a90b5cfa30f0ec8d7d919b3dc0603ed3043", + "zh:58d91e08a8fe38ddda2c03013d1ba77ff4e21a91fef0a425575b5af7bf7f4ebc", + "zh:644a61f963483c8eba7f8427650c4956e1d8c59710a11bb2691ad8996ee14a9c", + "zh:6745d5d80006c375b104a005cfc007f90e2cb547cf9e96c886d453be3307a399", + "zh:8dcac392ca182af40b823c6721833de1325c279b1e5bdcddf1a1cf2789f3558a", + "zh:9eb284d3e9a14d4d64e2a7a865be45ec0eee32ec16c51bcc1722c0449bfb9fab", + "zh:a4eb4973cc1d9ac4911733d62f5b0e53fbc7b90112efa312918c0320966e276e", + "zh:ce58ae8014b2981bbc875bc7ad4cdeb93957370bfa4308eeb193155ee988fbec", + "zh:d3f0f3bec0a6f4759948749cdb0eb64e4415507a82c79659f1ede894f96f4976", + ] +} diff --git a/03-backup/scaleway/env/03-backup-dev-bucket.tfvars b/03-backup/scaleway/env/03-backup-dev-bucket.tfvars new file mode 100644 index 0000000..2c00036 --- /dev/null +++ b/03-backup/scaleway/env/03-backup-dev-bucket.tfvars @@ -0,0 +1,11 @@ +bucket_name = "backup-dev-id" +region = "fr-par" +project_id = "6283c05b-a4c7-4f83-a75f-83adad236d54" + +ci_application_id = "4d50bbbd-e8a2-4b51-80c5-6aa47de669f7" + +# Lifecycle defaults are accepted for dev: +# retention_days = 365 +# noncurrent_version_expiry_days = 30 +# cold_storage_enabled = true +# cold_storage_transition_days = 90 diff --git a/03-backup/scaleway/iam.tf b/03-backup/scaleway/iam.tf new file mode 100644 index 0000000..c7c8bfa --- /dev/null +++ b/03-backup/scaleway/iam.tf @@ -0,0 +1,29 @@ +resource "scaleway_iam_application" "kubernetes" { + name = "backup-k8s-${terraform.workspace}" + description = "Kubernetes workload identity for ${terraform.workspace} — grants pods object read/write on the backup bucket via Infisical → ESO → Secret (no administrative rights, managed by terraform: 03-backup/scaleway/)." +} + +resource "scaleway_iam_policy" "kubernetes" { + name = "backup-k8s-objects-${terraform.workspace}" + description = "Object-level read/write on the backup project. No bucket-level permissions — cannot delete or reconfigure the bucket." + application_id = scaleway_iam_application.kubernetes.id + + rule { + project_ids = [var.project_id] + permission_set_names = ["ObjectStorageObjectsRead", "ObjectStorageObjectsWrite"] + } +} + +# Scaleway requires every API key to carry an expiry. time_rotating keeps +# the expiry self-renewing: once the window elapses, the next apply rotates +# the key. Update the Kubernetes Secret (via ESO re-sync) after each rotation. +resource "time_rotating" "kubernetes_key" { + rotation_days = 365 +} + +resource "scaleway_iam_api_key" "kubernetes" { + application_id = scaleway_iam_application.kubernetes.id + description = "Backup workload credentials for ${terraform.workspace}. Consumed via Infisical → ESO → Kubernetes Secret." + default_project_id = var.project_id + expires_at = time_rotating.kubernetes_key.rotation_rfc3339 +} diff --git a/03-backup/scaleway/infisical.tf b/03-backup/scaleway/infisical.tf new file mode 100644 index 0000000..e55d3d7 --- /dev/null +++ b/03-backup/scaleway/infisical.tf @@ -0,0 +1,23 @@ +resource "infisical_secret_folder" "backup" { + project_id = var.infisical_workspace_id + environment_slug = var.infisical_env_slug + folder_path = "/" + name = trimprefix(var.infisical_folder_path, "/") + description = "Backup workload credentials (managed by terraform: 03-backup/scaleway/)." +} + +resource "infisical_secret" "access_key" { + name = "BACKUP_ACCESS_KEY" + value = scaleway_iam_api_key.kubernetes.access_key + env_slug = var.infisical_env_slug + workspace_id = var.infisical_workspace_id + folder_path = infisical_secret_folder.backup.path +} + +resource "infisical_secret" "secret_key" { + name = "BACKUP_SECRET_KEY" + value = scaleway_iam_api_key.kubernetes.secret_key + env_slug = var.infisical_env_slug + workspace_id = var.infisical_workspace_id + folder_path = infisical_secret_folder.backup.path +} diff --git a/03-backup/scaleway/main.tf b/03-backup/scaleway/main.tf new file mode 100644 index 0000000..cb2a1b1 --- /dev/null +++ b/03-backup/scaleway/main.tf @@ -0,0 +1,58 @@ +resource "scaleway_object_bucket" "backup" { + name = var.bucket_name + region = var.region + + versioning { + enabled = var.versioning_enabled + } + + lifecycle_rule { + id = "backup-retention" + enabled = true + + expiration { + days = var.retention_days + } + + # FinOps safeguard: avoids paying for stale superseded versions. + # The true backup retention policy (frequency, tiers, RTO/RPO) will be + # defined at the backup-solution layer (e.g. Velero schedule), not here. + noncurrent_version_expiration { + noncurrent_days = var.noncurrent_version_expiry_days + } + + dynamic "transition" { + for_each = var.cold_storage_enabled ? [var.cold_storage_transition_days] : [] + content { + days = transition.value + storage_class = "GLACIER" + } + } + } + + # Deletion intentionally NOT protected at the provider level — see spec FR-014. + # Bucket deletion is a manual-only, human-operator action with admin credentials. + # Scaleway bucket policies do not support s3:DeleteBucket as an action, so the + # protection relies on two layers: (1) prevent_destroy below blocks terraform destroy, + # (2) no destroy trigger in the backup CI workflow blocks automated deletion. + + lifecycle { + prevent_destroy = true + + precondition { + condition = !var.cold_storage_enabled || var.cold_storage_transition_days < var.retention_days + error_message = "cold_storage_transition_days (${var.cold_storage_transition_days}) must be strictly less than retention_days (${var.retention_days}) when cold_storage_enabled is true. See spec FR-016." + } + } +} + +resource "scaleway_object_bucket_server_side_encryption_configuration" "backup" { + bucket = scaleway_object_bucket.backup.name + region = var.region + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} diff --git a/03-backup/scaleway/outputs.tf b/03-backup/scaleway/outputs.tf new file mode 100644 index 0000000..9b94874 --- /dev/null +++ b/03-backup/scaleway/outputs.tf @@ -0,0 +1,19 @@ +output "bucket_name" { + description = "Provisioned backup bucket name." + value = scaleway_object_bucket.backup.name +} + +output "bucket_region" { + description = "Region the backup bucket was created in." + value = scaleway_object_bucket.backup.region +} + +output "bucket_endpoint" { + description = "S3-compatible endpoint URL for the backup bucket." + value = "https://s3.${var.region}.scw.cloud/${var.bucket_name}" +} + +output "workload_access_key" { + description = "Public access key for the scoped Kubernetes backup workload identity. The secret key is in Infisical only." + value = scaleway_iam_api_key.kubernetes.access_key +} diff --git a/03-backup/scaleway/variables.tf b/03-backup/scaleway/variables.tf new file mode 100644 index 0000000..a45b15f --- /dev/null +++ b/03-backup/scaleway/variables.tf @@ -0,0 +1,88 @@ +variable "bucket_name" { + description = "Backup bucket name. Must include the environment name (e.g. backup-dev-id). See spec FR-015." + type = string +} + +variable "region" { + description = "Scaleway region for the bucket." + type = string + default = "fr-par" +} + +# ── Lifecycle ──────────────────────────────────────────────────────────────── + +variable "versioning_enabled" { + description = "Enable bucket versioning. Once enabled, can only be suspended, never disabled." + type = bool + default = true +} + +variable "retention_days" { + description = "Days before current-version objects expire." + type = number + default = 365 +} + +variable "noncurrent_version_expiry_days" { + description = "Days before non-current object versions are deleted." + type = number + default = 30 +} + +variable "cold_storage_enabled" { + description = "Enable transition of objects to GLACIER storage class." + type = bool + default = true +} + +variable "cold_storage_transition_days" { + description = "Days before objects are transitioned to GLACIER. Only evaluated when cold_storage_enabled = true. Must be less than retention_days (FR-016)." + type = number + default = 90 +} + +# ── Identity ───────────────────────────────────────────────────────────────── + +variable "project_id" { + description = "Scaleway project ID for bucket and IAM resource scoping." + type = string +} + +variable "ci_application_id" { + description = "IAM application ID of the github-ci identity (from 01-iam/bootstrap/scaleway outputs). Reserved for a future bucket policy Deny statement if Scaleway adds s3:DeleteBucket support." + type = string + default = "" +} + +# ── Infisical ──────────────────────────────────────────────────────────────── + +variable "infisical_workspace_id" { + description = "Infisical project (workspace) ID." + type = string + default = "7ecb6ed4-058a-46cd-ac9f-7e792469cf0f" +} + +variable "infisical_env_slug" { + description = "Infisical environment slug." + type = string + default = "staging" +} + +variable "infisical_folder_path" { + description = "Infisical folder path where backup workload credentials are written." + type = string + default = "/backup" +} + +variable "infisical_client_id" { + description = "Infisical universal auth client ID. Set for local development; leave empty when using OIDC in CI." + type = string + default = "" +} + +variable "infisical_client_secret" { + description = "Infisical universal auth client secret. Set for local development; leave empty when using OIDC in CI." + type = string + sensitive = true + default = "" +} diff --git a/03-backup/scaleway/version.tf b/03-backup/scaleway/version.tf new file mode 100644 index 0000000..bff9de7 --- /dev/null +++ b/03-backup/scaleway/version.tf @@ -0,0 +1,35 @@ +terraform { + backend "s3" { + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + workspace_key_prefix = "backup/scaleway" + key = "terraform.tfstate" + encrypt = true + use_lockfile = true + } + + required_providers { + scaleway = { + source = "scaleway/scaleway" + version = "~> 2.0" + } + infisical = { + source = "infisical/infisical" + version = "~> 0.16" + } + time = { + source = "hashicorp/time" + version = "~> 0.12" + } + } +} + +provider "scaleway" {} + +provider "infisical" { + auth = { + # Uncomment `universal` and comment `oidc` when running terraform locally. + # universal = {} + oidc = {} + } +} diff --git a/CLAUDE.md b/CLAUDE.md index f220d55..76ba276 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,7 +142,8 @@ The CI **identity & governance foundation**: **keyless GitHub-OIDC → AWS** acc For additional context about technologies to be used, project structure, -shell commands, and other important information, read the current plan +shell commands, and other important information, read the current plan: +`specs/001-backup-s3-foundation/plan.md` ## Roadmap conventions diff --git a/mise.toml b/mise.toml index 046867d..bc2853d 100644 --- a/mise.toml +++ b/mise.toml @@ -67,4 +67,5 @@ terraform -chdir=01-iam/bootstrap/infisical providers lock -platform=dar terraform -chdir=01-iam/ci-managed/aws-state-access providers lock -platform=darwin_arm64 -platform=linux_amd64 terraform -chdir=02-cluster/local providers lock -platform=darwin_arm64 -platform=linux_amd64 terraform -chdir=02-cluster/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=03-backup/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 """