@gambi97/keel-cli 0.1.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.
@@ -0,0 +1,103 @@
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
@@ -0,0 +1,91 @@
1
+ name: Drift detection
2
+
3
+ # Runs a read-only plan on a schedule and opens (or updates) an issue when the
4
+ # real infrastructure no longer matches the code, e.g. after a change made in
5
+ # the Scaleway console.
6
+
7
+ on:
8
+ schedule:
9
+ - cron: "0 6 * * 1" # every Monday, 06:00 UTC
10
+ workflow_dispatch: {}
11
+
12
+ permissions:
13
+ contents: read
14
+ issues: write
15
+
16
+ env:
17
+ # The Scaleway keys double as S3 credentials for the state backend, so the
18
+ # same secret feeds both variables: one credential, one place to rotate.
19
+ AWS_ACCESS_KEY_ID: ${{ secrets.SCW_ACCESS_KEY }}
20
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.SCW_SECRET_KEY }}
21
+ SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
22
+ SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
23
+ SCW_DEFAULT_PROJECT_ID: ${{ secrets.SCW_DEFAULT_PROJECT_ID }}
24
+ SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_DEFAULT_ORGANIZATION_ID }}
25
+ TF_VAR_infisical_client_id: ${{ secrets.INFISICAL_CLIENT_ID }}
26
+ TF_VAR_infisical_client_secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
27
+ TF_VAR_infisical_project_id: ${{ vars.INFISICAL_PROJECT_ID }}
28
+ TF_VAR_infisical_host: ${{ vars.INFISICAL_HOST }}
29
+ TF_IN_AUTOMATION: "true"
30
+
31
+ jobs:
32
+ drift:
33
+ name: drift (${{ matrix.environment }})
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ environment: [staging, prod]
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - uses: hashicorp/setup-terraform@v3
43
+ with:
44
+ terraform_version: "~1.12"
45
+ terraform_wrapper: false
46
+
47
+ - name: Write backend config
48
+ run: |
49
+ cat > backend.hcl <<EOF
50
+ bucket = "${{ vars.TF_STATE_BUCKET }}"
51
+ region = "${{ vars.SCW_REGION }}"
52
+ endpoints = {
53
+ s3 = "https://s3.${{ vars.SCW_REGION }}.scw.cloud"
54
+ }
55
+ EOF
56
+
57
+ - name: Terraform init
58
+ run: terraform init -input=false -backend-config=backend.hcl
59
+
60
+ - name: Select workspace
61
+ run: terraform workspace select -or-create ${{ matrix.environment }}
62
+
63
+ - name: Terraform plan (drift check)
64
+ id: plan
65
+ # -lock=false: read-only check, must never contend with a real apply.
66
+ run: |
67
+ set +e
68
+ terraform plan -input=false -lock=false -detailed-exitcode \
69
+ -var-file=${{ matrix.environment }}.tfvars -no-color > plan.txt 2>&1
70
+ code=$?
71
+ set -e
72
+ echo "exitcode=$code" >> "$GITHUB_OUTPUT"
73
+ tail -50 plan.txt
74
+ if [ "$code" = "1" ]; then exit 1; fi
75
+
76
+ - name: Report drift
77
+ if: steps.plan.outputs.exitcode == '2'
78
+ env:
79
+ GH_TOKEN: ${{ github.token }}
80
+ ENVIRONMENT: ${{ matrix.environment }}
81
+ run: |
82
+ title="Infrastructure drift detected (${ENVIRONMENT})"
83
+ body="Scheduled \`terraform plan\` for **${ENVIRONMENT}** found differences between the code and the real infrastructure.
84
+
85
+ Review the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) and either revert the manual change or align the code with a PR."
86
+ existing=$(gh issue list --state open --search "in:title \"$title\"" --json number --jq '.[0].number')
87
+ if [ -n "$existing" ]; then
88
+ gh issue comment "$existing" --body "Drift still present on $(date -u +%F). See ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
89
+ else
90
+ gh issue create --title "$title" --body "$body"
91
+ fi
@@ -0,0 +1,63 @@
1
+ name: Terraform plan
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ env:
11
+ # The Scaleway keys double as S3 credentials for the state backend, so the
12
+ # same secret feeds both variables: one credential, one place to rotate.
13
+ AWS_ACCESS_KEY_ID: ${{ secrets.SCW_ACCESS_KEY }}
14
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.SCW_SECRET_KEY }}
15
+ SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
16
+ SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
17
+ SCW_DEFAULT_PROJECT_ID: ${{ secrets.SCW_DEFAULT_PROJECT_ID }}
18
+ SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_DEFAULT_ORGANIZATION_ID }}
19
+ TF_VAR_infisical_client_id: ${{ secrets.INFISICAL_CLIENT_ID }}
20
+ TF_VAR_infisical_client_secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
21
+ TF_VAR_infisical_project_id: ${{ vars.INFISICAL_PROJECT_ID }}
22
+ TF_VAR_infisical_host: ${{ vars.INFISICAL_HOST }}
23
+ TF_IN_AUTOMATION: "true"
24
+
25
+ jobs:
26
+ plan:
27
+ name: plan (${{ matrix.environment }})
28
+ runs-on: ubuntu-latest
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ environment: [staging, prod]
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - uses: hashicorp/setup-terraform@v3
37
+ with:
38
+ terraform_version: "~1.12"
39
+
40
+ - name: Terraform fmt
41
+ run: terraform fmt -check -recursive
42
+
43
+ - name: Write backend config
44
+ run: |
45
+ cat > backend.hcl <<EOF
46
+ bucket = "${{ vars.TF_STATE_BUCKET }}"
47
+ region = "${{ vars.SCW_REGION }}"
48
+ endpoints = {
49
+ s3 = "https://s3.${{ vars.SCW_REGION }}.scw.cloud"
50
+ }
51
+ EOF
52
+
53
+ - name: Terraform init
54
+ run: terraform init -input=false -backend-config=backend.hcl
55
+
56
+ - name: Select workspace
57
+ run: terraform workspace select -or-create ${{ matrix.environment }}
58
+
59
+ - name: Terraform validate
60
+ run: terraform validate
61
+
62
+ - name: Terraform plan
63
+ run: terraform plan -input=false -var-file=${{ matrix.environment }}.tfvars
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 __PROJECT_NAME__ contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Serverless infrastructure on [Scaleway](https://www.scaleway.com), managed
4
+ with Terraform, deployed by GitHub Actions, with application secrets in
5
+ [Infisical](https://infisical.com). Generated by
6
+ [keel](https://github.com/Gambi97/keel).
7
+
8
+ This repository contains **infrastructure only**. The application itself
9
+ lives elsewhere: it is any Docker image pushed to the registry provisioned
10
+ here.
11
+
12
+ ## What this repo manages
13
+
14
+ | Resource | Purpose |
15
+ |---|---|
16
+ | Registry namespace | Private container registry for the app images |
17
+ | Container namespace + Serverless Container | Runs the app, scales to zero when idle |
18
+ | Serverless SQL Database (Postgres) | One independent database per environment |
19
+ | IAM application + API key | Dedicated least-privilege database credential for the app |
20
+
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.
26
+
27
+ **There are no secrets in this repository**, and none should ever be added.
28
+ Credentials live in GitHub Actions encrypted secrets (CI) and in Infisical
29
+ (application).
30
+
31
+ > Naming heads-up: `backend.tf` configures the Terraform **state backend**,
32
+ > the bucket where Terraform records what it has created. It has nothing to
33
+ > do with your application's backend, which is just the Docker image you
34
+ > deploy.
35
+
36
+ ## How deploys work
37
+
38
+ ```
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
41
+ ```
42
+
43
+ - `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.
49
+ - State is locked during applies (S3-native locking, `use_lockfile`), so
50
+ concurrent runs cannot corrupt it.
51
+ - A scheduled **drift detection** workflow (Monday 06:00 UTC, or manual via
52
+ the Actions tab) runs a read-only plan and opens an issue if the real
53
+ infrastructure diverged from the code.
54
+
55
+ ## First deploy
56
+
57
+ 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.
60
+
61
+ 2. **Build and push the app image** (any Dockerfile, listening on port 8080
62
+ by default):
63
+
64
+ ```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
68
+ ```
69
+
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`).
74
+
75
+ 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:
78
+
79
+ | Variable | Environment | Notes |
80
+ |---|---|---|
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` |
83
+
84
+ Every secret in the Infisical environment is injected into the container
85
+ as a secret environment variable, so adding an app secret is: add it in
86
+ Infisical, re-run the apply (any merge to `main`).
87
+
88
+ ## Basic Auth on staging
89
+
90
+ Scaleway Serverless Containers expose public endpoints with no built-in auth,
91
+ so staging protection is enforced **by the application**: the container
92
+ receives `BASIC_AUTH_ENABLED=true` plus the credentials from Infisical, and
93
+ 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`.
95
+
96
+ ## Day-2 operations
97
+
98
+ - **Scale**: adjust `min_scale` / `max_scale` (instances) in the tfvars;
99
+ `cpu_limit`, `memory_limit`, `container_port`, `db_min_cpu` / `db_max_cpu`
100
+ are variables with sensible defaults you can override the same way.
101
+ - **Rotate an app secret**: change it in Infisical, trigger an apply. No
102
+ commit needed.
103
+ - **Rotate the database credential**: `terraform apply
104
+ -replace=module.app_stack.scaleway_iam_api_key.db` (or via a PR touching
105
+ it); the pipeline re-syncs `DATABASE_URL` automatically.
106
+ - **Rotate Scaleway/Infisical credentials**: update the GitHub Actions
107
+ secrets (repo Settings > Secrets and variables > Actions).
108
+ - **Add a custom domain**: add a `scaleway_container_domain` resource in
109
+ `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.
112
+ - **Pin provider versions**: after any local `terraform init`, commit the
113
+ generated `.terraform.lock.hcl` so CI resolves the exact same provider
114
+ builds on every run.
115
+
116
+ ## Running Terraform locally (optional)
117
+
118
+ CI is the source of truth, but plans can be run locally:
119
+
120
+ ```sh
121
+ cp backend.hcl.example backend.hcl
122
+ export AWS_ACCESS_KEY_ID=<scaleway-access-key> # state backend
123
+ export AWS_SECRET_ACCESS_KEY=<scaleway-secret-key>
124
+ export SCW_ACCESS_KEY=<scaleway-access-key> # provider
125
+ export SCW_SECRET_KEY=<scaleway-secret-key>
126
+ export SCW_DEFAULT_PROJECT_ID=<project-id>
127
+ export SCW_DEFAULT_ORGANIZATION_ID=<organization-id>
128
+ export TF_VAR_infisical_client_id=<machine-identity-client-id>
129
+ export TF_VAR_infisical_client_secret=<machine-identity-client-secret>
130
+ export TF_VAR_infisical_project_id=<infisical-project-id>
131
+ export TF_VAR_infisical_host=https://app.infisical.com
132
+
133
+ terraform init -backend-config=backend.hcl
134
+ terraform workspace select -or-create staging
135
+ terraform plan -var-file=staging.tfvars
136
+ ```
137
+
138
+ Avoid local applies: they race against CI on the same state.
139
+
140
+ ## Configuration reference
141
+
142
+ - **Committed tfvars** (`staging.tfvars`, `prod.tfvars`): non-sensitive knobs
143
+ only (name, region, scaling, image).
144
+ - **GitHub Actions secrets**: `SCW_ACCESS_KEY`, `SCW_SECRET_KEY`,
145
+ `SCW_DEFAULT_PROJECT_ID`, `SCW_DEFAULT_ORGANIZATION_ID`,
146
+ `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`. The workflows reuse the
147
+ Scaleway keys as `AWS_*` for the state backend.
148
+ - **GitHub Actions variables**: `TF_STATE_BUCKET`, `SCW_REGION`,
149
+ `INFISICAL_PROJECT_ID`, `INFISICAL_HOST`.
150
+ - **Infisical**: one environment per workspace (`staging`, `prod`), all
151
+ application secrets.
152
+
153
+ ## Troubleshooting
154
+
155
+ - **Plan fails with 403 on the state bucket**: the `SCW_*` secrets don't
156
+ match a Scaleway key with Object Storage access.
157
+ - **Plan fails reading Infisical secrets**: check `INFISICAL_PROJECT_ID` and
158
+ that the machine identity has access to the project and both environments.
159
+ - **Apply green but no container**: expected until `container_image` is set.
160
+ - **App can't reach the database**: `DATABASE_URL` is only synced after an
161
+ apply; check the value in Infisical for that environment. It must contain
162
+ a username and password (the dedicated IAM credential), not placeholders.
163
+ - **Apply fails creating IAM resources**: the CI Scaleway key needs IAM
164
+ permissions (IAMManager) to create the app's database credential.
@@ -0,0 +1,28 @@
1
+ # Terraform (.terraform.lock.hcl is intentionally NOT ignored: commit it
2
+ # for reproducible provider versions)
3
+ .terraform/
4
+ *.tfstate
5
+ *.tfstate.*
6
+ crash.log
7
+ crash.*.log
8
+ *.tfplan
9
+
10
+ # Local backend config (contains bucket coordinates, kept out of the repo)
11
+ backend.hcl
12
+
13
+ # Local overrides and secrets: never commit credentials
14
+ *.auto.tfvars
15
+ override.tf
16
+ override.tf.json
17
+ *_override.tf
18
+ *_override.tf.json
19
+ .env
20
+ .env.*
21
+
22
+ # Bootstrap resume file
23
+ .keel.json
24
+
25
+ # Editors / OS
26
+ .DS_Store
27
+ .idea/
28
+ .vscode/
@@ -0,0 +1,9 @@
1
+ # Copy to backend.hcl (git-ignored) for local terraform runs:
2
+ # cp backend.hcl.example backend.hcl
3
+ # terraform init -backend-config=backend.hcl
4
+ bucket = "__TF_STATE_BUCKET__"
5
+ region = "__REGION__"
6
+
7
+ endpoints = {
8
+ s3 = "https://s3.__REGION__.scw.cloud"
9
+ }
@@ -0,0 +1,19 @@
1
+ # Remote state on Scaleway Object Storage (S3-compatible).
2
+ # Bucket, region and endpoint are provided at init time:
3
+ # terraform init -backend-config=backend.hcl
4
+ # Credentials come from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
5
+ # (set them to your Scaleway access key / secret key).
6
+ # Each workspace stores its state under env:/<workspace>/.
7
+ # use_lockfile enables S3-native state locking (Terraform >= 1.10); Scaleway
8
+ # Object Storage supports the conditional writes it relies on.
9
+ terraform {
10
+ backend "s3" {
11
+ key = "terraform.tfstate"
12
+ use_lockfile = true
13
+ skip_credentials_validation = true
14
+ skip_region_validation = true
15
+ skip_requesting_account_id = true
16
+ skip_metadata_api_check = true
17
+ skip_s3_checksum = true
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ # Application secrets live in Infisical, one environment per Terraform workspace.
2
+ data "infisical_secrets" "app" {
3
+ env_slug = var.environment
4
+ workspace_id = var.infisical_project_id
5
+ folder_path = "/"
6
+ }
7
+
8
+ module "app_stack" {
9
+ source = "./modules/app_stack"
10
+
11
+ project_name = var.project_name
12
+ environment = var.environment
13
+ region = var.region
14
+ container_image = var.container_image
15
+ container_port = var.container_port
16
+ cpu_limit = var.cpu_limit
17
+ memory_limit = var.memory_limit
18
+ min_scale = var.min_scale
19
+ max_scale = var.max_scale
20
+ db_min_cpu = var.db_min_cpu
21
+ db_max_cpu = var.db_max_cpu
22
+
23
+ enable_basic_auth = var.enable_basic_auth
24
+
25
+ secret_environment_variables = {
26
+ for name, secret in data.infisical_secrets.app.secrets : name => secret.value
27
+ }
28
+ }
@@ -0,0 +1,70 @@
1
+ locals {
2
+ name = "${var.project_name}-${var.environment}"
3
+ }
4
+
5
+ resource "scaleway_registry_namespace" "this" {
6
+ name = local.name
7
+ region = var.region
8
+ is_public = false
9
+ }
10
+
11
+ resource "scaleway_container_namespace" "this" {
12
+ name = local.name
13
+ region = var.region
14
+ }
15
+
16
+ resource "scaleway_sdb_sql_database" "this" {
17
+ name = local.name
18
+ region = var.region
19
+ min_cpu = var.db_min_cpu
20
+ max_cpu = var.db_max_cpu
21
+ }
22
+
23
+ # Dedicated least-privilege credential for the application: it can reach the
24
+ # database and nothing else. Serverless SQL authenticates with IAM: username
25
+ # is the application ID, password its API secret key.
26
+ resource "scaleway_iam_application" "db" {
27
+ name = "${local.name}-db"
28
+ description = "Database access for ${local.name} (managed by Terraform)"
29
+ }
30
+
31
+ resource "scaleway_iam_policy" "db_access" {
32
+ name = "${local.name}-db-access"
33
+ description = "Read/write access to the ${local.name} Serverless SQL database"
34
+ application_id = scaleway_iam_application.db.id
35
+
36
+ rule {
37
+ project_ids = [scaleway_sdb_sql_database.this.project_id]
38
+ permission_set_names = ["ServerlessSQLDatabaseReadWrite"]
39
+ }
40
+ }
41
+
42
+ resource "scaleway_iam_api_key" "db" {
43
+ application_id = scaleway_iam_application.db.id
44
+ description = "Database credential for ${local.name} (managed by Terraform)"
45
+ }
46
+
47
+ # The container is gated on an image being available: registry and database
48
+ # are provisioned first, the container appears once an image has been pushed
49
+ # and container_image is set.
50
+ resource "scaleway_container" "this" {
51
+ count = var.container_image == "" ? 0 : 1
52
+
53
+ name = var.project_name
54
+ namespace_id = scaleway_container_namespace.this.id
55
+ image = var.container_image
56
+ port = var.container_port
57
+ cpu_limit = var.cpu_limit
58
+ memory_limit_bytes = var.memory_limit * 1024 * 1024
59
+ min_scale = var.min_scale
60
+ max_scale = var.max_scale
61
+ privacy = "public"
62
+
63
+ environment_variables = var.enable_basic_auth ? {
64
+ BASIC_AUTH_ENABLED = "true"
65
+ } : {}
66
+
67
+ # BASIC_AUTH_USER / BASIC_AUTH_PASSWORD / DATABASE_URL arrive here from
68
+ # Infisical; the app is responsible for enforcing Basic Auth when enabled.
69
+ secret_environment_variables = var.secret_environment_variables
70
+ }
@@ -0,0 +1,23 @@
1
+ output "container_url" {
2
+ value = var.container_image == "" ? null : scaleway_container.this[0].public_endpoint
3
+ }
4
+
5
+ output "registry_endpoint" {
6
+ value = scaleway_registry_namespace.this.endpoint
7
+ }
8
+
9
+ output "database_url" {
10
+ # Ready-to-use connection string: dedicated IAM application as username,
11
+ # its API secret key as password (see the connect-to-a-database docs).
12
+ value = format(
13
+ "postgres://%s:%s@%s",
14
+ scaleway_iam_application.db.id,
15
+ scaleway_iam_api_key.db.secret_key,
16
+ trimprefix(scaleway_sdb_sql_database.this.endpoint, "postgres://"),
17
+ )
18
+ sensitive = true
19
+ }
20
+
21
+ output "container_namespace_id" {
22
+ value = scaleway_container_namespace.this.id
23
+ }
@@ -0,0 +1,55 @@
1
+ variable "project_name" {
2
+ type = string
3
+ }
4
+
5
+ variable "environment" {
6
+ type = string
7
+ }
8
+
9
+ variable "region" {
10
+ type = string
11
+ }
12
+
13
+ variable "container_image" {
14
+ type = string
15
+ default = ""
16
+ }
17
+
18
+ variable "container_port" {
19
+ type = number
20
+ }
21
+
22
+ variable "cpu_limit" {
23
+ type = number
24
+ }
25
+
26
+ variable "memory_limit" {
27
+ type = number
28
+ }
29
+
30
+ variable "min_scale" {
31
+ type = number
32
+ }
33
+
34
+ variable "max_scale" {
35
+ type = number
36
+ }
37
+
38
+ variable "db_min_cpu" {
39
+ type = number
40
+ }
41
+
42
+ variable "db_max_cpu" {
43
+ type = number
44
+ }
45
+
46
+ variable "enable_basic_auth" {
47
+ type = bool
48
+ }
49
+
50
+ variable "secret_environment_variables" {
51
+ description = "Secrets injected into the container, sourced from Infisical."
52
+ type = map(string)
53
+ sensitive = true
54
+ default = {}
55
+ }