Skip to content

Commit 281ee18

Browse files
authored
Merge pull request #1 from madebydna/aws_msk_with_iam_support
Support for AWS MSK with IAM authentication
2 parents f799f61 + ed87e48 commit 281ee18

File tree

4 files changed

+84
-1
lines changed

4 files changed

+84
-1
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ source 'https://rubygems.org'
33
# Specify your gem's dependencies in fluent-plugin-kafka.gemspec
44
gemspec
55

6+
gem 'json', '2.7.3' # override of 2.7.4 version
67
gem 'rdkafka', ENV['RDKAFKA_VERSION_MIN_RANGE'], ENV['RDKAFKA_VERSION_MAX_RANGE'] if ENV['USE_RDKAFKA']

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,45 @@ If you want to use zookeeper related parameters, you also need to install zookee
2929

3030
## Usage
3131

32+
### :exclamation: In this fork: MSK IAM Authentication Support for `rdkafka2` Output Type
33+
34+
This fork adds support for using MSK IAM authentication with the `rdkafka2` output type in Fluentd. Authentication and authorization with an MSK cluster are facilitated through a base64-encoded signed URL, which is generated by the [aws-msk-iam-sasl-signer-ruby](https://github.com/bruce-szalwinski-he/aws-msk-iam-sasl-signer-ruby) library.
35+
36+
The `aws-msk-iam-sasl-signer-ruby` library provides an [example](https://github.com/bruce-szalwinski-he/aws-msk-iam-sasl-signer-ruby/tree/main/examples/rdkafka) for generating the OAuthBearer token using rdkafka, which is one of the core Kafka libraries supported by the Fluentd fluent-plugin-kafka plugin. This fork integrates that example into the `Fluent::Rdkafka2Output` class, enabling AWS IAM authentication.
37+
38+
The key change is the inclusion of a refresh callback:
39+
```ruby
40+
Rdkafka::Config.oauthbearer_token_refresh_callback = method(:refresh_token)
41+
```
42+
This callback triggers token generation when needed, ensuring continuous authentication with the MSK cluster.
43+
44+
#### Configuration Example
45+
To enable this feature, configure your Fluentd input as follows:
46+
47+
```
48+
<match *>
49+
@type rdkafka2
50+
# Kafka brokers to connect to (typically port 9098 or 9198 for IAM authentication)
51+
brokers <broker_addresses>
52+
# Topic to write events to
53+
topic_key test-topic-1
54+
default_topic test-topic-1
55+
56+
# AWS Region (required)
57+
aws_msk_region us-east-1
58+
59+
# Use a shared producer for the connection (required)
60+
share_producer true
61+
62+
# MSK IAM authentication settings (required)
63+
rdkafka_options {
64+
"security.protocol": "sasl_ssl",
65+
"sasl.mechanisms": "OAUTHBEARER"
66+
}
67+
</match>
68+
```
69+
With this configuration, Fluentd will handle the token refresh and manage the connection to your MSK cluster using AWS IAM authentication.
70+
3271
### Common parameters
3372

3473
#### SSL authentication
@@ -563,7 +602,7 @@ You need to install rdkafka gem.
563602
</match>
564603

565604
`rdkafka2` supports `discard_kafka_delivery_failed_regex` parameter:
566-
- `discard_kafka_delivery_failed_regex` - default: nil - discard the record where the Kafka::DeliveryFailed occurred and the emitted message matches the given regex pattern, such as `/unknown_topic/`.
605+
- `discard_kafka_delivery_failed_regex` - default: nil - discard the record where the Kafka::DeliveryFailed occurred and the emitted message matches the given regex pattern, such as `/unknown_topic/`.
567606

568607
If you use v0.12, use `rdkafka` instead.
569608

fluent-plugin-kafka.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Gem::Specification.new do |gem|
1919
gem.add_dependency "fluentd", [">= 0.10.58", "< 2"]
2020
gem.add_dependency 'ltsv'
2121
gem.add_dependency 'ruby-kafka', '>= 1.5.0', '< 2'
22+
gem.add_dependency 'rdkafka'
23+
gem.add_dependency 'aws-msk-iam-sasl-signer'
2224
gem.add_development_dependency "rake", ">= 0.9.2"
2325
gem.add_development_dependency "test-unit", ">= 3.0.8"
2426
gem.add_development_dependency "test-unit-rr", "~> 1.0"

lib/fluent/plugin/out_rdkafka2.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'fluent/plugin/kafka_plugin_util'
55

66
require 'rdkafka'
7+
require 'aws_msk_iam_sasl_signer'
78

89
begin
910
rdkafka_version = Gem::Version::create(Rdkafka::VERSION)
@@ -101,6 +102,8 @@ class Fluent::Rdkafka2Output < Output
101102
config_param :unrecoverable_error_codes, :array, :default => ["topic_authorization_failed", "msg_size_too_large"],
102103
:desc => 'Handle some of the error codes should be unrecoverable if specified'
103104

105+
config_param :aws_msk_region, :string, :default => nil, :desc => 'AWS region for MSK'
106+
104107
config_section :buffer do
105108
config_set_default :chunk_keys, ["topic"]
106109
end
@@ -205,10 +208,17 @@ def add(level, message = nil)
205208
end
206209
end
207210
}
211+
# HERE -----------------
208212
Rdkafka::Config.logger = log
209213
config = build_config
210214
@rdkafka = Rdkafka::Config.new(config)
211215

216+
217+
if config[:"security.protocol"] == "sasl_ssl" && config[:"sasl.mechanisms"] == "OAUTHBEARER"
218+
Rdkafka::Config.oauthbearer_token_refresh_callback = method(:refresh_token)
219+
end
220+
# HERE -----------------
221+
212222
if @default_topic.nil?
213223
if @use_default_for_unknown_topic || @use_default_for_unknown_partition_error
214224
raise Fluent::ConfigError, "default_topic must be set when use_default_for_unknown_topic or use_default_for_unknown_partition_error is true"
@@ -289,16 +299,47 @@ def build_config
289299
config[:"sasl.password"] = @password if @password
290300
config[:"enable.idempotence"] = @idempotent if @idempotent
291301

302+
# sasl.mechnisms and security.protocol are set as rdkafka_options
292303
@rdkafka_options.each { |k, v|
293304
config[k.to_sym] = v
294305
}
295306

296307
config
297308
end
298309

310+
def refresh_token(_config, _client_name)
311+
log.info("+--- Refreshing token")
312+
client = get_producer
313+
# This will happen once upon initialization and is expected to fail, as the producer isnt set yet
314+
# We will set the token manually after creation and after that this refresh method will work
315+
unless client
316+
log.info("Could not get shared client handle, unable to set/refresh token (this is expected one time on startup)")
317+
return
318+
end
319+
signer = AwsMskIamSaslSigner::MSKTokenProvider.new(region: @aws_msk_region)
320+
token = signer.generate_auth_token
321+
322+
if token
323+
client.oauthbearer_set_token(
324+
token: token.token,
325+
lifetime_ms: token.expiration_time_ms,
326+
principal_name: "kafka-cluster"
327+
)
328+
else
329+
client.oauthbearer_set_token_failure(
330+
"Failed to generate token."
331+
)
332+
end
333+
end
334+
299335
def start
300336
if @share_producer
301337
@shared_producer = @rdkafka.producer
338+
log.info("Created shared producer")
339+
if @aws_msk_region
340+
refresh_token(nil, nil)
341+
log.info("Set initial token for shared producer")
342+
end
302343
else
303344
@producers = {}
304345
@producers_mutex = Mutex.new

0 commit comments

Comments
 (0)