Skip to content

Commit 29f2ef4

Browse files
committed
Adds support for AWS OpenSearch Serverless (AOSS)
Why are these changes being introduced: * We are planning a migration to AOSS * We need to maintain our existing AWS OpenSearch Service (ES) integration while migrating to AOSS Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/USE-423 How does this address that need: * Added support for AWS OpenSearch Serverless (AOSS) using either expiring credentials by passing a session token or by assuming a role. * Configured the application to support AWS OpenSearch Serverless (AOSS) in addition to the existing AWS OpenSearch Service (ES). * Added logic to choose the appropriate client based on environment variables. * Implemented AWS SigV4 signing for AOSS authentication. Document any side effects to this change: * Updated lambda configuration to support session tokens. It does not have assume role configuration at this time, but we needed to support temporary credentials in the lambda to support them locally in OpenSearch Serverless (AOSS) so I included that in this change.
1 parent 7c02df2 commit 29f2ef4

4 files changed

Lines changed: 125 additions & 18 deletions

File tree

Gemfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
44
ruby '3.4.8'
55

66
gem 'aws-sdk-lambda'
7+
gem 'aws-sdk-sts'
8+
gem 'aws-sigv4'
79
gem 'bootsnap', require: false
810
gem 'devise'
911
gem 'faraday_middleware-aws-sigv4'
@@ -12,6 +14,7 @@ gem 'graphql'
1214
gem 'jwt'
1315
gem 'lograge'
1416
gem 'mitlibraries-theme', git: 'https://github.com/mitlibraries/mitlibraries-theme', tag: 'v1.4'
17+
gem 'opensearch-aws-sigv4'
1518
gem 'opensearch-ruby'
1619
gem 'puma'
1720
gem 'rack-attack'
@@ -24,7 +27,7 @@ gem 'sentry-ruby'
2427
gem 'uglifier'
2528

2629
group :production do
27-
gem 'connection_pool', '< 3' # 3.x requires keyword args; pin to 2.x for Rails 7.2.3
30+
gem 'connection_pool', '< 3' # 3.x requires keyword args; pin to 2.x for Rails 7.2.3
2831
gem 'pg'
2932
end
3033

Gemfile.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ GEM
102102
aws-sdk-lambda (1.176.0)
103103
aws-sdk-core (~> 3, >= 3.244.0)
104104
aws-sigv4 (~> 1.5)
105+
aws-sdk-sts (1.12.0)
106+
aws-sdk-core (~> 3, >= 3.110.0)
107+
aws-sigv4 (~> 1.1)
105108
aws-sigv4 (1.12.1)
106109
aws-eventstream (~> 1, >= 1.0.2)
107110
base64 (0.3.0)
@@ -277,6 +280,9 @@ GEM
277280
nokogiri (1.19.1)
278281
mini_portile2 (~> 2.8.2)
279282
racc (~> 1.4)
283+
opensearch-aws-sigv4 (1.3.0)
284+
aws-sigv4 (>= 1)
285+
opensearch-ruby (>= 1.0.1, < 4.0)
280286
opensearch-ruby (3.4.0)
281287
faraday (>= 1.0, < 3)
282288
multi_json (>= 1.0)
@@ -478,6 +484,8 @@ PLATFORMS
478484
DEPENDENCIES
479485
annotate
480486
aws-sdk-lambda
487+
aws-sdk-sts
488+
aws-sigv4
481489
bootsnap
482490
byebug
483491
capybara
@@ -497,6 +505,7 @@ DEPENDENCIES
497505
minitest (< 6)
498506
mitlibraries-theme!
499507
mocha
508+
opensearch-aws-sigv4
500509
opensearch-ruby
501510
pg
502511
puma

config/initializers/lambda.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
require 'aws-sdk-lambda'
22

33
def configure_lambda_client
4-
Aws::Lambda::Client.new(
5-
region: ENV.fetch('AWS_REGION', 'us-east-1'),
6-
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
7-
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY')
8-
)
4+
if ENV['AWS_SESSION_TOKEN'].present?
5+
Aws::Lambda::Client.new(
6+
region: ENV.fetch('AWS_REGION', 'us-east-1'),
7+
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
8+
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'),
9+
session_token: ENV.fetch('AWS_SESSION_TOKEN')
10+
)
11+
else
12+
Aws::Lambda::Client.new(
13+
region: ENV.fetch('AWS_REGION', 'us-east-1'),
14+
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
15+
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY')
16+
)
17+
end
918
end
1019

1120
Timdex::LambdaClient = configure_lambda_client

config/initializers/opensearch.rb

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,122 @@
1-
require 'faraday_middleware/aws_sigv4'
1+
require 'faraday_middleware/aws_sigv4' if ENV['AWS_OPENSEARCH'] == 'true' && ENV.fetch('AWS_AOSS', 'false') == 'false'
2+
require 'opensearch-aws-sigv4'
3+
require 'aws-sigv4'
24

5+
# Priority is given to AWS AOSS, then AWS OpenSearch, and finally vanilla OpenSearch
36
def configure_opensearch
4-
if ENV['AWS_OPENSEARCH'] == 'true'
7+
if ENV['AWS_AOSS'] == 'true'
8+
aws_aoss_client
9+
elsif ENV['AWS_OPENSEARCH'] == 'true'
510
aws_os_client
611
else
712
os_client
813
end
914
end
1015

16+
# os_client is used to connect to a standard OpenSearch cluster that does not require AWS SigV4 signing for
17+
# authentication. It creates a new OpenSearch::Client with logging enabled based on the OPENSEARCH_LOG
18+
# environment variable.
19+
#
20+
# @return [OpenSearch::Client] a client for connecting to a standard OpenSearch cluster
21+
# @note This is mostly used for connecting to a locally running OpenSearch instance
1122
def os_client
1223
OpenSearch::Client.new log: ENV.fetch('OPENSEARCH_LOG', false)
1324
end
1425

