Skip to content

Commit

Permalink
Enable ETag caching with Facia API requests
Browse files Browse the repository at this point in the history
With guardian/facia-scala-client#287, the Facia client has
been updated so that it can take advantage of ETag-caching, using the
https://github.com/guardian/etag-caching library already used in Frontend since
August 2023 with #26338.

As some changes were necessary to the `etag-caching` library to facilitate
this, we need to update the version of `etag-caching` used by Frontend to v7.0.0.

This does involve switching to AWS SDK v2 - not everywhere in Frontend, just for
where we use the Facia client!
  • Loading branch information
rtyley committed Jan 12, 2025
1 parent a9ed44d commit 5136617
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 43 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ val common = library("common")
awsKinesis,
awsS3,
awsSns,
awsSts,
awsSts, // AWS SDK v1 still used for CAPI-preview related code for now
awsV2Sts, // AWS SDK v2 used for Fronts API access
awsSqs,
awsSsm,
eTagCachingS3,
Expand Down
42 changes: 18 additions & 24 deletions common/app/common/configuration.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package common

import java.io.{File, FileInputStream}
import java.nio.charset.Charset
import java.util.Map.Entry

import com.amazonaws.AmazonClientException
import com.amazonaws.auth._
import com.amazonaws.auth.profile.ProfileCredentialsProvider
Expand All @@ -12,9 +8,15 @@ import common.Environment.{app, awsRegion, stage}
import conf.{Configuration, Static}
import org.apache.commons.io.IOUtils
import services.ParameterStore
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.core.exception.SdkClientException
import utils.AWSv2

import scala.jdk.CollectionConverters._
import java.io.{File, FileInputStream}
import java.nio.charset.Charset
import java.util.Map.Entry
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try}

