Rewriting to Haskell–Configuration

Posted on March 23, 2020 by Riccardo

Rewriting to Haskell (Series)

Rewriting to Haskell–IntroRewriting to Haskell–Project SetupRewriting to Haskell–DeploymentRewriting to Haskell–Automatic FormattingRewriting to Haskell–ConfigurationRewriting to Haskell–Standing on the shoulders of RailsRewriting to Haskell–Making GHC More NitpickyRewriting to Haskell–TestingRewriting to Haskell–LintingRewriting to Haskell–Parsing Query ParamsRewriting to Haskell–Parsing Query Params, AgainRewriting to Haskell–Errors

Coming from Rails we are used to employing yaml files to configure a web application. This is why we decided to do the same with Servant. As a matter of fact, we now have a configuration.yml file:

database:
  username: stream
  database: stream_development
  password: ""

application:
  aws_s3_access_key: "ABCD1234"
  aws_s3_secret_key: "EFGH5678"
  aws_s3_region: us-east-1
  aws_s3_bucket_name: stream-demo-bucket

That is great for development but how can we run test against the test database? Turns out that the package we use to parse the yaml file allows the use of ENV variables:

database:
  username: stream
  database: _env:DATABASE:stream_development
  password: ""

application:
  aws_s3_access_key: "ABCD1234"
  aws_s3_secret_key: "EFGH5678"
  aws_s3_region: us-east-1
  aws_s3_bucket_name: stream-demo-bucket

That is, now we can just run DATABASE=stream_test stack test!

In the repository we actually keep a configuration.yml.example file and git ignore configuration.yml to avoid leaking credentials:

database:
  username: stream
  database: _env:DATABASE:stream_development
  password: ""

application:
  aws_s3_access_key: "REPLACE_ME"
  aws_s3_secret_key: "REPLACE_ME"
  aws_s3_region: us-east-1
  aws_s3_bucket_name: ll-stream-demo

For production we use Ansible (with Ansible Vault) to put in place the correct configuration.yml. Plus, we instruct Hapistrano to make that file available for each deployment:

linked_files:
  - haskell/configuration.yml

To read the configuration inside the Servant application we use loadYamlSettings from the yaml package:

loadYamlSettings
    :: FromJSON settings
    => [FilePath] -- ^ run time config files to use, earlier files have precedence
    -> [Value] -- ^ any other values to use, usually from compile time config. overridden by files
    -> EnvUsage
    -> IO settings

In other words, given a type settings that is an instance of FromJSON we can decode yaml files into a value of that type. And this is how we do it for Stream:

data Configuration
  = Configuration
      { configurationDatabaseUser :: String,
        configurationDatabaseDatabase :: String,
        configurationDatabasePassword :: String,
        configurationApplicationAwsS3AccessKey :: AccessKey,
        configurationApplicationAwsS3SecretKey :: SecretKey,
        configurationApplicationAwsS3Region :: Region,
        configurationApplicationAwsS3BucketName :: BucketName
      }

instance FromJSON Configuration where
  parseJSON (Object x) = do
    database <- x .: "database"
    application <- x .: "application"
    Configuration
      <$> database .: "username"
      <*> database .: "database"
      <*> database .: "password"
      <*> application .: "aws_s3_access_key"
      <*> application .: "aws_s3_secret_key"
      <*> application .: "aws_s3_region"
      <*> application .: "aws_s3_bucket_name"

loadConfiguration :: IO Configuration
loadConfiguration =
  loadYamlSettings ["./configuration.yml"] [] useEnv

PinkLetter

It's one of the selected few I follow every week – Mateusz

Tired of RELEARNING webdev stuff?

  • A 100+ page book with the best links I curated over the years
  • An email once a week full of timeless software wisdom
  • Your recommended weekly dose of pink
  • Try before you buy? Check the archives.