@gambi97/keel-cli 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,12 +17,14 @@ here.
17
17
  | Container namespace + Serverless Container | Runs the app, scales to zero when idle |
18
18
  | Serverless SQL Database (Postgres) | One independent database per environment |
19
19
  | IAM application + API key | Dedicated least-privilege database credential for the app |
20
+ | Object Storage bucket (optional) | Per-environment file store, with its own least-privilege credential |
20
21
 
21
- Two environments, **staging** and **prod**, isolated via Terraform
22
- workspaces: same code, two separate states, differences confined to
23
- `staging.tfvars` / `prod.tfvars`. Staging expects the app to enforce Basic
24
- Auth (see below). State lives in the `__PROJECT_NAME__-tfstate` bucket on
25
- Scaleway Object Storage.
22
+ Each environment is isolated via a Terraform **workspace**: same code, one
23
+ independent state per environment, differences confined to the committed
24
+ `<env>.tfvars` files (one per environment — look at the `*.tfvars` in this
25
+ repo to see which ones your project has). Non-production environments expect
26
+ the app to enforce Basic Auth (see below). State lives in the
27
+ `__PROJECT_NAME__-tfstate` bucket on Scaleway Object Storage.
26
28
 
27
29
  **There are no secrets in this repository**, and none should ever be added.
28
30
  Credentials live in GitHub Actions encrypted secrets (CI) and in Infisical
@@ -31,21 +33,24 @@ Credentials live in GitHub Actions encrypted secrets (CI) and in Infisical
31
33
  > Naming heads-up: `backend.tf` configures the Terraform **state backend**,
32
34
  > the bucket where Terraform records what it has created. It has nothing to
33
35
  > do with your application's backend, which is just the Docker image you
34
- > deploy.
36
+ > deploy. The optional Object Storage bucket above is a separate,
37
+ > application-facing file store.
35
38
 
36
39
  ## How deploys work
37
40
 
38
41
  ```
39
- PR to main -> terraform fmt + validate + plan (staging & prod), shown as PR checks
40
- merge/push main -> apply staging -> manual approval (production gate) -> apply prod
42
+ PR to main -> terraform fmt + validate + plan (every environment), shown as PR checks
43
+ merge/push main -> apply each environment in order; any gated environment (production) waits for manual approval
41
44
  ```
42
45
 
43
46
  - `main` is protected: PRs need a green plan, force pushes are blocked.
44
- - The prod apply waits for approval on the `production` GitHub Environment.
45
- - After each apply, the pipeline writes a ready-to-use Postgres connection
46
- string to Infisical as `DATABASE_URL` for that environment. The credential
47
- inside it is a dedicated IAM application that can only read/write the
48
- database (least privilege), not your main API key.
47
+ - Environments apply in order, each `needs:` the previous one; a gated
48
+ environment (production) waits for approval on its GitHub Environment.
49
+ - After each apply, the pipeline syncs the secrets Terraform produced to
50
+ Infisical for that environment: a ready-to-use Postgres `DATABASE_URL`
51
+ (whose credential is a dedicated IAM application that can only read/write
52
+ the database, not your main API key) and, when Object Storage is enabled,
53
+ the `S3_*` coordinates.
49
54
  - State is locked during applies (S3-native locking, `use_lockfile`), so
50
55
  concurrent runs cannot corrupt it.
51
56
  - A scheduled **drift detection** workflow (Monday 06:00 UTC, or manual via
@@ -55,43 +60,45 @@ merge/push main -> apply staging -> manual approval (production gate) -> apply
55
60
  ## First deploy
56
61
 
57
62
  1. **Merge or push to `main`.** The first pipeline run creates registry and
58
- databases. The Serverless Container is intentionally skipped until an
59
- image exists (`container_image` is empty), so the first apply is green.
63
+ databases (and the Object Storage bucket, if enabled). The Serverless
64
+ Container is intentionally skipped until an image exists (`container_image`
65
+ is empty), so the first apply is green.
60
66
 
61
67
  2. **Build and push the app image** (any Dockerfile, listening on port 8080
62
- by default):
68
+ by default), replacing `<env>` with your target environment (e.g. staging):
63
69
 
