Tutorial - Configuration
What you’ll learn
- How Granitic defines configuration
- How to define different configuration files for different environments
- How JSON configuration files are merged together
Prerequisites
- Follow the Granitic installation instructions
- Read the before you start tutorial
- Either have completed tutorial 1 or clone the tutorial repo
and navigate to
json/002/recordstore
in your terminal.
Related GoDoc
https://godoc.org/github.com/graniticio/granitic/v2/config
Configuration
Granitic applications use JSON files to store configuration which is loaded when the application starts. Any valid JSON file is a valid configuration file:
{
"exampleBool": true,
"exampleNumber": 1.0,
"anotherNumber": 25,
"exampleString": "value",
"stringArray": ["a","b", "c"],
"numberArray": [1,2,3],
"exampleObject": {
"anotherString": "anotherValue"
}
}
Granitic uses the term config path to express the fully-qualified name of a variable in configuration. In the
above example exampleBool
is a config path and so is exampleObject.anotherString
Configuring a Granitic application
When a Granitic application starts, it looks for configuration files before doing anything else. The location of these
files is specified using the (-c
) command line parameter. The default value for -c
is config
, relative to the working directory from which you start your application, so in previous examples
running:
recordstore
is the equivalent of running:
recordstore -c config
The value of the -c parameter is expected to be a comma separated list of:
- Relative or absolute paths to JSON files
- Relative or absolute paths to directories
- Absolute URLs of HTTP or HTTPS resources
Starting your application with a specific config file
Support for directories and remote URIs will be discussed in a later tutorial. For now, open a terminal, navigate to your
recordstore folder
and run
grnc-bind && go build ./recordstore -c config/base.json
This runs your recordstore application, specifically stating that a single configuration file
config/config.json
should be used. Stop the application with CTRL+C
Injecting configuration into your components
The Go IoC container automatically injects configuration values into your components. Modify the file artist/get.go
so it looks like:
package artist
import (
"context"
"github.com/graniticio/granitic/v2/ws"
)
type GetLogic struct{
EnvLabel string
}
func (gl *GetLogic) Process(ctx context.Context, req *ws.Request, res *ws.Response) {
an := fmt.Sprintf("Hello, %s!", gl.EnvLabel)
res.Body = Info{
Name: an,
}
}
type Info struct {
Name string
}
We’ve added a new field to the struct, EnvLabel
and have changed our Hello, World! message to include the
value of that field. As the name suggests, this will vary depending on which environment our code is running in.
Now modify the artistHandler
definition in your comp-def/common.json
file so it looks like:
"artistHandler": {
"type": "handler.WsHandler",
"HTTPMethod": "GET",
"Logic": {
"type": "artist.GetLogic",
"EnvLabel": "$environment.label"
},
"PathPattern": "^/artist"
}
The value starting with the dollar symbol ($
) is a configuration promise - you are promising your application that Granitic will
find a value in configuration with the config path environment.label
and inject it into the artistHandler.Logic
component’s EnvLabel
field at application startup.
If you now run:
grnc-bind && go build && ./recordstore -c config/base.json
you will see an error message similar to:
07/Aug/2023:11:40:47 Z FATAL [grncContainer] No value found at environment.label
Granitic adopts a fail-fast model for configuration and will not allow an application to start if it relies on configuration
that is undefined. Rather than just adding the expected configuration to config/base.json
, we’ll
use this opportunity to show how configuration files can be used to make deploying your application in multiple locations
more straightforward.
Default values
It is sometimes desirable to be able to define a default value to use if no config exists instead of failing. This can be achieved
with default values. In your comp-def/common.json
file change the line:
"EnvLabel": "$environment.label"
to
"EnvLabel": "$environment.label(DEV)"
and trying re-running
grnc-bind && go build && ./recordstore -c config/base.json
and visit http://localhost:8080/artist You should see the response:
{
"Name": "Hello, DEV!"
}
It is strongly recommended that you do not store sensitive information such as passwords in default values as default values are compiled into your application executable.
Multiple configuration files
Only the simplest applications will use a single configuration file. Complex applications will split their configuration into multiple files to improve readability and maintainability, but all applications will want to separate the configuration that is common to each deployment of an application from the configuration that changes across different deployment environments and from one instance of an application to another.
Generally you would expect only your application’s common (or ‘base’) configuration to be checked into source control with environment specific configuration being generated dynamically by your build or configuration management system,
The rest of this tutorial simulates deploying an instance of a web-service across multiple environments and then multiple instances running on a single server.
Change your config/base.json
file so it looks like:
{
"Facilities": {
"HTTPServer": true,
"JSONWs": true
},
"environment": {
"label": "TEST"
}
}
Then create a new file:
/tmp/prod.json
{
"environment": {
"label": "PROD"
}
}
Configuration merging
We now have a value for the config path `environment.label’ defined in three places.
Default value: DEV
config/base.json
: TEST
/tmp/prod.json
: PROD
As you’ve only changed configuration files, you don’t need to rebuild. You can just run:
./recordstore -c config,/tmp/prod.json
And visit http://localhost:8080/artist
You’ll see the message has changed to Hello, PROD!
. This is due to Granitic’s process of configuration merging where
multiple configuration files are merged together to provide a single view of application configuration.
The order in which you specify your configuration files when starting your application is important. Any config path that is defined in multiple files is replaced with the rightmost definition. Try swapping the order of the configuration files and see what happens.
Using configuration to run multiple instances
To maximise use of resources, you may want to run multiple instances of a web service on a single host. This means each instance must have a different HTTP port assigned to it.
Create two new files:
/tmp/instance-1.json
{
"HTTPServer": {
"Port": 8081
}
}
/tmp/instance-2.json
{
"HTTPServer": {
"Port": 8082
}
}
You can now run:
./recordstore -c config,/tmp/instance-1.json
and in a separate terminal, navigate to the same folder and run:
./recordstore -c config,/tmp/instance-2.json
And you now have two separate instances of your recordstore
application running and listening on different ports.
Facility configuration
In previous examples, you will have noticed that the default HTTP port for Granitic applications is 8080. This is not
hard-coded, it is defined in another configuration file that is included with Granitic itself called a
facility configuration file
, which can be found under the facility/config
folder of your Granitic installation
or here on GitHub.
These files are serialised into your application’s executable so you don’t need to have Granitic installed on the environment you are running your application on. During application startup, Granitic merges this serialised view with your application’s configuration files .
Your application’s configuration files take precedence over the built-in facility configuration, so in this example the
value of HTTPServer.Port
in facility/config/httpserver.json
is replaced with the value in your /tmp/instance-1.json
or /tmp/instance-1.json
file.
Merging rules
The rules by which configuration two files are merged together are specified in the Granitic GoDoc, but the following example illustrates the key rules (note the configuration items are an illustration and do not relate to any specific Granitic features)
a.json
{
"server": {
"name": "localhost",
"network": {
"interfaces": ["192.168.0.2","127.0.0.1"],
"sslOnly": false,
"seed": 1.98311
},
"security":{
"mode": 0
}
}
}
b.json
{
"server": {
"name": "testserver",
"network": {
"interfaces": ["10.123.0.5"],
"certPath": "/tmp/cert.key",
"sslOnly": true
},
"metrics":{
"enabled": true
}
}
}
merged together becomes:
{
"server": {
"name": "testserver",
"network": {
"interfaces": ["10.123.0.5"],
"certPath": "/tmp/cert.key",
"sslOnly" true
},
"security":{
"mode": 0
},
"metrics":{
"enabled": true
}
}
}
Files are merged from left to right, the final value of a configuration item present in more than one file is the one defined in the rightmost file. The behaviour that might be most unexpected is how arrays are handled - the contents of arrays are not merged together, but replaced.
Recap
- Granitic applications store configuration in JSON files which are loaded when an application starts.
- Use multiple configuration files to organise your application and to support multiple environments and deployments.
- Configuration is injected into your Go objects by using config promises in your application’s component definition file. you change your component definitions.
- All of your configuration files are merged together with Granitic’s built-in facility configuration files to provide a single view of configuration.
Next
The next tutorial covers application logging