Cómo desplegar tu sitio web con Azure Static Web Apps y Terraform

danieljsaldana

Daniel J. Saldaña

Posted on April 26, 2024

Cómo desplegar tu sitio web con Azure Static Web Apps y Terraform

Bienvenidos a un tutorial paso a paso sobre cómo desplegar un sitio web estático utilizando Azure Static Web Apps mediante Terraform. Esta guía es ideal tanto para desarrolladores que están comenzando con infraestructura como código, como para aquellos que buscan una solución robusta y escalable para la entrega de sitios web.

Requisitos previos

Antes de comenzar, asegúrate de tener lo siguiente:

  1. Una cuenta de Azure.
  2. Una cuenta de GitHub.
  3. Terraform instalado en tu máquina local.
  4. Una zona DNS ya creada en Azure para tu dominio.
  5. Asegúrate de que tu dominio está configurado con los DNS de Azure para permitir la gestión de registros DNS desde Azure.

Configuración inicial

Primero, vamos a configurar nuestros proveedores en Terraform. Esto incluye tanto Azure como GitHub. Asegúrate de tener configuradas las credenciales de autenticación para Azure y el token de acceso para GitHub.

main.tf :

provider "azurerm" {
  features {}
  # Asegúrate de configurar la autenticación de Azure,
  # puede ser mediante variables de entorno, archivos de configuración, etc.
}

provider "github" {
  token = var.github_token
  owner = var.github_owner
}

Enter fullscreen mode Exit fullscreen mode

Definición de recursos

El siguiente paso es definir el recurso de Azure Static Web App en Terraform. Esto incluye especificaciones como el nombre, grupo de recursos, ubicación, entre otros. Además, configuraremos los secretos de GitHub Actions para integrar nuestro flujo de trabajo de CI/CD directamente desde GitHub.

main.tf (continuación):

resource "azurerm_static_web_app" "static_front" {
  count = var.create_resource ? 1 : 0

  name = var.name
  resource_group_name = var.resource_group_name
  location = var.location
  preview_environments_enabled = var.preview_environments_enabled
  sku_tier = var.sku_tier
  sku_size = var.sku_size
  tags = var.tags

  app_settings = {
    environment = var.app_settings_environment
    app_location = var.app_settings_app_location
    api_location = var.app_settings_api_location
    output_location = var.app_settings_output_location
  }
}

resource "github_actions_secret" "publishprofile" {
  count = length(azurerm_static_web_app.static_front) > 0 ? 1 : 0

  repository = var.app_repository
  secret_name = var.github_actions_secret_publishprofile_secret_name
  plaintext_value = azurerm_static_web_app.static_front[0].api_key
}

resource "github_repository_file" "azure_static_web_app_yml" {
  count = length(azurerm_static_web_app.static_front) > 0 ? 1 : 0

  repository = var.github_repository_file_repository
  branch = var.github_repository_file_branch
  file = ".github/workflows/azure-static-web-apps-${local.app_identifier}.yml"
  content = templatefile("./azure-static-web-app.tpl",
    {
      app_location = var.app_settings_app_location
      api_location = var.app_settings_api_location
      output_location = var.app_settings_output_location
    }
  )
  commit_message = "Add workflow (by Terraform)"
  commit_author = "Daniel Saldana"
  commit_email = "danieljesus.sp@gmail.com"
  overwrite_on_create = true
}

# Custom domain validation
resource "azurerm_static_web_app_custom_domain" "validation_domain" {
  count = try(!empty(var.azure_dns_record.name) && length(var.azure_dns_record.zone_name) > 0 && length(azurerm_static_web_app.static_front) > 0, false) ? 0 : 1
  depends_on = [azurerm_static_web_app.static_front]

  static_web_app_id = azurerm_static_web_app.static_front[0].id
  domain_name = var.azure_dns_record.name != "" ? format("%s.%s", var.azure_dns_record.name, var.azure_dns_record.zone_name) : var.azure_dns_record.zone_name
  validation_type = "dns-txt-token"
}