64
70
  ```sh
65
- docker login rg.__REGION__.scw.cloud/__PROJECT_NAME__-staging -u nologin --password-stdin <<< "$SCW_SECRET_KEY"
66
- docker build -t rg.__REGION__.scw.cloud/__PROJECT_NAME__-staging/app:latest .
67
- docker push rg.__REGION__.scw.cloud/__PROJECT_NAME__-staging/app:latest
71
+ docker login rg.__REGION__.scw.cloud/__PROJECT_NAME__-<env> -u nologin --password-stdin <<< "$SCW_SECRET_KEY"
72
+ docker build -t rg.__REGION__.scw.cloud/__PROJECT_NAME__-<env>/app:latest .
73
+ docker push rg.__REGION__.scw.cloud/__PROJECT_NAME__-<env>/app:latest
68
74
  ```
69
75
 
70
- 3. **Point the container at the image**: set `container_image` in
71
- `staging.tfvars` (and later in `prod.tfvars`, using the prod registry),
72
- open a PR, review the plan, merge. The apply creates the container and
73
- prints its URL (`terraform output container_url`).
76
+ 3. **Point the container at the image**: set `container_image` in the target
77
+ `<env>.tfvars`, open a PR, review the plan, merge. The apply creates the
78
+ container and prints its URL (`terraform output container_url`).
74
79
 
75
80
  4. **Fill in the real secrets in Infisical.** The bootstrap seeded
76
- placeholders; replace them with production values. The app reads
77
- everything from its environment:
81
+ placeholders; replace them with real values. The app reads everything from
82
+ its environment:
78
83
 
79
- | Variable | Environment | Notes |
84
+ | Variable | Where | Notes |
80
85
  |---|---|---|
81
- | `DATABASE_URL` | staging + prod | Complete connection string, auto-updated by the pipeline after each apply. No action needed |
82
- | `BASIC_AUTH_USER` / `BASIC_AUTH_PASSWORD` | staging | The app must enforce these when `BASIC_AUTH_ENABLED=true` |
86
+ | `DATABASE_URL` | every environment | Complete connection string, auto-updated by the pipeline after each apply. No action needed |
87
+ | `BASIC_AUTH_USER` / `BASIC_AUTH_PASSWORD` | non-production | The app must enforce these when `BASIC_AUTH_ENABLED=true` |
88
+ | `S3_BUCKET` / `S3_ENDPOINT` / `S3_REGION` / `S3_ACCESS_KEY` / `S3_SECRET_KEY` | every environment (if Object Storage enabled) | Auto-updated by the pipeline after each apply |
83
89
 
84
90
  Every secret in the Infisical environment is injected into the container
85
91
  as a secret environment variable, so adding an app secret is: add it in
86
92
  Infisical, re-run the apply (any merge to `main`).
87
93
 
88
- ## Basic Auth on staging
94
+ ## Basic Auth on non-production environments
89
95
 
90
96
  Scaleway Serverless Containers expose public endpoints with no built-in auth,
91
- so staging protection is enforced **by the application**: the container
97
+ so non-production protection is enforced **by the application**: the container
92
98
  receives `BASIC_AUTH_ENABLED=true` plus the credentials from Infisical, and
93
99
  the app is expected to respond `401` without them (a few lines of middleware
94
- in any framework). Prod does not set `BASIC_AUTH_ENABLED`.
100
+ in any framework). Production does not set `BASIC_AUTH_ENABLED`. Whether an
101
+ environment enables it is the `enable_basic_auth` flag in its `<env>.tfvars`.
95
102
 
96
103
  ## Day-2 operations
97
104
 
@@ -107,15 +114,21 @@ in any framework). Prod does not set `BASIC_AUTH_ENABLED`.
107
114
  secrets (repo Settings > Secrets and variables > Actions).
108
115
  - **Add a custom domain**: add a `scaleway_container_domain` resource in
109
116
  `modules/app_stack` pointing at the container, plus your DNS record.
