Couper gets configured by a configuration file and as of the 1.9 release Couper has an option to handle multiple configuration files and configuration directories. Both can be passed as arguments and will be processed in order.
This enables you to split up a Couper configuration by content or even by environment. This is solved by merging the top-level declarations and replacing nested ones. Details are explained in our merge documentation.
Let's say we have some API endpoints which could be extracted out of a base configuration which just describes basics like files, SPA serving or default environment variables.
We start with a simple SPA configuration and some file serving:
server {
files {
document_root = "/htdocs"
}
spa {
bootstrap_file = "/htdocs/index.html"
paths = ["/", "/app"]
}
set_response_headers = {
x-service = env.SERVICE_NAME
}
}
defaults {
environment_variables = {
SERVICE_NAME = "example"
}
}
The API part describes a service and the related environment default values for e.g. local development:
server {
api "serviceA" {
base_path = "/api/v1/service-a"
endpoint "/**" {
proxy {
url = "http://${env.SERVICE_A_ORIGIN}/"
}
}
}
}
defaults {
environment_variables = {
SERVICE_A_ORIGIN = "http://localhost:8080"
SERVICE_NAME = "service-a"
}
}
The environment_variables
map within the defaults
block is handled differently during the merge process: Its values
are merged by their key instead of replacing the whole map. This allows to override or add specific environment defaults.
The following configuration would be the one Couper will finally load if we provide the directory with these two files.
The Couper container basically runs already with the argument -d /conf
inside the container which enables us to just mount
our ./conf-a
directory to /conf
. Also, the Couper welcome page already exists within /htdocs
.
docker run --pull -v ${PWD}/conf-a:/conf -p 8080:8080 coupergateway/couper
server {
files {
document_root = "/htdocs"
}
spa {
bootstrap_file = "/htdocs/index.html"
}
set_response_headers = {
x-service = env.SERVICE_NAME
}
api "serviceA" {
base_path = "/api/v1/service-a"
endpoint "/**" {
proxy {
url = "http://${env.SERVICE_A_ORIGIN}/"
}
}
}
}
defaults {
environment_variables = {
SERVICE_A_ORIGIN = "localhost:8080"
SERVICE_NAME = "service-a"
}
}
We can verify the running configuration with calls to /
, /app
or /api/v1/service-a
endpoints:
curl -i http://localhost:8080/api/v1/service-a
# output
# HTTP/1.1 200 OK
# Couper-Request-Id: c9haiurm8vfs73bi7ku0
# Server: couper.io
# X-Service: service-a
The SERVICE_NAME
environment value example
got also replaced with service-a
.
Another possible case would be an access-control which may differ between a stage and production environment due to their complexity. Let's use this case to tailor a possible configuration setup.
Basically we will have some base configuration again. This can be a single file or a directory. To keep things simple, we will add all environment related configuration files during the container build process. If you want to ship a specific environment-based configuration file this conditional must be solved by your continuous-integration setup. If you have any questions, feel free to open a discussion.
We simply want to protect our file server. It's recommended to protect even your base with an unrealizable
access_control
since the related CI job could not work properly which may result in an unprotected production environment.
This is the reason why we will reference an undefined access-control. Couper won't start up in this case, and you may get
instant feedback from your deployment jobs.
For development purposes you could add a configuration file with an empty access_control
list to negate the undefined
reference.
server {
access_control = ["undefined"]
files {
document_root = "/htdocs"
}
}
FROM coupergateway/couper:latest
# copy base configuration
COPY *.hcl /conf/
# Switch to -f argument instead of -d
CMD [ "run", "-f", "/conf/couper.hcl" ]
So building the container with:
docker build -t couper-env-example -f Dockerfile .
and run:
docker run -p 8080:8080 couper-env-example
# output:
# {"level":"error","message":"accessControl is not defined: undefined"}
results in our expected error.
So let's build the image again and this time add the stage.hcl
to the build process:
server {
access_control = ["stage-ba"]
}
definitions {
basic_auth "stage-ba" {
password = env.STAGE_BA_PASSWD
}
}
defaults {
environment_variables = {
STAGE_BA_PASSWD = "test"
}
}
docker build -t couper-env-example -f Dockerfile .
Now we will run our image again and just combine the base (couper.hcl
) with our stage.hcl
file:
docker run -p 8080:8080 couper-env-example run -f /conf/couper.hcl -f /conf/stage.hcl
Just visit http://localhost:8080/ in your browser, so you will see a basic-auth prompt.
Your Stage environment is protected now. Enter test
as value into the password field, and you will see the Couper welcome page.
The advantage to switch to a complete other access_control
mechanism could be very handy if you have to use a specific SSO or other provider
to handle the permissions.
In this example, we keep it simpler and just switch from a very simple basic_auth
to a slightly more sophisticated one using htpasswd_file
instead of just a password
.
server {
access_control = ["production-ba"]
}
definitions {
basic_auth "production-ba" {
htpasswd_file = ".htpasswd"
}
}
Currently, there are two valid users with their passwords:
- alice with password ecila
- bob with password B06
We will build the docker image again to copy the newest files.
docker build -t couper-env-example -f Dockerfile .
Now run our image again:
docker run -p 8080:8080 couper-env-example run -f /conf/couper.hcl -f /conf/production.hcl
If we visit our endpoint again and enter one of the two valid user credentials, then we will get the already known welcome page again.
In a more real-world scenario, we would rather switch to a jwt
access control e.g. using jwks_url
to reference an identity provider's JWKS resource.