class BadConfigurationException(msg: String) extends RuntimeException(msg)
Expand Down Expand Up @@ -231,14 +233,10 @@ class GuardianConfiguration extends GuLogging {
lazy val capiPreviewRoleToAssume: Option[String] =
configuration.getStringProperty("content.api.preview.roleToAssume")

lazy val capiPreviewCredentials: AWSCredentialsProvider = {
new AWSCredentialsProviderChain(
List(
Some(new ProfileCredentialsProvider("capi")),
capiPreviewRoleToAssume.map(new STSAssumeRoleSessionCredentialsProvider.Builder(_, "capi").build()),
).flatten: _*,
)
}
lazy val capiPreviewCredentials: AWSCredentialsProvider = new AWSCredentialsProviderChain(
Seq(new ProfileCredentialsProvider("capi")) ++
capiPreviewRoleToAssume.map(new STSAssumeRoleSessionCredentialsProvider.Builder(_, "capi").build()): _*,
)

lazy val nextPreviousPageSize: Int =
configuration.getIntegerProperty("content.api.nextPreviousPageSize").getOrElse(50)
Expand Down Expand Up @@ -597,24 +595,20 @@ class GuardianConfiguration extends GuLogging {

lazy val stsRoleToAssume = configuration.getStringProperty("aws.cmsFronts.account.role")

def crossAccountMandatoryCredentials: AWSCredentialsProvider =
def crossAccountMandatoryCredentials: AwsCredentialsProvider =
crossAccountCredentials.getOrElse(
throw new BadConfigurationException("AWS credentials for cross account are not configured"),
)

lazy val crossAccountCredentials: Option[AWSCredentialsProvider] = faciatool.stsRoleToAssume.flatMap { role =>
val provider = new AWSCredentialsProviderChain(
new ProfileCredentialsProvider("cmsFronts"),
new STSAssumeRoleSessionCredentialsProvider.Builder(role, "frontend").build(),
)
lazy val crossAccountCredentials: Option[AwsCredentialsProvider] = faciatool.stsRoleToAssume.map { roleArn =>
val provider = AWSv2.stsCredentialsForDevAndProd("cmsFronts", roleArn)

// this is a bit of a convoluted way to check whether we actually have credentials.
// I guess in an ideal world there would be some sort of isConfigued() method...
// Check whether we actually have credentials!
try {
provider.getCredentials
Some(provider)
provider.resolveCredentials()
provider
} catch {
case ex: AmazonClientException =>
case ex: SdkClientException =>
log.error("amazon client cross account exception", ex)
throw ex
}
Expand Down
22 changes: 9 additions & 13 deletions common/app/services/fronts/FrontsApi.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
package services.fronts

import com.amazonaws.services.s3.{AmazonS3, AmazonS3Client}
import com.gu.facia.client.{AmazonSdkS3Client, ApiClient}
import com.gu.etagcaching.aws.sdkv2.s3.S3ObjectFetching
import com.gu.facia.client.{ApiClient, Environment}
import conf.Configuration
import utils.AWSv2.buildS3AsyncClient

import scala.concurrent.ExecutionContext

object FrontsApi {

def crossAccountClient(implicit ec: ExecutionContext): ApiClient = {
val client: AmazonS3 = AmazonS3Client.builder
.withCredentials(Configuration.faciatool.crossAccountMandatoryCredentials)
.withRegion(conf.Configuration.aws.region)
.build()
ApiClient(
Configuration.faciatool.crossAccountSourceBucket,
Configuration.facia.stage.toUpperCase,
AmazonSdkS3Client(client),
)
}
def crossAccountClient(implicit ec: ExecutionContext): ApiClient = ApiClient.withCaching(
Configuration.faciatool.crossAccountSourceBucket,
Environment(Configuration.facia.stage.toUpperCase),
S3ObjectFetching.byteArraysWith(buildS3AsyncClient(Configuration.faciatool.crossAccountMandatoryCredentials)),
)
}
25 changes: 22 additions & 3 deletions common/app/utils/AWSv2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.regions.Region.EU_WEST_1
import software.amazon.awssdk.services.s3.{S3AsyncClient, S3AsyncClientBuilder}
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest
import software.amazon.awssdk.services.sts.{StsClient, StsClientBuilder}

object AWSv2 {
val region: Region = EU_WEST_1
Expand All @@ -16,10 +19,26 @@ object AWSv2 {
lazy val credentials: AwsCredentialsProvider =
credentialsForDevAndProd("frontend", InstanceProfileCredentialsProvider.create())

def build[T, B <: AwsClientBuilder[B, T]](builder: B): T =
builder.credentialsProvider(credentials).region(region).build()
def build[T, B <: AwsClientBuilder[B, T]](builder: B, creds: AwsCredentialsProvider = credentials): T =
builder.credentialsProvider(creds).region(region).build()

val S3Async: S3AsyncClient = build[S3AsyncClient, S3AsyncClientBuilder](
def buildS3AsyncClient(creds: AwsCredentialsProvider): S3AsyncClient = build[S3AsyncClient, S3AsyncClientBuilder](
S3AsyncClient.builder().httpClientBuilder(NettyNioAsyncHttpClient.builder().maxConcurrency(250)),
creds,
)

val S3Async: S3AsyncClient = buildS3AsyncClient(credentials)

val STS: StsClient = build[StsClient, StsClientBuilder](StsClient.builder())

def stsCredentialsForDevAndProd(devProfile: String, roleArn: String): AwsCredentialsProvider =
credentialsForDevAndProd(
devProfile,
StsAssumeRoleCredentialsProvider
.builder()
.stsClient(STS)
.refreshRequest(AssumeRoleRequest.builder.roleSessionName("frontend").roleArn(roleArn).build)
.build(),
)

}
5 changes: 3 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object Dependencies {
val awsVersion = "1.12.758"
val awsSdk2Version = "2.26.27"
val capiVersion = "33.0.0"
val faciaVersion = "14.0.0"
val faciaVersion = "14.0.1"
val dispatchVersion = "0.13.1"
val romeVersion = "1.0"
val jerseyVersion = "1.19.4"
Expand All @@ -19,10 +19,11 @@ object Dependencies {
val awsEc2 = "com.amazonaws" % "aws-java-sdk-ec2" % awsVersion
val awsKinesis = "com.amazonaws" % "aws-java-sdk-kinesis" % awsVersion
val awsS3 = "com.amazonaws" % "aws-java-sdk-s3" % awsVersion
val eTagCachingS3 = "com.gu.etag-caching" %% "aws-s3-sdk-v2" % "4.0.1"
val eTagCachingS3 = "com.gu.etag-caching" %% "aws-s3-sdk-v2" % "7.0.0"
val awsSes = "com.amazonaws" % "aws-java-sdk-ses" % awsVersion
val awsSns = "com.amazonaws" % "aws-java-sdk-sns" % awsVersion
val awsSts = "com.amazonaws" % "aws-java-sdk-sts" % awsVersion
val awsV2Sts = "software.amazon.awssdk" % "sts" % awsSdk2Version
val awsSqs = "com.amazonaws" % "aws-java-sdk-sqs" % awsVersion
val awsSsm = "com.amazonaws" % "aws-java-sdk-ssm" % awsVersion
val awsElasticloadbalancing = "com.amazonaws" % "aws-java-sdk-elasticloadbalancing" % awsVersion
Expand Down

0 comments on commit 5136617

Please sign in to comment.