ScaleGrid
Posted on June 18, 2019
Given the popularity of our post on connecting MongoDB SSL with Self-Signed Certificates in Node.js, we decided to write a tutorial on connecting MongoDB with Ruby. In this blog, we'll show you how to connect to a MongoDB server configured with self-signed certificates for SSL using both the Ruby MongoDB driver and the popular Object-Document-Mapper (ODM) mongoid.
ScaleGrid currently uses self-signed certificates for SSL when creating nodes for a new cluster. Additionally, we also provide you with the option of purchasing your own SSL certificates and configuring them on the MongoDB server, and you can email support@scalegrid.io to learn more about this offer.
Connecting to a Replica Set Using Ruby MongoDB Driver
We will use the latest stable Ruby MongoDB driver version 2.8 for this example. The 2.5.x versions of the driver have a known bug that inhibit them from working with ScaleGrid deployments. The Ruby version used in both the examples below is 2.6.3.
The connection options available for the driver are documented here, and the options we will need are:
- :ssl
- :ssl_verify
- :ssl_ca_cert.
First, find and copy your MongoDB connection string from the cluster details page on the ScaleGrid console:
The CA certificate file is also available for download from the cluster details page. Download and store the cert file at a location that is available to the application: Here's a snippet showing how to connect to a MongoDB replica set from Ruby:
require 'mongo' Mongo::Logger.logger.level = ::Logger::DEBUG MONGODB_CA_CERT = "/path/to/ca_cert.pem" MONGODB_CONN_URL = "mongodb://testuser:@SG-example-17026.servers.mongodirector.com:27017,SG-example-17027.servers.mongodirector.com:27017,SG-example-17028.servers.mongodirector.com:27017/test?replicaSet=RS-example-0&ssl=true" options = { ssl:true, ssl_verify: true, :ssl_ca_cert => MONGODB_CA_CERT } client = Mongo::Client.new(MONGODB_CONN_URL, options) db = client.database collections = db.collection_names puts "db #{db.name} has collections #{collections}" client.close
To keep the example simple, we have specified the connection string and the cert file path directly in the code snippet - you would generally either put them in a yaml file or specify them as environment variables. Also, the example sets the log level to DEBUG
so that any connectivity issues can be debugged. It should be changed to a less verbose level once connectivity issues have been sorted out.
Connecting Using Mongoid
The mongoid version we will use in our example is the latest stable version - 7.0.2. We will use a yaml file to provide configuration to mongoid, and the details of such a config file are documented here. The SSL-specific configuration options we will need to connect to our replica set are:
- ssl
- ssl_verify
- ssl_ca_cert
Our yml file:
development: # Configure available database clients. (required) clients: # Define the default client. (required) default: # A uri may be defined for a client: # uri: 'mongodb://user:password@myhost1.mydomain.com:27017/my_db' # Please see driver documentation for details. Alternatively, you can define the following: # # Define the name of the default database that Mongoid can connect to. # (required). database: test # Provide the hosts the default client can connect to. Must be an array # of host:port pairs. (required) hosts: - SG-example-17026.servers.mongodirector.com:27017 - SG-example-17027.servers.mongodirector.com:27017 - SG-example-17028.servers.mongodirector.com:47100 options: # The name of the user for authentication. user: 'testuser' # The password of the user for authentication. password: 'pwd' # The user's database roles. roles: - 'readWrite' # Change the default authentication mechanism. Valid options are: :scram, # :mongodb_cr, :mongodb_x509, and :plain. (default on 3.0 is :scram, default # on 2.4 and 2.6 is :plain) auth_mech: :scram # The database or source to authenticate the user against. (default: admin) auth_source: test # Force the driver to connect in a specific way instead of auto- # discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct # when connecting to hidden members of a replica set. connect: :replica_set ... ... # The name of the replica set to connect to. Servers provided as seeds that do # not belong to this replica set will be ignored. replica_set: RS-example-0 # Whether to connect to the servers via ssl. (default: false) ssl: true # Whether or not to do peer certification validation. (default: true) ssl_verify: true # The file containing a set of concatenated certification authority certifications # used to validate certs passed from the other end of the connection. ssl_ca_cert: /path/to/ca_cert.pem # Configure Mongoid specific options. (optional) options: # Set the Mongoid and Ruby driver log levels. (default: :info) log_level: :debug
Connection example:
gem 'mongoid', '7.0.2' require 'mongoid' Mongoid.load!("/path/to/mongoid.yml", :development) # Not using any of the ODM features - just fetch the underlying mongo client and attempt to connect client = Mongoid::Clients.default db = client.database collections = db.collection_names puts "db #{db.name} has collections #{collections}" Mongoid::Clients.disconnect
Again, in production Ruby on Rails applications, the yaml file path would be picked up from the environment variables.
Testing Failover Behavior
Like other MongoDB drivers, the Ruby MongoDB driver is also designed to internally recognize changes in topology due to events like failover. However, it is good to test and validate the behavior of the driver during failovers to avoid surprises in production.
Like my previous post on MongoDB PyMongo, we can write a perpetual writer test program to observe the failover behavior of the driver.
The easiest way to induce failover is to run the rs.stepDown() command:
RS-example-0:PRIMARY> rs.stepDown() 2019-04-18T19:44:42.257+0530 E QUERY [thread1] Error: error doing query: failed: network error while attempting to run command 'replSetStepDown' on host 'SG-example-1.servers.mongodirector.com:27017' : DB.prototype.runCommand@src/mongo/shell/db.js:168:1 DB.prototype.adminCommand@src/mongo/shell/db.js:185:1 rs.stepDown@src/mongo/shell/utils.js:1305:12 @(shell):1:1 2019-04-18T19:44:42.261+0530 I NETWORK [thread1] trying reconnect to SG-example-1.servers.mongodirector.com:27017 (X.X.X.X) failed 2019-04-18T19:44:43.267+0530 I NETWORK [thread1] reconnect SG-example-1.servers.mongodirector.com:27017 (X.X.X.X) ok RS-example-0:SECONDARY>
Here are the relevant parts of our test code:
require 'mongo' ... logger = Logger.new(STDOUT) logger.level = Logger::INFO MONGODB_CA_CERT = "/path/to/ca_cert.pem" MONGODB_CONN_URL = "mongodb://testuser:@SG-example-17026.servers.mongodirector.com:27017,SG-example-17027.servers.mongodirector.com:27017,SG-example-17028.servers.mongodirector.com:27017/test?replicaSet=RS-example-0&ssl=true" options = { ssl:true, ssl_verify: true, :ssl_ca_cert => MONGODB_CA_CERT } begin logger.info("Attempting to connect...") client = Mongo::Client.new(MONGODB_CONN_URL, options) i = 0 loop do db = client.database collection = db[:test] begin doc = {"idx": i, "date": DateTime.now, "text": SecureRandom.base64(3) } result = collection.insert_one(doc) logger.info("Record inserted - id: #{result.inserted_id}") i += 1 sleep(3) rescue Mongo::Error => e logger.error("Mong Error seen: #{e.message}") logger.error(e.backtrace) logger.info("Retrying...") end end logger.info("Done") rescue => err logger.error("Exception seen: #{err.message}") logger.error(err.backtrace) ensure client.close unless client.nil? end
This continuously writes entries like these to the test collection on the test database:
RS-test-0:PRIMARY> db.test.find() { "_id" : ObjectId("5cf50ff1896cd172a4f7c6ee"), "idx" : 0, "date" : ISODate("2019-06-03T12:17:53.008Z"), "text" : "HTvd" } { "_id" : ObjectId("5cf50ff6896cd172a4f7c6ef"), "idx" : 1, "date" : ISODate("2019-06-03T12:17:58.697Z"), "text" : "/e5Z" } { "_id" : ObjectId("5cf50ff9896cd172a4f7c6f0"), "idx" : 2, "date" : ISODate("2019-06-03T12:18:01.940Z"), "text" : "quuw" } { "_id" : ObjectId("5cf50ffd896cd172a4f7c6f1"), "idx" : 3, "date" : ISODate("2019-06-03T12:18:05.194Z"), "text" : "gTyY" } { "_id" : ObjectId("5cf51000896cd172a4f7c6f2"), "idx" : 4, "date" : ISODate("2019-06-03T12:18:08.442Z"), "text" : "VDXX" } { "_id" : ObjectId("5cf51003896cd172a4f7c6f3"), "idx" : 5, "date" : ISODate("2019-06-03T12:18:11.691Z"), "text" : "UY87" } ...
Let's see the behavior during a failover:
I, [2019-06-03T17:53:25.079829 #9464] INFO -- : Attempting to connect... I, [2019-06-03T17:53:30.577099 #9464] INFO -- : Record inserted - id: 5cf5113f896cd124f8f31062 I, [2019-06-03T17:53:33.816528 #9464] INFO -- : Record inserted - id: 5cf51145896cd124f8f31063 I, [2019-06-03T17:53:37.047043 #9464] INFO -- : Record inserted - id: 5cf51148896cd124f8f31064 I, [2019-06-03T17:53:40.281537 #9464] INFO -- : Record inserted - id: 5cf5114c896cd124f8f31065 I, [2019-06-03T17:53:43.520010 #9464] INFO -- : Record inserted - id: 5cf5114f896cd124f8f31066 I, [2019-06-03T17:53:46.747080 #9464] INFO -- : Record inserted - id: 5cf51152896cd124f8f31067 I, [2019-06-03T17:53:49.978077 #9464] INFO -- : Record inserted - id: 5cf51155896cd124f8f31068 <<< Failover initiated here E, [2019-06-03T17:53:52.980434 #9464] ERROR -- : Mong Error seen: EOFError: end of file reached (for x.x.x.x:27017 (sg-example-17026.servers.mongodirector.com:27017, TLS)) E, [2019-06-03T17:53:52.980533 #9464] ERROR -- : ["C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/mongo-2.8.0/lib/mongo/socket.rb:300:in `rescue in handle_errors'", "C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/mongo-2.8.0/lib/mongo/socket.rb:294:in `handle_errors'", "C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/mongo-2.8.0/lib/mongo/socket.rb:126:in `read'", "C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/mongo-2.8.0/lib/mongo/protocol/message.rb:139:in `deserialize'",... ... I, [2019-06-03T17:53:52.980688 #9464] INFO -- : Retrying... W, [2019-06-03T17:53:52.981575 #9464] WARN -- : Retrying ismaster on sg-example-17026.servers.mongodirector.com:27017 due to: Mongo::Error::SocketError EOFError: end of file reached (for x.x.x.x:27017 (sg-example-17026.servers.mongodirector.com:27017, TLS)) I, [2019-06-03T17:54:06.293100 #9464] INFO -- : Record inserted - id: 5cf51163896cd124f8f3106a I, [2019-06-03T17:54:09.547716 #9464] INFO -- : Record inserted - id: 5cf51169896cd124f8f3106b I, [2019-06-03T17:54:12.806636 #9464] INFO -- : Record inserted - id: 5cf5116c896cd124f8f3106c
It is evident that if correct errors are caught and read/writes retried, the driver will automatically detect the topology change and reconnect to the new master. For writes, the option :retry_writes ensure that the driver will retry once on its own before notifying the application of an error.
There are also multiple driver timeouts that can be tweaked based on the exact behavior and latency you see on your setup. These are documented here.
Troubleshooting
If you're having trouble connecting to your SSL-enabled MongoDB deployment, here are a few tips for debugging:
- First, verify that you can actually connect to the MongoDB server from the server where your application is running. The simplest way to do this is to install mongo shell on the client machine. On Linux, you wouldn't need to install the entire MongoDB server - you can choose to just install the shell separately. Once the shell is available, try to use the 'Command Line Syntax' we provide to attempt to connect to the server.
- If you are unable to connect via the mongo shell, it means that the client machine is unable to reach the port 27017 of the MongoDB servers. Look at your Security Group, VPC, and ScaleGrid firewall settings to ensure that there is connectivity between the client and server machines.
- If network connectivity is correct, the next thing to check is that you are using versions of Ruby, mongoid, and mongo gem that are compatible with the version of your MongoDB server.
- If you have confirmed that the driver versions are correct, try running a sample Ruby script, similar to the example we provided above, on the IRB. A step-by-step execution might point out where the issue is.
- If the test script runs fine, but you are still unable to connect with mongoid, attempt to run a simple test script, like the example we provided above.
- If you are still having trouble connecting to your instance, please write to us at support@scalegrid.io with detailed results of the above troubleshooting steps and with the exact versions of Ruby, mongoid and mongo driver you are using. The Gemfile.lock will provide you with the exact versions.
If you are new to ScaleGrid and want to give this tutorial a try, sign up for a free 30-day trial to explore the platform and test out connecting MongoDB to your Ruby application.
Posted on June 18, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.