Cómo crear un servicio Net Core multiplataforma

Imagen para el post de crear servicios para .Net Core

Después de varias semanas hablando sobre las maravillas de Terraform o de Integración y Despliegue continuos (CI/CD), hoy vengo a hablaros de un problema con el que me he encontrado con un proyecto que tengo entre manos.

Por necesidades del proyecto, quiero hacer un servicio .Net Core, y poder instalarlo en cualquier plataforma. Esto no es ningún problema en Linux por ejemplo, pero para poder correr un servicio en Windows hay que cumplir con ciertos requisitos. Para .Net Framework existen múltiples herramientas que nos lo permiten, como puede ser un proyecto de instalador de servicio o utilizar Topself, pero para .Net Core ya es otra cosa…

Para que un servicio se pueda instalar y funcione en Windows, tiene que heredar de «ServiceBase«, lo que difiere un poco de cómo funciona en otras plataformas, y hace que crear un servicio multiplataforma no sea algo directo. Esto se puede solucionar fácilmente con un poco de código.

Creando nuestro servicio Net Core

Para empezar, vamos a crear una aplicación de consola .Net Core:

La imagen muestra la creación de un proyecto de consola net core

Para poder hospedar un servicio cualquiera, vamos a necesitar añadir el paquete NuGet «Microsoft.Extensions.Hosting«, y además, para poder correrlo cuando funcione en Windows, vamos a necesitar también el paquete NuGet «System.ServiceProcess.ServiceController«. Una vez que los hemos añadido, vamos a crear nuestro IHost:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var host = new HostBuilder()
                 .ConfigureHostConfiguration(configHost =>
                 {
                     //Configuración del host
                 })
                 .ConfigureAppConfiguration((hostContext, configApp) =>
                 {
                     //Configuración de la aplicacion

                 })
                .ConfigureServices((hostContext, services) =>
                {
                    //Configuración de los servicios
                    services.AddHostedService<LifetimeHostedService>();
                });
            if (!Debugger.IsAttached) //En caso de no haber debugger, lanzamos como servicio
            {
                await host.RunServiceAsync();
            }
            else
            {
                await host.RunConsoleAsync();
            }
        }
    }
}

Al igual que haríamos en un proyecto ASP NET Core en el Startup.cs y en Program.cs, podemos configurar las diferentes partes de nuestro servicio Net Core mediante los métodos:

  • «ConfigureHostConfiguration» (en Program.cs)
  • «ConfigureAppConfiguration» («Configure» en Startup.cs)
  • «ConfigureServices» (en Startup.cs)

En este caso, estamos levantando un IHost genérico al cual le vamos a inyectar la dependencia del servicio/servicios que queremos ejecutar registrándolos mediante «AddHostedService».

