| name | smaqit.infrastructure-provision-cyso |
| description | Use when provisioning cloud infrastructure for a project's target application on Cyso Cloud (OpenStack) using Terraform. Covers application credential sourcing, Object Storage backend initialization, SSH keypair variable configuration, `terraform init/plan/apply`, and fixed IP retrieval. Produces a running Cyso VM accessible via SSH, with Cinder data volume attached and security group configured on ports 22/80/443. Also use when re-running Terraform after infrastructure changes or when an operator invokes `/provision.cyso`. |
| metadata | {"version":"1.5.0"} |
Provision Target: Cyso Cloud
Only for provisioning_mode: provision or existing-owned. If targeting a VM a different
project owns and manages via its own Terraform state (co-hosting), do not invoke this skill at
all — use provisioning_mode: existing-shared instead (see smaqit.new-greenfield-project
Phase 4/5). Step 0 below is a defense-in-depth guard for a direct/manual invocation that bypasses
that orchestration.
Canonical vs. vendored guard scripts
scripts/plan-guard.sh and scripts/ownership-guard.sh in this skill directory are canonical —
edit them here. smaqit.infrastructure-cicd-generate copies both, verbatim, into a target
project's deployment/scripts/ at generation time (full/existing-owned modes only), so the
generated deploy.yml/provision.yml have a real, checked-in path to invoke them from on a
GitHub-hosted runner, which has no access to the smaqit skill directory. The vendored copies are
generated output — they are overwritten the next time cicd-generate runs, and should never
be hand-edited in a target project expecting the edit to persist.
Pre-conditions (one-time manual steps — complete before first run)
- Cyso Cloud account with access to region
ams2
- Application credential created in Cyso Cloud Portal → Access → Credentials; credential ID and secret loaded into Vault at
secret/<project-slug>/cyso and secret/machines/<machine-slug>/cyso (<machine-slug> defaults to <project-slug> when this project provisions its own new machine — see Step 2)
- Object Storage state bucket created in Cyso dashboard (private); S3 access key + secret key loaded into Vault at
secret/<project-slug>/tfstate and secret/machines/<machine-slug>/tfstate
- Fine-grained GitHub PAT with
variables:write loaded into Vault at secret/<project-slug>/github
- Terraform 1.14+ installed locally
- Local Vault running and unsealed (
smaqit.infrastructure-vault-loader complete)
- No manual SSH keypair step — Step 2 below generates
secret/machines/<machine-slug>/base-ssh
automatically if this is a fresh machine; Terraform installs its public half at apply time (Step 7)
Steps
-
Ownership pre-flight guard — run before touching Vault or Terraform:
scripts/ownership-guard.sh deployment/terraform
If a target VM is already declared (the VM_HOST env var, or the VM_HOST repository
variable read directly via gh variable get) but no openstack_compute_instance_v2 resource
in this project's Terraform state has a matching IP, the script exits non-zero and stops here
— this is the existing-shared case (a VM another project owns), and this skill must not be
invoked for it. VM_HOST is a GitHub Actions variable, not a secret (see
smaqit.infrastructure-repo-config) — that's what lets this guard read it back directly
instead of depending on the caller having already exported it. If no target is declared yet,
or the declared target already matches a resource this project's state owns, the script exits
0 and it is safe to continue to Step 1.
-
Unlock Vault:
Invoke smaqit.infrastructure-vault-loader and confirm Vault is running, unsealed, and all
secret/<project-slug>/* paths are populated. Do not proceed until this is confirmed.
-
Resolve the machine slug, ensure its base credential exists, then fetch credentials from
Vault into the shell environment:
export VAULT_ADDR=http://127.0.0.1:8200
export PROJECT_SLUG=<project-slug>
export MACHINE_SLUG=<machine-slug>
if ! vault kv get "secret/machines/${MACHINE_SLUG}/base-ssh" > /dev/null 2>&1; then
TMPDIR_BASE=$(mktemp -d) && trap 'rm -rf "$TMPDIR_BASE"' EXIT
ssh-keygen -t ed25519 -f "${TMPDIR_BASE}/base_key" -N "" -q
vault kv put "secret/machines/${MACHINE_SLUG}/base-ssh" \
private_key=@"${TMPDIR_BASE}/base_key" \
public_key="$(cat "${TMPDIR_BASE}/base_key.pub" | tr -d '\n')" > /dev/null
rm -rf "$TMPDIR_BASE" && trap - EXIT
fi
export TF_VAR_app_credential_id=$(vault kv get -field=app_credential_id secret/${PROJECT_SLUG}/cyso)
export TF_VAR_app_credential_secret=$(vault kv get -field=app_credential_secret secret/${PROJECT_SLUG}/cyso)
export TF_VAR_github_token=$(vault kv get -field=token secret/${PROJECT_SLUG}/github)
export TF_VAR_ssh_public_key=$(vault kv get -field=public_key secret/machines/${MACHINE_SLUG}/base-ssh)
export AWS_ACCESS_KEY_ID=$(vault kv get -field=access_key secret/${PROJECT_SLUG}/tfstate)
export AWS_SECRET_ACCESS_KEY=$(vault kv get -field=secret_key secret/${PROJECT_SLUG}/tfstate)
Use TF_VAR_github_token — not GITHUB_TOKEN (reserved by Actions) and not TF_VAR_GITHUB_TOKEN
(case-sensitive; Terraform maps TF_VAR_github_token → var.github_token). The public key is
already newline-stripped at Vault load time — do not tr -d '\n' again.
Terraform state backend credentials (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) and the Cyso
API credential (TF_VAR_app_credential_*) remain sourced from secret/<project-slug>/* here —
this session did not verify a live terraform apply, so migrating those two to the machine
namespace as well is left as a documented follow-up (see Findings) rather than an unverified
change to the provisioning credential path.
-
Confirm Ubuntu image ID and flavor (catalog values change occasionally):
openstack image list | grep "Ubuntu 22.04"
openstack flavor list
If IDs differ from the defaults in deployment/terraform/main.tf, update that file before continuing.
Note: openstack CLI must be available; auth is implicit via TF_VAR_app_credential_* env vars
set above — no OpenRC file is required.
-
Navigate to Terraform directory:
cd deployment/terraform
-
Initialize Terraform:
terraform init
Confirms remote state backend connectivity. .terraform.lock.hcl is committed; if regeneration is
needed: terraform providers lock -platform=linux_amd64 -platform=darwin_arm64.
-
Review the plan via the guard script — never a bare terraform plan. Expected resources on
first apply: 1 Nova VM, 1 boot volume (20 GB), 1 security group (ports 22/80/443), 1 keypair,
1 GitHub Actions variable.
scripts/plan-guard.sh .
Exits non-zero and names the specific resource(s) if the plan would delete or replace anything
(e.g. a user_data edit forcing VM replacement) — a deterministic gate, not a prompt to review
the plan carefully. Do not run terraform apply if this script exits non-zero.
-
Apply (only after plan-guard.sh exits 0):
terraform apply
After apply, note the fixed_ip output — this is the public address. The floating IP is provisioned
but does not route on Cyso's flat network; ignore it.
7.5. Register the machine and bootstrap this project's own app-specific key. This project does
not get a shortcut around getting its own distinct key, even though it provisioned the machine:
vault kv put "secret/machines/${MACHINE_SLUG}/metadata" \
host="<fixed_ip>" \
provider="cyso" \
owner_project="${PROJECT_SLUG}" > /dev/null
bash [SMAQIT_SKILLS_DIR]/smaqit.infrastructure-vault-loader/scripts/bootstrap-app-to-machine.sh \
"${PROJECT_SLUG}" "${MACHINE_SLUG}"
The bootstrap script generates a distinct keypair for ${PROJECT_SLUG}, uses the machine's
base-ssh credential to authorize it, stores it at secret/apps/${PROJECT_SLUG}/ssh, and
verifies it authenticates — before this step, only base-ssh is live on the VM.
- Verify SSH access (should already be confirmed by the bootstrap script's own verification
in Step 7.5; re-check manually if needed):
TMPKEY=$(mktemp) && trap "rm -f $TMPKEY" EXIT
vault kv get -field=private_key secret/apps/${PROJECT_SLUG}/ssh > "$TMPKEY"
chmod 600 "$TMPKEY"
ssh -i "$TMPKEY" ubuntu@<fixed_ip>
Output
- Cyso VM running and SSH-accessible at
fixed_ip
- Cinder data volume attached (appears as
/dev/sdb inside VM)
- Security group open on ports 22, 80, 443
- Terraform state stored remotely in the
<project-slug>-tfstate Object Storage bucket
Scope
- Does NOT bootstrap the VM post-provision — use
smaqit.infrastructure-vm-bootstrap for that.
- Does NOT deploy the application — use
smaqit.infrastructure-deploy-rsync for that.
- Does NOT configure nginx or Docker inside the VM (handled by cloud-init user-data in
main.tf).
- Floating IP is non-functional on Cyso's flat network — use
outputs.fixed_ip exclusively.
Examples
Input: Cyso account set up, OpenRC file ready, state bucket created. Operator invokes /provision.cyso.
Output: VM at fixed_ip (e.g. <vm-fixed-ip>), Cinder data volume attached as /dev/sdb, SSH
accessible with the project's deploy keypair, Terraform state in <project-slug>-tfstate. Ready for
smaqit.infrastructure-vm-bootstrap.
Gotchas
-
Floating IP does not route — Cyso assigns publicly-routable IPs directly to VM interfaces on the
flat network. openstack_networking_floatingip_associate_v2 does not make it publicly accessible.
Always use outputs.fixed_ip; never the floating IP value.
-
Data volume is /dev/sdb, not /dev/vdb — Cyso presents Cinder volumes as SCSI devices, not
virtio-blk. lsblk shows it as sdb (or sdc if multiple volumes are attached).
-
Keypair trailing-newline drift — Terraform will show the keypair needing replacement on a
non-first apply if the stored key string differs by a trailing newline. Strip the newline when
loading into Vault:
vault kv put secret/machines/<machine-slug>/base-ssh public_key="$(cat ~/.ssh/<key>.pub | tr -d '\n')" ...
The Vault fetch (vault kv get -field=public_key) does not add a newline, so subsequent
applies are stable. The variable name is TF_VAR_ssh_public_key (all lowercase) — matches
var.ssh_public_key in variables.tf.
-
Provider lock file platform mismatch — .terraform.lock.hcl is committed with hashes for
linux_amd64 and darwin_arm64. Regenerate if running on a different platform:
terraform providers lock -platform=<platform>.
-
Application credential must be project-scoped — a user-level credential without project scope
will fail on resource creation.
-
Do not run terraform destroy — it tears down live infrastructure. Not part of any deployment
or acceptance step.
-
Any user_data edit forces VM replacement — always run scripts/plan-guard.sh first — a
destroy-then-recreate can happen from something as small as a comment-only edit inside the
user_data content (whether inline or loaded via file()). Never run terraform apply on the
strength of a manually eyeballed terraform plan; let the script gate it.
-
Never patch a live, Terraform-managed VM out-of-band (SSH, console) instead of through
Terraform — e.g. fixing a broken user_data step by SSHing in and running the corrected
command directly on the VM. The running VM and Terraform's declared config immediately diverge:
Terraform still thinks the old user_data was applied, but the file on disk now says
something different. The next plan sees only that the user_data hash changed and proposes a
replace (destroy + recreate) to reconcile it — which reassigns fixed_ip and breaks anything
depending on it (VM_HOST, DNS, bookmarked URLs). If a main.tf fix is applied manually to
unblock deployment right now, immediately follow up in the same session with either
lifecycle { ignore_changes = [user_data] } on the instance resource (correct when cloud-init
only matters at first boot, as it usually does) or a deliberate terraform apply -replace=...
once the replacement is genuinely wanted and reviewed. Do not leave the drift undocumented or
unresolved across sessions — run scripts/plan-guard.sh again afterward to confirm the plan is
clean.
Completion
Failure Handling
| Situation | Action |
|---|
scripts/ownership-guard.sh exits non-zero | Do NOT proceed. A target VM is declared but not owned by this project's Terraform state — use provisioning_mode: existing-shared instead, which skips this skill entirely |
| Required input not provided | Request the missing information before proceeding |
| Gathered input is ambiguous | Flag the ambiguity and ask for clarification |
| Subagent invocation fails | Report the failure with context; do not silently retry |
| Output artifact already exists | Confirm with user before overwriting |
openstack token issue fails | Re-source OpenRC; check if application credential has expired |
terraform init backend connectivity failure | Verify AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY are set; check Object Storage endpoint in backend.tf |
| Image ID not found | Run openstack image list; update variables.tf with current ID |
| Keypair replacement shown in plan | Strip trailing newline; re-export TF_VAR_ssh_public_key using tr -d '\n' |
| SSH access fails after apply | Verify security group allows port 22; confirm key fingerprint matches uploaded keypair |
scripts/plan-guard.sh exits non-zero | Do NOT apply. Review the named resource(s); if the destroy/replace is genuinely intentional, apply manually outside this guarded path with explicit sign-off |