@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.
- package/README.md +107 -48
- package/dist/bootstrap/github.js +97 -37
- package/dist/bootstrap/infisical.js +70 -21
- package/dist/bootstrap/scaleway.js +102 -17
- package/dist/config.js +124 -14
- package/dist/contracts.js +46 -0
- package/dist/generate.js +60 -4
- package/dist/index.js +239 -128
- package/dist/prompts.js +270 -63
- package/dist/ui.js +18 -6
- package/package.json +1 -1
- package/templates/.github/scripts/sync-secrets.sh +65 -0
- package/templates/.github/workflows/terraform-drift.yml +19 -1
- package/templates/.github/workflows/terraform-plan.yml +19 -1
- package/templates/README.md +56 -42
- package/templates/_partials/apply-header.yml +33 -0
- package/templates/_partials/apply-job.yml +34 -0
- package/templates/env.tfvars +8 -0
- package/templates/main.tf +12 -1
- package/templates/modules/app_stack/main.tf +34 -0
- package/templates/modules/app_stack/outputs.tf +18 -0
- package/templates/modules/app_stack/variables.tf +5 -0
- package/templates/outputs.tf +22 -0
- package/templates/variables.tf +8 -2
- package/templates/.github/scripts/sync-database-url.sh +0 -49
- package/templates/.github/workflows/terraform-apply.yml +0 -103
- package/templates/prod.tfvars +0 -7
- package/templates/staging.tfvars +0 -7
package/templates/README.md
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 (
|
|
40
|
-
merge/push main -> apply
|
|
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
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
59
|
-
|
|
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__
|
|
66
|
-
docker build -t rg.__REGION__.scw.cloud/__PROJECT_NAME__
|
|
67
|
-
docker push rg.__REGION__.scw.cloud/__PROJECT_NAME__
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
77
|
-
|
|
81
|
+
placeholders; replace them with real values. The app reads everything from
|
|
82
|
+
its environment:
|
|
78
83
|
|
|
79
|
-
| Variable |
|
|
84
|
+
| Variable | Where | Notes |
|
|
80
85
|
|---|---|---|
|
|
81
|
-
| `DATABASE_URL` |
|
|
82
|
-
| `BASIC_AUTH_USER` / `BASIC_AUTH_PASSWORD` |
|
|
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
|
|
94
|
+
## Basic Auth on non-production environments
|
|
89
95
|
|
|
90
96
|
Scaleway Serverless Containers expose public endpoints with no built-in auth,
|
|
91
|
-
so
|
|
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).
|
|
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
|
|
111
|
-
|
|
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
|
|
135
|
-
terraform plan -var-file
|
|
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** (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
package/templates/outputs.tf
CHANGED
|
@@ -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
|
+
}
|
package/templates/variables.tf
CHANGED
|
@@ -13,8 +13,8 @@ variable "environment" {
|
|
|
13
13
|
type = string
|
|
14
14
|
|
|
15
15
|
validation {
|
|
16
|
-
condition = contains(
|
|
17
|
-
error_message = "environment must be
|
|
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
|
package/templates/prod.tfvars
DELETED
package/templates/staging.tfvars
DELETED