Para poder probar, el servicio que hemos registrado es este (tiene que implementar IHostedService):

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    public class LifetimeHostedService : IHostedService, IDisposable
    {
        ILogger<LifetimeHostedService> logger;
        private Timer _timer;
        private string _path;
        public LifetimeHostedService(ILogger<LifetimeHostedService> logger, IHostingEnvironment hostingEnvironment)
        {
            this.logger = logger;
            _path = $"{hostingEnvironment.ContentRootPath}PostServicioCore.txt";
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            try
            {
                WriteToFile("Inicia el servicio");
                _timer = new Timer((e) => WriteTimeToFile(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
            }
            catch (Exception ex)
            {
                logger.LogError($"{ex.Message},{ex.StackTrace}");
            }
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);
            WriteToFile("Finaliza el servicio");
            return Task.CompletedTask;
        }

        private void WriteTimeToFile()
        {
            WriteToFile(DateTime.Now.ToString());
        }

        private void WriteToFile(string message)
        {
            if (!File.Exists(_path))
            {
                using (var sw = File.CreateText(_path))
                {
                    sw.WriteLine(message);
                }
            }
            else
            {
                using (var sw = File.AppendText(_path))
                {
                    sw.WriteLine(message);
                }
            }
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
}

En él, simplemente se añaden mensajes a un fichero que se encuentra junto al binario para indicar cuando empieza, cuando finaliza y cada minuto mientras esté activo, pero aquí sería donde vamos a poner que es lo que hace nuestro servicio (consultar una cola de mensajes, lecturas a una Db, etc).

Hasta aquí, este es el mismo concepto que utilizamos en ASP NET Core para crear servicios que corran en segundo plano en una web. Creamos el servicio heredando de «IHostedService» y lo registramos desde «ConfigureServices». El problema como decía al principio, es que los servicios en Windows no funcionan así, tienen que heredar de «ServiceBase».

Preparando nuestro servicio para Windows

En primer lugar, vamos a necesitar crear nuestra clase heredada:

using Microsoft.Extensions.Hosting;
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    public class ServiceBaseLifetime : ServiceBase, IHostLifetime
    {
        private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();

        public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
        {
            ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
        }

        private IApplicationLifetime ApplicationLifetime { get; }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            cancellationToken.Register(() => _delayStart.TrySetCanceled());
            ApplicationLifetime.ApplicationStopping.Register(Stop);

            new Thread(Run).Start(); //Ejecutamos la tarea en un hilo para bloquear y prevenir que IHost.StartAsync termine.
            return _delayStart.Task;
        }

        private void Run()
        {
            try
            {
                Run(this); // Bloqueamos la ejecución hasta que el servicio termine.
                _delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
            }
            catch (Exception ex)
            {
                _delayStart.TrySetException(ex);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Stop();
            return Task.CompletedTask;
        }

        // Se llama desde base.Run() cuando el servicio esta listo para funcionar.
        protected override void OnStart(string[] args)
        {
            _delayStart.TrySetResult(null);
            base.OnStart(args);
        }

        protected override void OnStop()
        {
            ApplicationLifetime.StopApplication();
            base.OnStop();
        }
    }
}

Sin ánimo de entrar muy en profundidad, ya que esta clase es una copia del ejemplo que ofrece Microsoft navegando entre los repositorios en GitHub, en ella básicamente se implementan los métodos propios de un servicio Windows y de la interfaz «IHostLifetime«. De este modo, la aplicación puede correr como un servicio en Windows y ser usada desde «IHost».

Esta clase tiene que estar en el contenedor de inyección de dependencias siempre que el servicio corra sobre Windows, así que vamos a añadir unos métodos de extensión que nos facilite incluirla si es necesario:

using System.Runtime.InteropServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    public static class ServiceBaseLifetimeHostExtensions
    {
        public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
        }

        public static Task RunServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            //Si es Windows, añadimos el ServiceBaseLifetime a la inyeccion de dependencias
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
            }
            else //Sino, ejecutamos normalmente
            {
                return hostBuilder.Build().RunAsync(cancellationToken);
            }
        }
    }
}

Gracias a la condición que pusimos al arrancar el IHost para detectar si estábamos en sin el debugger, y con la condición que añadimos dentro de la extensión RunAsServiceAsync, vamos a poder ejecutar nuestro programa como un servicio para Windows, para Linux o como un programa de consola (que es ideal para depurarlo).

Por último, si queremos instalar el servicio en Windows, primero vamos a publicarlo de manera que generemos un .exe indicándole al publicar que queremos que el «Tiempo de ejecución de destino» sea una versión de Windows»

La imagen muestra como indicarle el destino de la publicación

Una vez que hemos publicado nuestro .exe, vamos a tirar de «Service Controller Commands» para instalarlo. Desde la terminal:

sc create "Servicio Core" binpath= "C:\Users\jorge\source\repos\PostServicioNetCore\PostServicioNetCore\bin\Release\netcoreapp2.2\publish\PostServicioNetCore.exe"

Por el contrario, si queremos que el servicio sea para Linux, basta con que no generemos un .exe al publicar y lo registremos normalmente (como veremos en la próxima entrada aprovechando este código).

Con esto, ya tenemos nuestro servicio Net Core multiplataforma listo para ser instalado y ejecutado tanto en Windows como en Linux, y al que solo tenemos que ir añadiéndole la funcionalidad que queramos mediante nuevas implementaciones de «IHostedService».

Como siempre, dejo el código fuente en GitHub para que puedas descargártelo y probar.

«Terraformando» nuestra infraestructura desde Azure Pipelines

La imagen muestra los logos de terraform, azure pipelines y azure cloud

En la última entrada, hablamos sobre Terraform como herramienta para para gestionar nuestra infraestructura mediante código. También hemos hablado hace poco sobre el despliegue continuo desde Azure Pipeline con ARM. Como pudimos comprobar, trabajar con Terraform nos facilita la vida respecto a lo que es el trabajo con ARM directamente. Hoy vamos a unir las dos cosas utilizando Terraform desde Azure Pipelines.

