5 steps to bring DevX automation to your company
Get the guide →
✕

Remote Development Environments with RubyMine, Okteto, and Kubernetes

In the past, we've talked about how to develop remotely with VS Code and PyCharm. Today, I'm going to show you how you can use okteto to define and deploy a fully configured remote development environment for your ruby applications, how to integrate it with RubyMine, and how to use it to build the fastest development experience for Cloud Native applications.

The Okteto Developer platform allows you to spin up an entire development environment in Kubernetes with one click. This can be as simple as a single container or as complex as a microservice-based Cloud Native Application. You deploy your application with one click, select the component you're going to develop on, and you're ready to go in seconds.

Install Okteto

The Okteto CLI is an open-source single-binary application that allows you to deploy development environments (among other things) in any Kubernetes cluster. It works with Linux, macOS, and Windows. We'll be using it to create and launch our development environment. Follow the steps below to install it:

MacOS / Linux
$ curl https://get.okteto.com -sSfL | sh
Windows
Download https://downloads.okteto.com/cli/okteto.exe and add it to your `$PATH`.

Log in to Okteto Cloud

Okteto is compatible with any Kubernetes cluster, local or remote. To keep this example simple, we'll be using Okteto Cloud to deploy the development environment, but the same instructions apply if you're using your own Okteto Enterprise instance.

Run okteto context in your local console to connect your local machine with your Okteto Cloud account and download your credentials. If you don't have an account already, you'll be prompted to create a free account.

Run okteto context in your local console to create a free Okteto Cloud account and configure your Okteto CLI context. We'll be needing it later in the post.

okteto context use https://cloud.okteto.com
 ✓  Using context cindy @ cloud.okteto.com
 i  Run 'okteto kubeconfig' to update your kubectl credentials

Create a new Ruby project

Start by opening RubyMine and creating a new project for your application and development environment. Pick the "Empty Project" template and call it books.

New Project

Define the application manifests

Today, we're building a service to store and retrieve information about books. To keep it simple, it will only have two services: the ruby API, and a database to store the books. When building a new application, I like to begin by defining a 'skeleton' of the app. This allows me to deploy a minimalistic version of my application into my development environment, and then iterate directly there.

Our rest API will be using sinatra (a lightweight DSL for quickly creating web apps) and mysql to handle the data. So we'll start by creating a Gemfile with our dependencies. I'm also going to add ruby-debug-ide, so we can debug our application remotely (more on that later).

# Gemfile

source 'https://rubygems.org'

gem 'sinatra', require: 'sinatra/base'
gem 'sinatra-contrib'
gem 'activerecord'
gem 'mysql2'
gem 'sinatra-activerecord'
gem 'rake'
gem 'puma'

group :development do
  gem 'ruby-debug-ide'
end

Next, let's create the initial version of our service. Create a app.rb file with the following content:

# app.rb
require 'sinatra'
require 'sinatra/json'

class BooksApp < Sinatra::Base
  mime_type 'application/json'

  configure do
    set :port, 8080
    set :bind, '0.0.0.0'
  end

  before do
    content_type :json
  end

  get '/' do
    p '{"hello": "world"}'
  end

  run! if $0 == app_file
end

Now that we have the initial code, let's define the Dockerfile of the ruby API.

# Dockerfile
FROM ruby:2.7.1

WORKDIR /usr/src/app
COPY Gemfile /usr/src/app
RUN bundle install
COPY . /usr/src/app
EXPOSE 8080
CMD ["ruby", "app.rb"]

In this post, we're going to be using docker-compose, since that's a format that's pretty well known, and works great with Okteto Cloud. That being said, the approach described in this post will work with any other deployment tool/format, such as helm charts, or Kubernetes manifests.

Create a file named docker-compose.yaml at the root of your repository, paste the content below, and save it.

# docker-compose.yaml

version: "3"
services:
  books:
    image: okteto.dev/books
    build: .
    ports:
      - 8080:8080
    environment:
      MYSQL_DATABASE: books
      MYSQL_USER: okteto
      MYSQL_PASSWORD: passw0rd
    depends_on:
    - db

  db:
    image: mysql:8
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_DATABASE: books
      MYSQL_USER: okteto
      MYSQL_PASSWORD: passw0rd
      MYSQL_ROOT_PASSWORD: r00t
    volumes:
      - books-data:/var/lib/mysql

