Merge pull request #21 from its-corp/feature/multi-site-scalability
feat: multi-site scalability, locals refactor, README
This commit is contained in:
@@ -3,13 +3,53 @@ name: Terraform Apply
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'sites/**'
|
||||||
|
- 'modules/**'
|
||||||
|
- 'backend.hcl'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
detect-sites:
|
||||||
|
runs-on: self-hosted
|
||||||
|
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 -Rc 'select(. != "")' | jq -sc '.')
|
||||||
|
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 -Rc 'select(. != "")' | jq -sc '.')
|
||||||
|
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:
|
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
|
runs-on: self-hosted
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Reset git SSH override
|
||||||
|
run: git config --global --unset url."git@github.mpi-internal.com:".insteadOf || true
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -22,21 +62,23 @@ jobs:
|
|||||||
terraform_version: 1.5.0
|
terraform_version: 1.5.0
|
||||||
|
|
||||||
- name: Terraform Init
|
- name: Terraform Init
|
||||||
working-directory: sites/BCN01-LAB
|
working-directory: sites/${{ matrix.site }}
|
||||||
run: terraform init
|
run: |
|
||||||
|
terraform init \
|
||||||
|
-backend-config=../../backend.hcl \
|
||||||
|
-backend-config="key=${{ matrix.site }}/terraform.tfstate"
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_REGION: us-east-1
|
AWS_REGION: us-east-1
|
||||||
MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }}
|
|
||||||
|
|
||||||
- name: Terraform Apply
|
- name: Terraform Apply
|
||||||
working-directory: sites/BCN01-LAB
|
working-directory: sites/${{ matrix.site }}
|
||||||
run: terraform apply -auto-approve
|
run: terraform apply -auto-approve
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_REGION: us-east-1
|
AWS_REGION: us-east-1
|
||||||
MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }}
|
MERAKI_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }}
|
||||||
TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }}
|
TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }}
|
||||||
TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }}
|
TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }}
|
||||||
|
|||||||
+59
-15
@@ -3,15 +3,56 @@ name: Terraform Plan
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'sites/**'
|
||||||
|
- 'modules/**'
|
||||||
|
- 'backend.hcl'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
detect-sites:
|
||||||
|
runs-on: self-hosted
|
||||||
|
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 -Rc 'select(. != "")' | jq -sc '.')
|
||||||
|
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 -Rc 'select(. != "")' | jq -sc '.')
|
||||||
|
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:
|
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
|
runs-on: self-hosted
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Reset git SSH override
|
||||||
|
run: git config --global --unset url."git@github.mpi-internal.com:".insteadOf || true
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -24,24 +65,27 @@ jobs:
|
|||||||
terraform_version: 1.5.0
|
terraform_version: 1.5.0
|
||||||
|
|
||||||
- name: Terraform Init
|
- name: Terraform Init
|
||||||
working-directory: sites/BCN01-LAB
|
working-directory: sites/${{ matrix.site }}
|
||||||
run: terraform init
|
run: |
|
||||||
|
terraform init \
|
||||||
|
-backend-config=../../backend.hcl \
|
||||||
|
-backend-config="key=${{ matrix.site }}/terraform.tfstate"
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_REGION: us-east-1
|
AWS_REGION: us-east-1
|
||||||
|
|
||||||
- name: Terraform Plan
|
- name: Terraform Plan
|
||||||
id: plan
|
id: plan
|
||||||
working-directory: sites/BCN01-LAB
|
working-directory: sites/${{ matrix.site }}
|
||||||
run: terraform plan -no-color 2>&1 | tee plan_output.txt
|
run: terraform plan -no-color 2>&1 | tee plan_output.txt
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_REGION: us-east-1
|
AWS_REGION: us-east-1
|
||||||
MERAKI_DASHBOARD_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }}
|
MERAKI_API_KEY: ${{ secrets.MERAKI_DASHBOARD_API_KEY }}
|
||||||
TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }}
|
TF_VAR_radius_secret: ${{ secrets.RADIUS_SECRET }}
|
||||||
TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }}
|
TF_VAR_wifi_password_psk: ${{ secrets.WIFI_PASSWORD_PSK }}
|
||||||
|
|
||||||
- name: Comentar Plan en el PR
|
- name: Comentar Plan en el PR
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
@@ -49,11 +93,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
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;
|
const truncated = plan.length > 60000 ? plan.substring(0, 60000) + '\n...(truncado)' : plan;
|
||||||
github.rest.issues.createComment({
|
github.rest.issues.createComment({
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body: `## Terraform Plan - BCN01-LAB\n\`\`\`\n${truncated}\n\`\`\``
|
body: `## Terraform Plan - ${{ matrix.site }}\n\`\`\`\n${truncated}\n\`\`\``
|
||||||
});
|
});
|
||||||
|
|||||||
+16
@@ -9,6 +9,22 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
# Documentacion (HLD, LLD) - no se versiona en git
|
# Documentacion (HLD, LLD) - no se versiona en git
|
||||||
docs/
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,645 @@
|
|||||||
|
# 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/<SITE>/`:
|
||||||
|
|
||||||
|
| 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/<site>/
|
||||||
|
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/<site>-<description>` | `feature/BCN01-LAB-add-iot-vlan` |
|
||||||
|
| Commit | `feat(<site>): <description>` | `feat(BCN01-LAB): add IoT VLAN 112` |
|
||||||
|
| Bugfix branch | `fix/<site>-<description>` | `fix/BCN01-LAB-ssid-visible` |
|
||||||
|
| Bugfix commit | `fix(<site>): <description>` | `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/<site>-<description>`
|
||||||
|
2. Edit the relevant file(s) under `sites/<site>/`
|
||||||
|
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/<site>-<description>
|
||||||
|
|
||||||
|
# 2. Edit files, then stage and commit
|
||||||
|
git add sites/<site>/<file>.tf
|
||||||
|
git commit -m "feat(<site>): <description>"
|
||||||
|
|
||||||
|
# 3. Push the branch
|
||||||
|
git push origin feature/<site>-<description>
|
||||||
|
|
||||||
|
# 4. Open a PR (interactive) or directly in the browser
|
||||||
|
gh pr create --title "feat(<site>): <description>"
|
||||||
|
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/<site>/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/<site>/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/<site>/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/<site>/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 = {
|
||||||
|
"<vlan_id>" = {
|
||||||
|
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/<name>/` 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=<site>/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`.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
bucket = "eqt-terraform-state-629066559706-us-east-1-an"
|
||||||
|
region = "us-east-1"
|
||||||
|
dynamodb_table = "terraform-locks"
|
||||||
|
encrypt = true
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
terraform {
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
required_providers {
|
required_providers {
|
||||||
meraki = {
|
meraki = {
|
||||||
source = "CiscoDevNet/meraki"
|
source = "CiscoDevNet/meraki"
|
||||||
|
|||||||
@@ -9,3 +9,15 @@ output "vlan_ids" {
|
|||||||
description = "Mapa de VLAN ID => ID de recurso creado en el MX"
|
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 }
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ variable "firewall_rules" {
|
|||||||
policy = string
|
policy = string
|
||||||
protocol = string
|
protocol = string
|
||||||
src_cidr = string
|
src_cidr = string
|
||||||
src_port = string
|
src_port = optional(string, "any")
|
||||||
dest_cidr = string
|
dest_cidr = string
|
||||||
dest_port = string
|
dest_port = optional(string, "any")
|
||||||
syslog_enabled = optional(bool, false)
|
syslog_enabled = optional(bool, false)
|
||||||
}))
|
}))
|
||||||
default = []
|
default = []
|
||||||
@@ -178,8 +178,6 @@ variable "stack_routing_interfaces" {
|
|||||||
ip_address = string # IP estática del stack en esta VLAN
|
ip_address = string # IP estática del stack en esta VLAN
|
||||||
subnet = string # subred en formato CIDR, ej: "10.2.55.0/24"
|
subnet = string # subred en formato CIDR, ej: "10.2.55.0/24"
|
||||||
default_gateway = optional(string, null) # gateway para acceso a internet
|
default_gateway = optional(string, null) # gateway para acceso a internet
|
||||||
dns1 = optional(string, null) # DNS primario
|
|
||||||
dns2 = optional(string, null) # DNS secundario
|
|
||||||
}))
|
}))
|
||||||
default = []
|
default = []
|
||||||
description = "Interfaces L3 (SVIs) en stacks de switches para acceso de gestión al Dashboard de Meraki."
|
description = "Interfaces L3 (SVIs) en stacks de switches para acceso de gestión al Dashboard de Meraki."
|
||||||
|
|||||||
Generated
-25
@@ -1,25 +0,0 @@
|
|||||||
# This file is maintained automatically by "terraform init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.terraform.io/ciscodevnet/meraki" {
|
|
||||||
version = "1.9.0"
|
|
||||||
constraints = "1.9.0"
|
|
||||||
hashes = [
|
|
||||||
"h1:KmWz0JvCHdDd3AtuawxUwmW0VN3fooGw4CRaxiKHT5Y=",
|
|
||||||
"zh:0b9a7d32f331998a2a1531811667be44f799dfc03f6929f1414d2cab69f659f2",
|
|
||||||
"zh:179f791e2aa0ca6353541d90956548033b9ee0c880a096e48ce3ae3fe8a1862e",
|
|
||||||
"zh:2a1a32c6a8068c194e19859a7d88e0b95d0d9cbcf31444454b055ed62ace715a",
|
|
||||||
"zh:491812b74919d131f4ef3ba968d10b678275ed201428e2af7f53df40fd7e8cee",
|
|
||||||
"zh:4f5043f5165ee5199a61e4c15230d9f973ed0211a06600d75638f8369bac73fa",
|
|
||||||
"zh:5679d5a0d5dd370ff5d9321913f293f76be8f7ebc25e5cf1b45ceed9de803348",
|
|
||||||
"zh:58e1faba3d322bca68efb5cdac1ebe4e6d6f92834ebe5ccff8e491685620185c",
|
|
||||||
"zh:861b04ee4a498070cfb581488ddc3e90b25be895d35861c2a03a3b224d28e9b5",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
|
||||||
"zh:92ee52e0dd3372e6dc2ed21bf5b21124b646c4c1037477e66eae87614d814036",
|
|
||||||
"zh:a3a851a3ce0c32b17506da0b9370f9d31df8e9d5ed4b422dc09ff5337d4b4192",
|
|
||||||
"zh:cdac168b00fa658ec68862677cb0b00f356095654e6b1d0df823c330492fa753",
|
|
||||||
"zh:dc454d6b1051891c99051b92e61015d244eb320a5a491cbffb770a005e448898",
|
|
||||||
"zh:f14317688e068e40dc11f609c4dc4f81cfa50fbaa43dcbe0117725f1149e9d89",
|
|
||||||
"zh:fe0544ac117d0c643559f042996fa32f243988bc48d4d49875abad6b326f0e2e",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Pasos manuales — BCN01-LAB
|
|
||||||
|
|
||||||
Configuraciones que no pueden gestionarse via Terraform (limitaciones del provider v1.9.0)
|
|
||||||
y deben aplicarse directamente en el Meraki Dashboard.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Client VPN (L2TP/IPSec)
|
|
||||||
|
|
||||||
**Dashboard:** Security & SD-WAN → Client VPN
|
|
||||||
|
|
||||||
| Parámetro | Valor |
|
|
||||||
|-----------|-------|
|
|
||||||
| Estado | Enabled |
|
|
||||||
| Subnet VPN | `10.2.58.0/23` |
|
|
||||||
| Authentication | RADIUS |
|
|
||||||
| RADIUS server | IP del Okta RADIUS Agent, puerto `1812` |
|
|
||||||
| RADIUS secret | Ver secret de Okta RADIUS Agent |
|
|
||||||
|
|
||||||
> **Nota:** El provider `CiscoDevNet/meraki` v1.9.0 no incluye el resource
|
|
||||||
> `meraki_appliance_vpn_client_vpn`. Cuando el provider lo soporte, esta
|
|
||||||
> configuración deberá migrarse a `sites/BCN01-LAB/vpn.auto.tfvars`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OWE (Opportunistic Wireless Encryption) — SSID EQT-CORPO
|
|
||||||
|
|
||||||
**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.
|
|
||||||
@@ -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"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "meraki" {}
|
|
||||||
|
|
||||||
# Llamada al módulo meraki-site
|
|
||||||
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
|
|
||||||
wifi_password_psk = var.wifi_password_psk
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# Definición de la Organización
|
|
||||||
variable "organization_name" {
|
|
||||||
type = string
|
|
||||||
description = "Nombre exacto de tu organización en el Dashboard de Meraki"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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."
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user