En su momento comentábamos que una de las grandes ventajas de Terraform es que mantiene el estado de los recursos. Esto es algo muy útil y que con trabajo local no supone ningún problema, pero eso se nos queda corto si trabajamos desde el agente de Azure Pipelines. Esto es porque cuando el agente termine va a borrar los recursos, y esto incluye el fichero de estado del despliegue. Para solucionar este «inconveniente» y persistir el estado entre despliegues, vamos a utilizar un Azure Storage donde guardar el fichero.

Una vez aclarado esto, ¡vamos a crear una WebApp en Linux y conectarla a una base de datos Sql Server!

Codificando la infraestructura

Para poder generar la infraestructura y conectarla entre si sin revisión manual, vamos a utilizar la potencia que nos ofrece Terraform para relacionar recursos y así añadir datos como la cadena de conexión. En GitHub está el proyecto completo, dividido en secciones para facilitar su mantenimiento, y utilizando la posibilidad de modular partes para crear la Web App y el Sql Server.

Utilizando estas ventajas, vamos a generar la cadena de conexión de la base de datos utilizando los datos de salida y se lo vamos a pasar a la configuración de la webapp. Para eso, vamos a crear una salida en el módulo de sql server donde vamos a generar la cadena de conexión:

output "conection_string_value" {
  description = "Sql Server ConnectionString"
  value = "Server=${azurerm_sql_server.sqlServer.fully_qualified_domain_name};Initial Catalog=${azurerm_sql_database.sqlServerDb.name};User Id=${var.SQL_ADMIN_ID};Password=${var.SQL_PASSWORD};"
}

Con esto, vamos a poder utilizarla más adelante:

module "linuxwebapp" {
  //...
  conection_string_value = "${module.sqlserver.conection_string_value}"
}

El código de la infraestructura es fácil de seguir, pero aun así recomiendo echarle un ojo en profundidad para entender el concepto anterior.

Creando la integración continua

Para poder desplegar Terraform desde Azure Pipelines, el primer paso es crear el pipeline de integración. De esto hablamos hace algún tiempo en una entrada sobre CI, pero esta vez vamos a utilizar la interfaz gráfica en vez de yml. Como se pueden ver, los pasos son los mismos que cuando utilizábamos yml:

La imagen muestra el pipeline de integración

Aquí solo hay un pequeño cambio respecto a lo que vimos anteriormente, y es que tenemos que meter los ficheros de Terraform al artefacto para poder utilizarlos más adelante:

La imagen señala los campos que hay que rellenar en la tarea de copiar archivos

Para configurar esta nueva tarea, basta con decirle el nombre de la carpeta donde está el código Terraform que queremos desplegar, ponerle el filtro de selección (** para seleccionarlo todo), y por último crear una carpeta en el directorio del artefacto y pegar los ficheros Terraform en él.

El hecho de utilizar la interfaz gráfica para la integración es solo por cambiar, pero para el despliegue solo se puede utilizar la interfaz gráfica de momento.

Ejecutar Terraform desde Azure Pipelines

Una vez que tenemos la integración lista, vamos a crear una Release para que despliegue la infraestructura y el proyecto. Para ahorrarnos trabajo, vamos a utilizar el Task «Terraform Build & Release Tasks«, así que vamos a instalarlo en el pipeline:

La imagen muestra el botón para instalar las Task de Terraform en Azure Pipelines

Una vez que lo tenemos instalado, vamos a crear el pipeline donde instalaremos Terraform, lo inicializaremos y aplicaremos para desplegar los recursos, y por último publicaremos la web:

La imagen muestra el pipeline de release

Al igual que hacíamos con ARM, lo ideal sería tener diferentes ranuras donde tengamos las diferentes etapas de dev, pre, pro, etc.

Además, vamos a necesitar diferentes variables, que vamos a registrar también en el pipeline, para eso, vamos a la pestaña «Variables»:

La imagen señala la pestaña "Variables"

Y vamos a registrar las variables que necesitamos para nuestro Terraform:

La imagen muestra las variables del pipeline

Por convención, el pipeline le pasa directamente las variables que coincidan y empiecen por TF_VAR a Terraform siempre que no sean secretos. Esto es algo a tener en cuenta para evitarnos un comando apply larguísimo donde le pasemos muchísimas variables.

Volviendo al pipeline, lo primero que tenemos que hacer es instalar Terraform, para eso, vamos a utilizar la Task «Terraform Installer» y le vamos a indicar la versión de Terraform que queremos utilizar:

La imagen muestra donde indicar la versión

El siguiente paso, es configurar una Task de tipo «Terraform CLI» para ejecutar el comando init:

La imagen muestra la interfaz de la Task Terraform CLI

Dentro de esta Task, vamos a seleccionar el comando «init», y vamos a indicar la ruta donde está el código Terraform, por último, vamos a seleccionar el tipo de backend, que como dijimos al principio, será en Azure Storage, por lo tanto, seleccionamos «azurerm». Esto nos permite ampliar la configuración pulsando sobre «AzureRM Backend Configuration», y así indicarle los datos de configuración:

La imagen muestra la configuración del backend en el Task

Con esto listo, el último paso con Terraform es crear una tercera Task de tipo «Terraform CLI». Esta vez vamos a elegir el tipo de comando «apply», le vamos a indicar la ruta a donde están los ficheros de código Terrafom, y por último le vamos a indicar las opciones. Entre las opciones, vamos a indicarle «-auto-approve» para que no pida confirmación antes de desplegar los cambios, y le vamos a pasar todas las variables de tipo «secrets» mediante «-var VARIABLE=VARIABLE_PIPELINE» (recordemos que las demás variables se le pasan por convención):

Un ejemplo de las opciones utilizadas es:

-auto-approve -var AZURE_SUBSCRIPTION_ID=$(TF_VAR_AZURE_SUBSCRIPTION_ID) -var AZURE_CLIENT_ID=$(TF_VAR_AZURE_CLIENT_ID) -var AZURE_CLIENT_SECRET=$(TF_VAR_AZURE_CLIENT_SECRET) -var AZURE_TENANT_ID=$(TF_VAR_AZURE_TENANT_ID) -var SQL_PASSWORD=$(TF_VAR_SQL_PASSWORD)

Con esto, y si todo ha ido bien, ya vamos a conseguir desplegar los recursos en Azure. Para desplegar la Web, solo nos queda añadir un Task de tipo «Azure App Service Deploy» tal cual hicimos en el caso de ARM para desplegar la web.

Tras lanzar una release, dentro de nuestro portal en azure podremos encontrar algo como esto:

La imagen muestra los recursos de Azure deplegados mediante Terraform y Azure Pipelines

Para este ejemplo, hemos utilizado el template de ASP NET Core y le hemos añadido que ejecute las migraciones de la base de datos al iniciar, para que la web este lista para funcionar directamente.

Como siempre, he dejado el código fuente completo (tanto Terraform como la Web) en GitHub.

Conclusión

Como hemos podido comprobar, Terraform es una herramienta muy potente que nos facilita mucho la vida manejando infraestructura como código (IaC), además de permitirnos trabajar con múltiples proveedores. Podemos integrar perfectamente Terraform y Azure Pipelines, o cualquier otro servicio CI/CD utilizando los comandos directamente, por lo que es algo que vale la pena revisar y conocer.

Terraform: Dando forma a nuestra infraestructura

La imagen muestra el logo de terraform y el de azure

Acabamos de terminar la serie sobre CI/CD, y en ella hemos hablado de lo importante que es mantener unas estrategias de trabajo que nos permitan la detección de errores rápidamente. Como colofón final, vimos como aprovisionar y desplegar en Azure Web Apps desde el Azure Pipelines (o desde otros sitios) y así despreocuparnos de esa parte utilizando las plantillas ARM.

Utilizar ARM es muy potente, pero quizás es algo tedioso de utilizar… ahí es donde entra terraform. ¿Y que es terraform podrás preguntarte? Pues es una herramienta multiplataforma hecha en Go que nos permite una gran abstracción sobre ARM, pero no solo funciona con ARM, admite una lista de proveedores bastante larga y que puedes consultar aquí.

Además de permitirnos trabajar de forma declarativa, sin tener que bajar al fango, aporta una gran ventaja respecto a usar ARM, y es que terraform mantiene el estado. Es decir, la herramienta es capaz de almacenar el estado de las cosas que ya están desplegadas, evitando volver a desplegarlas si no hay cambios. Pero hablar es fácil, y que yo te cuente que algo es mejor o peor no sirve de mucho, así que vamos a probarlo para ver lo sencillo que es.

Instalando terraform

Vamos a ir a su web y descargarnos el binario para nuestro sistema operativo. En la web nos indican que tenemos que añadir la ruta donde hayamos puesto el binario al PATH. (En mi caso, «C:/Terraform»):

La imagen muestra la ruta de terraform en el PATH

Una vez que lo hayamos hecho, desde una terminal vamos a comprobar que todo esta bien ejecutando:

terraform version