volumes:
  books-data:
    driver_opts:
      size: 1Gi

Finally, let's build and deploy our development environment using the okteto CLI. Open a terminal in RubyMine, and run the command below:

okteto stack deploy --build

This command will build the image using Okteto's remote build service, push it to Okteto's registry, and then deploy the application we just created. Once the application has been deployed, log in to your Okteto Cloud to see the status and to access the endpoints.

Development Environment deployed

Define your development-time configuration

Now that we have a skeleton of our application, it's time to write some code. But instead of going old school and developing locally and then testing the changes remotely, we're going to develop our application directly in our remote development container.

Okteto uses a remote development container to help you build your applications directly in the cloud. Instead of having to write code, then build a container, and then deploy a new version of your application to see your changes, you simply write code and see the results of it reflected instantly in your remote development environment.

At a high level, a remote development container is a Docker container that contains everything you need to build and develop your application, such as:

  • One or more language runtimes (e.g., ruby, python, node)
  • SDKs for your language runtime (e.g., JDK, python-dev)
  • Binary dependencies (e.g., OpenSSL, git)
  • Tools to manage and install dependencies (e.g., bundler, rvm, bundler, yarn)
  • Tools to run your tests and analyze your code (e.g., rspec, cucumber)

Okteto looks for a particular file called okteto.yml to understand how to create your development container.

Create a file named okteto.yml in the books project and copy the following content:

name: books
command: bash
volumes:
- /usr/local/bundle/cache
sync:
- .:/usr/src/app
remote: 22000
forward:
- 8080:8080
- 1234:1234
- 3306:db:3306

This file is telling okteto the following about our development container:

  • That it's for the books service.
  • To run bash, so we can get a remote terminal.
  • To persist /usr/local/bundle/cache, so we can cache our dependencies.
  • To synchronize the contents of our local folder with /usr/src/app in the remote container.
  • To start a remote SSH server on port 22000, so RubyMine can SSH into the remote container.
  • To forward port 8080 to the remote environment, in case we want to access our API over localhost:8080.
  • To forward port 1234 to the remote environment, so we can start a remote debugging session with RubyMine.
  • To forward port 3306 of the db service to the remote environment, in case we want to use a local MySQL client.

Take a look at the manifest reference to learn the different configuration settings available.

Development time

Let's launch our development container. First, open a local terminal directly in RubyMine. Then, run the okteto up command on it to enable developer mode in the ruby service. When you enable developer mode, okteto will relaunch your service, applying all the configurations defined in your okteto.yml manifest on top of your existing configurations. All the other parts of your application (e.g. the database service, endpoints, etc...) will remain as is.

Since this is the first time you launch your development environment, the okteto CLI will ask you to create the .stignore file. Okteto uses this file to know what files to synchronize and which ones to skip. Type y and press enter to continue.

Development Environment Activated

As long as okteto up is running, Okteto will keep your remote terminal active, your code synchronized, and a SSH session connected.

Use your remote development container as an interpreter

By default, RubyMine will use your local ruby interpreter for your project. Instead of that, we will configure it to use our remote development container as the target directly. This way, we can guarantee that we always have the correct setup, independent of what happens in our local machine. To do this, we are going to take advantage of RubyMine's remote interpreters and Okteto's remote SSH server.

To add a remote interpreter:

  1. Open the Settings/Preferences dialog and go to the Language & Frameworks > Ruby SDK and Gems page.
  2. Click the Add icon and select New remote… in the drop-down.
  3. In the invoked dialog, select SSH from the options and click on the Add Interpreter... option in the menu.
  4. Fill in the following values in the SSH Configurations dialog, and press the OK button.

Add Interpreter

When you run okteto up the first time, Okteto will create an SSH key pair for you and save it at $HOME/.okteto/id_rsa_okteto and $HOME/.okteto/id_rsa_okteto.pub. Okteto will automatically configure the SSH server in your development environment to use these keys for authentication.

  1. Fill in the following values in the Configure Remote Ruby Interpreter dialog, and press the OK button.

Add SSH Interpreter

  1. Select the interpreter we just configured.