resource "azurerm_static_web_app_custom_domain" "validation_domain_www" {
  count = try(!empty(var.azure_dns_record.name) && length(var.azure_dns_record.zone_name) > 0 && length(azurerm_static_web_app.static_front) > 0, false) ? 0 : 1
  depends_on = [azurerm_static_web_app.static_front]

  static_web_app_id = azurerm_static_web_app.static_front[0].id
  domain_name = var.azure_dns_record.name != "" ? format("www.%s.%s", var.azure_dns_record.name, var.azure_dns_record.zone_name) : format("www.%s", var.azure_dns_record.zone_name)
  validation_type = "dns-txt-token"
}

# Custom domain validation
resource "azurerm_dns_txt_record" "dns_ip_record" {
  count = try(!empty(var.azure_dns_record.name) && length(var.azure_dns_record.zone_name) > 0 && length(azurerm_static_web_app.static_front) > 0, false) ? 0 : 1
  depends_on = [azurerm_static_web_app_custom_domain.validation_domain]

  name = var.azure_dns_record.name != "" ? var.azure_dns_record.name : "@"
  zone_name = var.azure_dns_record.zone_name
  resource_group_name = var.azure_dns_record.resource_group_name
  ttl = 300
  record {
    value = azurerm_static_web_app_custom_domain.validation_domain[0].validation_token
  }
}

resource "azurerm_dns_txt_record" "dns_ip_record_www" {
  count = try(!empty(var.azure_dns_record.name) && length(var.azure_dns_record.zone_name) > 0 && length(azurerm_static_web_app.static_front) > 0, false) ? 0 : 1
  depends_on = [azurerm_static_web_app_custom_domain.validation_domain_www]

  name = var.azure_dns_record.name != "" ? format("www.%s", var.azure_dns_record.name) : "www"
  zone_name = var.azure_dns_record.zone_name
  resource_group_name = var.azure_dns_record.resource_group_name
  ttl = 300
  record {
    value = azurerm_static_web_app_custom_domain.validation_domain_www[0].validation_token
  }
}

Enter fullscreen mode Exit fullscreen mode

Variables y Outputs

Para hacer nuestro código más reutilizable y parametrizable, definiremos variables para todos los inputs necesarios y configuraremos algunas salidas para obtener información relevante una vez que se aplique el Terraform.

variables.tf :

variable "github_token" {
  description = "GitHub token"
  type = string

  validation {
    condition = length(var.github_token) > 0
    error_message = "El token de GitHub debe tener al menos un carácter."
  }
}

variable "github_owner" {
  description = "GitHub owner"
  type = string

  validation {
    condition = length(var.github_owner) > 0
    error_message = "El propietario de GitHub debe tener al menos un carácter."
  }
}

variable "app_repository" {
  description = "The repository for the application"
  type = string

  validation {
    condition = length(var.app_repository) > 0
    error_message = "El repositorio de la aplicación debe tener al menos un carácter."
  }
}

variable "create_resource" {
  description = "Create the resource"
  type = bool

  validation {
    condition = var.create_resource == true || var.create_resource == false
    error_message = "El valor de create_resource debe ser verdadero o falso."
  }
}

variable "name" {
  description = "El nombre del servicio de la aplicación."
  type = string
}

variable "resource_group_name" {
  description = "El nombre del grupo de recursos donde se creará el servicio de la aplicación."
  type = string

  validation {
    condition = length(var.resource_group_name) > 0
    error_message = "El nombre del grupo de recursos del servicio de la aplicación debe tener al menos un carácter."
  }
}

variable "location" {
  description = "La ubicación de Azure donde se desplegará el servicio de la aplicación."
  type = string

  validation {
    condition = length(var.location) > 0
    error_message = "La ubicación del servicio de la aplicación debe tener al menos un carácter."
  }
}

