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 image with multiple container services using docker compose.

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

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 show 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.

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"

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

Within a Docker image, each container network is accessible by it’s 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 the startup.cs we can retrieve the environment parameter and value, then set the application configuration that corresponds to the catalog API URL.

To obtain the Catalog container URL from the environment we use the Options pattern (see previous post on how to use the options 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

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": "[email protected].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": "[email protected]",
    "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",
  },
...
]

That is all for today’s post.

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

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

Social media & sharing icons powered by UltimatelySocial