Home iOS & Swift Books Server-Side Swift with Vapor

37
Microservices, Part 2 Written by Tim Condon

In the previous chapter, you learned the basics of microservices and how to apply the architecture to the TIL application. In this chapter, you’ll learn about API gateways and how to make microservices accessible to clients. Finally, you’ll learn how to use Docker and Docker Compose to spin up the whole application.

The API gateway

The previous chapter introduced two microservices for the TIL application, one for acronyms and one for users. In a real application, you may have many more services for all different aspects of your application. It’s difficult for clients to integrate with an application made up of such a large number of microservices. Each client needs to know what each microservice does and the URL of each service. The client may even have to use different authentication methods for each service. A microservices architecture makes it hard to split a service into separate services. For example, moving authentication out of the users service in the TIL application would require an update to all clients.

One solution to this problem is the API gateway. An API gateway can aggregate requests from clients and distribute them to all required services. Additionally, an API gateway can retrieve results from multiple services and combine them into a single response.

Most cloud providers offer API gateway solutions to manage large numbers of microservices, but you can easily create your own. In this chapter, you’ll do just that.

Download the starter project for this chapter. The TILAppUsers and TILAppAcronyms projects are the same as the final projects from the previous chapter. There’s a new TILAppAPI project that contains the skeleton for the API gateway.

Starting the services

In Terminal, open three separate tabs. Ensure the MySQL, PostgreSQL and Redis Docker containers are running from the previous chapter. In Terminal, type the following:

docker ps

swift run
swift run
open Package.swift

Forwarding requests

In the TILAppAPI Xcode project, open UsersController.swift. Below boot(routes:) enter the following:

// 1
func getAllHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    return req.client.get("\(userServiceURL)/users")
}

// 2
func getHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    let id = try req.parameters.require("userID", as: UUID.self)
    return req.client.get("\(userServiceURL)/users/\(id)")
}

// 3
func createHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    return req.client.post("\(userServiceURL)/users") {
      createRequest in
      // 4
      try createRequest.content.encode(
        req.content.decode(CreateUserData.self))
  }
}
// 1
routeGroup.get(use: getAllHandler)
// 2
routeGroup.get(":userID", use: getHandler)
// 3
routeGroup.post(use: createHandler)
// 1
func getAllHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    return req.client.get("\(acronymsServiceURL)/")
}

// 2
func getHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    let id = 
      try req.parameters.require("acronymID", as: UUID.self)
    return req.client.get("\(acronymsServiceURL)/\(id)")
}
// 1
acronymsGroup.get(use: getAllHandler)
// 2
acronymsGroup.get(":acronymID", use: getHandler)

API Authentication

Logging in

Authentication for the API gateway works in exactly the same way as the microservices. First, you must allow a user to log in.

func loginHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    // 1
    return req.client.post("\(userServiceURL)/auth/login") {
      loginRequest in
        // 2
        guard let authHeader =
          req.headers[.authorization].first else {
            throw Abort(.unauthorized)
        }
        // 3
        loginRequest.headers.add(
          name: .authorization,
          value: authHeader)
    }
}
routeGroup.post("login", use: loginHandler)

Accessing protected routes

Back in Xcode, open AcronymsController.swift. Below getHandler(_:), create a new route handler to create an acronym:

func createHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    // 1
    return req.client.post("\(acronymsServiceURL)/") {
      createRequest in
        // 2
        guard let authHeader =
          req.headers[.authorization].first else {
            throw Abort(.unauthorized)
        }
        // 3
        createRequest.headers.add(
          name: .authorization,
          value: authHeader)
        // 4
        try createRequest.content.encode(
          req.content.decode(CreateAcronymData.self))
    }
}
acronymsGroup.post(use: createHandler)

func updateHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 1
    let acronymID = 
      try req.parameters.require("acronymID", as: UUID.self)
    // 2
    return req.client
      .put("\(acronymsServiceURL)/\(acronymID)") {
        updateRequest in
          // 3
          guard let authHeader =
            req.headers[.authorization].first else {
              throw Abort(.unauthorized)
          }
          // 4
          updateRequest.headers.add(
            name: .authorization,
            value: authHeader)
          // 5
          try updateRequest.content.encode(
            req.content.decode(CreateAcronymData.self))
    }
}

func deleteHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 6
    let acronymID = 
      try req.parameters.require("acronymID", as: UUID.self)
    // 7
    return req.client
      .delete("\(acronymsServiceURL)/\(acronymID)") {
        deleteRequest in
          // 8
          guard let authHeader =
            req.headers[.authorization].first else {
              throw Abort(.unauthorized)
          }
          // 9
          deleteRequest.headers.add(
            name: .authorization,
            value: authHeader)
    }
}
// 1
acronymsGroup.put(":acronymID", use: updateHandler)
// 2
acronymsGroup.delete(":acronymID", use: deleteHandler)

Handling relationships

In the previous chapter, you saw how relationships work with microservices. Getting relationships for different models is difficult for clients in an microservices architecture. You can use the API gateway to help simplify this.

Getting a user’s acronyms

In Xcode, open UsersController.swift. Below loginHandler(_:), add a new route handler to get a user’s acronyms:

func getAcronyms(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 1
    let userID = 
      try req.parameters.require("userID", as: UUID.self)
    // 2
    return req.client
      .get("\(acronymsServiceURL)/user/\(userID)")
}
routeGroup.get(":userID", "acronyms", use: getAcronyms)

Getting an acronym’s user