110
- - **Add an environment**: add a workspace, an `<env>.tfvars`, an Infisical
111
- environment with the same slug, and mirror one job in each workflow.
117
+ - **Add an environment**: add a `<env>.tfvars` at the repo root (the plan and
118
+ drift workflows discover environments from the tfvars files automatically),
119
+ create an Infisical environment with the same slug, and add a job in
120
+ `.github/workflows/terraform-apply.yml` (mirror an existing one and set its
121
+ `needs:` to chain after the previous environment). Note: the new `plan
122
+ (<env>)` check becomes required on `main` only after you add it in the
123
+ branch-protection settings.
112
124
  - **Pin provider versions**: after any local `terraform init`, commit the
113
125
  generated `.terraform.lock.hcl` so CI resolves the exact same provider
114
126
  builds on every run.
115
127
 
116
128
  ## Running Terraform locally (optional)
117
129
 
118
- CI is the source of truth, but plans can be run locally:
130
+ CI is the source of truth, but plans can be run locally (replace `<env>` with
131
+ one of your environments):
119
132
 
120
133
  ```sh
121
134
  cp backend.hcl.example backend.hcl
@@ -131,34 +144,35 @@ export TF_VAR_infisical_project_id=<infisical-project-id>
131
144
  export TF_VAR_infisical_host=https://app.infisical.com
132
145
 
133
146
  terraform init -backend-config=backend.hcl
134
- terraform workspace select -or-create staging
135
- terraform plan -var-file=staging.tfvars
147
+ terraform workspace select -or-create <env>
148
+ terraform plan -var-file=<env>.tfvars
136
149
  ```
137
150
 
138
151
  Avoid local applies: they race against CI on the same state.
139
152
 
140
153
  ## Configuration reference
141
154
 
142
- - **Committed tfvars** (`staging.tfvars`, `prod.tfvars`): non-sensitive knobs
143
- only (name, region, scaling, image).
155
+ - **Committed tfvars** (`<env>.tfvars`, one per environment): non-sensitive
156
+ knobs only (name, region, scaling, image, `enable_basic_auth`,
157
+ `enable_object_storage`).
144
158
  - **GitHub Actions secrets**: `SCW_ACCESS_KEY`, `SCW_SECRET_KEY`,
145
159
  `SCW_DEFAULT_PROJECT_ID`, `SCW_DEFAULT_ORGANIZATION_ID`,
146
160
  `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`. The workflows reuse the
147
161
  Scaleway keys as `AWS_*` for the state backend.
148
162
  - **GitHub Actions variables**: `TF_STATE_BUCKET`, `SCW_REGION`,
149
163
  `INFISICAL_PROJECT_ID`, `INFISICAL_HOST`.
150
- - **Infisical**: one environment per workspace (`staging`, `prod`), all
151
- application secrets.
164
+ - **Infisical**: one environment per workspace, all application secrets.
152
165
 
153
166
  ## Troubleshooting
154
167
 
155
168
  - **Plan fails with 403 on the state bucket**: the `SCW_*` secrets don't
156
169
  match a Scaleway key with Object Storage access.
157
170
  - **Plan fails reading Infisical secrets**: check `INFISICAL_PROJECT_ID` and
158
- that the machine identity has access to the project and both environments.
171
+ that the machine identity has access to the project and every environment.
159
172
  - **Apply green but no container**: expected until `container_image` is set.
160
173
  - **App can't reach the database**: `DATABASE_URL` is only synced after an
161
174
  apply; check the value in Infisical for that environment. It must contain
162
175
  a username and password (the dedicated IAM credential), not placeholders.
163
176
  - **Apply fails creating IAM resources**: the CI Scaleway key needs IAM
