diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..9a8c118 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,162 @@ +name: Deploy + +on: + push: + branches: ["main"] + paths: + - "tofu/oci/**" + pull_request: + branches: ["main"] + paths: + - "tofu/oci/**" + workflow_dispatch: + inputs: + reason: + description: "Reason for manual deploy" + required: false + default: "manual" + +permissions: + contents: read + pull-requests: write + +jobs: + plan: + name: Tofu Plan + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Fetch Talos image OCID from oci-talos-gitops-apps + id: talos_image + run: | + OCID=$(curl -fsSL \ + "https://raw.githubusercontent.com/syscode-labs/oci-talos-gitops-apps/main/omni/talos-image.yaml" \ + | grep '^oci_image_ocid:' | awk '{print $2}') + if [ -z "$OCID" ] || [ "$OCID" = '""' ] || [ "$OCID" = "null" ]; then + echo "ERROR: oci_image_ocid is empty in oci-talos-gitops-apps/omni/talos-image.yaml" + echo "Run the talos-images build workflow first to import a Talos image into OCI." + exit 1 + fi + echo "ocid=${OCID}" >> "$GITHUB_OUTPUT" + echo "Talos image OCID: ${OCID}" + + - name: Write OCI credentials + run: | + mkdir -p ~/.oci + printf '%s' "$OCI_API_KEY_PEM" > ~/.oci/oci_api_key.pem + chmod 600 ~/.oci/oci_api_key.pem + cat > ~/.oci/config < tofu/oci/backend-config.tfvars + env: + TF_BACKEND_CONFIG: ${{ secrets.TF_BACKEND_CONFIG }} + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v2.0.0 + with: + tofu_version: "1.11.5" + + - name: Tofu Init + working-directory: tofu/oci + run: tofu init -backend-config=backend-config.tfvars + + - name: Tofu Plan + working-directory: tofu/oci + run: | + tofu plan \ + -lock=false \ + -var="tenancy_ocid=$OCI_TENANCY_OCID" \ + -var="compartment_ocid=$OCI_COMPARTMENT_OCID" \ + -var="talos_image_ocid=${{ steps.talos_image.outputs.ocid }}" \ + -var="omni_join_token=$OMNI_JOIN_TOKEN" \ + -var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" \ + -var-file=terraform.tfvars \ + -out=tfplan + env: + OCI_TENANCY_OCID: ${{ secrets.OCI_TENANCY_OCID }} + OCI_COMPARTMENT_OCID: ${{ secrets.OCI_COMPARTMENT_OCID }} + OMNI_JOIN_TOKEN: ${{ secrets.OMNI_JOIN_TOKEN }} + TAILSCALE_AUTH_KEY: ${{ secrets.TAILSCALE_AUTH_KEY }} + TF_LOG: WARN + + - name: Upload plan + uses: actions/upload-artifact@v4 + with: + name: tfplan + path: tofu/oci/tfplan + retention-days: 1 + + apply: + name: Tofu Apply + runs-on: ubuntu-latest + needs: plan + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Write OCI credentials + run: | + mkdir -p ~/.oci + printf '%s' "$OCI_API_KEY_PEM" > ~/.oci/oci_api_key.pem + chmod 600 ~/.oci/oci_api_key.pem + cat > ~/.oci/config < tofu/oci/backend-config.tfvars + env: + TF_BACKEND_CONFIG: ${{ secrets.TF_BACKEND_CONFIG }} + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v2.0.0 + with: + tofu_version: "1.11.5" + + - name: Tofu Init + working-directory: tofu/oci + run: tofu init -backend-config=backend-config.tfvars + + - name: Download plan artifact + uses: actions/download-artifact@v4 + with: + name: tfplan + path: tofu/oci + + - name: Tofu Apply + working-directory: tofu/oci + run: tofu apply -lock=false -auto-approve tfplan + env: + OCI_TENANCY_OCID: ${{ secrets.OCI_TENANCY_OCID }} + TF_LOG: WARN diff --git a/tofu/oci/main.tf b/tofu/oci/main.tf index 2de1261..912140e 100644 --- a/tofu/oci/main.tf +++ b/tofu/oci/main.tf @@ -264,20 +264,25 @@ resource "oci_core_instance" "ampere_instance" { ) lifecycle { - # SAFETY: prevent_destroy blocks tofu from ever destroying an instance. - # Instances provisioned by the capacity watcher are imported into state — - # destroying and recreating them would lose data and reserved IPs. - # To intentionally remove an instance, set this to false in a separate PR. - prevent_destroy = true + replace_triggered_by = [terraform_data.omni_credentials] ignore_changes = [ source_details[0].source_id, # image OCID changes on new OCI image releases - metadata, # SSH keys / user_data managed outside tofu availability_domain, # may differ from var if instance was imported shape_config, # OCPUs/memory set at launch; resize via OCI console ] } } +# Tracks hashes of Omni join token + Tailscale auth key. +# Any change to either secret triggers replacement of all Ampere instances, +# ensuring the new credentials are baked into user_data on the next boot. +resource "terraform_data" "omni_credentials" { + input = { + join_token_hash = sha256(coalesce(var.omni_join_token, "")) + ts_key_hash = sha256(coalesce(var.tailscale_auth_key, "")) + } +} + # AMD E2.1.Micro Instances (x86-based, Always Free accounts only) # Node configuration is resolved in data.tf from var.micro_nodes + tier defaults. resource "oci_core_instance" "micro_instance" { diff --git a/tofu/oci/terraform.tfvars b/tofu/oci/terraform.tfvars new file mode 100644 index 0000000..d3aba02 --- /dev/null +++ b/tofu/oci/terraform.tfvars @@ -0,0 +1,9 @@ +omni_ready = true +omni_endpoint = "omni.wind-bearded.ts.net:8090" +oci_config_profile = "syscode-homelab" + +# talos_image_ocid — passed via CI var TALOS_IMAGE_OCID (GitHub variable) +# omni_join_token — passed via CI secret OMNI_JOIN_TOKEN +# tailscale_auth_key — passed via CI secret TAILSCALE_AUTH_KEY +# tenancy_ocid — passed via CI secret OCI_TENANCY_OCID +# compartment_ocid — passed via CI secret OCI_COMPARTMENT_OCID