Skip to content

Commit

Permalink
feat: add config "healthcheck-path" to respond 200 on
Browse files Browse the repository at this point in the history
AWS LB TG healthcheck cannot currently be made to send any custom Host
header. It always sends the target's (keter's) IP address in there.

Together with vhosting, this makes it impossible to healthcheck
individual webapps running under keter.

Have a partial remedy with a /keter-health endpoint, that if enabled
always responds with status 200.
  • Loading branch information
ulidtko committed Nov 10, 2023
1 parent cbac18c commit dac66e1
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 3 deletions.
5 changes: 3 additions & 2 deletions etc/keter-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ listeners:
session: true

# User to run applications as

# setuid: ubuntu

# Get the user's IP address from x-forwarded-for. Useful when sitting behind a
# load balancer like Amazon ELB.

# ip-from-header: true

# If set, this path will respond 200 OK to requests on any vhost
# healthcheck-path: /keter-health

# Control the port numbers assigned via APPROOT
# external-http-port: 8080
# external-https-port: 450
Expand Down
1 change: 1 addition & 0 deletions keter.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ library
Keter.Yaml.FilePath

other-modules: Keter.Aeson.KeyHelper
Paths_keter
ghc-options: -Wall
c-sources: cbits/process-tracker.c
hs-source-dirs: src
Expand Down
6 changes: 5 additions & 1 deletion src/Keter/Config/V10.hs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ data KeterConfig = KeterConfig
, kconfigProxyException :: !(Maybe F.FilePath)

, kconfigRotateLogs :: !Bool
, kconfigHealthcheckPath :: !(Maybe Text)
}

instance ToCurrent KeterConfig where
Expand All @@ -134,6 +135,7 @@ instance ToCurrent KeterConfig where
, kconfigMissingHostResponse = Nothing
, kconfigProxyException = Nothing
, kconfigRotateLogs = True
, kconfigHealthcheckPath = Nothing
}
where
getSSL Nothing = V.empty
Expand Down Expand Up @@ -162,6 +164,7 @@ defaultKeterConfig = KeterConfig
, kconfigMissingHostResponse = Nothing
, kconfigProxyException = Nothing
, kconfigRotateLogs = True
, kconfigHealthcheckPath = Nothing
}

instance ParseYamlFile KeterConfig where
Expand All @@ -187,6 +190,7 @@ instance ParseYamlFile KeterConfig where
<*> o .:? "missing-host-response-file"
<*> o .:? "proxy-exception-response-file"
<*> o .:? "rotate-logs" .!= True
<*> o .:? "app-crash-hook"

-- | Whether we should force redirect to HTTPS routes.
type RequiresSecure = Bool
Expand Down Expand Up @@ -317,7 +321,7 @@ instance ToCurrent RedirectConfig where

instance ParseYamlFile RedirectConfig where
parseYamlFile _ = withObject "RedirectConfig" $ \o -> RedirectConfig
<$> (Set.map CI.mk <$> ((o .: "hosts" <|> (Set.singleton <$> (o .: "host")))))
<$> (Set.map CI.mk <$> (o .: "hosts" <|> Set.singleton <$> o .: "host"))
<*> o .:? "status" .!= 303
<*> o .: "actions"
<*> o .:? "ssl" .!= SSLFalse
Expand Down
17 changes: 17 additions & 0 deletions src/Keter/Proxy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import System.FilePath (FilePath)
import Control.Monad.Logger
import Control.Exception (SomeException)
import Network.HTTP.Types (mkStatus,
status200,
status301, status302,
status303, status307,
status404, status502)
Expand All @@ -76,6 +77,9 @@ import qualified Network.TLS as TLS
import qualified System.Directory as Dir
import Keter.Context

import Data.Version (showVersion)
import qualified Paths_keter as Pkg

#if !MIN_VERSION_http_reverse_proxy(0,6,0)
defaultWaiProxySettings = def
#endif
Expand All @@ -91,6 +95,7 @@ data ProxySettings = MkProxySettings
, psManager :: !Manager
, psIpFromHeader :: Bool
, psConnectionTimeBound :: Int
, psHealthcheckPath :: !(Maybe ByteString)
, psUnknownHost :: ByteString -> ByteString
, psMissingHost :: ByteString
, psProxyException :: ByteString
Expand All @@ -107,6 +112,7 @@ makeSettings hostman = do
-- configuration option is in milliseconds
let psConnectionTimeBound = kconfigConnectionTimeBound * 1000
let psIpFromHeader = kconfigIpFromHeader
let psHealthcheckPath = encodeUtf8 <$> kconfigHealthcheckPath
pure $ MkProxySettings{..}
where
psHostLookup = HostMan.lookupAction hostman . CI.mk
Expand Down Expand Up @@ -178,6 +184,10 @@ withClient isSecure = do


getDest :: ProxySettings -> Wai.Request -> IO (LocalWaiProxySettings, WaiProxyResponse)
-- respond to healthckecks, regardless of Host header value and presence
getDest MkProxySettings{..} req | psHealthcheckPath == Just (Wai.rawPathInfo req)
= return (defaultLocalWaiProxySettings, WPRResponse healthcheckResponse)
-- inspect Host header to determine which App to proxy to
getDest cfg@MkProxySettings{..} req =
case Wai.requestHeaderHost req of
Nothing -> do
Expand Down Expand Up @@ -303,6 +313,13 @@ handleProxyException handleException onexceptBody except req respond = do
handleException req except
respond $ missingHostResponse onexceptBody

healthcheckResponse :: Wai.Response
healthcheckResponse = Wai.responseBuilder
status200
[("Content-Type", "text/plain; charset=utf-8")]
$ "Keter " <> (copyByteString . S8.pack . showVersion) Pkg.version
<> " is doing okay!\n"

defaultProxyException :: ByteString
defaultProxyException = "<!DOCTYPE html>\n<html><head><title>Welcome to Keter</title></head><body><h1>Welcome to Keter</h1><p>There was a proxy error, check the keter logs for details.</p></body></html>"

Expand Down
1 change: 1 addition & 0 deletions test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ headThenPostNoCrash = do
, psProxyException = ""
, psIpFromHeader = False
, psConnectionTimeBound = 5 * 60 * 1000
, psHealthcheckPath = Nothing
}

0 comments on commit dac66e1

Please sign in to comment.