Microsoft Azure PaaS: Comunicación interna entre roles de un Cloud Service

Cuando varios roles (Web roles y Worker roles) trabajan dentro de un mismo Cloud Service de la plataforma, estos tienen la posibilidad de comunicarse a través de conexiones externas o internas. Las externas se deben utilizar cuando es necesario comunicarse con clientes ajenos a nuestro servicio (fuera del datacenter de Azure), haciendo uso de un input endpoint. En el caso contrario, es posible que alguno de nuestros roles sólo necesiten dar servicio a otra parte de nuestra arquitectura, sin que sea necesario exponer los mismos en Internet, donde la mejor opción es utilizar un internal endpoint.

Estos extremos están asociados con una IP y un puerto, donde en el caso de los input endpoints somos nosotros quienes definimos a qué puerto queremos asociarlo y en el caso de los endpoints internos dicho puerto es asignado por la plataforma de manera dinámica. La gran diferencia a nivel de configuración entre unos y otros es que los input endpoints están balanceados a través de Azure (se utiliza como IP la que está asociada al balanceador) y en los internal endpoints se utiliza la IP privada de las instancias para establecer la comunicación.

Escenario de ejemplo: Frontal web + Web Apis

Para poder comprender mejor su utilidad, supongamos que tenemos una aplicación separada en roles donde tenemos por un lado el frontal web y por otro un conjunto de servicios web. De esta forma, si el día de mañana es necesario escalar sólo parte de la plataforma podremos hacerlo de forma más granular. Aunque esté diseñado de dicha manera, no queremos que nuestros servicios web queden expuestos a Internet, ya que son de consumo propio.

scenario communication for roles instances in Azure

Configuración de los endpoints

Lo primero que debemos hacer es definir los endpoints que vamos a utilizar. Para ello iremos rol por rol entrando en la sección de endpoints:

WebRoleA

Configuration WebRoleA Endpoints

En la configuración del WebRoleA no se ha alterado el endpoint que viene establecido por defecto, Endpoint1, el cual permite acceder al servicio de manera externa a través del puerto 80.

WebRoleB

Configuration WebRoleB Endpoints

En el caso del WebRoleB hemos creado un endpoint de tipo internal, donde podemos comprobar que el puerto público aparece deshabilitado. Si no establecemos ningún puerto privado, la plataforma eligirá uno al azar al realizar el despliegue.

WebRoleC

Configuration-WebRoleC-Endpoints

En el caso del WebRoleC la configuración es exactamente la misma que para el WebRoleB. En este caso, hemos elegido un puerto específico que será utilizado para la comunicación a través de este endpoint.

Comprobación de los endpoints habilitados en un Cloud Service

Un ejercicio interesante para conocer cuáles son los endpoints disponibles dentro de un cloud service es recorrer la información disponible dentro de RoleEnvironment.Roles. Se trata de una propiedad IDictionary<string, Role> que nos ofrece un listado con todos los roles que existen dentro de un cloud service, sus instancias y endpoints.

        public ActionResult Index()
        {
            var list = new List<string>();

            foreach (var roleDefinition in RoleEnvironment.Roles)
            {
                foreach (var roleInstance in roleDefinition.Value.Instances)
                {
                    foreach (var endpoint in roleInstance.InstanceEndpoints)
                    {
                        list.Add(string.Format("<strong>{0} - {1}</strong> <mark>{2}</mark>", roleInstance.Id, endpoint.Key, endpoint.Value.IPEndpoint));
                    }
                }
            }

            return View(list);
        }

Si accedemos a nuestro sitio usando el emulador veremos la siguiente información:

role endpoints list local

En este caso vemos que el resultado no es realista, ya que todas las IPs pertenecen a mi propio local y que la configuración del puerto en WebRoleC no se ha respetado. Es importante tenerlo en cuenta ya que no es posible trabajar con internal endpoints en local.

Para poder ver un resultado real de la configuración, he subido la solución a Azure y he ampliado a 8 instancias el WebRoleB:

role endpoints list cloud

Como podemos ver en la captura anterior, todos los endpoints del WebRoleA están utilizando la misma IP (la cual corresponde con la IP pública del Cloud Service). Las instancias del WebRoleB utilizan su IP privada y el puerto 20000 elegido dinámicamente por Azure. Por último, la única instancia del WebRoleC está haciendo uso del puerto 6000, el cual configuramos en el paso anterior.

Llamadas a nuestras Apis internas

