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
.
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.
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.
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:
- Open the
Settings/Preferences
dialog and go to theLanguage & Frameworks
>Ruby SDK and Gems
page. - Click the
Add
icon and selectNew remote…
in the drop-down. - In the invoked dialog, select
SSH
from the options and click on theAdd Interpreter...
option in the menu. - Fill in the following values in the
SSH Configurations
dialog, and press the OK button.
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.
- Fill in the following values in the
Configure Remote Ruby Interpreter
dialog, and press the OK button.
- Select the interpreter we just configured.
- Specify mappings between files of a local and remote project. To do this, click the
Edit Path Mappings
button on the top. In theEdit Project Path Mappings
dialog, specify the local and remote project root paths.
- 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
.
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 ourdocker-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: author
and 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:
- We updated a file in our local IDE, and it was automatically synchronized to our remote enviornment.
- We called the new API using an https endpoint (just like our end users would!).
- The API is already querying our DB (that's why we got an empty response).
- 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.
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!