Si todo está bien, debería devolvernos la versión actual. Si es así, ya tenemos terraform instalado. ¿A qué no ha sido tan difícil?

Aunque trabaja con ficheros *.tf que son texto plano, yo recomiendo utilizar Visual Studio Core con la extensión «Terraform«. Si tienes dudas sobre como instalar cualquiera de las dos, te dejo un enlace de CampusMVP donde explico cómo preparar un entorno de trabajo para .Net Core, al que solo hay que añadir la nueva extensión. Esto solo es una sugerencia, si prefieres utilizar cualquier otro entorno, o simplemente el block de notas y la terminal, todo funcionará igual de bien.

Creando nuestro código terraform

Para esta entrada, vamos a hacer un aprovisionamiento sencillo de una Azure Web App Windows, junto a su plan y su grupo de recursos desde terraform (para la siguiente vamos a hacer un escenario algo más complejo añadiendo un Sql Server a nuestra web). Para ello, vamos a crear un fichero al que llamaremos «main.tf» (el nombre no es importante). De hecho, terraform buscará todos los ficheros «*.tf» desde el nivel en el que se encuentre y unificará todos los archivos, por lo que es una buena idea separar los archivos por finalidades. En esta entrada vamos a utilizar un solo fichero para el código, pero una estructura más clara podría ser:

  • webapp.tf para el código de la web.
  • resourcegroup.tf para el código de grupo de recursos.
  • variables.tf para las variables que vamos a necesitar.

Definiendo el proveedor

Lo primero que vamos a hacer, es definir el proveedor junto a las 4 variables que necesita (para utilizar las variables, se hace con el formato «${var.NOMBRE_VARIABLE}» ):

provider "azurerm" {
  version         = ">= 1.6.0"
  subscription_id = "${var.ID_Suscripcion}"
  client_id       = "${var.ID_Aplicacion}"
  client_secret   = "${var.Password_Aplicacion}"
  tenant_id       = "${var.ID_Tenant}"
}
variable "ID_Suscripcion" {
  description = "ID de la suscripción de Azure"
}
variable "ID_Aplicacion" {
  description = "ID del la aplicación"
}
variable "Password_Aplicacion" {
  description = "Secreto de la aplicación"
}
variable "ID_Tenant" {
  description = "ID del directorio activo de Azure"
}

¿Y de dónde sacamos estos datos? Pues dos de ellas simplemente debemos consultarlas, para ello escribimos en la PowerShell:

az account list

Esto nos devolverá el ID de la suscripción y el directorio activo:

La imagen muestra el ID_Suscripcion y el ID_Tenant

Para los dos campos de aplicación, lo que vamos a hacer es registrar una aplicación en nuestro directorio activo. Esto se puede hacer desde el portal de Azure, o desde la propia terminal ejecutando:

az ad sp create-for-rbac -n "NombreAplicacion"

Eso nos va a devolver los datos que nos faltan:

La imagen muestra los datos de ID_Aplicacion y Password_Aplicacion

Si tienes problemas porque no se reconoce el comando «az», sigue este enlace para instalar Azure CLI.

Definiendo el Grupo de Recursos

Una vez que lo tenemos, vamos a añadir un grupo de recursos para nuestra Web App:

resource "azurerm_resource_group" "webapp" {
  name     = "${var.resource_group_name}"
  location = "${var.location}"
}
variable "location" {
  description = "Region donde queremos que se cree"
}
variable "resource_group_name" {
  description = "Nombre del grupo de recursos"
}

Definiendo el Service Plan

Ahora vamos a añadir el service plan:

resource "azurerm_app_service_plan" "webserviceplan" {
  name                = "${var.service_plan_name}"
  location            = "${azurerm_resource_group.webapp.location}"
  resource_group_name = "${azurerm_resource_group.webapp.name}"

  kind = "${var.plan_settings["kind"]}"

  sku {
    tier     = "${var.plan_settings["tier"]}"
    size     = "${var.plan_settings["size"]}"
    capacity = "${var.plan_settings["capacity"]}"
  }
}
variable "plan_settings" {
  type        = "map"
  description = "Definimos el tipo de plan que vamos a utilizar"

  default = {
    kind     = "Windows"  # Linux or Windows
    size     = "S1"
    capacity = 1
    tier     = "Standard"
  }
}
variable "service_plan_name" {
  description = "Nombre del service plan"
}

Una de las ventajas que podemos ver, es que terraform nos permite utilizar datos de los propios recursos desplegados utilizando el formato «${TIPO_DE_RECURSO.NOMBRE_RECURSO.DATO}». Por ejemplo, en:

