From 8ff53503db1daca9b4263cfefa129720e34f47d3 Mon Sep 17 00:00:00 2001 From: Xavier Lario Date: Mon, 20 Apr 2026 10:16:49 +0200 Subject: [PATCH] feat: multi-site scalability, locals refactor, README --- .github/workflows/apply.yml | 50 +- .github/workflows/plan.yml | 71 ++- .gitignore | 12 + README.md | 643 ++++++++++++++++++++++++++ backend.hcl | 4 + modules/meraki-site/main.tf | 1 + modules/meraki-site/outputs.tf | 12 + modules/meraki-site/variables.tf | 6 +- sites/BCN01-LAB/MANUAL_STEPS.md | 7 +- sites/BCN01-LAB/appliance.auto.tfvars | 15 - sites/BCN01-LAB/appliance.tf | 13 + sites/BCN01-LAB/firewall.auto.tfvars | 68 --- sites/BCN01-LAB/firewall.tf | 46 ++ sites/BCN01-LAB/main.tf | 54 +-- sites/BCN01-LAB/ssids.auto.tfvars | 57 --- sites/BCN01-LAB/ssids.tf | 46 ++ sites/BCN01-LAB/switch.auto.tfvars | 210 --------- sites/BCN01-LAB/switch.tf | 121 +++++ sites/BCN01-LAB/variables.tf | 208 +-------- sites/BCN01-LAB/vlans.auto.tfvars | 97 ---- sites/BCN01-LAB/vlans.tf | 93 ++++ sites/BCN01-LAB/wan.auto.tfvars | 32 -- sites/BCN01-LAB/wan.tf | 29 ++ 23 files changed, 1155 insertions(+), 740 deletions(-) create mode 100644 README.md create mode 100644 backend.hcl delete mode 100644 sites/BCN01-LAB/appliance.auto.tfvars create mode 100644 sites/BCN01-LAB/appliance.tf delete mode 100755 sites/BCN01-LAB/firewall.auto.tfvars create mode 100644 sites/BCN01-LAB/firewall.tf delete mode 100644 sites/BCN01-LAB/ssids.auto.tfvars create mode 100644 sites/BCN01-LAB/ssids.tf delete mode 100644 sites/BCN01-LAB/switch.auto.tfvars create mode 100644 sites/BCN01-LAB/switch.tf delete mode 100755 sites/BCN01-LAB/vlans.auto.tfvars create mode 100644 sites/BCN01-LAB/vlans.tf delete mode 100644 sites/BCN01-LAB/wan.auto.tfvars create mode 100644 sites/BCN01-LAB/wan.tf diff --git a/.github/workflows/apply.yml b/.github/workflows/apply.yml index b377a71..d0e54e1 100644 --- a/.github/workflows/apply.yml +++ b/.github/workflows/apply.yml @@ -3,10 +3,47 @@ name: Terraform Apply on: push: branches: [main] + paths: + - 'sites/**' + - 'modules/**' + - 'backend.hcl' jobs: + detect-sites: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.detect.outputs.matrix }} + has-changes: ${{ steps.detect.outputs.has-changes }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - id: detect + run: | + all_sites=$(find sites -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | sort | jq -sRc '.') + if git diff HEAD~1...HEAD --name-only | grep -qE '^(modules/|backend\.hcl)'; then + echo "has-changes=true" >> $GITHUB_OUTPUT + echo "matrix={\"site\": $all_sites}" >> $GITHUB_OUTPUT + else + changed=$(git diff HEAD~1...HEAD --name-only | grep '^sites/' | cut -d/ -f2 | sort -u | jq -sRc '.') + if [ "$changed" = "[]" ] || [ -z "$changed" ]; then + echo "has-changes=false" >> $GITHUB_OUTPUT + echo "matrix={\"site\": []}" >> $GITHUB_OUTPUT + else + echo "has-changes=true" >> $GITHUB_OUTPUT + echo "matrix={\"site\": $changed}" >> $GITHUB_OUTPUT + fi + fi + apply: - name: Terraform Apply - BCN01-LAB + needs: detect-sites + if: needs.detect-sites.outputs.has-changes == 'true' + strategy: + matrix: ${{ fromJSON(needs.detect-sites.outputs.matrix) }} + max-parallel: 1 # serializar applies para evitar conflictos de estado en DynamoDB + fail-fast: false + name: Terraform Apply - ${{ matrix.site }} runs-on: self-hosted steps: @@ -22,8 +59,11 @@ jobs: terraform_version: 1.5.0 - name: Terraform Init - working-directory: sites/BCN01-LAB - run: terraform init + working-directory: sites/${{ matrix.site }} + run: | + terraform init \ + -backend-config=../../backend.hcl \ + -backend-config="key=${{ matrix.site }}/terraform.tfstate" env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -31,7 +71,7 @@ jobs: MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }} - name: Terraform Apply - working-directory: sites/BCN01-LAB + working-directory: sites/${{ matrix.site }} run: terraform apply -auto-approve env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -39,4 +79,4 @@ jobs: AWS_REGION: us-east-1 MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }} TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }} - TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }} \ No newline at end of file + TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }} diff --git a/.github/workflows/plan.yml b/.github/workflows/plan.yml index 096270a..8ac72d0 100644 --- a/.github/workflows/plan.yml +++ b/.github/workflows/plan.yml @@ -3,10 +3,47 @@ name: Terraform Plan on: pull_request: branches: [main] + paths: + - 'sites/**' + - 'modules/**' + - 'backend.hcl' + - '.github/workflows/**' jobs: + detect-sites: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.detect.outputs.matrix }} + has-changes: ${{ steps.detect.outputs.has-changes }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: detect + run: | + all_sites=$(find sites -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | sort | jq -sRc '.') + if git diff origin/main...HEAD --name-only | grep -qE '^(modules/|backend\.hcl)'; then + echo "has-changes=true" >> $GITHUB_OUTPUT + echo "matrix={\"site\": $all_sites}" >> $GITHUB_OUTPUT + else + changed=$(git diff origin/main...HEAD --name-only | grep '^sites/' | cut -d/ -f2 | sort -u | jq -sRc '.') + if [ "$changed" = "[]" ] || [ -z "$changed" ]; then + echo "has-changes=false" >> $GITHUB_OUTPUT + echo "matrix={\"site\": []}" >> $GITHUB_OUTPUT + else + echo "has-changes=true" >> $GITHUB_OUTPUT + echo "matrix={\"site\": $changed}" >> $GITHUB_OUTPUT + fi + fi + plan: - name: Terraform Plan - BCN01-LAB + needs: detect-sites + if: needs.detect-sites.outputs.has-changes == 'true' + strategy: + matrix: ${{ fromJSON(needs.detect-sites.outputs.matrix) }} + fail-fast: false + name: Terraform Plan - ${{ matrix.site }} runs-on: self-hosted permissions: pull-requests: write @@ -24,24 +61,28 @@ jobs: terraform_version: 1.5.0 - name: Terraform Init - working-directory: sites/BCN01-LAB - run: terraform init + working-directory: sites/${{ matrix.site }} + run: | + terraform init \ + -backend-config=../../backend.hcl \ + -backend-config="key=${{ matrix.site }}/terraform.tfstate" env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: us-east-1 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: us-east-1 + MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }} - name: Terraform Plan id: plan - working-directory: sites/BCN01-LAB + working-directory: sites/${{ matrix.site }} run: terraform plan -no-color 2>&1 | tee plan_output.txt env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: us-east-1 - MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }} - TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }} - TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: us-east-1 + MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }} + TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }} + TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }} - name: Comentar Plan en el PR uses: actions/github-script@v7 @@ -49,11 +90,11 @@ jobs: with: script: | const fs = require('fs'); - const plan = fs.readFileSync('sites/BCN01-LAB/plan_output.txt', 'utf8'); + const plan = fs.readFileSync('sites/${{ matrix.site }}/plan_output.txt', 'utf8'); const truncated = plan.length > 60000 ? plan.substring(0, 60000) + '\n...(truncado)' : plan; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## Terraform Plan - BCN01-LAB\n\`\`\`\n${truncated}\n\`\`\`` + body: `## Terraform Plan - ${{ matrix.site }}\n\`\`\`\n${truncated}\n\`\`\`` }); diff --git a/.gitignore b/.gitignore index 57cb55b..1b60805 100755 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,15 @@ # Documentacion (HLD, LLD) - no se versiona en git docs/ +# Claude Code +CLAUDE.md + +# Terraform runtime artifacts +plan_output.txt +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json + diff --git a/README.md b/README.md new file mode 100644 index 0000000..8171d1a --- /dev/null +++ b/README.md @@ -0,0 +1,643 @@ +# EQT Network — Meraki Infrastructure as Code + +All Meraki network configuration is managed as Infrastructure as Code using [Terraform](https://www.terraform.io/) with the [`CiscoDevNet/meraki`](https://registry.terraform.io/providers/CiscoDevNet/meraki/latest) provider (v1.9.0). Every change is deployed through a GitHub Actions CI/CD pipeline — **no one runs `terraform apply` locally**. Terraform state is stored remotely in an S3 bucket with DynamoDB locking to prevent concurrent modifications. + +--- + +## TL;DR + +> [!CAUTION] +> **Never run `terraform apply` locally.** All applies go through the GitHub Actions pipeline to ensure auditability and prevent state drift. + +### Modifying an existing site + +**1. Find the right file** in `sites//`: + +| What you want to change | File | +|------------------------|------| +| VLAN IDs, subnets, gateway IPs, DHCP | `vlans.tf` | +| Wi-Fi SSIDs | `ssids.tf` | +| Firewall rules | `firewall.tf` | +| Switch ports, stacks, 802.1X policies | `switch.tf` | +| MX LAN ports | `appliance.tf` | +| WAN IPs, Warm Spare (HA) | `wan.tf` | +| Organization or network name | `main.tf` (top `locals` block) | + +**2.** Edit the value inside the `locals { }` block. All device names (stacks, switches, MX) must match the **exact display name** in the Meraki Dashboard. + +**3. Commit, push and open a PR** — see [Step by step — VS Code](#step-by-step--vs-code) or [Step by step — CLI](#step-by-step--cli). + +GitHub Actions runs `terraform plan` automatically and posts the output as a PR comment. Review the plan, then merge — `terraform apply` runs automatically on merge. + +--- + +### Adding a new site + +```bash +cp -r sites/BCN01-LAB sites/MAD01 +``` + +Then edit only these values in the copied files: + +**`main.tf`** — module name, `organization_name` and `network_name`: +```hcl +locals { + organization_name = "..." # exact org name in Meraki Dashboard + network_name = "MAD01" # exact network name in Meraki Dashboard +} + +module "mad01" { # rename to match the new site + source = "../../modules/meraki-site" + # everything else stays the same +} +``` + +**`vlans.tf`** — replace subnets and gateway IPs with the new site's IP ranges. + +**`ssids.tf`** — update RADIUS server IPs if different. + +**`firewall.tf`** — update any CIDRs that reference site-specific subnets. + +**`switch.tf`** — replace stack/switch names (`bcn01-lab-stack01` → actual name in MAD01's Dashboard). + +**`appliance.tf`** and **`wan.tf`** — update MX device names and WAN IPs. + +**`variables.tf`** — do not touch. It is identical across all sites. + +Open a PR — the workflow detects `sites/MAD01/` automatically, no workflow changes needed. + +--- + +## Table of Contents + +1. [Repository Structure](#1-repository-structure) +2. [How It Works — Architecture Overview](#2-how-it-works--architecture-overview) +3. [Change Workflow](#3-change-workflow) +4. [Making a Change to an Existing Site](#4-making-a-change-to-an-existing-site) +5. [Adding a New Site](#5-adding-a-new-site) +6. [Configuration Reference](#6-configuration-reference) +7. [GitHub Actions Workflows](#7-github-actions-workflows) +8. [Sensitive Variables and Secrets](#8-sensitive-variables-and-secrets) +9. [Running Terraform Locally (plan only)](#9-running-terraform-locally-plan-only) +10. [Manual Steps — Provider Limitations](#10-manual-steps--provider-limitations) + +--- + +## 1. Repository Structure + +``` +. +├── backend.hcl # Shared S3 backend config (bucket, region, DynamoDB table) +├── modules/ +│ └── meraki-site/ # Reusable module — all Meraki resource logic lives here +│ ├── main.tf # Resource definitions (VLANs, SSIDs, firewall, switches, WAN, HA) +│ ├── variables.tf # All input variable declarations with types and defaults +│ └── outputs.tf # Exported values (network_id, vlan_ids, stack_ids, device_serials) +├── sites/ +│ └── BCN01-LAB/ # One directory per physical site +│ ├── main.tf # Terraform backend + organization/network locals + module call +│ ├── variables.tf # Only two sensitive vars: radius_secret, wifi_password_psk +│ ├── vlans.tf # locals: VLAN definitions (IDs, subnets, DHCP) +│ ├── ssids.tf # locals: Wireless SSID configuration +│ ├── firewall.tf # locals: L3 firewall rules +│ ├── switch.tf # locals: Switch ports, stacks, 802.1X policies +│ ├── appliance.tf # locals: MX LAN port configuration +│ ├── wan.tf # locals: WAN uplinks and Warm Spare (HA) +│ └── MANUAL_STEPS.md # Steps that cannot be automated (provider limitations) +└── .github/ + └── workflows/ + ├── plan.yml # Runs terraform plan on Pull Requests + └── apply.yml # Runs terraform apply on merge to main +``` + +> **One directory per site.** Each directory under `sites/` is a fully independent Terraform root module with its own remote state. Sites share the `modules/meraki-site` module but have no shared state between them. +> +> **No variable boilerplate.** Site configuration lives in `locals {}` blocks — no need to re-declare types and defaults that already exist in the module. The only `variables.tf` in a site holds the two sensitive variables that must arrive via `TF_VAR_*` environment variables. + +--- + +## 2. How It Works — Architecture Overview + +### Module pattern + +The `modules/meraki-site` module encapsulates all Meraki resource logic. A site directory is a thin wrapper that calls the module with site-specific locals and declares the remote backend: + +``` +sites/BCN01-LAB/ + *.tf (locals) ──► main.tf ──► module "meraki-site" ──► Meraki API + │ + modules/meraki-site/ + main.tf (resources) + variables.tf +``` + +### Dynamic resource resolution + +Terraform never needs device serials hardcoded. At plan time, the module: + +- Calls `data "meraki_network_devices"` to build a `name → serial` map for MX and standalone switches +- Calls `data "meraki_switch_stacks"` to resolve stack names to their member serials + +This means you reference devices by their **Dashboard display name** in all configuration files. + +### Port range expansion + +Switch port configuration accepts ranges like `"1-24"`, `"47-48"`, or `"1-3,5,47"`. The module expands these into individual port resources at plan time. A single config entry can configure dozens of ports. + +### VLAN and L3 gateway + +The module creates L3 VLAN interfaces on the MX for every VLAN with a `subnet` defined. VLANs without a subnet (e.g. a pure-switching WAN VLAN) are created as L2-only and excluded from the MX gateway resources. + +### Firewall rules + +`meraki_appliance_l3_firewall_rules` **replaces the entire rule set** on every apply. The list in `firewall.tf` is authoritative. Rules are evaluated top-down; always end the list with an explicit deny-all rule. + +### SSID split + +The Meraki API rejects the `wpa_encryption_mode` attribute for SSIDs with `auth_mode = "open"`. The module handles this internally by splitting SSIDs into two resources — one for open SSIDs and one for all others. No action needed from the operator. + +--- + +## 3. Change Workflow + +> **Never run `terraform apply` locally.** All applies go through GitHub Actions to ensure auditability and prevent state drift. + +Every change follows this Git-based process: + +``` +1. Create a feature branch +2. Edit the relevant .tf file under sites// +3. Commit the changes +4. Push the branch and open a Pull Request +5. GitHub Actions runs terraform plan and posts the output as a PR comment +6. Team member reviews the plan output in the PR +7. Approve & merge → GitHub Actions runs terraform apply automatically +``` + +### Branch and commit naming + +| Type | Pattern | Example | +|------|---------|---------| +| Branch | `feature/-` | `feature/BCN01-LAB-add-iot-vlan` | +| Commit | `feat(): ` | `feat(BCN01-LAB): add IoT VLAN 112` | +| Bugfix branch | `fix/-` | `fix/BCN01-LAB-ssid-visible` | +| Bugfix commit | `fix(): ` | `fix(BCN01-LAB): set EQT-CORPO-OWE-OK to hidden` | + +### Step by step — VS Code + +1. Click the branch name in the bottom-left status bar → **Create new branch** → enter `feature/-` +2. Edit the relevant file(s) under `sites//` +3. Open the **Source Control** panel (`Ctrl+Shift+G` / `Cmd+Shift+G`) +4. Click **`+`** next to each changed file (or next to "Changes" to stage all) +5. Type the commit message in the text box and click **Commit** +6. Click **Publish Branch** — this pushes the branch to GitHub +7. Open a PR: + - **Option A** — GitHub will show a banner in the repo: *"Compare & pull request"*. Click it. + - **Option B** — Install the [GitHub Pull Requests](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github) extension and create the PR directly from VS Code without opening the browser. + +### Step by step — CLI + +```bash +# 1. Create the branch +git checkout -b feature/- + +# 2. Edit files, then stage and commit +git add sites//.tf +git commit -m "feat(): " + +# 3. Push the branch +git push origin feature/- + +# 4. Open a PR (interactive) or directly in the browser +gh pr create --title "feat(): " +gh pr create --web +``` + +> The `gh` CLI must be installed and authenticated (`gh auth login`). + +--- + +## 4. Making a Change to an Existing Site + +For a quick reference on which file to edit, see the [TL;DR](#tldr) at the top. The examples below show the syntax for the most common changes. + +### Example: adding a firewall rule + +Edit `sites//firewall.tf`. The `src_port` and `dest_port` fields default to `"any"` and can be omitted: + +```hcl +locals { + firewall_rules = [ + # ... existing rules ... + { + comment = "Allow IoT to NTP server" + policy = "allow" + protocol = "udp" + src_cidr = "10.2.60.0/24" # IoT VLAN + dest_cidr = "10.2.56.10/32" # NTP server + dest_port = "123" + }, + { + comment = "Deny all other traffic" + policy = "deny" + protocol = "any" + src_cidr = "any" + dest_cidr = "any" + }, + ] +} +``` + +### Example: adding a VLAN + +Edit `sites//vlans.tf`. The map key is the VLAN ID: + +```hcl +locals { + switch_vlans = { + # ... existing VLANs ... + "112" = { + name = "IOT" + subnet = "10.2.60.0/24" + appliance_ip = "10.2.60.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.60.1", end = "10.2.60.49" } + ] + } + } +} +``` + +### Example: configuring switch ports + +Edit `sites//switch.tf`. Use `switch_stack_port_configs` to apply a config to all members of a stack, or `switch_named_port_configs` to target a specific switch by name: + +```hcl +locals { + # Apply to all members of the stack + switch_stack_port_configs = [ + { + stack_name = "bcn01-lab-stack01" # exact name from Dashboard + port_range = "1-44" + type = "access" + vlan = 100 # fallback VLAN if RADIUS doesn't assign one + access_policy_type = "Custom access policy" + access_policy_number = 1 # references the DOT1X-CORPO policy + }, + ] + + # Target a specific stack member by display name + switch_named_port_configs = [ + { + switch_name = "bcn01-lab-sw01" + port_range = "45-48" + type = "trunk" + vlan = 109 # native (untagged) VLAN + allowed_vlans = "all" + }, + ] +} +``` + +`port_range` supports single ports (`"1"`), ranges (`"1-24"`), and mixed (`"1-3,5,47"`). + +### Example: adding a wireless SSID + +Edit `sites//ssids.tf`. Meraki numbers SSIDs from 0 to 14: + +```hcl +locals { + wireless_ssids = [ + # ... existing SSIDs ... + { + number = 3 + name = "EQT-IOT" + enabled = true + auth_mode = "psk" + encryption_mode = "wpa" + wpa_encryption_mode = "WPA3 Transition Mode" + # Password is injected via TF_VAR_wifi_password_psk (GitHub Secret) + ip_assignment_mode = "Bridge mode" + use_vlan_tagging = true + default_vlan_id = 112 + }, + ] +} +``` + +**Auth mode reference:** + +| `auth_mode` | Use case | Notes | +|-------------|----------|-------| +| `"open"` | Open network | `wpa_encryption_mode` must be omitted | +| `"open-enhanced"` | OWE (Opportunistic Wireless Encryption) | Use with `wpa_encryption_mode = "WPA3 only"` | +| `"psk"` | WPA2/WPA3 with shared password | Requires `encryption_mode = "wpa"` | +| `"8021x-radius"` | Enterprise 802.1X | Requires `radius_servers` list | + +--- + +## 5. Adding a New Site + +Adding a new site requires creating one new directory. The GitHub Actions workflows detect it automatically — no workflow changes needed. + +### Step 1 — Copy an existing site + +```bash +cp -r sites/BCN01-LAB sites/MAD01 +``` + +### Step 2 — Update `sites/MAD01/main.tf` + +Change the module name and the two locals at the top: + +```hcl +locals { + organization_name = "..." # exact org name in Meraki Dashboard + network_name = "MAD01" # exact network name in Meraki Dashboard +} + +module "mad01" { # rename to match the new site + source = "../../modules/meraki-site" + # everything else stays the same +} +``` + +The S3 state key is derived automatically from the directory name (`MAD01/terraform.tfstate`) — no manual backend configuration needed. + +### Step 3 — Update the config files + +Replace BCN01-LAB-specific values with the new site's actual configuration. See the [TL;DR](#adding-a-new-site) for the per-file summary, and the [Configuration Reference](#6-configuration-reference) for the full schema of each block. + +> Device names (`stack_name`, `switch_name`, MX names) must match the **exact display names** in the Meraki Dashboard for that network. + +### Step 4 — Open a PR + +See [Step by step — VS Code](#step-by-step--vs-code) or [Step by step — CLI](#step-by-step--cli). + +GitHub Actions detects the new `sites/MAD01/` directory, runs `terraform plan`, and posts the output as a PR comment. Review the plan, then merge to apply. + +### Step 5 — Review `MANUAL_STEPS.md` + +After the initial apply, check `sites/MAD01/MANUAL_STEPS.md` for any Dashboard steps that could not be automated. See [Section 10](#10-manual-steps--provider-limitations) for known provider limitations. + +--- + +## 6. Configuration Reference + +### VLANs (`vlans.tf`) + +```hcl +switch_vlans = { + "" = { + name = string # Display name + subnet = optional string # CIDR, e.g. "10.2.32.0/21". Null = L2 only (no MX gateway) + appliance_ip = optional string # MX gateway IP within the subnet + dhcp_handling = optional string # "Run a DHCP server" (default) + # "Relay DHCP to another server" + # "Do not respond to DHCP requests" + reserved_ip_ranges = optional list of { + comment = string + id = string # unique identifier, e.g. "static" + start = string # first IP to reserve + end = string # last IP to reserve + } + } +} +``` + +### Firewall rules (`firewall.tf`) + +Rules are applied **in order**. The last rule should always be an explicit deny-all. The entire list replaces the Dashboard rules on every apply. + +```hcl +firewall_rules = [ + { + comment = string # Human-readable description + policy = "allow" | "deny" + protocol = "any" | "tcp" | "udp" | "icmp" + src_cidr = string # CIDR or "any" + src_port = optional string # Port or "any" (default: "any") + dest_cidr = string # CIDR or "any" + dest_port = optional string # Port or "any" (default: "any") + syslog_enabled = optional bool # default: false + }, +] +``` + +### Wireless SSIDs (`ssids.tf`) + +```hcl +wireless_ssids = [ + { + number = number # Meraki SSID slot (0–14) + name = string + enabled = optional bool # default: true + visible = optional bool # false = hidden SSID. default: true + auth_mode = string # "open", "open-enhanced", "psk", "8021x-radius" + encryption_mode = optional string # "wpa" required for psk; null otherwise + wpa_encryption_mode = optional string # "WPA3 only", "WPA3 Transition Mode". Null for open + splash_page = optional string # default: "None" + ip_assignment_mode = optional string # default: "Bridge mode" + use_vlan_tagging = optional bool # default: false + default_vlan_id = optional number + radius_servers = optional list of { + host = string # RADIUS server IP + port = number + # secret is injected from TF_VAR_radius_secret — never put it here + } + }, +] +``` + +### Switch access policies (`switch.tf`) + +```hcl +switch_access_policies = [ + { + name = string # referenced by access_policy_number in port configs + access_policy_type = optional string # "802.1x" (default), "Hybrid authentication" + host_mode = optional string # "Multi-Auth" (default) + radius_failed_auth_vlan_id = optional number # fallback VLAN if RADIUS unreachable + radius_re_authentication_interval = optional number # seconds. 0 = disabled + radius_servers = list of { + host = string + port = number + } + }, +] +``` + +### Switch port configs (`switch.tf`) + +Three methods — use whichever fits: + +```hcl +# 1. By explicit serial +switch_port_configs = [ + { + serial = "XXXX-XXXX-XXXX" + port_range = "1-24" + type = "access" | "trunk" + vlan = optional number # access VLAN (access) or native VLAN (trunk) + allowed_vlans = optional string # trunk only. default: "all" + access_policy_type = optional string # "Open" (default) or "Custom access policy" + access_policy_number = optional number # index of the policy in switch_access_policies + }, +] + +# 2. By stack name — applies to ALL members of the stack +switch_stack_port_configs = [ + { + stack_name = "bcn01-lab-stack01" # exact Dashboard name + port_range = "1-44" + # ... same fields as above ... + }, +] + +# 3. By switch display name — resolves serial dynamically +switch_named_port_configs = [ + { + switch_name = "bcn01-lab-sw01" # exact Dashboard name + port_range = "1,2,3" + # ... same fields as above ... + }, +] +``` + +### MX WAN and HA (`wan.tf`) + +```hcl +mx_wan_uplinks = [ + { + name = "BCN01-F04-MX01" # exact Dashboard device name + wan1_static_ip = "x.x.x.x" + wan1_static_subnet_mask = "255.255.255.240" + wan1_static_gateway_ip = "x.x.x.x" + wan1_static_dns = ["8.8.8.8", "8.8.4.4"] + }, +] + +mx_warm_spare = { + enabled = true + spare_name = "BCN01-F04-MX02" # exact Dashboard device name + uplink_mode = "virtual" + virtual_ip1 = "x.x.x.x" # floating VIP on WAN1 + virtual_ip2 = "x.x.x.x" # floating VIP on WAN2 (if applicable) +} +``` + +--- + +## 7. GitHub Actions Workflows + +Both workflows use a `detect-sites` job that dynamically determines which sites to plan or apply based on which files changed. + +| Trigger | Workflow | Action | +|---------|----------|--------| +| Pull Request → `main` | `plan.yml` | Runs `terraform plan` for each changed site, posts output as PR comment | +| Push to `main` (merge) | `apply.yml` | Runs `terraform apply` for each changed site, serialized | + +**Site detection logic:** + +- `modules/` or `backend.hcl` changed → all sites planned/applied +- Only `sites//` changed → only that site planned/applied +- No relevant files changed → workflow skips entirely + +### `plan.yml` — Pull Request + +``` +PR opened/updated + │ + ▼ +detect-sites (ubuntu-latest) — reads git diff, builds site matrix + │ + ▼ +plan (self-hosted, matrix per site, parallel) + ├── terraform init + ├── terraform plan → plan_output.txt + └── Post plan as PR comment +``` + +### `apply.yml` — Merge to main + +``` +Merge to main + │ + ▼ +detect-sites (ubuntu-latest) + │ + ▼ +apply (self-hosted, matrix per site, max-parallel: 1) + ├── terraform init + └── terraform apply -auto-approve +``` + +`max-parallel: 1` serializes applies across sites to avoid DynamoDB lock contention. + +### Backend initialization + +```bash +terraform init \ + -backend-config=../../backend.hcl \ # shared: bucket, region, dynamodb_table + -backend-config="key=/terraform.tfstate" # site-specific state path +``` + +--- + +## 8. Sensitive Variables and Secrets + +Two variables must **never** appear in any `.tf` file. They are injected at runtime via environment variables: + +| Variable | GitHub Secret | Injected as | +|----------|--------------|-------------| +| `radius_secret` | `RADIUS_SECRET` | `TF_VAR_radius_secret` | +| `wifi_password_psk` | `WIFI_PASSWORD_PSK` | `TF_VAR_wifi_password_psk` | + +All other required GitHub Secrets: + +| Secret | Purpose | +|--------|---------| +| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | S3 backend (state storage) | +| `MERAKI_DASHBOARD_API_KEY` | Meraki API authentication | + +The RADIUS secret is shared across all RADIUS servers (SSIDs and switch 802.1X policies). If a site requires a different secret, a new GitHub Secret and a separate variable must be added. + +--- + +## 9. Running Terraform Locally (plan only) + +Local `terraform plan` is useful for debugging. `terraform apply` must never be run locally. + +```bash +export MERAKI_DASHBOARD_API_KEY="your-api-key" +export AWS_ACCESS_KEY_ID="..." +export AWS_SECRET_ACCESS_KEY="..." +export AWS_REGION="us-east-1" +export TF_VAR_radius_secret="..." +export TF_VAR_wifi_password_psk="..." + +cd sites/BCN01-LAB + +terraform init \ + -backend-config=../../backend.hcl \ + -backend-config="key=BCN01-LAB/terraform.tfstate" + +terraform plan +``` + +> The first `terraform init` downloads the provider binary into `.terraform/`. This directory is gitignored. + +--- + +## 10. Manual Steps — Provider Limitations + +Some Meraki features are not yet supported by the `CiscoDevNet/meraki` provider v1.9.0 and must be configured directly in the Meraki Dashboard. Each site directory should include a `MANUAL_STEPS.md` documenting any steps that cannot be automated for that site. + +For `BCN01-LAB`, see [`sites/BCN01-LAB/MANUAL_STEPS.md`](sites/BCN01-LAB/MANUAL_STEPS.md). + +| Feature | Status | Notes | +|---------|--------|-------| +| Client VPN (L2TP/IPSec) | Manual | No resource exists in provider v1.9.0 | +| OWE initial activation | Warning | Provider manages `auth_mode = "open-enhanced"` correctly; a one-time Dashboard confirmation may be needed after the very first apply | + +When a previously manual step becomes supported by the provider, migrate it to the appropriate `.tf` config file and remove it from `MANUAL_STEPS.md`. diff --git a/backend.hcl b/backend.hcl new file mode 100644 index 0000000..80ff5ed --- /dev/null +++ b/backend.hcl @@ -0,0 +1,4 @@ +bucket = "eqt-terraform-state-629066559706-us-east-1-an" +region = "us-east-1" +dynamodb_table = "terraform-locks" +encrypt = true diff --git a/modules/meraki-site/main.tf b/modules/meraki-site/main.tf index 24225fb..4ca7118 100755 --- a/modules/meraki-site/main.tf +++ b/modules/meraki-site/main.tf @@ -1,4 +1,5 @@ terraform { + required_version = ">= 1.5.0" required_providers { meraki = { source = "CiscoDevNet/meraki" diff --git a/modules/meraki-site/outputs.tf b/modules/meraki-site/outputs.tf index cb25d9e..b6a6d72 100755 --- a/modules/meraki-site/outputs.tf +++ b/modules/meraki-site/outputs.tf @@ -9,3 +9,15 @@ output "vlan_ids" { description = "Mapa de VLAN ID => ID de recurso creado en el MX" value = { for k, v in meraki_appliance_vlan.mx_gateways : k => v.vlan_id } } + +# IDs de los stacks de switches +output "stack_ids" { + description = "Mapa de nombre de stack => stack ID" + value = local.stack_ids +} + +# Seriales de los dispositivos de red por nombre +output "device_serials" { + description = "Mapa de nombre de dispositivo => serial" + value = local.device_serials +} diff --git a/modules/meraki-site/variables.tf b/modules/meraki-site/variables.tf index f7bc4b0..04e172b 100755 --- a/modules/meraki-site/variables.tf +++ b/modules/meraki-site/variables.tf @@ -18,9 +18,9 @@ variable "firewall_rules" { policy = string protocol = string src_cidr = string - src_port = string + src_port = optional(string, "any") dest_cidr = string - dest_port = string + dest_port = optional(string, "any") syslog_enabled = optional(bool, false) })) default = [] @@ -178,8 +178,6 @@ variable "stack_routing_interfaces" { ip_address = string # IP estática del stack en esta VLAN subnet = string # subred en formato CIDR, ej: "10.2.55.0/24" default_gateway = optional(string, null) # gateway para acceso a internet - dns1 = optional(string, null) # DNS primario - dns2 = optional(string, null) # DNS secundario })) default = [] description = "Interfaces L3 (SVIs) en stacks de switches para acceso de gestión al Dashboard de Meraki." diff --git a/sites/BCN01-LAB/MANUAL_STEPS.md b/sites/BCN01-LAB/MANUAL_STEPS.md index 698a63e..c53d8dc 100644 --- a/sites/BCN01-LAB/MANUAL_STEPS.md +++ b/sites/BCN01-LAB/MANUAL_STEPS.md @@ -25,9 +25,8 @@ y deben aplicarse directamente en el Meraki Dashboard. ## OWE (Opportunistic Wireless Encryption) — SSID EQT-CORPO +> **Note:** This is not a provider limitation. The provider manages `auth_mode = "open-enhanced"` correctly. + **Dashboard:** Wireless → SSIDs → EQT-CORPO → Edit settings → Security -Activar manualmente **"Opportunistic Wireless Encryption"**. - -> Terraform gestiona `auth_mode = "open-enhanced"` correctamente, pero la -> activación inicial de OWE puede requerir confirmación manual en el Dashboard. +After the first `terraform apply`, verify that **"Opportunistic Wireless Encryption"** is active in the Dashboard. In some cases Meraki requires a one-time manual confirmation click to enable OWE on a new SSID, even though Terraform has already pushed the correct configuration. diff --git a/sites/BCN01-LAB/appliance.auto.tfvars b/sites/BCN01-LAB/appliance.auto.tfvars deleted file mode 100644 index 0602d59..0000000 --- a/sites/BCN01-LAB/appliance.auto.tfvars +++ /dev/null @@ -1,15 +0,0 @@ -# Configuración de puertos LAN del firewall MX -# port_id: número del puerto físico en el MX -# type: "trunk" o "access" -# vlan: VLAN nativa (untagged) en trunk, o VLAN de acceso en access -appliance_ports = [ - { - # Puerto 7: trunk hacia el stack de switches - # VLAN nativa 109 (MANAGEMENT), permite todas las VLANs - port_id = "7" - enabled = true - type = "trunk" - vlan = 109 # MANAGEMENT - VLAN nativa (untagged) - allowed_vlans = "all" - }, -] diff --git a/sites/BCN01-LAB/appliance.tf b/sites/BCN01-LAB/appliance.tf new file mode 100644 index 0000000..9c83cee --- /dev/null +++ b/sites/BCN01-LAB/appliance.tf @@ -0,0 +1,13 @@ +locals { + appliance_ports = [ + { + # Port 7: trunk toward the switch stack + # Native VLAN 109 (MANAGEMENT), allows all VLANs + port_id = "7" + enabled = true + type = "trunk" + vlan = 109 # MANAGEMENT — native (untagged) VLAN + allowed_vlans = "all" + }, + ] +} diff --git a/sites/BCN01-LAB/firewall.auto.tfvars b/sites/BCN01-LAB/firewall.auto.tfvars deleted file mode 100755 index e8ba1f2..0000000 --- a/sites/BCN01-LAB/firewall.auto.tfvars +++ /dev/null @@ -1,68 +0,0 @@ -# Reglas de firewall L3 -firewall_rules = [ - { - # Permite que los switches (VLAN MANAGEMENT) lleguen a internet para acceder al Dashboard de Meraki - comment = "Permitir MANAGEMENT a internet (acceso Dashboard Meraki)" - policy = "allow" - protocol = "any" - src_cidr = "10.2.55.0/24" # VLAN 109 - MANAGEMENT - src_port = "any" - dest_cidr = "any" - dest_port = "any" - syslog_enabled = false - }, - { - # Permite que los APs (VLAN APs) lleguen a internet para acceder al Dashboard de Meraki - comment = "Permitir APs a internet (acceso Dashboard Meraki)" - policy = "allow" - protocol = "any" - src_cidr = "10.2.54.0/24" # VLAN 108 - APs - src_port = "any" - dest_cidr = "any" - dest_port = "any" - syslog_enabled = false - }, - { - # Permite que los clientes GUEST (VLAN GUEST) lleguen a internet - comment = "Permitir GUEST a internet" - policy = "allow" - protocol = "any" - src_cidr = "10.2.40.0/21" # VLAN 101 - GUEST - src_port = "any" - dest_cidr = "any" - dest_port = "any" - syslog_enabled = false - }, - { - # Permite que los clientes SERVERS (VLAN SERVERS) lleguen a internet - comment = "Permitir SERVERS a internet" - policy = "allow" - protocol = "any" - src_cidr = "10.2.56.0/24" # VLAN 102 - SERVERS - src_port = "any" - dest_cidr = "any" - dest_port = "any" - syslog_enabled = false - }, - { - # Permite que los clientes GUEST a SERVERS, TEMPORAL - comment = "Permitir GUEST a SERVERS" - policy = "allow" - protocol = "any" - src_cidr = "10.2.40.0/21" # VLAN 101 - GUEST - src_port = "any" - dest_cidr = "10.2.56.0/24" # VLAN 102 - SERVERS - dest_port = "any" - syslog_enabled = false - }, - { - comment = "Denegar el resto del trafico de salida" - policy = "deny" - protocol = "any" - src_cidr = "any" - src_port = "any" - dest_cidr = "any" - dest_port = "any" - syslog_enabled = false - } -] diff --git a/sites/BCN01-LAB/firewall.tf b/sites/BCN01-LAB/firewall.tf new file mode 100644 index 0000000..0932336 --- /dev/null +++ b/sites/BCN01-LAB/firewall.tf @@ -0,0 +1,46 @@ +locals { + firewall_rules = [ + { + comment = "Allow MANAGEMENT to internet (Meraki Dashboard access)" + policy = "allow" + protocol = "any" + src_cidr = "10.2.55.0/24" # VLAN 109 - MANAGEMENT + dest_cidr = "any" + }, + { + comment = "Allow APs to internet (Meraki Dashboard access)" + policy = "allow" + protocol = "any" + src_cidr = "10.2.54.0/24" # VLAN 108 - APs + dest_cidr = "any" + }, + { + comment = "Allow GUEST to internet" + policy = "allow" + protocol = "any" + src_cidr = "10.2.40.0/21" # VLAN 101 - GUEST + dest_cidr = "any" + }, + { + comment = "Allow SERVERS to internet" + policy = "allow" + protocol = "any" + src_cidr = "10.2.56.0/24" # VLAN 110 - SERVERS + dest_cidr = "any" + }, + { + comment = "Allow GUEST to SERVERS (temporary)" + policy = "allow" + protocol = "any" + src_cidr = "10.2.40.0/21" # VLAN 101 - GUEST + dest_cidr = "10.2.56.0/24" # VLAN 110 - SERVERS + }, + { + comment = "Deny all other outbound traffic" + policy = "deny" + protocol = "any" + src_cidr = "any" + dest_cidr = "any" + }, + ] +} diff --git a/sites/BCN01-LAB/main.tf b/sites/BCN01-LAB/main.tf index c93df61..1041ade 100755 --- a/sites/BCN01-LAB/main.tf +++ b/sites/BCN01-LAB/main.tf @@ -1,42 +1,32 @@ -# Configuración de Terraform y Provider terraform { - required_version = ">= 1.5.0" - required_providers { - meraki = { - source = "CiscoDevNet/meraki" - version = "1.9.0" - } - } - - backend "s3" { - bucket = "eqt-terraform-state-629066559706-us-east-1-an" - key = "BCN01-LAB/terraform.tfstate" - region = "us-east-1" - dynamodb_table = "terraform-locks" - encrypt = true - } + backend "s3" {} # Config injected via -backend-config in CI (see backend.hcl at repo root) + # required_version and required_providers are declared once in modules/meraki-site/main.tf } provider "meraki" {} -# Llamada al módulo meraki-site +locals { + organization_name = "Adevinta Information Services SLU" + network_name = "BCN01-LAB" +} + module "bcn01_lab" { source = "../../modules/meraki-site" - organization_name = var.organization_name - network_name = var.network_name - switch_vlans = var.switch_vlans - firewall_rules = var.firewall_rules - wireless_ssids = var.wireless_ssids - radius_secret = var.radius_secret - switch_access_policies = var.switch_access_policies - switch_port_configs = var.switch_port_configs - switch_stack_port_configs = var.switch_stack_port_configs - switch_named_port_configs = var.switch_named_port_configs - switch_management_vlan = var.switch_management_vlan - stack_routing_interfaces = var.stack_routing_interfaces - appliance_ports = var.appliance_ports - mx_wan_uplinks = var.mx_wan_uplinks - mx_warm_spare = var.mx_warm_spare + organization_name = local.organization_name + network_name = local.network_name + switch_vlans = local.switch_vlans + firewall_rules = local.firewall_rules + wireless_ssids = local.wireless_ssids + radius_secret = var.radius_secret wifi_password_psk = var.wifi_password_psk + switch_access_policies = local.switch_access_policies + switch_port_configs = local.switch_port_configs + switch_stack_port_configs = local.switch_stack_port_configs + switch_named_port_configs = local.switch_named_port_configs + switch_management_vlan = local.switch_management_vlan + stack_routing_interfaces = local.stack_routing_interfaces + appliance_ports = local.appliance_ports + mx_wan_uplinks = local.mx_wan_uplinks + mx_warm_spare = local.mx_warm_spare } diff --git a/sites/BCN01-LAB/ssids.auto.tfvars b/sites/BCN01-LAB/ssids.auto.tfvars deleted file mode 100644 index 209bcb4..0000000 --- a/sites/BCN01-LAB/ssids.auto.tfvars +++ /dev/null @@ -1,57 +0,0 @@ -# SSIDs wireless - BCN01 -# NOTA: El shared secret de RADIUS NO está aquí. -# Se pasa como variable de entorno TF_VAR_radius_secret (GitHub secret: RADIUS_SECRET) - -wireless_ssids = [ - { - number = 0 - name = "EQT-CORPO" - enabled = true - auth_mode = "open-enhanced" # OWE (Opportunistic Wireless Encryption) - # Meraki API utiliza "open-enhanced" para indicar Enhanced Open / OWE. - # Este valor refleja exactamente lo que está configurado en el Dashboard. - wpa_encryption_mode = "WPA3 only" - splash_page = "Password-protected with custom RADIUS" - ip_assignment_mode = "Bridge mode" - use_vlan_tagging = true - default_vlan_id = 100 - redirect_url = "https://www.adevinta.com" - radius_servers = [ - { - host = "15.15.15.15" - port = 1912 - } - ] - }, - { - number = 2 - name = "EQT-CORPO-OWE-OK" - enabled = true - visible = false # SSID oculto — no hace broadcast del nombre - auth_mode = "open" - wpa_encryption_mode = null # open no admite wpa_encryption_mode - splash_page = "Password-protected with custom RADIUS" - ip_assignment_mode = "Bridge mode" - use_vlan_tagging = true - default_vlan_id = 100 - redirect_url = "https://www.adevinta.com" - radius_servers = [ - { - host = "15.15.15.15" - port = 1912 - } - ] - }, - { - number = 1 - name = "EQT-GUEST" - enabled = true - auth_mode = "psk" # Modo para contraseña compartida - encryption_mode = "wpa" # Requerido por la API Meraki para PSK - wpa_encryption_mode = "WPA3 Transition Mode" - # psk se inyecta via TF_VAR_wifi_password_psk (GitHub secret WIFI_PASSWORD_PSK) - ip_assignment_mode = "Bridge mode" - use_vlan_tagging = true - default_vlan_id = 101 - } -] diff --git a/sites/BCN01-LAB/ssids.tf b/sites/BCN01-LAB/ssids.tf new file mode 100644 index 0000000..47fcd8f --- /dev/null +++ b/sites/BCN01-LAB/ssids.tf @@ -0,0 +1,46 @@ +locals { + wireless_ssids = [ + { + number = 0 + name = "EQT-CORPO" + enabled = true + auth_mode = "open-enhanced" # OWE (Opportunistic Wireless Encryption) + wpa_encryption_mode = "WPA3 only" + splash_page = "Password-protected with custom RADIUS" + ip_assignment_mode = "Bridge mode" + use_vlan_tagging = true + default_vlan_id = 100 + redirect_url = "https://www.adevinta.com" + radius_servers = [ + { host = "15.15.15.15", port = 1912 } + ] + }, + { + number = 2 + name = "EQT-CORPO-OWE-OK" + enabled = true + visible = false # Hidden SSID — no broadcast + auth_mode = "open" + splash_page = "Password-protected with custom RADIUS" + ip_assignment_mode = "Bridge mode" + use_vlan_tagging = true + default_vlan_id = 100 + redirect_url = "https://www.adevinta.com" + radius_servers = [ + { host = "15.15.15.15", port = 1912 } + ] + }, + { + number = 1 + name = "EQT-GUEST" + enabled = true + auth_mode = "psk" + encryption_mode = "wpa" + wpa_encryption_mode = "WPA3 Transition Mode" + # Password injected via TF_VAR_wifi_password_psk (GitHub Secret: WIFI_PASSWORD_PSK) + ip_assignment_mode = "Bridge mode" + use_vlan_tagging = true + default_vlan_id = 101 + }, + ] +} diff --git a/sites/BCN01-LAB/switch.auto.tfvars b/sites/BCN01-LAB/switch.auto.tfvars deleted file mode 100644 index 681bf71..0000000 --- a/sites/BCN01-LAB/switch.auto.tfvars +++ /dev/null @@ -1,210 +0,0 @@ -# Configuración de switches MS - BCN01-LAB -# El shared secret de RADIUS se pasa via TF_VAR_radius_secret (GitHub secret: RADIUS_SECRET) - -# --- POLÍTICAS DE ACCESO 802.1X --- -switch_access_policies = [ - { - name = "DOT1X-CORPO" - access_policy_type = "Hybrid authentication" - host_mode = "Multi-Auth" - radius_accounting_enabled = false - radius_re_authentication_interval = 0 - url_redirect_walled_garden_enabled = false - - # VLAN a la que cae el puerto si el RADIUS no responde - radius_failed_auth_vlan_id = 101 # GUEST - - radius_servers = [ - { - host = "15.15.15.15" - port = 1912 - } - ] - } -] - -# --- PUERTOS DE SWITCH --- -# El serial aparece en Dashboard > Switches > nombre del switch > Overview. -# access_policy_number: número auto-asignado por Meraki a la política creada arriba -# (visible en Dashboard > Switches > Switch settings > Access policies) -# -# Ejemplo con los tres tipos de puerto: -# switch_port_configs = [ -# -# # Puertos de acceso general con 802.1X (PCs, portátiles) -# # Autenticación: 802.1X → MAB → VLAN GUEST si falla RADIUS -# # La VLAN final la asigna Okta dinámicamente; vlan=100 es el fallback estático -# { -# serial = "XXXX-XXXX-XXXX" -# port_range = "1-20" -# type = "access" -# vlan = 100 # ACCESS - fallback si Okta no devuelve VLAN -# access_policy_type = "Custom access policy" -# access_policy_number = 1 # id de la política DOT1X-CORPO -# }, -# -# # Puertos designados para impresoras (sin 802.1X) -# # VLAN asignada estáticamente en el puerto - las Group Policies en switches no asignan VLAN -# { -# serial = "XXXX-XXXX-XXXX" -# port_range = "21-24" -# type = "access" -# vlan = 103 # PRINTERS - VLAN fija en el puerto -# access_policy_type = "Open" -# }, -# -# # Puertos designados para APs (sin 802.1X) -# # Igual que impresoras: VLAN fija en el puerto -# { -# serial = "XXXX-XXXX-XXXX" -# port_range = "25-27" -# type = "access" -# vlan = 108 # APs - VLAN fija en el puerto -# access_policy_type = "Open" -# }, -# -# # Puerto de uplink (trunk, sin autenticación) -# { -# serial = "XXXX-XXXX-XXXX" -# port_range = "28" -# type = "trunk" -# access_policy_type = "Open" -# }, -# -# ] -switch_port_configs = [] - -# --- PUERTOS DE STACK --- -# Terraform resuelve automáticamente los seriales de todos los miembros del stack. -# El stack_name debe coincidir exactamente con el nombre en Dashboard > Switches > Stacks. -# Terraform aplicará el mismo port_range a CADA switch del stack (ambos de 48 puertos). -# -# Ejemplo para bnc01-lab-stack01 (2x 48 puertos): -# switch_stack_port_configs = [ -# -# # Puertos 1-44: acceso general con 802.1X (PCs, portátiles) -# { -# stack_name = "bnc01-lab-stack01" -# port_range = "1-44" -# type = "access" -# vlan = 100 # ACCESS - fallback si Okta no devuelve VLAN -# access_policy_type = "Custom access policy" -# access_policy_number = 1 # id de la política DOT1X-CORPO -# }, -# -# # Puertos 45-46: impresoras (VLAN fija, sin 802.1X) -# { -# stack_name = "bnc01-lab-stack01" -# port_range = "45-46" -# type = "access" -# vlan = 103 # PRINTERS -# access_policy_type = "Open" -# }, -# -# # Puertos 47-48: APs (VLAN fija, sin 802.1X) -# { -# stack_name = "bnc01-lab-stack01" -# port_range = "47-48" -# type = "access" -# vlan = 108 # APs -# access_policy_type = "Open" -# }, -# -# ] -switch_stack_port_configs = [ - { - stack_name = "bcn01-lab-stack01" - port_range = "6" - type = "access" - vlan = 101 # GUEST - access_policy_type = "Open" - }, - { - # Puerto 44: ISP router (acceso WAN) - stack_name = "bcn01-lab-stack01" - port_range = "44" - name = "ISP router 1" - type = "access" - vlan = 111 # WAN - access_policy_type = "Open" - }, - { - # Puerto 45: WAN1 del MX primary - stack_name = "bcn01-lab-stack01" - port_range = "45" - name = "WAN 1 BCN01-F04-MX01" - type = "access" - vlan = 111 # WAN - access_policy_type = "Open" - }, - { - # Puerto 46: WAN1 del MX spare - stack_name = "bcn01-lab-stack01" - port_range = "46" - name = "WAN 1 BCN01-F04-MX02" - type = "access" - vlan = 111 # WAN - access_policy_type = "Open" - }, - { - # Puerto 47: uplink LAN del MX primary - stack_name = "bcn01-lab-stack01" - port_range = "47" - name = "UPLINK LAN BCN01-F04-MX01" - type = "trunk" - vlan = 109 # MANAGEMENT - VLAN nativa (untagged) - allowed_vlans = "all" - access_policy_type = "Open" - }, - { - # Puerto 48: uplink LAN del MX spare - stack_name = "bcn01-lab-stack01" - port_range = "48" - name = "UPLINK LAN BCN01-F04-MX02" - type = "trunk" - vlan = 109 # MANAGEMENT - VLAN nativa (untagged) - allowed_vlans = "all" - access_policy_type = "Open" - }, -] - -# Puertos de un switch concreto (miembro individual del stack) -# El serial se encuentra en: Dashboard > Switches > eqt-lab-st01-sw01 > Overview -switch_named_port_configs = [ - { - # Puerto 1 de eqt-lab-st01-sw01 → VLAN SERVERS (estática, sin autenticación) - switch_name = "eqt-lab-st01-sw01" - port_range = "1" - name = "Servers" - type = "access" - vlan = 101 # SERVERS - access_policy_type = "Open" - }, - { - # Puertos 2 y 3 de eqt-lab-st01-sw01 → APs (trunk, nativa 108 APs, tageadas 100 ACCESS y 101 GUEST) - switch_name = "eqt-lab-st01-sw01" - port_range = "2,3" - name = "AP" - type = "trunk" - vlan = 108 # APs - VLAN nativa (untagged) - allowed_vlans = "100,101,108" # ACCESS + GUEST + APs - access_policy_type = "Open" - }, -] - -# VLAN de gestión de los switches del site -switch_management_vlan = 109 - -# Interfaces L3 en el stack para acceso de gestión al Dashboard de Meraki -stack_routing_interfaces = [ - { - stack_name = "bcn01-lab-stack01" - name = "MANAGEMENT" - vlan_id = 109 - ip_address = "10.2.55.2" - subnet = "10.2.55.0/24" - default_gateway = "10.2.55.1" - dns1 = "8.8.8.8" - dns2 = "8.8.4.4" - }, -] diff --git a/sites/BCN01-LAB/switch.tf b/sites/BCN01-LAB/switch.tf new file mode 100644 index 0000000..0b1bbc7 --- /dev/null +++ b/sites/BCN01-LAB/switch.tf @@ -0,0 +1,121 @@ +locals { + # 802.1X access policies + # The RADIUS shared secret is injected from var.radius_secret — never put it here. + switch_access_policies = [ + { + name = "DOT1X-CORPO" + access_policy_type = "Hybrid authentication" + host_mode = "Multi-Auth" + radius_accounting_enabled = false + radius_re_authentication_interval = 0 + url_redirect_walled_garden_enabled = false + radius_failed_auth_vlan_id = 101 # Fallback to GUEST if RADIUS unreachable + radius_servers = [ + { host = "15.15.15.15", port = 1912 } + ] + }, + ] + + # Ports by explicit serial — use when targeting a switch directly by serial number + # Example: + # switch_port_configs = [ + # { + # serial = "XXXX-XXXX-XXXX" + # port_range = "1-20" + # type = "access" + # vlan = 100 # ACCESS — fallback if RADIUS doesn't assign a VLAN + # access_policy_type = "Custom access policy" + # access_policy_number = 1 # references DOT1X-CORPO above + # }, + # ] + switch_port_configs = [] + + # Ports by stack name — Terraform resolves serials for all stack members automatically. + # The same port_range is applied to EVERY switch in the stack. + switch_stack_port_configs = [ + { + stack_name = "bcn01-lab-stack01" + port_range = "6" + type = "access" + vlan = 101 # GUEST + access_policy_type = "Open" + }, + { + stack_name = "bcn01-lab-stack01" + port_range = "44" + name = "ISP router 1" + type = "access" + vlan = 111 # WAN + access_policy_type = "Open" + }, + { + stack_name = "bcn01-lab-stack01" + port_range = "45" + name = "WAN 1 BCN01-F04-MX01" + type = "access" + vlan = 111 # WAN + access_policy_type = "Open" + }, + { + stack_name = "bcn01-lab-stack01" + port_range = "46" + name = "WAN 1 BCN01-F04-MX02" + type = "access" + vlan = 111 # WAN + access_policy_type = "Open" + }, + { + stack_name = "bcn01-lab-stack01" + port_range = "47" + name = "UPLINK LAN BCN01-F04-MX01" + type = "trunk" + vlan = 109 # MANAGEMENT — native (untagged) VLAN + allowed_vlans = "all" + access_policy_type = "Open" + }, + { + stack_name = "bcn01-lab-stack01" + port_range = "48" + name = "UPLINK LAN BCN01-F04-MX02" + type = "trunk" + vlan = 109 # MANAGEMENT — native (untagged) VLAN + allowed_vlans = "all" + access_policy_type = "Open" + }, + ] + + # Ports by switch display name — targets a specific stack member without knowing its serial + switch_named_port_configs = [ + { + switch_name = "eqt-lab-st01-sw01" + port_range = "1" + name = "Servers" + type = "access" + vlan = 101 # SERVERS + access_policy_type = "Open" + }, + { + switch_name = "eqt-lab-st01-sw01" + port_range = "2,3" + name = "AP" + type = "trunk" + vlan = 108 # APs — native (untagged) VLAN + allowed_vlans = "100,101,108" # ACCESS + GUEST + APs + access_policy_type = "Open" + }, + ] + + switch_management_vlan = 109 + + # L3 routing interfaces on the stack (SVIs for management access to Meraki Dashboard) + stack_routing_interfaces = [ + { + stack_name = "bcn01-lab-stack01" + name = "MANAGEMENT" + vlan_id = 109 + ip_address = "10.2.55.2" + subnet = "10.2.55.0/24" + default_gateway = "10.2.55.1" + }, + ] +} diff --git a/sites/BCN01-LAB/variables.tf b/sites/BCN01-LAB/variables.tf index 390e8ec..e6aaeac 100755 --- a/sites/BCN01-LAB/variables.tf +++ b/sites/BCN01-LAB/variables.tf @@ -1,209 +1,15 @@ -# Definición de la Organización -variable "organization_name" { - type = string - description = "Nombre exacto de tu organización en el Dashboard de Meraki" -} +# Sensitive variables — injected via TF_VAR_* environment variables (GitHub Secrets). +# Never put values for these in any .tf file. -# Definición de la Red -variable "network_name" { - type = string - description = "Nombre de la red (Network) donde reside el switch" -} - - -# Reglas de firewall L3 -variable "firewall_rules" { - type = list(object({ - comment = string - policy = string - protocol = string - src_cidr = string - src_port = string - dest_cidr = string - dest_port = string - syslog_enabled = optional(bool, false) - })) - default = [] - description = "Lista de reglas de firewall L3 para el site" -} - -# SSIDs wireless -variable "wireless_ssids" { - type = list(object({ - number = number - name = string - enabled = optional(bool, true) - auth_mode = string - psk = optional(string, null) - encryption_mode = optional(string, null) # "wpa" para PSK; null para open/OWE - splash_page = optional(string, "None") - wpa_encryption_mode = optional(string, "WPA3 only") - ip_assignment_mode = optional(string, "Bridge mode") - use_vlan_tagging = optional(bool, false) - default_vlan_id = optional(number, null) - redirect_url = optional(string, "") - radius_servers = optional(list(object({ - host = string - port = number - })), []) - })) - default = [] - description = "Lista de SSIDs wireless a configurar en el site" -} - -variable "wifi_password_psk" { - type = string - description = "Password para la SSID WPA2 desde GitHub Secrets" - sensitive = true -} - -# Shared secret para servidores RADIUS (sensible, no incluir en tfvars) variable "radius_secret" { type = string sensitive = true default = "" - description = "Shared secret para autenticación RADIUS. Pasar via TF_VAR_radius_secret" + description = "RADIUS shared secret. Pass via TF_VAR_radius_secret (GitHub Secret: RADIUS_SECRET)." } -# Definición de VLANs -variable "switch_vlans" { - type = map(object({ - name = string - subnet = optional(string, null) - appliance_ip = optional(string, null) - dhcp_handling = optional(string, "Run a DHCP server") - reserved_ip_ranges = optional(list(object({ - comment = string - id = string - start = string - end = string - })), []) - })) - description = "Mapa de configuraciones de VLAN. La clave del mapa será el ID de la VLAN" -} - - -# Políticas de acceso 802.1X para switches -variable "switch_access_policies" { - type = list(object({ - name = string - access_policy_type = optional(string, "802.1x") - host_mode = optional(string, "Multi-Auth") - radius_accounting_enabled = optional(bool, false) - radius_testing_enabled = optional(bool, false) - radius_coa_support_enabled = optional(bool, false) - radius_failed_auth_vlan_id = optional(number, null) - radius_re_authentication_interval = optional(number, 0) - url_redirect_walled_garden_enabled = optional(bool, false) - radius_servers = list(object({ - host = string - port = number - })) - })) - default = [] - description = "Políticas de acceso 802.1X para switches MS" -} - -# Configuración de puertos de switch -variable "switch_port_configs" { - type = list(object({ - serial = string - port_range = string - name = optional(string, "") - type = optional(string, "access") - vlan = optional(number, null) - allowed_vlans = optional(string, "all") - access_policy_type = optional(string, "Open") - access_policy_number = optional(number, null) - })) - default = [] - description = "Puertos de switch a configurar con 802.1X. Requiere serial del switch." -} - -variable "switch_stack_port_configs" { - type = list(object({ - stack_name = string - port_range = string - name = optional(string, "") - type = optional(string, "access") - vlan = optional(number, null) - allowed_vlans = optional(string, "all") - access_policy_type = optional(string, "Open") - access_policy_number = optional(number, null) - })) - default = [] - description = "Puertos de stack a configurar por nombre. Terraform resuelve los seriales automáticamente." -} - -variable "switch_named_port_configs" { - type = list(object({ - switch_name = string - port_range = string - name = optional(string, "") - type = optional(string, "access") - vlan = optional(number, null) - allowed_vlans = optional(string, "all") - access_policy_type = optional(string, "Open") - access_policy_number = optional(number, null) - })) - default = [] - description = "Puertos de switch a configurar por nombre de dispositivo. Terraform resuelve el serial automáticamente." -} - -variable "switch_management_vlan" { - type = number - default = null - description = "VLAN ID de gestión para los switches del site." -} - -variable "stack_routing_interfaces" { - type = list(object({ - stack_name = string - name = string - vlan_id = number - ip_address = string - subnet = string - default_gateway = optional(string, null) - dns1 = optional(string, null) - dns2 = optional(string, null) - })) - default = [] - description = "Interfaces L3 (SVIs) en stacks de switches para acceso de gestión al Dashboard." -} - -variable "appliance_ports" { - type = list(object({ - port_id = string - enabled = optional(bool, true) - type = optional(string, "access") - vlan = optional(number, null) - allowed_vlans = optional(string, "all") - drop_untagged_traffic = optional(bool, false) - })) - default = [] - description = "Configuración de puertos LAN del firewall MX." -} - -variable "mx_warm_spare" { - type = object({ - enabled = optional(bool, true) - spare_name = string - uplink_mode = optional(string, "virtual") - virtual_ip1 = optional(string, null) - virtual_ip2 = optional(string, null) - }) - default = null - description = "Configuración Warm Spare (HA) del MX." -} - -variable "mx_wan_uplinks" { - type = list(object({ - name = string # Nombre del dispositivo en el Dashboard - wan1_static_ip = optional(string, null) - wan1_static_subnet_mask = optional(string, null) - wan1_static_gateway_ip = optional(string, null) - wan1_static_dns = optional(list(string), null) - })) - default = [] - description = "Configuración WAN1 estática de los MX. El serial se resuelve automáticamente por nombre de dispositivo." +variable "wifi_password_psk" { + type = string + sensitive = true + description = "PSK Wi-Fi password. Pass via TF_VAR_wifi_password_psk (GitHub Secret: WIFI_PASSWORD_PSK)." } diff --git a/sites/BCN01-LAB/vlans.auto.tfvars b/sites/BCN01-LAB/vlans.auto.tfvars deleted file mode 100755 index dbfa75c..0000000 --- a/sites/BCN01-LAB/vlans.auto.tfvars +++ /dev/null @@ -1,97 +0,0 @@ -# Nombre exacto que aparece en tu Dashboard de Meraki -organization_name = "Adevinta Information Services SLU" -network_name = "BCN01-LAB" - -# Configuración de las VLANs (L3) -# La clave (ej. "10") es el ID de la VLAN -switch_vlans = { - "100" = { - name = "ACCESS" - subnet = "10.2.32.0/21" - appliance_ip = "10.2.32.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.32.1", end = "10.2.32.49" } - ] - } - "101" = { - name = "GUEST" - subnet = "10.2.40.0/21" - appliance_ip = "10.2.40.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.40.1", end = "10.2.40.49" } - ] - } - "102" = { - name = "VC" - subnet = "10.2.48.0/24" - appliance_ip = "10.2.48.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.48.1", end = "10.2.48.49" } - ] - } - "103" = { - name = "PRINTERS" - subnet = "10.2.49.0/24" - appliance_ip = "10.2.49.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.49.1", end = "10.2.49.49" } - ] - } - "104" = { - name = "DISPLAYS" - subnet = "10.2.50.0/24" - appliance_ip = "10.2.50.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.50.1", end = "10.2.50.49" } - ] - } - "105" = { - name = "BOOKING" - subnet = "10.2.51.0/24" - appliance_ip = "10.2.51.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.51.1", end = "10.2.51.49" } - ] - } - "106" = { - name = "BADGE_READERS" - subnet = "10.2.52.0/24" - appliance_ip = "10.2.52.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.52.1", end = "10.2.52.49" } - ] - } - "107" = { - name = "CCTV" - subnet = "10.2.53.0/24" - appliance_ip = "10.2.53.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.53.1", end = "10.2.53.49" } - ] - } - "108" = { - name = "APs" - subnet = "10.2.54.0/24" - appliance_ip = "10.2.54.1" - reserved_ip_ranges = [ - { comment = "Estáticas reservadas", id = "static", start = "10.2.54.1", end = "10.2.54.49" } - ] - } - "109" = { - name = "MANAGEMENT" - subnet = "10.2.55.0/24" - appliance_ip = "10.2.55.1" - dhcp_handling = "Do not respond to DHCP requests" - } - "110" = { - name = "SERVERS" - subnet = "10.2.56.0/24" - appliance_ip = "10.2.56.1" - dhcp_handling = "Do not respond to DHCP requests" - } - "111" = { - name = "WAN" - dhcp_handling = "Do not respond to DHCP requests" - # Sin L3: sin subnet ni appliance_ip (VLAN de switching puro hacia el ISP) - } -} \ No newline at end of file diff --git a/sites/BCN01-LAB/vlans.tf b/sites/BCN01-LAB/vlans.tf new file mode 100644 index 0000000..8e53b65 --- /dev/null +++ b/sites/BCN01-LAB/vlans.tf @@ -0,0 +1,93 @@ +locals { + switch_vlans = { + "100" = { + name = "ACCESS" + subnet = "10.2.32.0/21" + appliance_ip = "10.2.32.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.32.1", end = "10.2.32.49" } + ] + } + "101" = { + name = "GUEST" + subnet = "10.2.40.0/21" + appliance_ip = "10.2.40.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.40.1", end = "10.2.40.49" } + ] + } + "102" = { + name = "VC" + subnet = "10.2.48.0/24" + appliance_ip = "10.2.48.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.48.1", end = "10.2.48.49" } + ] + } + "103" = { + name = "PRINTERS" + subnet = "10.2.49.0/24" + appliance_ip = "10.2.49.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.49.1", end = "10.2.49.49" } + ] + } + "104" = { + name = "DISPLAYS" + subnet = "10.2.50.0/24" + appliance_ip = "10.2.50.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.50.1", end = "10.2.50.49" } + ] + } + "105" = { + name = "BOOKING" + subnet = "10.2.51.0/24" + appliance_ip = "10.2.51.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.51.1", end = "10.2.51.49" } + ] + } + "106" = { + name = "BADGE_READERS" + subnet = "10.2.52.0/24" + appliance_ip = "10.2.52.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.52.1", end = "10.2.52.49" } + ] + } + "107" = { + name = "CCTV" + subnet = "10.2.53.0/24" + appliance_ip = "10.2.53.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.53.1", end = "10.2.53.49" } + ] + } + "108" = { + name = "APs" + subnet = "10.2.54.0/24" + appliance_ip = "10.2.54.1" + reserved_ip_ranges = [ + { comment = "Static reserved", id = "static", start = "10.2.54.1", end = "10.2.54.49" } + ] + } + "109" = { + name = "MANAGEMENT" + subnet = "10.2.55.0/24" + appliance_ip = "10.2.55.1" + dhcp_handling = "Do not respond to DHCP requests" + } + "110" = { + name = "SERVERS" + subnet = "10.2.56.0/24" + appliance_ip = "10.2.56.1" + dhcp_handling = "Do not respond to DHCP requests" + } + "111" = { + name = "WAN" + dhcp_handling = "Do not respond to DHCP requests" + # No subnet or appliance_ip — L2-only switching VLAN toward the ISP + } + } +} diff --git a/sites/BCN01-LAB/wan.auto.tfvars b/sites/BCN01-LAB/wan.auto.tfvars deleted file mode 100644 index 1edc225..0000000 --- a/sites/BCN01-LAB/wan.auto.tfvars +++ /dev/null @@ -1,32 +0,0 @@ -# Configuración WAN1 estática de los firewalls MX -# El nombre debe coincidir exactamente con el nombre del dispositivo en el Dashboard -# Dashboard > Security & SD-WAN > Appliance > nombre del dispositivo - -# Warm Spare (HA): VIP flotante entre primary y spare -# La IP de salida del tráfico será siempre la VIP -mx_warm_spare = { - enabled = true - spare_name = "BCN01-F04-MX02" - uplink_mode = "virtual" - virtual_ip1 = "213.229.159.148" # VIP WAN1 - virtual_ip2 = "10.212.160.40" # VIP WAN2 -} - -mx_wan_uplinks = [ - { - # MX Primary - name = "BCN01-F04-MX01" # TODO: ajustar al nombre real en el Dashboard - wan1_static_ip = "213.229.159.145" - wan1_static_subnet_mask = "255.255.255.240" # /28 - wan1_static_gateway_ip = "213.229.159.147" - wan1_static_dns = ["8.8.8.8", "8.8.4.4"] - }, - { - # MX Spare (Warm Spare / HA) - name = "BCN01-F04-MX02" # TODO: ajustar al nombre real en el Dashboard - wan1_static_ip = "213.229.159.146" - wan1_static_subnet_mask = "255.255.255.240" # /28 - wan1_static_gateway_ip = "213.229.159.147" - wan1_static_dns = ["8.8.8.8", "8.8.4.4"] - }, -] diff --git a/sites/BCN01-LAB/wan.tf b/sites/BCN01-LAB/wan.tf new file mode 100644 index 0000000..a233436 --- /dev/null +++ b/sites/BCN01-LAB/wan.tf @@ -0,0 +1,29 @@ +locals { + # Warm Spare (HA) — floating VIP between primary and spare MX + mx_warm_spare = { + enabled = true + spare_name = "BCN01-F04-MX02" + uplink_mode = "virtual" + virtual_ip1 = "213.229.159.148" # Floating VIP on WAN1 + virtual_ip2 = "10.212.160.40" # Floating VIP on WAN2 + } + + # Static WAN1 configuration for each MX + # Device serials are resolved automatically from the display name + mx_wan_uplinks = [ + { + name = "BCN01-F04-MX01" + wan1_static_ip = "213.229.159.145" + wan1_static_subnet_mask = "255.255.255.240" # /28 + wan1_static_gateway_ip = "213.229.159.147" + wan1_static_dns = ["8.8.8.8", "8.8.4.4"] + }, + { + name = "BCN01-F04-MX02" + wan1_static_ip = "213.229.159.146" + wan1_static_subnet_mask = "255.255.255.240" # /28 + wan1_static_gateway_ip = "213.229.159.147" + wan1_static_dns = ["8.8.8.8", "8.8.4.4"] + }, + ] +}