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.