variable "preview_environments_enabled" {
  description = "Habilitar la creación de entornos de vista previa."
  type = bool

  validation {
    condition = var.preview_environments_enabled == true || var.preview_environments_enabled == false
    error_message = "El valor de preview_environments_enabled debe ser verdadero o falso."
  }
}

variable "sku_tier" {
  description = "El nivel de SKU del servicio de la aplicación."
  type = string

  validation {
    condition = var.sku_tier == "Free" || var.sku_tier == "Basic"
    error_message = "El nivel de SKU del servicio de la aplicación debe ser Free o Basic."
  }
}

variable "sku_size" {
  description = "El tamaño de SKU del servicio de la aplicación."
  type = string

  validation {
    condition = var.sku_size == "Free" || var.sku_size == "Small"
    error_message = "El tamaño de SKU del servicio de la aplicación debe ser Free o Small."
  }
}

variable "tags" {
  description = "Los tags del servicio de la aplicación."
  type = map(string)

  validation {
    condition = length(var.tags) > 0
    error_message = "Los tags del servicio de la aplicación deben tener al menos un carácter."
  }
}

variable "app_settings_environment" {
  description = "La variable de entorno ENVIRONMENT del servicio de la aplicación."
  type = string

  validation {
    condition = length(var.app_settings_environment) > 0
    error_message = "La variable de entorno ENVIRONMENT del servicio de la aplicación debe tener al menos un carácter."
  }
}

variable "app_settings_app_location" {
  description = "La variable de entorno app_location del servicio de la aplicación."
  type = string

  validation {
    condition = length(var.app_settings_app_location) > 0
    error_message = "La variable de entorno app_location del servicio de la aplicación debe tener al menos un carácter."
  }
}

variable "app_settings_api_location" {
  description = "La variable de entorno api_location del servicio de la aplicación."
  type = string

  validation {
    condition = length(var.app_settings_api_location) > 0
    error_message = "La variable de entorno api_location del servicio de la aplicación debe tener al menos un carácter."
  }
}

variable "app_settings_output_location" {
  description = "La variable de entorno output_location del servicio de la aplicación."
  type = string

  validation {
    condition = length(var.app_settings_output_location) > 0
    error_message = "La variable de entorno output_location del servicio de la aplicación debe tener al menos un carácter."
  }
}

# GitHub Actions secret
variable "github_actions_secret_publishprofile_repository" {
  description = "El repositorio donde se almacenará el secreto de GitHub Actions."
  type = string

  validation {
    condition = length(var.github_actions_secret_publishprofile_repository) > 0
    error_message = "El repositorio donde se almacenará el secreto de GitHub Actions debe tener al menos un carácter."
  }
}

variable "github_actions_secret_publishprofile_secret_name" {
  description = "El nombre del secreto de GitHub Actions."
  type = string

  validation {
    condition = length(var.github_actions_secret_publishprofile_secret_name) > 0
    error_message = "El nombre del secreto de GitHub Actions debe tener al menos un carácter."
  }
}

# GitHub Actions file
variable "github_repository_file_repository" {
  description = "El repositorio donde se almacenará el archivo de GitHub."
  type = string

  validation {
    condition = length(var.github_repository_file_repository) > 0
    error_message = "El repositorio donde se almacenará el archivo de GitHub debe tener al menos un carácter."
  }
}

variable "github_repository_file_branch" {
  description = "La rama donde se almacenará el archivo de GitHub."
  type = string

  validation {
    condition = length(var.github_repository_file_branch) > 0
    error_message = "La rama donde se almacenará el archivo de GitHub debe tener al menos un carácter."
  }
}

# Domain customizations for the static web app
variable "azure_dns_record" {
  description = "Los registros DNS personalizados para el servicio de la aplicación."
  type = map(string)

  validation {
    condition = length(var.azure_dns_record) > 0
    error_message = "Los registros DNS personalizados para el servicio de la aplicación deben tener al menos un carácter."
  }
}

Enter fullscreen mode Exit fullscreen mode

outputs.tf :

output "default_host_name" {
  value = length(azurerm_static_web_app.static_front) > 0 ? azurerm_static_web_app.static_front[0].default_host_name : ""
}

