Monday, August 3, 2020

Dockerizing Multiple Services - Integrating Angular with AspNetCore API via Docker Compose


Docker Angular ASP.NET Core  • Posted 2 months ago

Docker-Compose is a configuration file which contains instructions for the Docker about how services should be built from respective Dockerfiles. While a Dockerfile aims at creating and customizing application containers by means of base images and instructions, the Docker-Compose file works on top of the Dockerfile and helps developers in running docker containers with complex runtime specifications such as ports, volumes and so on.

Docker-Compose also facilitates developers in creating "multiple services" and facilitates these containers to interact among themselves. In this article, let's look at how we can create "multiple services" from respective Dockerfiles and facilitate an application from one docker container call an API from another container.

Handling Multi-Service Communication - Example:

To illustrate our case, let's take the example of an Angular application which calls an AspNetCore API to fetch data and render it on a grid. Assume that we shall deploy the Angular application and the AspNetCore API application as separate containers and each component runs independently in its own isolated environment.

To facilitate routing requests to these applications, we make use of an nginx webserver which has work as a reverse-proxy for these two components. This nginx component works as an interface to the outside network and routes requests coming to it to respective components based on the request path.

The Routing Approach - Flow:

The setup works as follows - for request made to the domain, say http://localhost:80 an nginx server listens over the port 80 and receives the request. If the request corresponds to an API endpoint represented by the path prefix /api, the nginx routes the request to the AspNetCore container which processes the request and responds back with data. If the request path doesn't contain a prefix /api, the nginx component assumes it as a request to the Angular component and routes it to the Angular component.

data/Admin/2020/5/docker-multiple-services-dia.PNG

To create the routing specifications in the nginx server, we make use of a route configuration file called as default.conf which contains the routes and how nginx needs to behave for each route. The conf file shall be as below:

upstream client {
    server client:80;
}

upstream dncapi {
    server dncapi:80;
}

server {
    listen 80;

    location / {
        proxy_pass http://client;
    }

    location /api {
        proxy_pass http://dncapi;
    }
}

Observe that for paths "/" and "/api" represented by the "location" configuration, we specify proxy forward to domains "http://client" and "http://dncapi" which represents the Angular and AspNetCore components respectively. We shall understand why we specify the labels "client" and "dncapi" instead of domain names for the components and how they're resolved as we progress.

To create the setup, let's begin by creating Dockerfile for each of these three components which represent on how the containers are to be built.

Configuring the nginx Proxy component:

The nginx component setup is a simple and straight-forward affair, where we pull the base nginx image and copy the route specification configuration onto a specific path under the nginx directory.

FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf

Configuring the Angular component:

The Angular application calls for an API endpoint for data which is rendered on a grid. The API to be used for this functionality is configured under a ts class as below:

export class ApiConstants {
    public static uri: string = "/api";
}

One can wonder why we haven't specified any domain while calling for the API. Since this is an Angular application which generally runs on the client browser, the logic runs outside the container which means that any Docker related configuration can't get resolved at it point. To solve this, we bring in the concept of a Reverse Proxy - the nginx component which routes all incoming requests to either Angular component or AspNetCore component respectively for the given path prefix. This ensures that although the browser makes API call to its own domain, it is further routed internally by the nginx so that the user can never know where the API request actually goes into.

To create the Dockerfile, We follow the same steps on how we can create and deploy an Angular application via Docker that we saw before. The process requires us to first build the Angular application for release and then copy the generated binaries (the index.html and the js files) onto a webserver which hosts these static files. We make use of another nginx instance to host the Angular component files and listen on a specific port - port 80 by default.

The Dockerfile looks like below:

FROM node:14.2.0-alpine3.11 as build
WORKDIR /app

RUN npm install -g @angular/cli

COPY ./package.json .
RUN npm install
COPY . .
RUN ng build --prod

FROM nginx as runtime
COPY --from=build /app/dist /usr/share/nginx/html

Configuring the AspNetCore component:

The AspNetCore component houses the API which returns data to the Angular component. To keep things simple, we're not looking much at how the API component fetches data or from where it fetches it. In real-world scenarios, the component reads data from a data store such as SQL Server or a Cache component such as Redis. While in our example, we don't use any of these mechanisms and instead use a hard coded list of values to be passed for all requests.

To create the Dockerfile, We use the same approach on how we can create and deploy an AspNetCore application via Docker that we used before. The process requires us to first build the AspNetCore application for release within a Docker container that provides the SDK and then copy the generated binaries (the dll files) onto another AspNetCore container which contains the runtime to run these dlls.

The Dockerfile for the AspNetCore component looks as below:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /app

COPY *.csproj .
COPY *.config .
RUN dotnet restore --disable-parallel --configfile ./nuget.config 

COPY . .
RUN dotnet publish -c Release -o build

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
WORKDIR /app

COPY --from=build /app/build .
ENTRYPOINT [ "dotnet", "AngularDncApp.dll" ]

Setting up the Docker-Compose:

The twist in the tale arises in the docker-compose file, which configures and boots up each of these three components (Angular, AspNetCore and Nginx) together. We tag each of these components with a specific "service" name, which uniquely identifies each component.

The yaml file looks like below:

version: "3"
services: 
    nginx:
        build: 
            context: ./Nginx
            dockerfile: Dockerfile
        ports: 
            - 80:80
        restart: always
    client:
        build:
            context: ./Client
            dockerfile: Dockerfile
        ports: 
            - 5000:80
    dncapi:
        build: 
            context: ./API
            dockerfile: Dockerfile
        ports: 
            - 3000:80

One major advantage of using docker-compose to boot up multiple services is that:

The Docker creates and groups all the services configured under the docker-compose file into a single subnet and takes care of data communication among these services by means of their "service name".

Observe the service names specified for each component to be booted up inside the docker-compose file. The conf file we have configured inside our nginx component specified the domains for Angular and AspNetCore components as:

location / {
    proxy_pass http://client;
}

location /api {
    proxy_pass http://dncapi;
}

Which is exactly the same as the service names for these components respectively: client for Angular and dncapi for AspNetCore components. During execution, for every request that the nginx proxy receives from the outside world, the request is routed to any of these two components as http://dncapi/api/endpoint or http://client/index.html and the Docker looks up under its own "route mapping table" for a matching component name for the service name specified in the route. And if any component name matches, Docker resolves the service name to the IP of that component and routes to it. This is the biggest advantage of using a docker-compose, since it groups up all the services together and is responsible for maintaining the mapping table.

To run this setup, we give the command:

> docker-compose up

Which builds and deploys every component specified within its configuration file aka the docker-compose.yaml file and groups all of them into a single subnet.

"Ensure that the ports specified in the default.conf for proxy_pass MUST match the ports under which the services run inside their Docker containers."

##
Successfully tagged angulardncapp_dncapi:latest
Creating angulardncapp_dncapi_1 ... done
Creating angulardncapp_nginx_1  ... done
Creating angulardncapp_client_1 ... done
Attaching to 
angulardncapp_dncapi_1, 
angulardncapp_nginx_1,
angulardncapp_client_1

And to verify if this installation works, we browse to http://localhost:80 on our browser, on which the nginx component listens to. The nginx component further routes the request to Angular component http://client:80 or AspNetCore API component http://dncapi:80 based on the request and the application shows up the data from the API rendered on the grid.

data/Admin/2020/5/docker-multiple-services.PNG

You can find the working project used in this article under the Git repository: https://github.com/referbruv/docker-compose-multi-services-app

No comments:

Post a Comment