Para poder comprobar que las llamadas internas funcionan correctamente, he creado el siguiente ejemplo:

    <script>
        $(function () {

            function success(result) {
                alert(result);
            }

            function failure(error) {
                alert(error.statusText);
            }

            $.ajax({ url: '/Home/CallApi', data: { role: 'B' }, type: 'GET' }).then(success).fail(failure);

            $.ajax({ url: '/Home/CallApi', data: { role: 'C' }, type: 'GET' }).then(success).fail(failure);

        });
    </script>

En el WebRoleA he añadido este código en JavaScript, el cual realiza una llamada al mismo frontal indicando cuál es el role al que quiere acceder. En el lado del back-end se han creado los siguientes métodos:

        public string CallApi(string role)
        {
            var webClient = new WebClient();

            var endpoint = GetEndpoint(role);

            if (endpoint != null)
            {
                var data = webClient.OpenRead(string.Format("http://{0}/api/values", endpoint.IPEndpoint));
                var reader = new StreamReader(data);
                var result = reader.ReadToEnd();
                return string.Format("{0} (Instance {1})", result, endpoint.RoleInstance.Id);
            }

            return string.Format("You don't have access to WebRole{0}", role);
        }

        public RoleInstanceEndpoint GetEndpoint(string role)
        {
            var random = new Random();

            var endpoints = RoleEnvironment.Roles["WebRole" + role].Instances.Select(i => i.InstanceEndpoints["InternalApi" + role]).ToArray();

            if (endpoints.Count() > 0)
                return endpoints[random.Next(0, endpoints.Count() - 1)];
            else
                return null;
        }

En primer lugar está CallApi que es la acción que llamamos desde cliente. En ella creamos un objeto del tipo WebClient para realizar la llamada al rol correspondiente. Para poder recuperar la IP y el puerto del rol al que vamos a realizar la consulta, se ha creado un método llamado GetEndpoint, donde accedemos a las instancias del role pasado como parámetro y creamos un array con todos los endpoints disponibles. En este ejemplo se devuelve de manera aleatoria la instancia que atenderá la petición.

Volviendo a CallApi, si el valor de endpoint es null avisaremos al cliente de que no tenemos acceso al servicio. En el caso contrario realizaremos la llamada y devolveremos el contenido en forma de string.

Tal y como está la configuración en este punto el ejemplo funciona correctamente, atendiendo a las peticiones tanto el WebRoleB como WebRoleC:

Demo Internal Endpoints

No obstante, según nuestro esquema de comunicación inicial, en mi arquitectura es necesario que el WebRoleA pueda acceder al WebRoleB pero que no le sea posible comunicarse directamente con el WebRoleC ¿Cómo lo hacemos posible? Necesitamos un paso adicional añadiendo Network Traffic Rules a nuestro Cloud Service.

NetworkTrafficRules

Tal y como está configurada la aplicación ahora, todos los roles dentro de la solución son capaces de comunicarse con el resto. También es posible que no deseemos este comportamiento y que, dentro del Cloud Service, queramos ser más restrictivos. Dentro del archivo ServiceDefinition es posible utilizar el nodo NetworkTrafficRules para definir una serie de reglas que nos permitan restringir el acceso a los roles a través de los endpoints.

  <NetworkTrafficRules>
    <OnlyAllowTrafficTo>
      <Destinations>
        <RoleEndpoint endpointName="InternalApiB" roleName="WebRoleB"/>
      </Destinations>
      <WhenSource matches="AnyRule">
        <FromRole roleName="WebRoleA"/>
      </WhenSource>
    </OnlyAllowTrafficTo>
    <OnlyAllowTrafficTo>
      <Destinations>
        <RoleEndpoint endpointName="InternalApiC" roleName="WebRoleC"/>
      </Destinations>
      <WhenSource matches="AnyRule">
        <FromRole roleName="WebRoleB"/>
      </WhenSource>
    </OnlyAllowTrafficTo>
  </NetworkTrafficRules>

Tal y como habíamos definido al inicio del post, en el archivo de configuración he creado dos elementos del tipo OnlyAllowTrafficTo donde indico qué roles pueden acceder a los endpoints creados en WebRoleB y WebRoleC. Es posible crear tantos elementos del tipo OnlyAllowTrafficTo como sean necesarios y, además, es importante ver que el filtro se realiza a nivel de endpoint y no a nivel de rol, por lo que sería posible dar acceso a un rol por un extremo y limitarlo por otros.

La forma de percatarnos de que no tenemos acceso a un endpoint es a través de RoleEnvironment.Roles, ya que no devolverá ningún endpoint al que no tengamos acceso desde el rol donde se está solicitando esta información.

Restricted access WebRoleC

En la imagen anterior se puede comprobar que el endpoint para el WebRoleC no aparece listado y que tampoco es posible solicitar una petición al mismo.

Espero que sea de utilidad.

¡Saludos!