Finish the interpreter's configuration

  1. Specify mappings between files of a local and remote project. To do this, click the Edit Path Mappings button on the top. In the Edit Project Path Mappings dialog, specify the local and remote project root paths.

Edit Mappings

  1. Press the OK button to finish the interpreter configuration.

From now on, your project will directly use the interpreter in your remote development environment instead of the local one. To try it out, simply right click in app.rb and select Run App.

Application running directly in the remote container

Develop your application in your remote development environment

Now that we have our development environment up and running, it's time to build our API. To keep things simple, we'll be using ActiveRecord to handle our data layer.

First, we'll create config/database.yml file to load our MySQL credentials:

# config/database.yml
development:
  adapter: mysql2
  encoding: utf8
  pool: 5
  username:  <%= ENV['MYSQL_USER'] %>
  password: <%= ENV['MYSQL_PASSWORD'] %>
  database: <%= ENV['MYSQL_DATABASE'] %>
  host: db

You'll notice that we're using environment variables to get our DB connection information. Where are they coming from? Well, one of the benefits of using okteto up when developing is that your development container automatically inherits all the configuration settings of your services. Every environment variable we defined on our docker-compose.yaml early on is available here. Just like in production!

Open app.rb, require activerecord, and establish the connection to our database:

# app.rb
require 'sinatra'
require 'sinatra/json'
require "sinatra/activerecord"
class BooksApp < Sinatra::Base
  mime_type 'application/json'

  register Sinatra::ActiveRecordExtension
  set :database_file, 'config/database.yml'

  configure do
    set :port, 8080
    set :bind, '0.0.0.0'
  end

  before do
    content_type :json
  end

  get '/' do
    p '{"hello": "world"}'
  end

  run! if $0 == app_file
end

Now create a Rakefile file and require the rake tasks as well as your app:

# Rakefile
require "sinatra/activerecord/rake"

namespace :db do
  task :load_config do
    require "./app"
  end
end

With this, we can create the first version of our database schema. Using the terminal where we ran okteto up earlier, run the command below:

root@books-5cb9d464f8-qlhz6:/usr/src/app# bundle exec rake db:create_migration NAME=create_books

This will create a migration file in your migrations directory (db/migrate), ready for editing. Notice how, even though we created the command on our remote development container, Okteto automatically synchronized the files to your local folder. Cool no?

Open the migrate script that was automatically created, and update it as shown below. This will create a table called books, with two fields: authorand title.

class CreateBooks < ActiveRecord::Migration[6.1]
  def change
    create_table :books do |t|
      t.string :title
      t.string :author
    end
  end
end

Now run the command below to migrate the database:

$ bundle exec rake db:migrate

With our database and migration scripts configured, let's update app.rb to read and write from the database.

First, we're going to add a hot reloader. This will help us go faster: instead of having to restart the process manually everytime we change it, sinatra will detect any code changes and do it for us.

# app.rb
require 'sinatra'
require 'sinatra/json'
require "sinatra/activerecord"
require 'sinatra/reloader'

class BooksApp < Sinatra::Base
  mime_type 'application/json'

  register Sinatra::ActiveRecordExtension
  set :database_file, 'config/database.yml'

  configure do
    set :port, 8080
    set :bind, '0.0.0.0'
  end

  configure :development do
    register Sinatra::Reloader
  end

  before do
    content_type :json
  end

  get '/' do
    @books = Book.all
    @books.to_json
  end

  run! if $0 == app_file
end

After adding the hot reloader, start the service by right clicking on app.rb and selecting the Run "app" option. Thanks to our hot reloader, this is the last time we need do to this. From now on, all we need to do is write code and validate our changes 😎.

Go ahead and add the function to return all the books from the database to app.rb:

# app.rb
require 'sinatra'
require "sinatra/activerecord"
require 'sinatra/reloader'
require 'sinatra/json'

class Book < ActiveRecord::Base
end

class BooksApp < Sinatra::Base
  mime_type 'application/json'

  register Sinatra::ActiveRecordExtension
  set :database_file, 'config/database.yml'

  configure do
    set :port, 8080
    set :bind, '0.0.0.0'
  end

  configure :development do
    register Sinatra::Reloader
  end

  before do
    content_type :json
  end

  get '/' do
    @books = Book.all
    @books.to_json
  end

  run! if $0 == app_file