Getting a user’s acronyms looks the same as other requests in the microservice as the client knows the user’s ID. Getting the user for a particular acronym is more complicated. Open AcronymsController.swift and add a new route handler to do this below deleteHandler(_:):

func getUserHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 1
    let acronymID = 
      try req.parameters.require("acronymID", as: UUID.self)
    // 2
    return req
      .client
      .get("\(acronymsServiceURL)/\(acronymID)")
      .flatMapThrowing { response in
        // 3
        return try response.content.decode(Acronym.self)
      // 4
      }.flatMap { acronym in
        // 5
        return req
          .client
          .get("\(userServiceURL)/users/\(acronym.userID)")
    }
}
acronymsGroup.get(":acronymID", "user", use: getUserHandler)

Running everything in Docker

You now have three microservices that make up your TIL application. These microservices also require another three databases to work. If you’re developing a client application, or another microservice, there’s a lot to run to get started. You may also want to run everything in Linux to check your services deploy correctly. Like in Chapter 11, “Testing”, you’re going to use Docker Compose to run everything.

Injecting in service URLs

Currently the application hard codes the URLs for the different microservices to localhost. You must change this to run them in Docker Compose. Back in Xcode in TILAppAPI, open AcronymsController.swift. Replace the definitions of userServiceURL and acronymsServiceURL with the following:

let acronymsServiceURL: String
let userServiceURL: String

init(
  acronymsServiceHostname: String,
  userServiceHostname: String) {
    acronymsServiceURL =
      "http://\(acronymsServiceHostname):8082"
    userServiceURL = "http://\(userServiceHostname):8081"
}
let userServiceURL: String
let acronymsServiceURL: String

init(
  userServiceHostname: String,
  acronymsServiceHostname: String) {
    userServiceURL = "http://\(userServiceHostname):8081"
    acronymsServiceURL =
      "http://\(acronymsServiceHostname):8082"
}
let usersHostname: String
let acronymsHostname: String

// 1
if let users = Environment.get("USERS_HOSTNAME") {
  usersHostname = users
} else {
  usersHostname = "localhost"
}

// 2
if let acronyms = Environment.get("ACRONYMS_HOSTNAME") {
  acronymsHostname = acronyms
} else {
  acronymsHostname = "localhost"
}

// 3
try app.register(collection: UsersController(
  userServiceHostname: usersHostname,
  acronymsServiceHostname: acronymsHostname))
try app.register(collection: AcronymsController(
  acronymsServiceHostname: acronymsHostname,
  userServiceHostname: usersHostname))
let authHostname: String

init(authHostname: String) {
  self.authHostname = authHostname
}
"http://\(authHostname):8081/auth/authenticate"
let authHostname: String
// 1
if let host = Environment.get("AUTH_HOSTNAME") {
  authHostname = host
} else {
  authHostname = "localhost"
}
// 2
let authGroup = routes.grouped(
  UserAuthMiddleware(authHostname: authHostname))

The Docker Compose file

In the root directory containing all three projects, create a new file called docker-compose.yml and open it in an editor of your choice. Add the following to define the version and database services:

# 1
version: '3'
services:
  # 2
  postgres:
    image: "postgres"
    environment:
      - POSTGRES_DB=vapor_database
      - POSTGRES_USER=vapor_username
      - POSTGRES_PASSWORD=vapor_password
  # 3
  mysql:
    image: "mysql"
    environment:
      - MYSQL_USER=vapor_username
      - MYSQL_PASSWORD=vapor_password
      - MYSQL_DATABASE=vapor_database
      - MYSQL_RANDOM_ROOT_PASSWORD=yes
  # 4
  redis:
    image: "redis"
  # 1
  til-users:
    # 2
    depends_on:
      - postgres
      - redis
    # 3
    build:
      context: ./TILAppUsers
      dockerfile: Dockerfile
    # 4
    environment:
      - DATABASE_HOST=postgres
      - REDIS_HOSTNAME=redis
      - PORT=8081
      - ENVIRONMENT=production
  # 1
  til-acronyms:
    # 2
    depends_on:
      - mysql
      - til-users
    # 3
    build:
      context: ./TILAppAcronyms
      dockerfile: Dockerfile
    # 4
    environment:
      - DATABASE_HOST=mysql
      - PORT=8082
      - ENVIRONMENT=production
      - AUTH_HOSTNAME=til-users
  # 1
  til-api:
    # 2
    depends_on:
      - til-users
      - til-acronyms
    # 3
    ports:
      - "8080:8080"
    # 4
    build:
      context: ./TILAppAPI
      dockerfile: Dockerfile
    # 5
    environment:
      - USERS_HOSTNAME=til-users
      - ACRONYMS_HOSTNAME=til-acronyms
      - PORT=8080
      - ENVIRONMENT=production

Modifying Dockerfiles

Before you can run everything, you must change the Dockerfiles. Docker Compose starts the different containers in the requested order but won’t wait for them to be ready to accept connections. This causes issues if your Vapor application tries to connect to a database before the database is ready. In TILAppAcronyms, open Dockerfile and replace:

ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
ENTRYPOINT sleep 20 && \
  ./Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT

Running everything

You’re now ready to spin up your application in Docker Compose. In Terminal, in the directory containing docker-compose.yml, enter the following:

docker-compose up

Where to go from here?

In this chapter, you learned how to use Vapor to create an API gateway. This makes it simple for clients to interact with your different microservices. You learned how to send requests between different microservices and return single responses. You also learned how to use Docker Compose to build and start all the microservices and link them together.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.