Remote Development Environments with RubyMine, Okteto, and Kubernetes

Ramiro Berrelleza
Okteto
Published in
12 min readAug 13, 2021
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 loginin 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.

$ okteto loginAuthentication will continue in your default browser
You can also open a browser and navigate to the following address:
...
✓ Logged in as rberrelleza
✓ Updated context 'cloud_okteto_com' in '/Users/ramiro/.kube/config'
Run 'okteto namespace' every time you need to activate your Okteto context again

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.

Create a new project in RubyMine

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 Gemfilewith 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.rbfile 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 Dockerfileof 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.yamlat 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 oktetoCLI. 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.

Your development environment running in Okteto Cloud

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.ymlto understand how to create your development container.

Create a file named okteto.ymlin the booksproject 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 oktetothe following about our development container:

  • That it’s for the booksservice.
  • 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/appin 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 dbservice 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 upcommand 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.ymlmanifest 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 .stignorefile. Okteto uses this file to know what files to synchronize and which ones to skip. Type yand press enter to continue.

Your development environment is active!

As long as okteto upis 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/Preferencesdialog and go to the Language & Frameworks> Ruby SDK and Gemspage.
  2. Click the Addicon and select New remote…in the drop-down.
  3. In the invoked dialog, select SSHfrom the options and click on the Add Interpreter...option in the menu.
  4. Fill in the following values in the SSH Configurationsdialog, and press the OK button.
Add okteto's SSH configuration

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

5. Fill in the following values in the Configure Remote Ruby Interpreterdialog, and press the OK button.

Finish the interpreter configuration

6. Select the interpreter we just configured.

Add the interpreter

7. Specify mappings between files of a local and remote project. To do this, click the Edit Path Mappingsbutton on the top. In the Edit Project Path Mappingsdialog, specify the local and remote project root paths.

Don't forget to set the mappings

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

Add okteto's to your SSH configurations

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.rband select Run App.

Your application running remotely

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.ymlfile 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 upwhen developing is that your development container automatically inherits all the configuration settings of your services. Every environment variable we defined on our docker-compose.yamlearly 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 Rakefilefile 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 upearlier, 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 on your remote terminal:

root@books-5cb9d464f8-qlhz6:/usr/src/app# bundle exec rake db:migrate

With our database and migration scripts configured, let’s update app.rbto 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.rband 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.rband 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!

Debug directly in Kubernetes with okteto

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

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!

--

--