The DRY principle (Don’t Repeat Yourself) is one of the most important principles in software development. It emphasizes reducing repetition of code by using abstractions or reusable components. In microservices architecture, where API gateways like Apache APISIX are central to managing APIs, maintaining DRY configuration becomes essential to reduce maintenance overhead, increase readability, and avoid potential misconfigurations.

Apache APISIX is an open-source, dynamic, high-performance API gateway for microservices. It allows you to manage traffic, authenticate requests, apply rate-limiting, and much more through flexible configuration. However, configuring each service can become cumbersome if there’s too much repetition in the configuration file.

In this article, we’ll explore how to apply the DRY principle effectively when configuring Apache APISIX, along with code examples to showcase these principles in practice.

Why Applying DRY to Apache APISIX Configuration is Important

When dealing with multiple services and API routes, you might face scenarios where the configuration logic repeats. This can lead to:

  • Complexity: Repeated configuration makes the configuration file long and harder to maintain.
  • Increased risk of errors: If one part of the configuration changes, you might forget to update all instances of it.
  • Wasted time: Rewriting or copying the same configuration is inefficient, especially for large-scale applications.

Applying DRY principles to the configuration not only streamlines the process but also minimizes the likelihood of misconfigurations, especially when managing a large number of APIs.

Strategies for Applying DRY in Apache APISIX

There are several techniques that can help you maintain DRY configurations in Apache APISIX:

Use of Route Inheritance

In Apache APISIX, you can leverage route inheritance to apply common configurations (such as plugins) to multiple routes. This is done by defining a parent route with common configurations, which subsequent routes inherit from.

Example of Route Inheritance

json

{
"id": "parent-route",
"uris": ["/api/v1/*"],
"plugins": {
"key-auth": {
"key": "my-secret-key"
},
"rate-limit": {
"rate": 100,
"burst": 50
}
}
}

In this example, we define a parent-route for all APIs under /api/v1/*. It includes configurations for key-auth and rate-limiting plugins. Now, any specific API route under /api/v1/ can inherit from this configuration without redefining these plugins.

To apply a child route, we can specify the route for specific endpoints, like so:

json

{
"id": "child-route-1",
"uris": ["/api/v1/users"],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:8080": 1
}
}
}

Here, the child route /api/v1/users inherits the plugins defined in the parent route, without repeating them.

Global Plugins Configuration

Another way to reduce redundancy is by using global plugins. Global plugins are applied to all routes automatically without specifying them individually.

Example of Global Plugins

json

{
"plugins": {
"prometheus": {},
"key-auth": {
"key": "default-key"
}
}
}

Global plugins allow you to define a standard configuration for all routes. For example, applying prometheus for monitoring or key-auth for authentication across all APIs.

If a specific route needs different settings, it can override the global configuration for that plugin.

Using Upstreams to Centralize Service Definitions

An upstream is an abstraction over a set of backend services. Instead of defining the backend service repeatedly for each route, you can define it once in the upstream section and reference it across multiple routes.

Example of Upstream Configuration

json

{
"id": "my-upstream",
"type": "roundrobin",
"nodes": {
"192.168.1.1:80": 1,
"192.168.1.2:80": 1
}
}

You can now reference this upstream in multiple routes:

json

{
"id": "route-1",
"uris": ["/service1"],
"upstream_id": "my-upstream"
}
{
“id”: “route-2”,
“uris”: [“/service2”],
“upstream_id”: “my-upstream”
}

Using upstreams allows you to avoid repeating backend service definitions, making your configurations much cleaner and easier to manage.

Reusable Plugin Configurations

Plugins are often reused across different routes. Instead of configuring plugins with the same settings for each route, you can define reusable plugin configurations. APISIX allows you to define plugin configurations and apply them to various routes.

Example of Plugin Configuration

json

{
"id": "common-plugins",
"plugins": {
"key-auth": {
"key": "my-secret-key"
},
"rate-limit": {
"rate": 50,
"burst": 10
}
}
}

Once the plugin configuration is defined, it can be referenced by routes:

json

{
"id": "route-3",
"uris": ["/api/v2/orders"],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:8081": 1
}
},
"plugin_id": "common-plugins"
}

By centralizing plugin configuration, you ensure that common settings are not repeated across the configuration files.

Environment-Specific Variables

Another way to keep your configuration DRY is by using environment variables. This helps in avoiding repetition of sensitive information like API keys, database connection strings, and host URLs.

Example of Using Environment Variables

yaml

apisix:
ssl:
key: $SSL_KEY
cert: $SSL_CERT

In this case, the SSL key and certificate are fetched from environment variables. You can reuse these variables across multiple parts of the configuration.

Lua Scripts for Complex Configurations

For more complex logic, Apache APISIX supports Lua scripts. Lua scripts can be used for tasks like dynamic routing, request/response transformation, and rate-limiting logic. By placing the common logic in a Lua script, you can avoid rewriting the same logic in multiple places.

Example of Lua Script for Rate-Limiting

lua

local function limit_rate()
local rate_limit = 100
local burst = 50
— Rate limiting logic here
— Return rate limit status based on the input
end

You can reuse this script in multiple routes by loading it in the configuration.

json

{
"id": "route-4",
"uris": ["/api/v2/inventory"],
"plugins": {
"lua-resty-limit": {
"script": "limit_rate"
}
}
}

Parameterized Routing

If the only thing changing across routes is a small parameter like the API version or service ID, you can use parameterized routes to define a template route that matches multiple URIs dynamically.

Example of Parameterized Routing

json

{
"id": "versioned-route",
"uris": ["/api/v{version}/{service}"],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:8082": 1
}
},
"plugins": {
"key-auth": {
"key": "my-dynamic-key"
}
}
}

This parameterized route applies the same logic across multiple versions of your API, reducing the need to define separate routes for each version or service.

Conclusion

Applying the DRY principle to Apache APISIX configuration can significantly enhance the maintainability, scalability, and readability of your API gateway configurations. By leveraging techniques like route inheritance, global plugins, upstreams, reusable plugin configurations, environment variables, and Lua scripting, you can create a more efficient, error-resistant, and easily maintainable configuration structure.

In large-scale deployments, avoiding repetition is crucial to maintain agility. As configurations grow, a DRY approach prevents duplication, reduces human errors, and improves manageability. Embracing DRY principles in Apache APISIX not only results in cleaner configurations but also ensures smoother scaling and easier adjustments when expanding your services.