diff --git a/README.md b/README.md index fd8204b..5cd7887 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ # Gatehouse + +## Configuration + +The app configuration has three levels of precedence. Any values repeated at a higher level will override those at a lower +level. +The levels are, in order of precedence: + +### 1. SSM parameters +Secret and private settings are stored as +[AWS SSM parameters](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html), +named in the format: +`//identity/gatehouse/` +where `param key` is a slash-separated +[Hocon](https://github.com/lightbend/config/blob/main/HOCON.md) key. + +Eg. +`/CODE/identity/gatehouse/play/http/secret/key` +would give us a `play.http.secret.key` value. + +Secrets are stored as `SecureString` parameters. +Private settings are stored as `String` parameters. + +### 2. Stage-specific settings +Settings that aren't private but vary between deployment stages are set in the stage-specific config files in +the `conf` directory. These might be values that we expose in browsers, for example. + +### 3. Global settings +Finally, settings that aren't private and are the same for all deployment stages are set in the `conf/application.conf` +file. diff --git a/app/load/AppComponents.scala b/app/load/AppComponents.scala index eb7075f..bb8e0a7 100644 --- a/app/load/AppComponents.scala +++ b/app/load/AppComponents.scala @@ -14,26 +14,6 @@ import scala.util.Using class AppComponents(context: Context) extends BuiltInComponentsFromContext(context) with HttpFiltersComponents { - private val region = Region.EU_WEST_1 - - private val stage = context.initialConfiguration.getOptional[String]("stage").getOrElse("DEV") - - private lazy val secretKey: String = { - val request = GetParameterRequest.builder - .name(s"/$stage/identity/gatehouse/playSecret") - .withDecryption(true) - .build() - Using.resource(SsmClient.builder.region(region).build()) { - _.getParameter(request).parameter.value - } - } - - override def configuration: Configuration = - if (stage == "DEV") - super.configuration - else - Configuration("play.http.secret.key" -> secretKey).withFallback(super.configuration) - override def httpFilters: Seq[EssentialFilter] = super.httpFilters :+ new RequestLoggingFilter(materializer) lazy val healthCheckController = new controllers.HealthCheckController(controllerComponents) diff --git a/app/load/AppLoader.scala b/app/load/AppLoader.scala index 819de0e..7cae28e 100644 --- a/app/load/AppLoader.scala +++ b/app/load/AppLoader.scala @@ -1,14 +1,52 @@ package load +import com.gu.conf.{ + ComposedConfigurationLocation, + ConfigurationLoader, + ResourceConfigurationLocation, + SSMConfigurationLocation +} +import com.gu.{AppIdentity, AwsIdentity} +import com.typesafe.config.Config import play.api.* import play.api.ApplicationLoader.Context +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider + +import scala.util.{Failure, Success, Try} class AppLoader extends ApplicationLoader { + private val appName = "gatehouse" + + private def buildConfig(context: Context): Try[Config] = { + val credentialsProvider = DefaultCredentialsProvider.create() + val isDev = context.environment.mode == Mode.Dev + for { + identity <- + if (isDev) + Success(AwsIdentity(app = appName, stack = "identity", stage = "DEV", region = "eu-west-1")) + else + AppIdentity.whoAmI(defaultAppName = appName, credentialsProvider) + config <- Try(ConfigurationLoader.load(identity, credentialsProvider) { case identity: AwsIdentity => + ComposedConfigurationLocation( + List( + SSMConfigurationLocation.default(identity), + ResourceConfigurationLocation(s"${identity.stage}.conf"), + ) + ) + }) + } yield config + } + override def load(context: Context): Application = { - LoggerConfigurator(context.environment.classLoader).foreach { - _.configure(context.environment, context.initialConfiguration, Map.empty) + LoggerConfigurator(context.environment.classLoader) foreach { _.configure(context.environment) } + buildConfig(context) match { + case Success(config) => + val newContext = + context.copy(initialConfiguration = Configuration(config).withFallback(context.initialConfiguration)) + new AppComponents(newContext).application + case Failure(exception) => + throw exception } - new AppComponents(context).application } } diff --git a/app/logging/LogEntry.scala b/app/logging/LogEntry.scala index 3120593..a9f6bd8 100644 --- a/app/logging/LogEntry.scala +++ b/app/logging/LogEntry.scala @@ -21,7 +21,6 @@ private[logging] object LogEntry { def requestAndResponse(request: RequestHeader, response: Result, duration: Long): LogEntry = { val fields = commonFields(request, duration) ++ Map( "status" -> response.header.status, - "content_length" -> response.header.headers.getOrElse(CONTENT_LENGTH, 0), "content_length" -> response.header.headers.get(CONTENT_LENGTH).map(_.toInt).getOrElse(0), ) val message = diff --git a/build.sbt b/build.sbt index 39e0140..e6e82a9 100644 --- a/build.sbt +++ b/build.sbt @@ -15,8 +15,8 @@ lazy val root = (project in file(".")) s"-J-Dlogs.home=/var/log/${packageName.value}", ), libraryDependencies ++= Seq( - "software.amazon.awssdk" % "ssm" % "2.23.10", "net.logstash.logback" % "logstash-logback-encoder" % "7.3", + ("com.gu" %% "simple-configuration-ssm" % "1.6.4").cross(CrossVersion.for3Use2_13), "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.1" % Test, ), ) diff --git a/cdk/lib/__snapshots__/gatehouse.test.ts.snap b/cdk/lib/__snapshots__/gatehouse.test.ts.snap index 7132437..236a54d 100644 --- a/cdk/lib/__snapshots__/gatehouse.test.ts.snap +++ b/cdk/lib/__snapshots__/gatehouse.test.ts.snap @@ -887,7 +887,7 @@ aws s3 cp 's3://", "Ref": "DistributionBucketName", }, "/identity/TEST/gatehouse/gatehouse.deb' '/gatehouse/gatehouse.deb' -dpkg -i /gatehouse/gatehouse.deb && echo "stage=TEST" | sudo tee "/etc/gatehouse/stage.conf" > /dev/null", +dpkg -i /gatehouse/gatehouse.deb", ], ], }, diff --git a/cdk/lib/gatehouse.ts b/cdk/lib/gatehouse.ts index 10a4949..d4e37c8 100644 --- a/cdk/lib/gatehouse.ts +++ b/cdk/lib/gatehouse.ts @@ -34,7 +34,7 @@ export class Gatehouse extends GuStack { userData: { distributable: { fileName: `${ec2App}.deb`, - executionStatement: `dpkg -i /${ec2App}/${ec2App}.deb && echo "stage=${this.stage}" | sudo tee "/etc/gatehouse/stage.conf" > /dev/null`, + executionStatement: `dpkg -i /${ec2App}/${ec2App}.deb`, }, }, certificateProps: { diff --git a/conf/CODE.conf b/conf/CODE.conf new file mode 100644 index 0000000..87e60ec --- /dev/null +++ b/conf/CODE.conf @@ -0,0 +1 @@ +include "application.conf" diff --git a/conf/DEV.conf b/conf/DEV.conf new file mode 100644 index 0000000..87e60ec --- /dev/null +++ b/conf/DEV.conf @@ -0,0 +1 @@ +include "application.conf" diff --git a/conf/PROD.conf b/conf/PROD.conf new file mode 100644 index 0000000..87e60ec --- /dev/null +++ b/conf/PROD.conf @@ -0,0 +1 @@ +include "application.conf" diff --git a/conf/application.conf b/conf/application.conf index 7b9ab5e..4f71813 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -1,6 +1,3 @@ # https://www.playframework.com/documentation/latest/Configuration -include file("/etc/gatehouse/stage.conf") - -play.application.loader=load.AppLoader - +play.application.loader = load.AppLoader play.filters.hosts.routeModifiers.whiteList = [anyhost]