Docker containers
.NET .NET Core Containers Continuous Deployment DevOps Docker Web API YAML

Cross Container API Calls in a Docker Image

Welcome to today’s post.

In a previous post I showed how to build and configure Docker container with multiple image services using docker compose.

Today I will be showing how to communicate between services within a Docker image.

I will first give an overview of what image containers are within the Docker container.

API Services as Image Containers

Recall that each image container has the following characteristics:

  1. It has a self-contained network.
  2. It has a unique host port.
  3. It is accessible from other container services within the docker image.

From the above characteristics we can configure any of our container services to be accessible to another container service.

In the example I will be showing through both docker compose and .NET Core how to integrate one container API service to another through a REST based HTTP call.

A diagram that illustrates the above is shown below:

Cross Container API Call

In this post, I will cover the following tasks:

  1. Using the docker compose environment variables to integrate two API services.
  2. Making necessary changes within a .NET Core API to facilitate calls to another API service.
  3. Testing the container service API interaction.

Integration of API Services with Docker Compose

Below is the docker-compose script that will allow us to build the container, and in addition to supply the source container API with the necessary parameter(s) to make the call to the destination API:

#docker-compose.yml (Base)
version: '3.4'
services:
  identity-api:
    image: bookloanidentityapi:${TAG:-latest}
    environment:
      - DB_CONN_STR=Server=172.x.x.x,1433;Database=aspnet-IdentityDb;User Id=xxxxx;Password=xxxxx;
    ports:
      - "5100:80"

  catalog-api:
    image: bookloancatalogapi:${TAG:-latest}
    environment:
      - DB_CONN_STR=Server=172.x.x.x,1433;Database=aspnet-BookCatalog;User Id=xxxxx;Password=xxxxx;
    ports:
      - "5110:80"

  loan-api:
    image: bookloanloanapi:${TAG:-latest}
    environment:
      - DB_CONN_STR=Server=172.x.x.x,1433;Database=aspnet-BookCatalog;User Id=xxxxx;Password=xxxxx;
      - URL_CATALOG_API=http://catalog-api:80/
    ports:
      - "5120:80"

Notice that each API container image (a container network) has its own self-contained database and one of the API container images can make API calls directly to one of the other API container images (loan-api calling catalog-api) with its URL_CATALOG_API environment variable. In addition, each API container image is its own contained network accessible with its own port.

The goal is to get our container network service loan-api to be able to make calls to the container network service catalog-api.

Configuration of a .NET Core API Service to call another API Service

Within a Docker image, each container network is accessible by its name, so we will need to pass an environment variable to the container service that is going to make the API call. In the above script we pass the catalog API URL to the loan-api container service.

environment:
	…
    - URL_CATALOG_API=http://catalog-api:80/

Our container service then retrieves the environment variable and uses it in our HTTP REST call.

In startup.cs, we can retrieve the environment parameter and value, then set the application configuration that corresponds to the catalog API URL (URL_CATALOG_API).

To obtain the Catalog container URL from the environment we use the IOptions pattern (see my post on how to use the IOptions pattern in .NET Core).

To override the Catalog API URL from the app settings within our web API we can use the PostConfigure() method as shown:

public void ConfigureServices(IServiceCollection services)
{
	…
    services.AddOptions();
    services.PostConfigure<AppConfiguration>(opt =>
    {
        if (isInDockerContainer)
        {
            opt.UrlCatalogAPI = Environment.GetEnvironmentVariable("URL_CATALOG_API");
        }
    });
	…
    services.Configure<AppConfiguration>(Configuration.GetSection("AppSettings"));
}

Our controllers and services would then call the other container’s API service using the configured environment value.

The controller and service methods are show below:

Loan API controller:

[HttpGet("api/[controller]/GetLoanReportAllMembers")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetLoanReportAllMembers()
{
    var loanReportAllMembers = await _reportService.OnLoanReport();
    return Ok(loanReportAllMembers);
}

Loan API Loan Service:

public async Task<List<BookLoan.Models.BookStatusViewModel>> OnLoanReport(
    string currentuser = null)
{
    List<BookLoan.Models.BookStatusViewModel> loanstats = new List<Models.BookStatusViewModel>();

    var books = await _bookService.GetBooks();

    foreach (Models.BookViewModel book in books)
    {
	    … gets loan status from loan table
    }
    return loanstats;
}

The book service that calls the catalog-api container service is shown below:

Loan API Book Service:

public async Task<List<BookViewModel>> GetBooks()
{
    string bearerToken = _context.Request.Headers["Authorization"].FirstOrDefault();
    if (bearerToken == null)
        throw new Exception(String.Format("Cannot get books: No authentication token."));

     bearerToken = bearerToken.Replace("Bearer", "");

     HttpResponseMessage response = null;
     var delayedRetry = _apiServiceRetryWithDelay;

     await delayedRetry.RunAsync(async () =>
     {
         response = await _apiServiceHelper.GetAPI(
             _appConfiguration.Value.UrlCatalogAPI + "api/Book/List",
             null,
             bearerToken
         );
     });
     List<BookViewModel> bookViews = new List<BookViewModel>();

     if (response.IsSuccessStatusCode)
     {
		.. get books from response and build output list.
     }
     else
     {
		.. error handling
     }

     if (bookViews == null)
     {
         throw new Exception(String.Format("Books cannot be found."));
     }

     return bookViews;
 }

Provided we use the proper container network address in our API URL

http://catalog-api:80/

then the cross-container call will work. However, if we use an invalid URL such as:

http://localhost:80/

Then the following error will occur:

loan-api_1      | fail: BookLoan.Helpers.ApiServiceRetry[0]
loan-api_1      |       Error ApiServiceRetry(): System.Net.Http.HttpRequestException. Message: Cannot assign requested address. Inner Message: Cannot assign requested address

Testing Cross Container Service API Interaction

To test the cross-communication, we can make the following call to the loan API to retrieve a list of books to use within it’s report API.

curl -X GET "http://localhost:5120/api/Loan/GetLoanReportAllMembers" -H "accept: */*" -H 
"Authorization: Bearer xyz"

Output (truncated for brevity):

[
  {
    "status": "On Loan",
    "dateLoaned": "2020-12-26T00:00:00",
    "dateDue": "2021-12-09T00:00:00",
    "dateReturn": "0001-01-01T00:00:00",
    "borrower": "test@bookloan.com",
    "onShelf": false,
    "id": 2,
    "title": "The Alchemist (O Alquimista)",
    "author": "Paulo Coelho",
    "yearPublished": 1988,
    "genre": "fantasy",
    "edition": "8",
    "isbn": "112233",
    "location": "sydney",
  },
  {
    "status": "Overdue",
    "dateLoaned": "2020-12-15T00:00:00",
    "dateDue": "2020-12-29T00:00:00",
    "dateReturn": "0001-01-01T00:00:00",
    "borrower": "andy@adb.com.au",
    "onShelf": false,
    "id": 3,
    "title": "The Little Prince (Le Petit Prince)",
    "author": "Antoine de Saint-Exupéry",
    "yearPublished": 1943,
    "genre": "fantasy",
    "edition": "4",
    "isbn": "123338",
    "location": "sydney",
  },
...
]

The above discussion has shown how you can take existing API containers within a Docker image and configure them for communication via HTTP REST API calls.

In a future post I will show how to combine our knowledge and produce a basic image containing a microservice gateway.

That is all for today’s post.

I hope you found today’s post useful and informative.

Social media & sharing icons powered by UltimatelySocial