location  = "${azurerm_resource_group.webapp.location}"

Le estamos indicando que la localización del service plan, queremos que sea la misma que la del recurso «azurerm_resource_group» con el nombre «webapp». Trabajando de esta manera, terraform es capaz de calcular el grafo de dependencias para que todo se pueda aprovisionar sin problemas.

Definiendo la Web App

Por último, solo nos queda añadir el Web App:

resource "azurerm_app_service" "webapp" {
  name                = "${var.name}"
  location            = "${azurerm_resource_group.webapp.location}"
  resource_group_name = "${azurerm_resource_group.webapp.name}"
  app_service_plan_id = "${azurerm_app_service_plan.webserviceplan.id}"
}
variable "name" {
  description = "Nombre de la Web App"
}

Hay muchos parámetros, que podríamos poner en el propio código, pero entonces no sería reutilizable… Terarform nos permite indicarle todos estos parámetros cuando vayamos a hacer el despliegue en el propio comando, pero como puedes intuir, es muy tedioso. Así que vamos a utilizar otra de las opciones que nos da terraform, que es cargar tener un segundo fichero de variables con sus valores.

Si usas git, hay que añadirlo en el .gitignore o nuestras claves se irán al repositorio.

Para poder tener este fichero, creamos un segundo fichero con el formato «*.auto.tfvars», por ejemplo, «variables.auto.tfvars», donde iremos rellenando los valores de esta forma:

ID_Suscripcion = "----"
ID_Aplicacion = "----"
Password_Aplicacion = "----"
ID_Tenant = "----"
name = "postterraformbasic"
service_plan_name = "planpostterraformbasic"
resource_group_name = "postterraform"
location = "westeurope"

A la vista está que este código es más fácil de seguir y mantener que si fuese ARM directamente, aunque hay algunas cosas que no están soportadas aun, y puede hacer falta incluir algo de ARM en algunos casos…

Desplegando con terraform

Una vez que tenemos todo el código, y las variables listas, vamos a desplegar, para ello vamos a inicializar la herramienta con:

terraform init
La imagen muestra la salida del comando init

Una vez hecho esto, vamos a comprobar si existe algún error con:

terraform validate

En caso de que no exista ningún error, lo siguiente es calcular los cambios (aunque este paso se puede omitir):

terraform plan

La salida de este comando nos va a mostrar algo como esto:

La imagen muestra la salida del comando terraform plan

Como salida, nos da las acciones que va a ejecutar en detalle, pero solo son a nivel informativo, para ejecutar el despliegue, utilizamos:

terraform apply

Esto nos devuelve algo parecido al comando plan, donde nos dice que es lo que va a hacer, pero ahora, nos pide que confirmemos escribiendo «yes» si queremos que despliegue:

La imagen muestra la petición de confirmación de terraform

Tras escribir «yes», se inicia el proceso. Una vez termine, la salida será algo asi:

La imagen muestra el resultado de finalización

Como terraform almacena el estado, si volvemos a intentar desplegar lo mismo, volviendo a ejecutar:

terraform apply

Obtenemos el resultado esperado, y nos dice que no hay ningún cambio pendiente:

La imagen muestra como el comando apply termina con 0 cambios

Si por el contrario hubiésemos cambiado algo en el código, el resultado hubiese sido un cambio en nuestra infraestructura. Para terminar con las opciones que tenemos, existe una manera de eliminar los recursos desplegados:

terraform destroy

Volverá a pedirnos confirmación, y basta con que volvamos a escribir «yes»:

La imagen muestra las acciones que va a hacer el comando terraform destroy
La imagen muestra el final del comando terraform destroy

Conclusión

Aunque ha sido un ejemplo simple, se puede ver que terraform es una herramienta muy potente que nos quita mucho trabajo a la hora de desplegar infraestructura desde código al ser declarativo, persistir el estado y encima calcular las dependencias automáticamente. Existe una lista detallada de las opciones disponibles en la documentación oficial, a la cual recomiendo echarle un ojo.

En la próxima entrada vamos a ver un ejemplo más en profundidad creando una web en Linux conectada a un Sql Server, de modo que tengamos una infraestructura funcional, y además desplegaremos la web desde Azure Pipelines para poder comparar el proceso en el caso de ARM y en el caso de Terraform.

Si quieres ver el código completo, dejo un gist. También os dejo el enlace a un repo muy interesante de un workshop sobre terraform de un compañero.