26+
# aws_os_client is used to connect to AWS OpenSearch Service which requires AWS SigV4 signing for authentication. It
27+
# creates a new OpenSearch::Client and configures it to use the aws_sigv4 middleware for request signing. The middleware
28+
# is configured with the AWS region, access key ID, secret access key, and optionally a session token if using temporary
29+
# credentials. The OPENSEARCH_URL environment variable is used to specify the endpoint of the OpenSearch cluster.
30+
#
31+
# @return [OpenSearch::Client] a client for connecting to AWS OpenSearch Service (ES)
32+
# @note This is the legacy method for this application and will be removed when we migrate to AOSS.
33+
# @note AWS OpenSearch Service can use long-lived access keys, unlike AWS AOSS which requires temporary credentials
34+
# obtained by assuming a role.
1535
def aws_os_client
16-
OpenSearch::Client.new log: ENV.fetch('OPENSEARCH_LOG', false), url: ENV['OPENSEARCH_URL'] do |config|
36+
OpenSearch::Client.new log: ENV.fetch('OPENSEARCH_LOG', false), url: ENV.fetch('OPENSEARCH_URL', nil) do |config|
37+
Rails.logger.debug "Configuring AWS OpenSearch Service client with URL: #{ENV.fetch('OPENSEARCH_URL', nil)}"
1738
# personal keys use expiring credentials with tokens
1839
if ENV['AWS_SESSION_TOKEN'].present?
40+
Rails.logger.debug "Using temporary credentials with session token"
1941
config.request :aws_sigv4,
20-
service: 'es',
21-
region: ENV['AWS_REGION'],
22-
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
23-
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
24-
session_token: ENV['AWS_SESSION_TOKEN']
42+
service: 'es',
43+
region: ENV.fetch('AWS_REGION', nil),
44+
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID', nil),
45+
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil),
46+
session_token: ENV['AWS_SESSION_TOKEN']
2547
# application keys don't use tokens
2648
else
49+
Rails.logger.debug "Using long-lived credentials without session token"
2750
config.request :aws_sigv4,
28-
service: 'es',
29-
region: ENV['AWS_REGION'],
30-
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
31-
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
51+
service: 'es',
52+
region: ENV.fetch('AWS_REGION', nil),
53+
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID', nil),
54+
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
3255
end
3356
end
3457
end
3558

59+
# aws_aoss_client is used to connect to AWS OpenSearch Serverless (AOSS) which has a different authentication mechanism
60+
# than AWS OpenSearch Service. It uses AWS SigV4 signing for authentication, and the OpenSearch::Aws::Sigv4Client is
61+
# specifically designed to handle this type of authentication.
62+
#
63+
# @return [OpenSearch::Aws::Sigv4Client] a client for connecting to AWS OpenSearch Serverless (AOSS)
64+
# @note this configuration uses temporary credentials obtained by assuming a role or via the AWS console, unlike
65+
# AWS OpenSearch Service which can use long-lived access keys directly.
66+
def aws_aoss_client
67+
Rails.logger.debug "Configuring AWS AOSS client with URL: #{ENV.fetch('OPENSEARCH_URL', nil)}"
68+
69+
signer = Aws::Sigv4::Signer.new(
70+
service: 'aoss',
71+
region: ENV.fetch('AWS_REGION', nil),
72+
credentials_provider: credentials
73+
)
74+
75+
OpenSearch::Aws::Sigv4Client.new(
76+
{
77+
host: ENV.fetch('OPENSEARCH_URL', nil),
78+
log: true
79+
},
80+
signer
81+
)
82+
end
83+
84+
def credentials
85+
if ENV.fetch('AWS_SESSION_TOKEN', false).present?
86+
Rails.logger.debug "Using temporary credentials with session token"
87+
temporary_credentials
88+
else
89+
Rails.logger.debug "Using long-lived credentials and assuming role"
90+
assume_role_credentials
91+
end
92+
end
93+
94+
# personal keys use expiring credentials with tokens, so we use them directly without assuming a role
95+
# application keys use long-lived credentials and assume a role to get temporary credentials for AOSS
96+
def temporary_credentials
97+
Aws::Credentials.new(
98+
ENV.fetch('AWS_ACCESS_KEY_ID', nil),
99+
ENV.fetch('AWS_SECRET_ACCESS_KEY', nil),
100+
ENV.fetch('AWS_SESSION_TOKEN', nil)
101+
)
102+
end
103+
104+
# AWS AOSS uses temporary credentials that are obtained by assuming a role. The
105+
# Aws::AssumeRoleCredentials class is used to get these temporary credentials. It requires the ARN of
106+
# the role to assume, a session name, and a client for the AWS Security Token Service (STS) which is
107+
# used to perform the AssumeRole operation. It uses the AWS region and access keys from the
108+
# environment variables to create the STS client. When the session token expires, the
109+
# Aws::AssumeRoleCredentials will automatically refresh the credentials by calling AssumeRole again.
110+
def assume_role_credentials
111+
Aws::AssumeRoleCredentials.new(
112+
role_arn: ENV.fetch('AWS_AOSS_ROLE_ARN', nil),
113+
role_session_name: 'timdex-opensearch',
114+
client: Aws::STS::Client.new(
115+
region: ENV.fetch('AWS_REGION', nil),
116+
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID', nil),
117+
secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
118+
)
119+
)
120+
end
121+
36122
Timdex::OSClient = configure_opensearch

0 commit comments

Comments
 (0)