 For coming to my presentation, today I want to talk about building serverless Ruby bots. So some time ago, I was looking for an apartment to rent in the center of Zagreb. For those of you that don't know, Zagreb is the capital city of Croatia, which is a country located in central Europe. If you want to rent apartments in Croatia, you check out listings on an online classified platform that's called NewSkolo. NewSkolo is basically the same thing as Craigslist you have here in the US. I had a problem while trying to find an apartment. I had a hard time forcing myself to check out the classifies each day multiple times. Well, you probably all know this, but the good apartments usually get rented out within an hour or two. So to tackle my problem, I decided to build a small bot that would scrape that web page with all the listings every five minutes and shoot me an email when a new apartment comes around. This technique was actually really successful because I was basically always the first one to call the advertisers and I managed to find an apartment fairly quickly. So the technical background of the bot was really simple. I used a couple of gems. I used HTTP Party to fetch the content of the web page. I used the Ogre gem, which is an HTML parser really similar to Nocogiri, but I don't know, better. Then I used the ActionMailer gem to shoot myself an email. I wrapped it up all in a rake task and invoked that rake task with the benevolver gem. And the script ran on my local computer and that was good enough for me. If I'm not on my computer, I rarely check my emails. After some time, I decided to reuse the script and I wanted to run it consistently every five minutes, no matter if my computer is on or off, so I had to move it from my personal computer to a server. However, I didn't fancy the idea of renting a server just to run a simple Ruby script, enter serverless. I knew at that point that the ideal infrastructure for what I need would be a serverless one, a place where I could just put my function on and run it periodically without taking care of servers. Thankfully, Amazon, Microsoft, Google, IBM and many other vendors provide the infrastructure for this. It's usually called serverless or FOSS, function as a service. And I wanted to try out the most used FOSS and the pioneer in the area, so I tried AWS Lambda. AWS Lambda lets you run code without provisioning or managing servers. You upload your code in a zip file. You set some triggers to run it and that's it. You're up and running in a couple of minutes. It executes your code only when needed. It scales automatically and you pay only for the compute time you consume. Now, Lambda's got support for multiple languages such as Python, Java and NodeJSC sharp, but it has one problem, it doesn't support Ruby. So the AWS team asked the public more than three years ago on Twitter, whether they should first support Ruby or Golang. And unfortunately for us, the vote was 54% for Golang and 46% for Ruby. And then more than two years later, this tweet came out announcing Golang support. It took them a while to add it, basically over two years. And so I would guess that Ruby support isn't that near at all. There have been some rumors about it, but there are no official announcements. So no Ruby. I wanted to stick to Ruby and try it out on Lambda. Although there isn't any official support for Ruby on AWS Lambda, you can run JavaScript, Java or Python and invoke shell commands. And since we can invoke shell commands, that means we can rep our Ruby script and package it up and run it on AWS Lambda. So there are at least three, but actually more ways to package Ruby and run it on AWS Lambda. I tried these three, TravelingRuby, MRuby and JRuby. So let's introduce each of them. So TravelingRuby. If you ever wanted to distribute a CLI built in Ruby, your end users would have to have Ruby installed or God forbid have a Ruby version manager installed or something like that. And they would have to install Bundler and they would have to install all of the gems. And then if you do an update, they would probably have to do some parts of that again. TravelingRuby is a project that tackles the problem of distributing CLI apps. TravelingRuby is a project by Devs from Fusion that support portable Ruby binaries that can run on any Linux, this through OS 6 or Windows. It allows Ruby app developers to distribute a single package to end users without the end users needing to install Ruby or any of the gems. Now, MRuby is something totally different. MRuby is an interpreter for the Ruby programming language with the attention of being lightweight and easily embeddable for various microcontrollers and IoT devices. It's created by Maths and its first letter might stand for either Maths or minimal or embeddable, however contrived that might be. So MRuby hands out various ways to run Ruby code and they're easily packaged in an executable binary that can be run on any Linux, this through OS 6 or Windows, so it's not a problem to run it on Lambda. And then JRuby, unlike our canonical Ruby implementation, JRuby runs on the JVM, the Java virtual machine. It provides a way for us to embed Ruby into any Java application and have two way access between Java and Ruby code. And since AWS Lambda supports Java, it's possible to run JRuby on it. So I wanted to try all the rubies and basically check out how they perform. Since we have three totally different implementations, I decided to write a minimalistic bot that has all of its code in a single file with minimum dependencies basically depending only on the standard library. So the first thing I had to do was shrink the code base. I had a bunch of gems in, five, and I needed to get rid of them. So a jump file looked like this. I easily got rid of HTTP Party cuz I could just use net HTTP. It's good enough. And I got rid of ActionMailer, again using net HTTP, just because I used Mailgon and Mailgon has an API which you can just ping over HTTP. And I got rid of whenever and rake cuz Lambda's got its own scheduler. And the only one I couldn't get rid of was the Ogre gem. So since the web page that I was getting the data from didn't have a JSON API or anything like that, you could say it has an HTML API. Like, I had two parts that HTML somehow. And JRuby and traveling Ruby have Nocogiri and Ogre, which you can use. But mruby on the other side, that's so much. So I went on and tried to find a HTML parser in mruby and didn't manage to. What I did eventually was post a question on the official mruby repository asking whether there's some HTML parser in mruby and who answered actually maths. And he said, you have three ways to tackle this. If it's really simple, you may use regular expressions. And I was kinda like, yeah, well, I don't wanna use regular expressions for parsing HTML that I could go, it's a slippery slope. Then the second one was supporting a simple Ruby HTML parser, which unfortunately I didn't find cuz most of the HTML parsers that are in Ruby actually have C level dependencies. So I don't know, it would be really hard to do that. And the third one, the hardest one is to write a wrapper over libxml, which is the hardest one. And which would basically mean re-implementing no kogiri in some way in mruby. So I looked at how the listings look like, how the HTML looks like. I saw that each listing has a time element with a date diamond attribute and against all of my beliefs, I said, okay, regular expressions it is. So let's look at what the Ruby bot code looks like. It's really simple. So I had a simple class called new apartments that would get a URL to scrape off of, which would basically be a URL that says, I want apartments in this region. It has to have this square footage. It has to be under this price, so on and so on. I shoot out an HTTP request and scan whether there's any new apartments that have been published in the last five minutes. And I had a simple class that was just shooting an email and I glued it all together by basically calling if there are any new apartments by the search URL called the mail gun emailer. And that's 60 lines of code, super simple. So let's start with the first implementation. Implementing the bot with traveling Ruby or the packaged MRI runtimes. What do you need when you want to use traveling Ruby? What do you need to do is actually download the runtimes first. You have to download the runtimes for each platform that you're gonna run your bot on. So I had to download for two platforms, an OS6 for my OS6 machine, my Dev Machine and Linux for running it on AWS Lambda. You can easily curl that down from an S3 repository provided by Fusion. Then you just pull it down and once you delete the zip files and unpackage it, your boilerplate bot code would look like this. Or if we expand it, we can see that each of those platform dependent folders has a bin folder inside of it and a Ruby script that's used for running. Whereas basically the Ruby runtime is. So if I copy over my bot code to a file called main RB, the structure would look something like this. And if I wanted to execute the bot with the Ruby that's installed in my machine, I would just do this. And if I wanted to invoke it with the Ruby that I pulled off of the S3, I would just do this and just check whether everything's working. And everything worked perfectly fine from the beginning. So that's not all that you need to do to upload the bot to AWS Lambda. You actually have to, since Lambda unfortunately does not accept direct shell commands, you need to create a wrapper over it to execute it. Actually, they don't write this wrapper, similar ones can be found online. What it basically does is spawns a child process executing that shell command. And that's it. And after that, you need to zip it all up or package it all up in a zip file to upload it to AWS Lambda. And what you need to do is package three things, the runtime, the source code, the main RB file, and the JavaScript wrapper. And after that, you have to go to the AWS stand in panel. And there are five easy steps to follow and just to configure your AWS Lambda serverless function. So creating a function, you have to just name it. Select a runtime. Then after that, you have to select a trigger. There's several ones to choose from, like the API gateway so you can ping your serverless bot, but we'll choose the CloudWatch events scheduler that enables us to run our bot periodically. Then we need to create a rule for the trigger, which is basically, we'll just name it and run it for one minute or five minutes or whatever. And upload the bot code in a zip file. And the last thing you need to do is set a handler. So what's a handler? You have to explicitly tell Lambda to look into your zip file, check for a file, and check for the method that's in and invoke it that way. And that's it. We can now harvest the benefits of our Ruby bot and we can get our email whenever a new apartment shows around. So implementing the bot with M-Ruby, this one was interesting. So there are two ways to build apps with M-Ruby. You can download the M-Ruby source directly from GitHub, or you can use the M-Ruby CLI, which is a platform for building native command line applications for Linux, OSX, and Windows. So I decided I didn't want another level of abstraction and wanted to get a little bit deep down. So I said I'm going to build it from the M-Ruby source. So what I did was clone the M-Ruby repo. Once I cloned it, I got a bunch of files, but these three are most interesting. I got an empty bin folder, a build config file which acts as a gem file or a gem spec, and a mini rake file. And this mini rake file, once you invoke it, all of a sudden your bin folder is not empty anymore. It has an M-Ruby file and an MIRB file. What it does is it actually creates run times based on the build config and your operating system and creates an M-Ruby file, which is the equivalent of Ruby. And MIRB, which is M-Ruby's IRB. Now, if I were to just create a simple hello world file and if I wanted to invoke it with M-Ruby, I would just do bin M-Ruby hello world Rb. And that's it. That works. But so I copied the code over to the bot code over to the main Rb file. And I all of a sudden got undefined method require for main. So that was like the error on the first line. And M-Ruby doesn't support the require method because it works kind of differently. So remember how I said in the intro how M-Ruby is lightweight? Well, it really is. It doesn't, it's meant for embedded systems. And it's, it doesn't come with everything included. So some of the things that you might have in the standard library, you would have to add M-Ruby gems for it. So I had a couple of issues. I had to find replacement for net HTTP. M-Ruby doesn't have out of box support for environment variables or even for regular expressions. So I had to add to our build config file all of those dependencies. I easily found them. And I had to recompile my, my binaries with the mini rake command. And that kind of worked. I had to do some tweaks like custom parts. Like I had to build a custom parcel for dates and stuff like that. But after 15 minutes, it worked on my machine. So executing the code with this would send an email to myself. That was great. But now I had to upload it to AWS Lambda, which means I had to do cross compilation because I'm running on OS 6 and, and Lambda's running on Linux. Now, I basically just recompiled it on a Linux machine and zipped it all up the same way as I did with the traveling Ruby. And that's it. It worked perfectly. The remaining instructions are basically the same as if I was uploading traveling Ruby. And now implementing the bot with JRuby. So there are several, several ways. I think the most easiest way that I found was there's an AWS Lambda JRuby repository, which already has a pre-packaged formula for building Ruby functions on AWS Lambda with JRuby. So once you clone it, you're, you're going to see you have a couple of things in it. It's going to be a lib folder with a JRuby jar with has, which has the JRuby runtime. Going to have a source folder where you can put all of your main RB files and you're going to have a Gradle task to compile all of that together into a zip file. So if I were to paste my code into that main RB file and just build the project with the simple Gradle command, I would get a zip file. Just one thing to note, I had some trouble doing this. I tried doing it on my OSX machine. I guess I maybe had different versions of Java or something like that. So I ended up actually compiling all of that or building all of that on a Linux machine as well. And now the only difference between uploading the previous bots is that we're going to use Java 8 instead of Node.js. And that's it. Although there isn't any official support for Ruby, we managed to run three different versions or three different implementations of Ruby on top of AWS Lambda. That's great. So let's talk a bit about metrics and numbers and compare these three approaches and code size, memory consumption, and speed. And afterwards I'll explain Lambda's pricing. So as expected, the least code size we got was with M-Ruby, and that's no surprise at all. However, all three are OK in terms of not exceeding Lambda's restrictions on the code size per function, which is 50 megabytes. Then the memory consumption. I was kind of surprised that M-Ruby had a larger memory footprint, but your mileage may vary definitely. This was my case. I'd also expect the memory consumption of J-Ruby to be the highest, however I didn't expect it something simple like this to be 150 megabytes. The only thing here is with J-Ruby is that it goes over the basic billing plan, so the 128 megabyte model. So you would have to opt in for a more expensive plan. And I even had issues running it on anything that's below 448 megabytes, so I'm not sure what the issue is here. And then execution time, and I'm going to explain what the cold start is and what a warm-up is. So M-Ruby is not supposed to be faster than regular Ruby, and our numbers actually prove it. And J-Ruby, when you first try to execute it, is a lot slower than the two, and that's no surprise at all. Even a simple print hello world program in J-Ruby can take up to five seconds, simply because of the long warm-up the JBM needs. Let's talk a bit about cold start. So when your code is triggered for the first time, a cloud provider initializes a new container, and it needs to basically warm up, it needs to do all the work, and on subsequent calls, it reuses the same container unless like roughly four hours past, or something like that. What it basically does is it shoves the runtime in a request context, and it reuses it on subsequent requests. So after it's warmed up, traveling Ruby and M-Ruby have about the same execution time as they did on a cold start. However, J-Ruby outperforms them really well. It performs it in 1.2 milliseconds. Sorry, 1.2 seconds. And as said, your mileage may vary. These are my numbers. I have two I-O calls. One to scrape the web page. One to shoot an email. And so it really may vary for other cases. So why do these metrics even matter? So AWS Lambda's pricing. AWS Lambda's pricing is a two-tier pricing. It basically looks at it at two levels. First one is the number of requests, or the number of times your function was called. And the other one is the sum of durations of our functions took to execute, and the memory allocated expressed in gigabytes seconds. So on a free tier, you get 1 million requests, and 400,000 gigabytes seconds per month. With 1 million times per month, we could roughly run this function every three seconds. And the 400,000 gigabytes seconds per month might be kind of confusing. Basically what it is is the memory size multiplied by the amount of seconds your functions take to execute. Let me show you an example. If you have a serverless function that has 120, you allocated 128 megabytes for it, and it runs for four seconds, it's going to consume 0.5 gigabytes seconds. So if we ran the bot every minute, and this is kind of roughly for all three the same, we would spend around 20,000 gigabytes seconds. JRuby outperforms, and Ruby and traveling Ruby on subsequent requests. However, it does allocate more memory, so it kind of comes up to the same thing. So if our bot spends 20,000 gigabytes seconds, that's basically nothing. We have 400,000 of those on a free tier, and that's a really good bargain. So choosing the right Ruby for the job. Traveling Ruby is really cool. It has low memory consumption, well known environment. However, the support is not great. There has been some activity lately, but there hasn't been no active maintaining or releases of new versions, and there's a discussion on their GitHub repo, what's going to be with the future of it. And then M-Ruby is really cool, because it has low memory consumption. It's lightweight. However, the library support isn't that great. I had to try four different HTTP libraries, and each of them had their own problems, especially with HTTPS. One point I resorted to just curling stuff. So that's kind of a problem. And cross compilation, you have to do that initially. But sorry, not initially. You have to do that every time you change your code. But there's the M-Ruby CLI, which should take care of all of that for you, so you can give that a try. And then JRuby, the simple packaging with an asterisk, because once you get it right, it's really simple. But it's kind of hard to get it right, at least for me it was, because I had to switch machines. As I said, some Java version wasn't really working great on my computer. And it's got fast execution, which is really great after the warm-up it takes. However, it has high memory consumption. And one thing that I didn't like that I had kind of hard time debugging is if I get Java errors, I basically don't know what to do. So it was a lot of trial and error. So I talked about AWS Lambda, and I talked about these three Ruby versions. So let's talk a bit about some alternatives. There's Ruby Packer, which is a project that compiles a whole application into one executable. I haven't used it myself, however, as much as I know some companies do use it in production. And then there's Ruby Snap, which is used for packaging and distributing apps. It's worked on by Hiroshi from the Ruby Core team. It's only used for Linux. And it's still a work in progress. But if you want to see it, it's in the main organization, in the main Ruby Core organization on GitHub. You can check the source out. You can check the source out there. So I talked a lot about Lambda. So let's mention some alternatives to that. There's Azure Functions, which have a similar language support, or even now even more language is supported. However, not that many integrations as Lambda has. And it has a pending feature request on their GitHub to support Ruby, however, it's not even in the experimental stage on their platform. So I'm not sure when it's going to be there. And then there's Google Cloud Functions, which just came out of beta a couple of months ago. But they, however, only support Node.js and Python. And then there are options to run Ruby with Docker on some of these serverless platforms using Kubernetes framework called KubeList, which is a serverless framework built on top of Kubernetes. However, to be honest, that is not the simplicity I would look for when trying to run a serverless function. So you can definitely say there are various different options. It's really interesting. However, none of them really seem ideal. You could argue most of them are really hacky. None of them is really, really that good. So that all kind of changed about a month ago to about two months ago when Apache OpenWisk, an open source serverless platform, announced that they are coming with Ruby support. And that's great. But to be honest, I wasn't that hyped about it at first because I was like, OK, it's an open source serverless platform, which means that I would have to host my serverless function and take care of servers for my serverless thing and kind of didn't make sense to me. I think I found out really quickly after that is that IBM Cloud Functions are actually built on top of Apache Open Wisk, which makes IBM Cloud Functions, or IBM, the first provider to support Ruby on their serverless platform, which is great. When you want to create a serverless function, you get a dropdown menu, and you can select Ruby 2.5 on there, which is really great, but they're really up to date with that. And they even offer an inline editor with syntax highlighting. They could have some improvements. It's really early on. Like if you do a syntax error or whatever, it just yells out, we couldn't run Ruby, and that's it. But I guess that those improvements are going to come soon. And another great thing that came, I think, last week is the serverless framework support. So the serverless framework is the most popular serverless framework for building serverless applications on top of AWS, Lambda, Apache OpenWisk, Azure Functions, and basically all of these major providers. So what you can do is run serverless create with a template OpenWiskRuby, and I think this template option should land this week in and generate some boilerplate for your serverless function. It gets a couple of files. The two main ones are the handler RB, which you can put your Ruby function in, and a serverless YAML file, which is just a configuration file where you have to say what the plugins are, what the runtime is, how to invoke it, and that's it. In a couple of minutes, you just have to do serverless deploy, and it's on IBM Cloud Functions, which is really great. And support like this is great in terms of when maybe AWS, Lambda, and other major providers start supporting Ruby. Then there's a Jets framework, which was announced a couple of months ago. It's a serverless framework for Ruby. I thought it was kind of new, but the author only announced it I think in August or September, but it actually has more than 2,200 commits, which is kind of like a huge Ruby project. And what it does is you can generate an application which really looks like rails in some ways, like you have a router, and you have a controller, and each of those routes maps to a separate Lambda function, and the author even fixed some of the warmups, so subsequent calls should be pretty efficient, because he's basically shoving the Ruby runtime into the request contacts from AWS Lambda. And then the last thing that is really cool, which also came about a month or something ago, is FastRubyIO, which is a hobby project by Paulo Arruda. He basically built a serverless platform on his DigitalOcean account and a Ruby CLI, so you can just generate a Ruby serverless function and deploy it in a couple of minutes. Note, this is a hobby project, as much as I understand. This is on the author's DigitalOcean. He might have some ways to expand that, but there are currently no ways to access your functions or a dashboard or something like that, but it's a really, really cool proof of concept. So, in conclusion, this was a really fun experiment for me to try multiple flavors of Ruby. I'm glad that I had the opportunity to show you that you can run Ruby on any of these serverless providers, no matter if they actually support Ruby or not. And this doesn't have to only affect serverless if you have maybe some platform that doesn't have Ruby. Maybe these are some of your ways you can do that. However, personally, I think this is far from perfect, and we're not there yet, but hopefully we will be soon. IBM's move is a great move forward, and there's a lot of new things happening in the serverless area related to Ruby, especially in the last couple of months. And hopefully this will push all the other major providers to go forward with supporting Ruby. If you want to see all the major providers supporting Ruby, there's an open petition on serverlessruby.org that has almost 1,500 signatures. You just log in with your GitHub, and the purpose is to get all of these big vendors to support Ruby. That's all folks. My name is Damir Svartan. As much as you understood from the beginning of my presentation, I come from Croatia, and that's where I lived up until six months ago. Since then, I moved to the San Francisco Bay Area where I started to work for Netflix. Netflix is currently hiring Ruby developers on multiple positions, and if you're interested in hearing more about it, please feel free to catch me after the talk or just visit this URL. That's it. Thank you.