Cómo desplegar tu sitio web con Azure Static Web Apps y Terraform
Daniel J. Saldaña
Posted on April 26, 2024
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:
- Una cuenta de Azure.
- Una cuenta de GitHub.
- Terraform instalado en tu máquina local.
- Una zona DNS ya creada en Azure para tu dominio.
- 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
}
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
}
}
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."
}
}
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 : ""
}
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) : ""
}
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"
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"
}
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
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!
Posted on April 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.