164
- permissions (IAMManager) to create the app's database credential.
177
+ permissions (IAMManager) to create the app's database (and Object Storage)
178
+ credential.
@@ -0,0 +1,33 @@
1
+ name: Terraform apply
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ concurrency:
11
+ group: terraform-apply
12
+ cancel-in-progress: false
13
+
14
+ env:
15
+ # The Scaleway keys double as S3 credentials for the state backend, so the
16
+ # same secret feeds both variables: one credential, one place to rotate.
17
+ AWS_ACCESS_KEY_ID: ${{ secrets.SCW_ACCESS_KEY }}
18
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.SCW_SECRET_KEY }}
19
+ SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
20
+ SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
21
+ SCW_DEFAULT_PROJECT_ID: ${{ secrets.SCW_DEFAULT_PROJECT_ID }}
22
+ SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_DEFAULT_ORGANIZATION_ID }}
23
+ INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
24
+ INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
25
+ INFISICAL_PROJECT_ID: ${{ vars.INFISICAL_PROJECT_ID }}
26
+ INFISICAL_HOST: ${{ vars.INFISICAL_HOST }}
27
+ TF_VAR_infisical_client_id: ${{ secrets.INFISICAL_CLIENT_ID }}
28
+ TF_VAR_infisical_client_secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
29
+ TF_VAR_infisical_project_id: ${{ vars.INFISICAL_PROJECT_ID }}
30
+ TF_VAR_infisical_host: ${{ vars.INFISICAL_HOST }}
31
+ TF_IN_AUTOMATION: "true"
32
+
33
+ jobs:
@@ -0,0 +1,34 @@
1
+ apply-__ENV_SLUG__:
2
+ name: apply (__ENV_SLUG__)
3
+ runs-on: ubuntu-latest
4
+ __NEEDS_LINE__ # The GitHub environment may carry a required-reviewer rule, gating this job.
5
+ environment: __GH_ENVIRONMENT__
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+
9
+ - uses: hashicorp/setup-terraform@v3
10
+ with:
11
+ terraform_version: "~1.12"
12
+ terraform_wrapper: false
13
+
14
+ - name: Write backend config
15
+ run: |
16
+ cat > backend.hcl <<EOF
17
+ bucket = "${{ vars.TF_STATE_BUCKET }}"
18
+ region = "${{ vars.SCW_REGION }}"
19
+ endpoints = {
20
+ s3 = "https://s3.${{ vars.SCW_REGION }}.scw.cloud"
21
+ }
22
+ EOF
23
+
24
+ - name: Terraform init
25
+ run: terraform init -input=false -backend-config=backend.hcl
26
+
27
+ - name: Select workspace
28
+ run: terraform workspace select -or-create __ENV_SLUG__
29
+
30
+ - name: Terraform apply
31
+ run: terraform apply -input=false -auto-approve -var-file=__ENV_SLUG__.tfvars
32
+
33
+ - name: Sync secrets to Infisical
34
+ run: ./.github/scripts/sync-secrets.sh __ENV_SLUG__
@@ -0,0 +1,8 @@
1
+ # Non-sensitive __ENVIRONMENT__ configuration. Safe to commit.
2
+ project_name = "__PROJECT_NAME__"
3
+ region = "__REGION__"
4
+ environment = "__ENVIRONMENT__"
5
+ enable_basic_auth = __ENABLE_BASIC_AUTH__
6
+ enable_object_storage = __ENABLE_OBJECT_STORAGE__
7
+ min_scale = __MIN_SCALE__
8
+ max_scale = __MAX_SCALE__
package/templates/main.tf CHANGED
@@ -3,6 +3,16 @@ data "infisical_secrets" "app" {
3
3
  env_slug = var.environment
4
4
  workspace_id = var.infisical_project_id
5
5
  folder_path = "/"
6
+
7
+ lifecycle {
8
+ # Guards against applying one environment's tfvars into another
9
+ # environment's state (e.g. prod.tfvars while the staging workspace is
10
+ # selected), which would create prod-named resources in staging state.
11
+ precondition {
12
+ condition = terraform.workspace == var.environment
13
+ error_message = "The selected Terraform workspace must match var.environment: run `terraform workspace select <env>` before plan/apply with <env>.tfvars."
14
+ }
15
+ }
6
16
  }
7
17
 
