Generalized networking rules
Closed this issue · 3 comments
Right now the only networking rules that are configurable are the subdomains at which services are made accessible. Architect generates an API gateway to fulfill these traffic rules and act as the external load balancer for the application. These subdomains get configured from environment config files using the ingress.subdomain
field as follows. The below will deploy the 10 services included in the hipster-shop demo along with an API gateway to fulfill traffic from shop.<env-name>.<acct-name>.arc.domains
to the frontend webapp:
services:
hipster-shop-demo/frontend:latest:
ingress:
subdomain: shop
This configuration, while useful to expose services safely to the outside world, does not provide as robust configurability as we would like for traffic between consumers and services. For this reason, we'd like to create a more generalized approach to configuring networking logic between services internally and externally.
Proposal
Instead of subdomains
inside of service interfaces, we'd like to let operators setup N gateways that can enforce networking configuration rules both from external and internal sources. Some of the goals of this feature include:
- Allow operators to setup gateways that broker and apply rules to internal traffic (much like a service mesh)
- Allow gateways to be configured with advanced configuration options (e.g. rate limiting, circuit breaking, etc)
- Allow operators to apply different gateways to subsets of the services inside the environment (e.g. legacy vs. new code paths, and different protection rules)
- Allow operators to setup gateways that route to the same service for different purposes (e.g. different product lines need different auth).
# Environment config file
gateways:
# The primary external gateway that makes architect/cloud available
- external: true
host_rewrites:
- shop.hipster.com
# Every environment is still assigned an Architect domain that can be used for routing.
- shop.${architect.environment.domain}
services:
- hipster-shop-demo/frontend:*
# Operators can provide their own SSL certs or configure rules for cert generation
tls:
certificate: file:./local/cert
private_key:
value_from:
vault: architect
key: ssl/architect.io
# Operators can configure how additional rules for how connections are to be handled
http:
timeout: 10s
retries: 3
max_connections: 100
# An internal service mesh that applies to all inter-process traffic
- external: false
host_rewrites:
- ${service.name}.private.hipster.com
services:
- *
services:
hipster-shop-demo/frontend: latest
Since this issue was created, we've decided to change the way a component spec is setup to allow it to include N services instead of forcibly holding a 1:1 relationship between a component and a service. See #224 for details.
This change also forces us to consider gateways and component networking in a more thoughtful way. Since components can contain N services, gateway and interface logic must be described by component creators to dictate which services can be accessed by others and the routing logic needed to resolve them. This change opens a great opportunity for us to add 1st class support for generalized API gateway rules where component creators can control the routing rules relevant to their own services.
Proposal
In order to democratize API gateways in our framework, we want to allow components to be registered with basic routing rules that dictate which services are available for consumption and how they can be resolved. This includes the registration of interfaces (each of which will be allocated a unique host/port combo), and the assertion of rules that map the interface to services inside the component. A simple example is as follows:
name: architect/test
parameters:
ROOT_DB_USER:
default: architect
ROOT_DB_PASS:
required: true
API_DB_USER:
default: ${ parameters.ROOT_DB_USER }
API_DB_PASS:
default: ${ parameters.ROOT_DB_PASS }
API_DB_NAME:
default: architect
interfaces:
public:
backend:
host: ${ services.backend.host }
port: ${ services.backend.ports.public }
admin:
backend:
host: ${ services.backend.host }
port: ${ services.backend.ports.admin }
services:
db:
image: postgres:11
ports:
postgres: 5432
environment:
POSTGRES_USER: ${ parameters.ROOT_DB_USER }
POSTGRES_PASSWORD: ${ parameters.ROOT_DB_PASS }
backend:
image: architect/backend:latest
ports:
public: 8080
admin: 8081
environment:
DB_ADDR: postgres://${ parameters.API_DB_USER }:${ parameters.API_DB_PASS }@${ services.db.interfaces.postgres.host }:${ services.db.interfaces.postgres.port }/${ parameters.API_DB_NAME }
We've outlined two interfaces for our component in the above example: 1) public and 2) admin. Each of these interfaces represent independently resolvable addresses, and each routes traffic to one of the two ports on the API service in our component. What is notably left off is an interface that connects to the component's DB. Since we don't want anyone else to touch it, we simply neglect to create an interface that maps to it and we've successfully ensured that the service is privately allocated and only accessible to my component.
Working with dependencies
To extend this example, let's create a service that depends on the component above:
name: architect/frontend
interfaces:
webapp:
backend:
host: ${ services.webapp.host }
port: ${ services.webapp.ports.web }
dependencies:
architect/test: latest
services:
webapp:
image: architect/frontend:latest
ports:
web: 8080
environment:
API_ADDR: ${ dependencies['architect/test'].interfaces.public.url }
The above example cites a dependency on our architect/test
component described earlier, but notably only connects to the public
interface. Since there is only a reference to one of the two interfaces of architect/test
, we only create a network policy allowing traffic between the frontend and the public
interface of the test service.
As part of our work on our component spec, we've made another round of changes to the way interfaces are described so as to better conform to our expression syntax:
name: architect/test
parameters:
ROOT_DB_USER:
default: architect
ROOT_DB_PASS:
required: true
API_DB_USER:
default: ${ parameters.ROOT_DB_USER }
API_DB_PASS:
default: ${ parameters.ROOT_DB_PASS }
API_DB_NAME:
default: architect
interfaces:
public: ${ services.backend.interfaces.public.url }
admin:
description: Exposes the admin interface to upstream traffic
url: ${ services.backend.interfaces.admin.url }
services:
db:
image: postgres:11
interfaces:
postgres:
port: 5432
protocol: postgres
environment:
POSTGRES_USER: ${ parameters.ROOT_DB_USER }
POSTGRES_PASSWORD: ${ parameters.ROOT_DB_PASS }
backend:
image: architect/backend:latest
interfaces:
public: 8080
admin:
port: 8081
protocol: http
environment:
DB_ADDR: ${ services.db.interfaces.postgres.url }/${ parameters.API_DB_NAME }
DB_USER: ${ parameters.API_DB_USER }
DB_PASS: ${ parameters.API_DB_PASS }