Enter fullscreen mode Exit fullscreen mode

Configuración Local

Configuramos algunos valores locales que serán útiles para referencias internas dentro de nuestras configuraciones de Terraform.

local.tf :

locals {
  app_identifier = length(azurerm_static_web_app.static_front) > 0 ? element(split(".", azurerm_static_web_app.static_front[0].default_host_name), 0) : ""
}

Enter fullscreen mode Exit fullscreen mode

Archivo de Workflow para GitHub Actions

Para automatizar el proceso de build y deploy de nuestro sitio, utilizaremos GitHub Actions. A continuación, se muestra cómo configurar el archivo de workflow que reside en tu repositorio de GitHub.

azure-static-web-app.tpl :

name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - production
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - production

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v3
        with:
          submodules: true
          lfs: false
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: $${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: $${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: ${ app_location } # App source code path
          api_location: ${ api_location } # Api source code path - optional
          output_location: ${ output_location } # Built app content directory - optional
          ###### End of Repository/Build Configurations ######

  close_pull_request_job:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
    steps:
      - name: Close Pull Request
        id: closepullrequest
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: $${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          action: "close"

Enter fullscreen mode Exit fullscreen mode

Configuración de variables de Terraform

Finalmente, debemos crear un archivo terraform.tfvars donde especificaremos los valores de las variables que hemos definido. Asegúrate de ajustar los valores según tus necesidades antes de aplicar Terraform.

terraform.tfvars :

# This file contains the variables that will be used in the main configuration file
create_resource = true

# GitHub token
github_token = "YOUR_GITHUB"

# GitHub owner
github_owner = "danieljsaldana"

# GitHub repository
app_repository = "danieljsaldana-portfolio"

# Azure app service name
name = "danieljsaldana-static-front"
resource_group_name = "danieljsaldana_dev"
location = "westeurope"
preview_environments_enabled = true
sku_tier = "Free"
sku_size = "Free"
tags = {
  Project = "Daniel J. Saldaña",
  Tier = "Pago por uso",
  Environment = "Producción"
}

# App settings
app_settings_environment = "production"
app_settings_app_location = "/"
app_settings_api_location = "/"
app_settings_output_location = "dist/"

# GitHub Actions secret
github_actions_secret_publishprofile_repository = "danieljsaldana-portfolio"
github_actions_secret_publishprofile_secret_name = "AZURE_STATIC_WEB_APPS_API_TOKEN"

# GitHub Actions file
github_repository_file_repository = "danieljsaldana-portfolio"
github_repository_file_branch = "production"

# Domain customizations for the static web app
azure_dns_record = {
  name = "goliat" # "subdomain"
  zone_name = "ocrend.dev" # "domain"
  resource_group_name = "danieljsaldana_dev" # "resource_group"
}

Enter fullscreen mode Exit fullscreen mode

Aplicando la configuración

Una vez que hayas revisado y configurado todos los archivos, ejecuta los siguientes comandos en tu terminal para inicializar Terraform y aplicar la configuración:

terraform init
terraform apply

Enter fullscreen mode Exit fullscreen mode

Espera a que Terraform termine de desplegar todos los recursos y, si todo va bien, verás los detalles de tu nuevo sitio web estático en Azure, incluyendo su URL en la salida del comando.

¡Y eso es todo! Has configurado con éxito un sitio web estático en Azure utilizando Terraform y GitHub Actions. Esta configuración no solo te permite desplegar rápidamente sitios web, sino que también aprovecha las prácticas modernas de DevOps para mantener y escalar tus proyectos de manera eficiente.

Espero que este tutorial te haya sido útil y que ahora sientas mayor confianza al trabajar con Azure, GitHub y Terraform. Si tienes alguna pregunta o comentario, no dudes en dejar un comentario abajo. ¡Feliz codificación!

💖 💪 🙅 🚩
danieljsaldana
Daniel J. Saldaña

Posted on April 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related