8
18
  module "app_stack" {
@@ -20,7 +30,8 @@ module "app_stack" {
20
30
  db_min_cpu = var.db_min_cpu
21
31
  db_max_cpu = var.db_max_cpu
22
32
 
23
- enable_basic_auth = var.enable_basic_auth
33
+ enable_basic_auth = var.enable_basic_auth
34
+ enable_object_storage = var.enable_object_storage
24
35
 
25
36
  secret_environment_variables = {
26
37
  for name, secret in data.infisical_secrets.app.secrets : name => secret.value
@@ -44,6 +44,40 @@ resource "scaleway_iam_api_key" "db" {
44
44
  description = "Database credential for ${local.name} (managed by Terraform)"
45
45
  }
46
46
 
47
+ # Optional Object Storage bucket for application files (uploads, assets, …),
48
+ # with a dedicated least-privilege credential just like the database: this
49
+ # application can reach Object Storage and nothing else. Its access/secret key
50
+ # and the bucket coordinates are synced to Infisical after each apply.
51
+ resource "scaleway_object_bucket" "files" {
52
+ count = var.enable_object_storage ? 1 : 0
53
+ name = "${local.name}-files"
54
+ region = var.region
55
+ }
56
+
57
+ resource "scaleway_iam_application" "storage" {
58
+ count = var.enable_object_storage ? 1 : 0
59
+ name = "${local.name}-storage"
60
+ description = "Object Storage access for ${local.name} (managed by Terraform)"
61
+ }
62
+
63
+ resource "scaleway_iam_policy" "storage_access" {
64
+ count = var.enable_object_storage ? 1 : 0
65
+ name = "${local.name}-storage-access"
66
+ description = "Object Storage access for ${local.name}"
67
+ application_id = scaleway_iam_application.storage[0].id
68
+
69
+ rule {
70
+ project_ids = [scaleway_sdb_sql_database.this.project_id]
71
+ permission_set_names = ["ObjectStorageFullAccess"]
72
+ }
73
+ }
74
+
75
+ resource "scaleway_iam_api_key" "storage" {
76
+ count = var.enable_object_storage ? 1 : 0
77
+ application_id = scaleway_iam_application.storage[0].id
78
+ description = "Object Storage credential for ${local.name} (managed by Terraform)"
79
+ }
80
+
47
81
  # The container is gated on an image being available: registry and database
48
82
  # are provisioned first, the container appears once an image has been pushed
49
83
  # and container_image is set.
@@ -21,3 +21,21 @@ output "database_url" {
21
21
  output "container_namespace_id" {
22
22
  value = scaleway_container_namespace.this.id
23
23
  }
24
+
25
+ output "object_bucket_name" {
26
+ value = var.enable_object_storage ? scaleway_object_bucket.files[0].name : null
27
+ }
28
+
29
+ output "object_bucket_endpoint" {
30
+ value = var.enable_object_storage ? "https://s3.${var.region}.scw.cloud" : null
31
+ }
32
+
33
+ output "object_storage_access_key" {
34
+ value = var.enable_object_storage ? scaleway_iam_api_key.storage[0].access_key : null
35
+ sensitive = true
36
+ }
37
+
38
+ output "object_storage_secret_key" {
39
+ value = var.enable_object_storage ? scaleway_iam_api_key.storage[0].secret_key : null
40
+ sensitive = true
41
+ }
@@ -47,6 +47,11 @@ variable "enable_basic_auth" {
47
47
  type = bool
48
48
  }
49
49
 
50
+ variable "enable_object_storage" {
51
+ type = bool
52
+ default = false
53
+ }
54
+
50
55
  variable "secret_environment_variables" {
51
56
  description = "Secrets injected into the container, sourced from Infisical."
52
57
  type = map(string)
@@ -18,3 +18,25 @@ output "container_namespace_id" {
18
18
  description = "Scaleway Containers namespace ID."
19
19
  value = module.app_stack.container_namespace_id
20
20
  }
21
+
22
+ output "object_bucket_name" {
23
+ description = "Object Storage bucket for application files (null unless enabled)."
24
+ value = module.app_stack.object_bucket_name
25
+ }
26
+
27
+ # The complete set of secrets the pipeline pushes to Infisical after each apply:
28
+ # always DATABASE_URL, plus the S3_* coordinates when Object Storage is enabled.
29
+ output "infisical_secrets" {
30
+ description = "Secrets synced to Infisical after each apply."
31
+ sensitive = true
32
+ value = merge(
33
+ { DATABASE_URL = module.app_stack.database_url },
34
+ var.enable_object_storage ? {
35
+ S3_BUCKET = module.app_stack.object_bucket_name
36
+ S3_ENDPOINT = module.app_stack.object_bucket_endpoint
37
+ S3_REGION = var.region
38
+ S3_ACCESS_KEY = module.app_stack.object_storage_access_key
39
+ S3_SECRET_KEY = module.app_stack.object_storage_secret_key
40
+ } : {}
41
+ )
42
+ }
@@ -13,8 +13,8 @@ variable "environment" {
13
13
  type = string
14
14
 
15
15
  validation {
16
- condition = contains(["staging", "prod"], var.environment)
17
- error_message = "environment must be either \"staging\" or \"prod\"."
16
+ condition = contains(__ENV_SLUGS_TF__, var.environment)
17
+ error_message = "environment must be one of the configured environments."
18
18
  }
19
19
  }
20
20
 
@@ -72,6 +72,12 @@ variable "enable_basic_auth" {
72
72
  default = false
73
73
  }
74
74
 
75
+ variable "enable_object_storage" {
76
+ description = "When true, provision an Object Storage bucket for the app plus a dedicated least-privilege credential; its coordinates are synced to Infisical as S3_*."
77
+ type = bool
78
+ default = false
79
+ }
80
+
75
81
  variable "infisical_host" {
76
82
  description = "Infisical instance URL."
77
83
  type = string
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Pushes the ready-to-use database connection string (dedicated IAM
3
- # credential included) to Infisical as DATABASE_URL.
4
- # Usage: sync-database-url.sh <staging|prod>
5
- # Expects: INFISICAL_HOST, INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET,
6
- # INFISICAL_PROJECT_ID in the environment; terraform init already run.
7
- set -euo pipefail
8
-
9
- ENVIRONMENT="${1:?usage: sync-database-url.sh <staging|prod>}"
10
-
11
- DB_URL="$(terraform output -raw database_url)"
12
- if [ -z "$DB_URL" ]; then
13
- echo "No database_url in outputs, skipping Infisical sync."
14
- exit 0
15
- fi
16
- echo "::add-mask::$DB_URL"
17
-
18
- ACCESS_TOKEN="$(curl -sS --fail-with-body -X POST \
19
- "$INFISICAL_HOST/api/v1/auth/universal-auth/login" \
20
- -H "Content-Type: application/json" \
21
- -d "{\"clientId\":\"$INFISICAL_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CLIENT_SECRET\"}" \
22
- | jq -r .accessToken)"
23
- echo "::add-mask::$ACCESS_TOKEN"
24
-
25
- payload="$(jq -n \
26
- --arg workspaceId "$INFISICAL_PROJECT_ID" \
27
- --arg environment "$ENVIRONMENT" \
28
- --arg value "$DB_URL" \
29
- '{workspaceId: $workspaceId, environment: $environment, secretPath: "/", secretValue: $value, type: "shared"}')"
30
-
31
- # Update the placeholder seeded at bootstrap; create the secret if missing.
32
- status="$(curl -sS -o /dev/null -w "%{http_code}" -X PATCH \
33
- "$INFISICAL_HOST/api/v3/secrets/raw/DATABASE_URL" \
34
- -H "Authorization: Bearer $ACCESS_TOKEN" \
35
- -H "Content-Type: application/json" \
36
- -d "$payload")"
37
-
38
- if [ "$status" = "404" ]; then
39
- curl -sS --fail-with-body -o /dev/null -X POST \
40
- "$INFISICAL_HOST/api/v3/secrets/raw/DATABASE_URL" \
41
- -H "Authorization: Bearer $ACCESS_TOKEN" \
42
- -H "Content-Type: application/json" \
43
- -d "$payload"
44
- elif [ "$status" -ge 300 ]; then
45
- echo "Failed to update DATABASE_URL in Infisical (HTTP $status)" >&2
46
- exit 1
47
- fi
48
-
49
- echo "DATABASE_URL synced to Infisical ($ENVIRONMENT)."
@@ -1,103 +0,0 @@
1
- name: Terraform apply
2
-
3
- on:
4
- push:
5
- branches: [main]
6
-
7
- permissions:
8
- contents: read
9
-
10
- concurrency:
11
- group: terraform-apply
12
- cancel-in-progress: false
13
-
14
- env:
15
- # The Scaleway keys double as S3 credentials for the state backend, so the
16
- # same secret feeds both variables: one credential, one place to rotate.
17
- AWS_ACCESS_KEY_ID: ${{ secrets.SCW_ACCESS_KEY }}
18
- AWS_SECRET_ACCESS_KEY: ${{ secrets.SCW_SECRET_KEY }}
19
- SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
20
- SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
21
- SCW_DEFAULT_PROJECT_ID: ${{ secrets.SCW_DEFAULT_PROJECT_ID }}
22
- SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_DEFAULT_ORGANIZATION_ID }}
23
- INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
24
- INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
25
- INFISICAL_PROJECT_ID: ${{ vars.INFISICAL_PROJECT_ID }}
26
- INFISICAL_HOST: ${{ vars.INFISICAL_HOST }}
27
- TF_VAR_infisical_client_id: ${{ secrets.INFISICAL_CLIENT_ID }}
28
- TF_VAR_infisical_client_secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
29
- TF_VAR_infisical_project_id: ${{ vars.INFISICAL_PROJECT_ID }}
30
- TF_VAR_infisical_host: ${{ vars.INFISICAL_HOST }}
31
- TF_IN_AUTOMATION: "true"
32
-
33
- jobs:
34
- apply-staging:
35
- name: apply (staging)
36
- runs-on: ubuntu-latest
37
- environment: staging
38
- steps:
39
- - uses: actions/checkout@v4
40
-
41
- - uses: hashicorp/setup-terraform@v3
42
- with:
43
- terraform_version: "~1.12"
44
- terraform_wrapper: false
45
-
46
- - name: Write backend config
47
- run: |
48
- cat > backend.hcl <<EOF
49
- bucket = "${{ vars.TF_STATE_BUCKET }}"
50
- region = "${{ vars.SCW_REGION }}"
51
- endpoints = {
52
- s3 = "https://s3.${{ vars.SCW_REGION }}.scw.cloud"
53
- }
54
- EOF
55
-
56
- - name: Terraform init
57
- run: terraform init -input=false -backend-config=backend.hcl
58
-
59
- - name: Select workspace
60
- run: terraform workspace select -or-create staging
61
-
62
- - name: Terraform apply
63
- run: terraform apply -input=false -auto-approve -var-file=staging.tfvars
64
-
65
- - name: Sync DATABASE_URL to Infisical
66
- run: ./.github/scripts/sync-database-url.sh staging
67
-
68
- apply-prod:
69
- name: apply (prod)
70
- runs-on: ubuntu-latest
71
- needs: apply-staging
72
- # The "production" GitHub Environment carries a required-reviewer rule,
73
- # so this job waits for manual approval.
74
- environment: production
75
- steps:
76
- - uses: actions/checkout@v4
77
-
78
- - uses: hashicorp/setup-terraform@v3
79
- with:
80
- terraform_version: "~1.12"
81
- terraform_wrapper: false
82
-
83
- - name: Write backend config
84
- run: |
85
- cat > backend.hcl <<EOF
86
- bucket = "${{ vars.TF_STATE_BUCKET }}"
87
- region = "${{ vars.SCW_REGION }}"
88
- endpoints = {
89
- s3 = "https://s3.${{ vars.SCW_REGION }}.scw.cloud"
90
- }
91
- EOF
92
-
93
- - name: Terraform init
94
- run: terraform init -input=false -backend-config=backend.hcl
95
-
96
- - name: Select workspace
97
- run: terraform workspace select -or-create prod
98
-
99
- - name: Terraform apply
100
- run: terraform apply -input=false -auto-approve -var-file=prod.tfvars
101
-
102
- - name: Sync DATABASE_URL to Infisical
103
- run: ./.github/scripts/sync-database-url.sh prod
@@ -1,7 +0,0 @@
1
- # Non-sensitive production configuration. Safe to commit.
2
- project_name = "__PROJECT_NAME__"
3
- region = "__REGION__"
4
- environment = "prod"
5
- enable_basic_auth = false
6
- min_scale = __PROD_MIN_SCALE__
7
- max_scale = __PROD_MAX_SCALE__
@@ -1,7 +0,0 @@
1
- # Non-sensitive staging configuration. Safe to commit.
2
- project_name = "__PROJECT_NAME__"
3
- region = "__REGION__"
4
- environment = "staging"
5
- enable_basic_auth = true
6
- min_scale = __STAGING_MIN_SCALE__
7
- max_scale = __STAGING_MAX_SCALE__