{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.13.0"
    },
    "blog_metadata": {
      "topic": "Why Azure SQL Free Tier Is More Strategic Than It Looks for Enterprise Modernization",
      "slug": "why-azure-sql-free-tier-is-more-strategic-than-it-looks-for-",
      "generated_by": "LinkedIn Post Generator + Azure OpenAI",
      "generated_at": "2026-08-20T22:52:41.209Z"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Why Azure SQL Free Tier Is More Strategic Than It Looks for Enterprise Modernization\n",
        "\n",
        "Azure SQL Free Tier is more than a pricing perk: it is a low-risk proving ground for enterprise modernization. This notebook turns the blog post into a hands-on validation flow so teams can test connectivity, schema portability, security assumptions, representative performance, and decision criteria before committing to a larger migration motion."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "%pip install -q pyodbc pandas"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import os\n",
        "import json\n",
        "import time\n",
        "from textwrap import dedent\n",
        "\n",
        "try:\n",
        "    import pyodbc\n",
        "except ImportError:\n",
        "    pyodbc = None\n",
        "\n",
        "try:\n",
        "    import pandas as pd\n",
        "except ImportError:\n",
        "    pd = None"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Strategic framing\n",
        "\n",
        "The core argument is that Azure SQL Free Tier should be treated as a modernization filter, not just a free database. In practice, it helps teams validate whether a bounded application has a credible managed-database path before spending heavily on migration governance, architecture reviews, and platform effort.\n",
        "\n",
        "### Candidate pattern\n",
        "- bounded app\n",
        "- low to moderate data volume\n",
        "- named owner\n",
        "- limited blast radius\n",
        "- real uncertainty about PaaS fit\n",
        "\n",
        "### Desired output\n",
        "A modernization dossier with:\n",
        "- dependency observations\n",
        "- schema portability findings\n",
        "- connectivity and auth results\n",
        "- operational ownership gaps\n",
        "- performance notes on representative queries\n",
        "- recommendation: stop, remediate, or advance"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Modernization funnel diagram\n",
        "\n",
        "This cell renders the blog's six-box path as structured data so the flow can be inspected in notebook form."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "modernization_flow = {\n",
        "    \"nodes\": [\n",
        "        \"Legacy App / Team\",\n",
        "        \"Azure SQL Free Tier\",\n",
        "        \"Prototype Modern Data Model\",\n",
        "        \"Validate Security + Connectivity\",\n",
        "        \"CI/CD + IaC Baseline\",\n",
        "        \"Promote to Paid Azure SQL / Managed Estate\"\n",
        "    ],\n",
        "    \"ownership\": {\n",
        "        \"Enterprise Architects\": \"Prototype Modern Data Model\",\n",
        "        \"Platform Team\": \"Validate Security + Connectivity\",\n",
        "        \"Developers\": \"CI/CD + IaC Baseline\"\n",
        "    }\n",
        "}\n",
        "\n",
        "print(json.dumps(modernization_flow, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Provision a low-friction Azure SQL Free Tier environment\n",
        "\n",
        "The original post used PowerShell and Azure CLI. This Python version prints equivalent Azure CLI commands so the provisioning flow can be reviewed, parameterized, and optionally executed outside the notebook.\n",
        "\n",
        "This is useful for standardizing naming, ownership, and repeatable setup for discovery-stage databases."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "resource_group = os.getenv(\"AZ_RESOURCE_GROUP\", \"rg-sql-free-demo\")\n",
        "location = os.getenv(\"AZ_LOCATION\", \"eastus\")\n",
        "server = os.getenv(\"AZ_SQL_SERVER_NAME\", f\"sqlfree{int(time.time())}\")\n",
        "database = os.getenv(\"AZ_SQL_DB\", \"appdb\")\n",
        "admin_user = os.getenv(\"AZ_SQL_ADMIN_USER\", \"sqladminuser\")\n",
        "admin_password = os.getenv(\"AZ_SQL_ADMIN_PASSWORD\", \"ChangeMe!UseKeyVault123\")\n",
        "\n",
        "commands = [\n",
        "    f\"az group create --name {resource_group} --location {location}\",\n",
        "    (\n",
        "        f\"az sql server create --resource-group {resource_group} --name {server} \"\n",
        "        f\"--location {location} --admin-user {admin_user} --admin-password {admin_password}\"\n",
        "    ),\n",
        "    (\n",
        "        f\"az sql db create --resource-group {resource_group} --server {server} \"\n",
        "        f\"--name {database} --edition GeneralPurpose --compute-model Serverless\"\n",
        "    )\n",
        "]\n",
        "\n",
        "print(\"Provisioning plan:\\n\")\n",
        "for cmd in commands:\n",
        "    print(cmd)\n",
        "\n",
        "print(\"\\nNote: Review current Azure SQL Free Tier eligibility and service options in the Azure documentation before execution.\")"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for connectivity validation\n",
        "\n",
        "Set these before running the next cell:\n",
        "- `AZURE_SQL_SERVER` — server FQDN, for example `myserver.database.windows.net`\n",
        "- `AZURE_SQL_DB` — database name\n",
        "- `AZURE_SQL_USER` — SQL admin or application user\n",
        "- `AZURE_SQL_PASSWORD` — password for the user\n",
        "\n",
        "Optional:\n",
        "- `AZURE_SQL_CONN_TIMEOUT` — connection timeout in seconds"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Validate connectivity early\n",
        "\n",
        "This test checks whether the team can actually reach Azure SQL and execute a basic query. If it fails, that is still useful: it exposes firewall, authentication, driver, DNS, or policy friction while the blast radius is still small."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "if pyodbc is None:\n",
        "    raise ImportError(\"pyodbc is not installed. Run the package installation cell first.\")\n",
        "\n",
        "server = os.getenv(\"AZURE_SQL_SERVER\", \"myserver.database.windows.net\")\n",
        "database = os.getenv(\"AZURE_SQL_DB\", \"appdb\")\n",
        "username = os.getenv(\"AZURE_SQL_USER\", \"sqladminuser\")\n",
        "password = os.getenv(\"AZURE_SQL_PASSWORD\", \"\")\n",
        "timeout = int(os.getenv(\"AZURE_SQL_CONN_TIMEOUT\", \"30\"))\n",
        "\n",
        "conn_str = (\n",
        "    \"Driver={ODBC Driver 18 for SQL Server};\"\n",
        "    f\"Server=tcp:{server},1433;\"\n",
        "    f\"Database={database};Uid={username};Pwd={password};\"\n",
        "    \"Encrypt=yes;TrustServerCertificate=no;\"\n",
        "    f\"Connection Timeout={timeout};\"\n",
        ")\n",
        "\n",
        "print(\"Attempting connection to:\", server)\n",
        "try:\n",
        "    with pyodbc.connect(conn_str) as conn:\n",
        "        cursor = conn.cursor()\n",
        "        cursor.execute(\"SELECT @@VERSION\")\n",
        "        version = cursor.fetchone()[0]\n",
        "        print(\"Connection successful. SQL engine version:\\n\")\n",
        "        print(version)\n",
        "except Exception as e:\n",
        "    print(\"Connectivity validation failed.\")\n",
        "    print(type(e).__name__, str(e))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Security guardrail validation with a firewall rule\n",
        "\n",
        "The blog emphasized applying security controls on day one. This Python cell generates the Azure CLI command needed to add a firewall rule so teams can validate secure access paths before broader rollout."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "resource_group = os.getenv(\"AZ_RESOURCE_GROUP\", \"rg-sql-free-demo\")\n",
        "server = os.getenv(\"AZ_SQL_SERVER_NAME\", \"sqlfree12345\")\n",
        "rule_name = os.getenv(\"AZ_SQL_FIREWALL_RULE\", \"AllowCorpIp\")\n",
        "start_ip = os.getenv(\"AZ_SQL_START_IP\", \"203.0.113.10\")\n",
        "end_ip = os.getenv(\"AZ_SQL_END_IP\", \"203.0.113.10\")\n",
        "\n",
        "firewall_cmd = (\n",
        "    f\"az sql server firewall-rule create --resource-group {resource_group} \"\n",
        "    f\"--server {server} --name {rule_name} \"\n",
        "    f\"--start-ip-address {start_ip} --end-ip-address {end_ip}\"\n",
        ")\n",
        "\n",
        "print(\"Firewall rule command:\\n\")\n",
        "print(firewall_cmd)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for benchmark and schema tests\n",
        "\n",
        "Set this before running the next cells:\n",
        "- `AZURE_SQL_CONN_STR` — full ODBC connection string using ODBC Driver 18 for SQL Server\n",
        "\n",
        "Example format:\n",
        "`Driver={ODBC Driver 18 for SQL Server};Server=tcp:<server>,1433;Database=<db>;Uid=<user>;Pwd=<password>;Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;`"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Benchmark a representative query\n",
        "\n",
        "This is not about winning a performance contest. It is a quick way to check whether obvious latency or query-pattern issues appear when the workload is exercised in Azure SQL."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "if pyodbc is None:\n",
        "    raise ImportError(\"pyodbc is not installed. Run the package installation cell first.\")\n",
        "\n",
        "conn_str = os.environ.get(\"AZURE_SQL_CONN_STR\")\n",
        "if not conn_str:\n",
        "    raise EnvironmentError(\"AZURE_SQL_CONN_STR is not set.\")\n",
        "\n",
        "conn = pyodbc.connect(conn_str)\n",
        "cur = conn.cursor()\n",
        "\n",
        "cur.execute(\"SELECT TOP 1000 object_id, name FROM sys.objects ORDER BY name\")\n",
        "start = time.perf_counter()\n",
        "rows = cur.fetchall()\n",
        "elapsed_ms = (time.perf_counter() - start) * 1000\n",
        "\n",
        "print(f\"Rows: {len(rows)}\")\n",
        "print(f\"Elapsed: {elapsed_ms:.2f} ms\")\n",
        "\n",
        "conn.close()"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Test a modernization pattern: relational plus JSON\n",
        "\n",
        "This example validates a simple schema deployment and a JSON query pattern side by side. It helps confirm that the application can create objects, insert data, and use SQL features relevant to modernization scenarios."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "if pyodbc is None:\n",
        "    raise ImportError(\"pyodbc is not installed. Run the package installation cell first.\")\n",
        "\n",
        "conn_str = os.environ.get(\"AZURE_SQL_CONN_STR\")\n",
        "if not conn_str:\n",
        "    raise EnvironmentError(\"AZURE_SQL_CONN_STR is not set.\")\n",
        "\n",
        "conn = pyodbc.connect(conn_str)\n",
        "cur = conn.cursor()\n",
        "\n",
        "cur.execute(\"IF OBJECT_ID('dbo.CustomerProfile') IS NOT NULL DROP TABLE dbo.CustomerProfile\")\n",
        "cur.execute(dedent(\"\"\"\n",
        "CREATE TABLE dbo.CustomerProfile (\n",
        "    CustomerId INT PRIMARY KEY,\n",
        "    Name NVARCHAR(100) NOT NULL,\n",
        "    Preferences NVARCHAR(MAX) NULL\n",
        ")\n",
        "\"\"\"))\n",
        "cur.execute(\n",
        "    \"INSERT INTO dbo.CustomerProfile (CustomerId, Name, Preferences) VALUES (?, ?, ?)\",\n",
        "    1, \"Adele Vance\", '{\"channels\":[\"email\",\"sms\"],\"region\":\"EMEA\"}'\n",
        ")\n",
        "cur.execute(\"SELECT Name, JSON_VALUE(Preferences, '$.region') AS Region FROM dbo.CustomerProfile\")\n",
        "result = cur.fetchone()\n",
        "print(result)\n",
        "\n",
        "conn.commit()\n",
        "conn.close()"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Developer-to-production promotion flow\n",
        "\n",
        "The original post also included a sequence diagram showing how a proven pattern moves from developer testing through security review and platform operations into a governed target environment. This cell captures that sequence as notebook data."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "promotion_sequence = [\n",
        "    {\"from\": \"Developer\", \"to\": \"Azure SQL Free Tier\", \"action\": \"Build schema and test queries\"},\n",
        "    {\"from\": \"Developer\", \"to\": \"Security Review\", \"action\": \"Validate auth, firewall, encryption\"},\n",
        "    {\"from\": \"Security Review\", \"to\": \"Developer\", \"action\": \"Approved baseline controls\"},\n",
        "    {\"from\": \"Developer\", \"to\": \"Platform Ops\", \"action\": \"Package migration + deployment steps\"},\n",
        "    {\"from\": \"Platform Ops\", \"to\": \"Enterprise SQL Target\", \"action\": \"Promote proven pattern to managed environment\"}\n",
        "]\n",
        "\n",
        "print(json.dumps(promotion_sequence, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Export deployment settings as reusable pipeline variables\n",
        "\n",
        "A successful pilot should become a repeatable delivery path. This Python version emits a summary object and example Azure DevOps variable commands so the experiment can be turned into a CI/CD baseline."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "server = os.getenv(\"AZ_SQL_SERVER_NAME\", \"sqlfree12345\")\n",
        "database = os.getenv(\"AZ_SQL_DB\", \"appdb\")\n",
        "fqdn = f\"{server}.database.windows.net\"\n",
        "\n",
        "summary = {\n",
        "    \"Server\": server,\n",
        "    \"Database\": database,\n",
        "    \"Fqdn\": fqdn\n",
        "}\n",
        "\n",
        "print(\"Azure DevOps variable commands:\\n\")\n",
        "print(f\"##vso[task.setvariable variable=SqlServerName]{server}\")\n",
        "print(f\"##vso[task.setvariable variable=SqlDatabaseName]{database}\")\n",
        "print(f\"##vso[task.setvariable variable=SqlServerFqdn]{fqdn}\")\n",
        "\n",
        "print(\"\\nSummary:\")\n",
        "print(json.dumps(summary, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Capture a modernization scorecard\n",
        "\n",
        "Pilots should not end in vibes. This scorecard records whether the key discovery outcomes were validated and what the recommended next step should be."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "scorecard = {\n",
        "    \"connectivity_validated\": True,\n",
        "    \"security_baseline_tested\": True,\n",
        "    \"schema_portability_confirmed\": True,\n",
        "    \"query_pattern_benchmarked\": True,\n",
        "    \"ci_cd_ready\": True,\n",
        "    \"next_step\": \"Promote to governed paid Azure SQL environment\"\n",
        "}\n",
        "\n",
        "print(json.dumps(scorecard, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Governed experiment checklist\n",
        "\n",
        "To avoid free-tier sprawl, each database should have:\n",
        "- an explicit owner\n",
        "- a business purpose\n",
        "- an expected decision date\n",
        "- entry criteria\n",
        "- exit criteria\n",
        "\n",
        "### Suggested entry criteria\n",
        "- bounded data size\n",
        "- noncritical initial use\n",
        "- named application owner\n",
        "- basic test plan\n",
        "- agreement that no-go is acceptable\n",
        "\n",
        "### Suggested exit criteria\n",
        "- sustained resource use beyond free allocation\n",
        "- production SLA requirements\n",
        "- growth beyond the size envelope\n",
        "- validated business value\n",
        "- clean security and support ownership"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Summary\n",
        "\n",
        "Azure SQL Free Tier is most valuable when used as a governed discovery stage for bounded applications with real uncertainty about PaaS fit. The goal is not to maximize free databases created, but to generate evidence: connectivity results, schema findings, security observations, performance notes, and a clear stop, remediate, or advance decision.\n",
        "\n",
        "## Next Steps\n",
        "\n",
        "1. Select 5 to 10 candidate applications where uncertainty is the blocker.\n",
        "2. Standardize a discovery-stage provisioning pattern with naming, tags, and review dates.\n",
        "3. Run connectivity, schema, security, and representative query tests.\n",
        "4. Capture a scorecard for each pilot.\n",
        "5. Promote only proven patterns into a governed paid Azure SQL environment.\n",
        "6. Review what would break first in your environment: intake discipline, security guardrails, or the handoff to paid deployment."
      ]
    }
  ]
}