DEV Community

ScaleGrid for ScaleGrid

Posted on • Edited on • Originally published at scalegrid.io

Connecting MongoDB to Ruby with Self-Signed Certificates for SSL

Connecting MongoDB to Ruby with Self-Signed Certificates for SSL

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:

MongoDB Connection String for Ruby Applications

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: MongoDB SSL Certificate and Command Line Syntax for Ruby ApplicationsHere'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.

Top comments (0)