Learn how to externalize configuration in Clojure applications using environment variables, configuration files, and tools like `env` and `cprop`, adhering to the 12-factor app principles.
In modern software development, the separation of configuration from code is a crucial practice that enhances flexibility, security, and scalability. This principle is prominently advocated by the 12-factor app methodology, which emphasizes storing configuration in the environment. This approach not only simplifies deployment across different environments but also ensures that sensitive information is kept out of the source code. In this section, we will explore various strategies and tools for externalizing configuration in Clojure applications, including the use of environment variables, configuration files, and libraries like env
and cprop
.
Externalizing configuration refers to the practice of separating configuration data from the application code. This separation allows for:
Environment variables are a common method for externalizing configuration. They are supported by all operating systems and provide a straightforward way to pass configuration data to applications.
Environment variables can be set in various ways depending on the operating system and deployment environment:
Unix/Linux/MacOS: Use the export
command in the terminal or set variables in shell configuration files like .bashrc
or .zshrc
.
export DATABASE_URL="jdbc:postgresql://localhost:5432/mydb"
export API_KEY="your-api-key"
Windows: Use the set
command in the command prompt or configure them in the System Properties.
set DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
set API_KEY=your-api-key
Docker: Use the -e
flag with docker run
or define them in a Dockerfile or Docker Compose file.
environment:
- DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
- API_KEY=your-api-key
In Clojure, environment variables can be accessed using the System/getenv
function. Here’s an example:
(defn get-config []
{:database-url (System/getenv "DATABASE_URL")
:api-key (System/getenv "API_KEY")})
(def config (get-config))
(println "Database URL:" (:database-url config))
(println "API Key:" (:api-key config))
While environment variables are suitable for simple configurations, more complex applications may benefit from using configuration files. Configuration files can be written in various formats such as EDN, JSON, YAML, or even plain text.
EDN (Extensible Data Notation) is a native format for Clojure, making it an excellent choice for configuration files. Here’s an example of an EDN configuration file:
;; config.edn
{:database-url "jdbc:postgresql://localhost:5432/mydb"
:api-key "your-api-key"
:log-level :info}
To load and parse this file in Clojure, you can use the clojure.edn/read-string
function along with slurp
:
(require '[clojure.edn :as edn])
(defn load-config [file-path]
(edn/read-string (slurp file-path)))
(def config (load-config "config.edn"))
(println "Configuration:" config)
For teams that prefer JSON or YAML, Clojure provides libraries like cheshire
for JSON and clj-yaml
for YAML. Here’s how you can load a JSON configuration file:
(require '[cheshire.core :as json])
(defn load-json-config [file-path]
(json/parse-string (slurp file-path) true))
(def config (load-json-config "config.json"))
(println "Configuration:" config)
Several libraries in the Clojure ecosystem facilitate configuration management, making it easier to handle environment variables and configuration files.
env
Library§The env
library provides a simple way to manage environment variables in Clojure applications. It allows you to define default values and types for your environment variables.
To use env
, add it to your project.clj
or deps.edn
:
;; project.clj
:dependencies [[environ "1.2.0"]]
Here’s an example of how to use env
:
(require '[environ.core :refer [env]])
(def config
{:database-url (env :database-url "jdbc:postgresql://localhost:5432/defaultdb")
:api-key (env :api-key "default-api-key")
:log-level (keyword (env :log-level "info"))})
(println "Configuration:" config)
cprop
Library§The cprop
library is another powerful tool for configuration management. It supports merging configurations from multiple sources, including environment variables, system properties, and configuration files.
To use cprop
, add it to your project.clj
or deps.edn
:
;; project.clj
:dependencies [[cprop "0.1.17"]]
Here’s an example of how to use cprop
:
(require '[cprop.core :refer [load-config]])
(def config (load-config :merge
[(System/getenv)
(System/getProperties)
"config.edn"]))
(println "Configuration:" config)
Keep Configuration Out of Code: Avoid hardcoding configuration values in your application code. Use environment variables or configuration files instead.
Use Environment Variables for Sensitive Data: Store sensitive information like API keys and passwords in environment variables to keep them out of version control.
Provide Default Values: When using environment variables, provide sensible default values to ensure your application can run in different environments without manual intervention.
Document Configuration Requirements: Clearly document the required configuration variables and their expected values to assist developers and operators in setting up the application.
Use a Consistent Format: Choose a configuration format (e.g., EDN, JSON, YAML) that suits your team’s needs and stick to it for consistency.
Validate Configuration: Implement validation logic to ensure that configuration values meet the expected criteria before using them in your application.
Let’s walk through a practical example of configuring a Clojure web application using the principles and tools discussed.
Suppose we have a web application that requires the following configuration:
Create an EDN configuration file named config.edn
:
;; config.edn
{:database-url "jdbc:postgresql://localhost:5432/mydb"
:api-key "your-api-key"
:log-level :info
:port 3000}
cprop
§Use the cprop
library to load the configuration, allowing overrides from environment variables:
(require '[cprop.core :refer [load-config]])
(def config (load-config :merge
[(System/getenv)
"config.edn"]))
(println "Configuration:" config)
Use the loaded configuration in your application code:
(defn start-server []
(let [{:keys [database-url api-key log-level port]} config]
(println "Starting server with configuration:")
(println "Database URL:" database-url)
(println "API Key:" api-key)
(println "Log Level:" log-level)
(println "Port:" port)
;; Initialize and start the web server here
))
(start-server)
When deploying the application, set the necessary environment variables to override the default values in config.edn
:
export DATABASE_URL="jdbc:postgresql://production-db:5432/proddb"
export API_KEY="production-api-key"
export LOG_LEVEL="warn"
export PORT=8080
Externalizing configuration is a best practice that enhances the flexibility, security, and scalability of Clojure applications. By leveraging environment variables, configuration files, and tools like env
and cprop
, developers can build robust applications that adapt seamlessly to different environments. Adhering to the 12-factor app principles ensures that your application remains maintainable and easy to deploy across various stages of development and production.