end

Save the file, and validate that our changes are working by calling the new API (remember to change the URL for your own) from your local terminal:

$ curl https://books-rberrelleza.staging.okteto.net/
[]

We got an empty list because our DB is empty. But now we know that everything works end to end!

Let's take a second to notice all the things that are happening behind the scenes here:

  1. We updated a file in our local IDE, and it was automatically synchronized to our remote enviornment.
  2. We called the new API using an https endpoint (just like our end users would!).
  3. The API is already querying our DB (that's why we got an empty response).
  4. I didn't have to rebuild, redeploy, or even reload my process to see my changes. It all happened automatically!

This is the magic of developing with Okteto: develop in a realistic development environment, with high velocity, and high confidence! 🚀

Let's repeat the process and add a function to add a book to the database:

# app.rb
require 'sinatra'
require "sinatra/activerecord"
require 'sinatra/reloader'
require 'sinatra/json'

class Book < ActiveRecord::Base
end

class BooksApp < Sinatra::Base
  mime_type 'application/json'

  register Sinatra::ActiveRecordExtension
  set :database_file, 'config/database.yml'

  configure do
    set :port, 8080
    set :bind, '0.0.0.0'
  end

  configure :development do
    register Sinatra::Reloader
  end

  before do
    content_type :json
  end

  get '/' do
    @books = Book.all
    @books.to_json
  end

  post '/' do
    new_book = MultiJson.load(request.body.read)
    @book = Book.new( new_book )
    if @book.save
      json @book
    else
      status 500
    end
  end

  run! if $0 == app_file
end

Save the file, and add a new book by executing the code below from a local terminal:

$ curl -XPOST https://books-rberrelleza.staging.okteto.net/ --data '{"title": "telegraph avenue", "author": "michael chabon"}'

[{"id":1,"title":"telegraph avenue","author":"michael chabon"}]

And there you have it! We just built a ruby service that reads and writes from a MySQL database, directly in our remote development environment.

Debug your application in your remote development environment

Debugging your application remotely follows a very similar approach. First, add a debugging breakpoint in your code (e.g. in the response of one of the GET functions). Then, right click on app.rb and the select Debug App. RubyMine will automatically install any required dependencies, and launch the debugger and connect to the remote process.

Go ahead and call your API using curl, or by directly navigating to the endpoint, and see how RubyMine will automatically stop right in your breakpoint!

If you're having dependency issues, you can also follow the instructions for remote debugging described in this post.

Application running under the debugger

Conclusions

In this post, we learned about the concept of remote development environments, why they are essential, and how you can use Okteto and RubyMine to build a Cloud Native application faster than ever.

But this post only covers the surface. Using remote development environments gives you a lot of extra benefits such as:

  • Eliminates the need for a local configuration.
  • Makes it simple to share the configuration with the rest of your team.
  • You don't need to run docker or a database locally.
  • You don't depend on your workstation's state.
  • You can easily share your development environment with your team.
  • It gives you the fastest feedback loop.
  • You use your favorite IDEs, debuggers, etc.
  • You can take advantage of incremental builds and hot reloaders.
  • You are developing in an environment as similar as possible to production.
  • You don't depend on CI/CD for validating all your changes.

If this problem sounds familiar to you, you should check out what we have built at Okteto. Please take a look at our getting started guide and start developing at the speed of the cloud.

Our mission at Okteto is to simplify the development of Cloud Native applications. Does this resonate with you? Do you have ideas, comments, or feedback? Join us at the #okteto channel in the Kubernetes community Slack and share your thoughts with the community!

Ramiro BerrellezaCEO & Co-founder View all posts

Automate Provisioning Any Dev Resource on Any Cloud Provider With Pulumi and Okteto

The Value It is common in today's landscape to build microservices-based applications that leverage resources like RDS databases, storage buckets, etc...

October 19, 2023
Avatar of Arsh SharmaAvatar of Arsh SharmaArsh Sharma

How Developers Can Seamlessly Collaborate When Building Microservice Apps

Building microservices based applications is inherently challenging. Given the multitude of components involved, it is unrealistic to expect any individual...

October 10, 2023
Avatar of Arsh SharmaAvatar of Arsh SharmaArsh Sharma