<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Igor Alexandrov</title>
    <description>The latest articles on DEV Community by Igor Alexandrov (@igor_alexandrov).</description>
    <link>https://dev.to/igor_alexandrov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F74083%2F8a192ed4-11a9-4803-a7cd-e6ede5c90431.jpeg</url>
      <title>DEV Community: Igor Alexandrov</title>
      <link>https://dev.to/igor_alexandrov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/igor_alexandrov"/>
    <language>en</language>
    <item>
      <title>How To Use Basecamp’s MRSK With AWS and GitHub</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Fri, 23 Jun 2023 13:57:12 +0000</pubDate>
      <link>https://dev.to/igor_alexandrov/how-to-use-basecamps-mrsk-with-aws-and-github-548p</link>
      <guid>https://dev.to/igor_alexandrov/how-to-use-basecamps-mrsk-with-aws-and-github-548p</guid>
      <description>&lt;p&gt;Hey fellow devs! 👋 Just published a new article on deploying Rails apps like a breeze! 🚀 Check it out here: &lt;a href="https://jetrockets.com/blog/how-to-use-basecamp-s-mrsk-with-aws-and-github"&gt;How to Use Basecamp's MRSK with AWS and GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I spill the beans on how you can leverage MRSK, AWS, and GitHub to streamline your Rails deployments. No more headaches or complicated setups! 😎&lt;/p&gt;

&lt;p&gt;Read the article to discover practical tips, tricks, and insights to make your deployment process smooth and efficient. Let's level up our dev game together!&lt;/p&gt;

&lt;p&gt;Join the conversation in the comments. Can't wait to hear your thoughts and experiences. Happy deploying! 💻💪&lt;/p&gt;

</description>
      <category>rails</category>
      <category>deployment</category>
      <category>kamal</category>
      <category>aws</category>
    </item>
    <item>
      <title>Scaling Rails: Docker &amp; AWS Beanstalk</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Wed, 20 May 2020 11:44:00 +0000</pubDate>
      <link>https://dev.to/jetrockets/scaling-rails-docker-aws-beanstalk-1lfl</link>
      <guid>https://dev.to/jetrockets/scaling-rails-docker-aws-beanstalk-1lfl</guid>
      <description>&lt;p&gt;Amazon AWS has always been our preferred infrastructure provider. We love to use AWS EC2 for our applications, AWS RDS for databases, and AWS ElastiсCache as the ‘key-value’ storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminology&lt;/strong&gt;&lt;br&gt;
Due to NDA and security-related restrictions we are unable to use actual project and client names, so here is some terminology we will use in this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X-Project – this will be the name of our project;&lt;/li&gt;
&lt;li&gt;x-project.com – the main domain;&lt;/li&gt;
&lt;li&gt;git://x-project.com/x-project-server – the repository with the Rails application;&lt;/li&gt;
&lt;li&gt;git://x-project.com/x-project-ci-image – the repository with the Docker images;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a better experience of exploring the code, we pulled all the config files out to a separate repository: &lt;a href="https://github.com/jetrockets/rails-elastic-beanstalk"&gt;https://github.com/jetrockets/rails-elastic-beanstalk&lt;/a&gt;. This repository contains two directories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x-project-ci-image&lt;/code&gt; contains files that were used in git://x-project.com/x-project-ci-image;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-project-server&lt;/code&gt; contains the files that were added to our Rails application from git://x-project.com/x-project-server.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inside &lt;code&gt;x-project-server/eb/production&lt;/code&gt; we have configs of all three of our EBS projects. An EBS project usually consists of two main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;code&gt;.elasticbeanstalk&lt;/code&gt; directory with the global scenario files for the project configuration and the Elastic Beanstalk environment: name, instance types, variables of the environment and the environment settings;&lt;/li&gt;
&lt;li&gt; &lt;code&gt;.ebextensions&lt;/code&gt; directory which contains a collection of scenario files for instance configuration. These files represent a set of instructions that are loaded automatically upon creation of a new instance or update of an existing one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;br&gt;
Usually we deploy our servers on EC2 using Ansible scripts and Capistrano, which is a common approach. However, this approach may lead to a state that Martin Fowler referred to as SnowflakeServer. Over time, the configuration of servers starts to have too many dependencies and it becomes very difficult to replicate quickly. That's why even if you're not using Docker on EC2 machines, it's vital to make all changes to the server by modifying the Ansible scripts.&lt;br&gt;
This ensures that if necessary, you can deploy a new environment relatively quickly.&lt;/p&gt;

&lt;p&gt;One of our recent projects was originally focused on a high load that was difficult to predict. We were building a chat room for online stores with enhanced retail sales capabilities. It was a great opportunity to try Elastic Beanstalk. To understand why, let's first look at what it is.&lt;/p&gt;

&lt;p&gt;AWS Elastic Beanstalk is an AWS manipulation service. A preset configuration can quickly set up several application instances, link them to a database or a cache, and set up a balancer between them. With EBS you can run the application code on several platforms: Puma or Passenger for Ruby, Python, Java, and of course you can run Docker containers. We would be satisfied with the Docker container option, so let's explore this further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;br&gt;
Our application was built using the following tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Rails application (web and Sidekiq);&lt;/li&gt;
&lt;li&gt;AnyCable for a websocket chat;&lt;/li&gt;
&lt;li&gt;Embeddable Widgets in HTML and JS which are integrated into client e-commerce platforms;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project required at least two environments: Production (for the live system) and Integration (for demo and testing purposes).&lt;/p&gt;

&lt;p&gt;Let’s start with the Rails app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration&lt;/strong&gt;&lt;br&gt;
By default, Rails 6 stores credentials in separate files specific to each environment. To learn more about this, go to Rails Guides. We opted to use environment variables for managing settings, because this approach could provide certain advantages when working with AWS. For this, we used a gem dotenv-rails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# git://x-project.com/x-project-server/Gemfile&lt;/span&gt;

&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'dotenv-rails'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environment variables are added from the  &lt;code&gt;.env&lt;/code&gt; file in the application root, which is convenient for local development. Moreover, we always store the  &lt;code&gt;.env.sample&lt;/code&gt; file inside the project, which contains a template for all project settings.&lt;/p&gt;

&lt;p&gt;In our experience, using &lt;code&gt;.env&lt;/code&gt; is a more convenient, clear, and consistent way to configure Rails, rather than using rails credentials. You can define the settings in a file, copy the finished file, or run Rails and transfer the settings directly using the command line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container Registry&lt;/strong&gt;&lt;br&gt;
Amazon offers a container management tool – Amazon Elastic Container Registry. Amazon ECR is a fully automated Docker container registry that makes it easy for developers to store, deploy, and manage Docker container images.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting Ready
&lt;/h2&gt;

&lt;p&gt;To work with git, we use a self-hosted version of GitLab, so we wanted to integrate GitLab &amp;amp; GitLab CI into the AWS infrastructure.&lt;br&gt;
We decided to store all the application settings in AWS. This means that GitLab-runner should not have access to these settings and so we had to make a second docker image, which receives the keys to work with AWS while the app is building. To do this, the following Dockerfile should be added to a separate repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# git://x-project.com/x-project-ci-image/aws-utils/Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; docker:latest     &lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; AWS_ACCESS_KEY_ID&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; AWS_SECRET_ACCESS_KEY&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; AWS_REGION&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; python py-pip python-dev build-base libressl-dev musl-dev libffi-dev git
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;awsebcli awscli s3cmd

&lt;span class="k"&gt;RUN  &lt;/span&gt;aws configure &lt;span class="nb"&gt;set &lt;/span&gt;aws_access_key_id &lt;span class="nv"&gt;$AWS_ACCESS_KEY_ID&lt;/span&gt;
&lt;span class="k"&gt;RUN  &lt;/span&gt;aws configure &lt;span class="nb"&gt;set &lt;/span&gt;aws_secret_access_key &lt;span class="nv"&gt;$AWS_SECRET_ACCESS_KEY&lt;/span&gt; 
&lt;span class="k"&gt;RUN  &lt;/span&gt;aws configure &lt;span class="nb"&gt;set &lt;/span&gt;region &lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The settings for  &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; &amp;amp; &lt;code&gt;AWS_REGION&lt;/code&gt; will be based on the GitLab environment variables while the app is building. To accomplish this we created a user in the Amazon IAM with the required permissions. In our case, we selected  &lt;code&gt;AWSElasticBeanstalkFullAccess&lt;/code&gt; (to work with EC2, S3, RDS, and other services), &lt;code&gt;AWSElasticBeanstalkMulticontainerDocker&lt;/code&gt; (to work with Docker images) &amp;amp; &lt;code&gt;ECR&lt;/code&gt;&lt;code&gt;F&lt;/code&gt;&lt;code&gt;ull&lt;/code&gt;&lt;code&gt;A&lt;/code&gt;&lt;code&gt;ccess&lt;/code&gt; (a custom policy with full ECR access). As a result of the app build, this container will be able to work with AWS API according to the set permissions, so the main application will be built on its basis.&lt;/p&gt;

&lt;p&gt;To make it easier to run tests and release the app, we decided to build an intermediate Docker image in order to launch rspec and collect images to run Rails &amp;amp; Sidekiq. This image already has everything you need: nodejs &amp;amp; yarn for compiling JS &amp;amp; CSS, and client PostgreSQL for connecting to RDS. It is also necessary to remember about static distribution, therefore we’ll add Nginx. We use ruby 2.6.5, as Rails (at the time of writing this article) still generates a large number of warnings with ruby 2.7.0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;
&lt;span class="c"&gt;# git://x-project.com/x-project-ci-image/app-image/Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION_IMAGE=2.6.5&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:${RUBY_VERSION_IMAGE}-slim-buster&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP_PATH="/app"&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP_USER=app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_VERSION=12&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; POSTGRES_VERSION=12&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--disabled-password&lt;/span&gt; &lt;span class="nt"&gt;--home&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Install packages&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="se"&gt;\
&lt;/span&gt;    gnupg2 &lt;span class="se"&gt;\
&lt;/span&gt;    lsb-release &lt;span class="se"&gt;\
&lt;/span&gt;    gcc &lt;span class="se"&gt;\
&lt;/span&gt;    g++ &lt;span class="se"&gt;\
&lt;/span&gt;    git &lt;span class="se"&gt;\
&lt;/span&gt;    openssh-client &lt;span class="se"&gt;\
&lt;/span&gt;    libxml2-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libxslt-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libjemalloc2 &lt;span class="se"&gt;\
&lt;/span&gt;    make &lt;span class="se"&gt;\
&lt;/span&gt;    nginx &lt;span class="se"&gt;\
&lt;/span&gt;    pkg-config &lt;span class="se"&gt;\
&lt;/span&gt;    file &lt;span class="se"&gt;\
&lt;/span&gt;    imagemagick

&lt;span class="c"&gt;# Nginx settings&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; /run/nginx.pid &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /run/nginx.pid &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /var/log/nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /var/lib/nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/nginx/conf.d &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /usr/share/nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;unlink&lt;/span&gt; /etc/nginx/sites-enabled/default

&lt;span class="c"&gt;# Forward request logs to Docker log collector&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /dev/stdout /var/log/nginx/access.log &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /dev/stderr /var/log/nginx/error.log

&lt;span class="c"&gt;# Add Node.js repo&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://deb.nodesource.com/node_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.x &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/nodesource.list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb-src https://deb.nodesource.com/node_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.x &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/apt/sources.list.d/nodesource.list

&lt;span class="c"&gt;# Add yarn repo&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://dl.yarnpkg.com/debian/ stable main"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/yarn.list

&lt;span class="c"&gt;# Install packages&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-qy&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    nodejs &lt;span class="se"&gt;\
&lt;/span&gt;    yarn

&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb http://apt.postgresql.org/pub/repos/apt/ &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-sc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;-pgdg main"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/postgresql.list

&lt;span class="c"&gt;# Install dependency for gem pg&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-qy&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=${APP_USER}:${APP_USER} . ${APP_PATH}&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; ${APP_USER}&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GEM_HOME=${APP_PATH}/bundle \&lt;/span&gt;
    BUNDLE_PATH=${APP_PATH}/bundle \
    BUNDLE_BIN=${APP_PATH}/bundle/bin \
    PATH=${APP_PATH}/bin:${APP_PATH}/bundle/bin:/usr/local/bin:$PATH

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; ${APP_PATH}&lt;/span&gt;

&lt;span class="c"&gt;# Install dependences&lt;/span&gt;
&lt;span class="c"&gt;# Ruby-2.6.5 supplied with bundler:1.17.2&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'2.1.4'&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;yarn &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--check-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build of both images is performed in the following scenario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# git://x-project.com/x-project-ci-image/.gitlab-ci.yml

variables:
   RUBY_VERSION_IMAGE: "2.6.5"
   NODE_VERSION: "12"
   APP_PATH: "/app"
   APP_USER: "app"

stages: 
  - build

build_image:
  # Docker image with pre-installed docker package 
  image: docker:latest
  stage: build
  before_script:
    # login to git://x-project.com/x-project-ci-image and pull images from regestry
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    # Pull all images for use caching. 
    # Statement "|| true" used if images not found and pipeline not failing
    - docker pull $CI_REGISTRY/x-project/x-project-ci-image/app-image:latest || true
    - docker pull $CI_REGISTRY/x-project/x-project-ci-image/aws-utils:latest || true

  script:
    # Build docker image with aws utils and and aws secrets
    - docker build --cache-from $CI_REGISTRY/x-project/x-project-ci-image/aws-utils:latest
      -t $CI_REGISTRY/x-project/x-project-ci-image/aws-utils
        --build-arg AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 
        --build-arg AWS_REGION=${AWS_REGION}
        --build-arg AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
        aws-utils

    # Build docker image with pre-installed linux packages, gems and frontend packages    
    - docker build --cache-from $CI_REGISTRY/x-project/x-project-ci-image/app-image:latest 
      -t $CI_REGISTRY/x-project/x-project-ci-image/ruby-image
        --build-arg APP_PATH=${APP_PATH}
        --build-arg APP_USER=${APP_USER}
        --build-arg NODE_VERSION=${NODE_VERSION}
        --build-arg RUBY_VERSION_IMAGE=${RUBY_VERSION_IMAGE}
        app-image

    # login to git://x-project.com/x-project_ci_image for push images
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

    # Push images to git://x-project.com/x-project_ci_image in regestry
    # See git://x-project.com/x-project_ci_image/container_registry
    - docker push $CI_REGISTRY/x-project/x-project-ci-image/aws-utils:latest    
    - docker push $CI_REGISTRY/x-project/x-project-ci-image/app-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything described above can be illustrated in the following diagram. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z0QTOYvE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/18/d325f43d-f7d3-4c23-a28e-9f8d287809c5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z0QTOYvE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/18/d325f43d-f7d3-4c23-a28e-9f8d287809c5.png" alt="Pipeline (ci-image)" width="800" height="1141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an App
&lt;/h2&gt;

&lt;p&gt;We use GitLab-runner for building as well as for the intermediate image.&lt;br&gt;
Building and deployment of the project begin after the code gets into the master or integration branch. Currently, our pipeline consists of five stages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZVdAqMbf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/19/2661b7e2-2a5b-4b88-ade0-4dc6d8ca82d0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZVdAqMbf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/19/2661b7e2-2a5b-4b88-ade0-4dc6d8ca82d0.png" alt="Pipeline" width="800" height="1858"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s skip &lt;code&gt;notify&lt;/code&gt; &amp;amp; &lt;code&gt;rspec&lt;/code&gt; to take a closer look at how &lt;code&gt;build&lt;/code&gt; &amp;amp; &lt;code&gt;deploy&lt;/code&gt; are going.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#.gitlab-ci.yml

docker-build:
  image: $BASE_IMAGES_URL:$CI_PROJECT_NAMESPACE-aws-utils
  stage: build
  environment:
    name: $CI_COMMIT_BRANCH
  only:
    - master
    - integration
  script:
    - $(aws ecr get-login --no-include-email --profile default)
    - aws s3 cp s3://s3.environments.x-project.com/.env.$RAILS_ENV .env
    - aws s3 cp s3://s3.environments.x-project.com/$RAILS_ENV.key config/credentials/$RAILS_ENV.key
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker pull $BASE_IMAGES_URL:$CI_PROJECT_NAMESPACE-app-image
    - docker build -t $REPOSITORY_URL:$REPOSITORY_TAG
      -t $REPOSITORY_URL:$RAILS_ENV-latest
      --build-arg APP_PATH=$APP_PATH
      --build-arg DATABASE_URL=$DATABASE_URL
      --build-arg RAILS_ENV=$RAILS_ENV
      --build-arg REDIS_URL=$REDIS_URL
      --build-arg S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
      --build-arg S3_BUCKET=$S3_BUCKET
      --build-arg S3_REGION=$S3_REGION
      --build-arg S3_SECRET_KEY=$S3_SECRET_KEY
      --build-arg SECRET_KEY_BASE=$SECRET_KEY_BASE
      --build-arg WEBHOOKS_API_URL=$WEBHOOKS_API_URL
      --build-arg WIDGET_SRC=$WIDGET_SRC .
    - docker push $REPOSITORY_URL:$REPOSITORY_TAG

    # "Hypervisor" settings
    - docker tag $REPOSITORY_URL:$REPOSITORY_TAG $REPOSITORY_URL:$RAILS_ENV-latest
    - docker push $REPOSITORY_URL:$RAILS_ENV-latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To build the image we use the previously prepared &lt;code&gt;aws-utils&lt;/code&gt; image with ready-to-go settings for &lt;code&gt;aws&lt;/code&gt;. Then we copy the file with the environment variables from S3 and run the build based on &lt;code&gt;app-image&lt;/code&gt;, which we also built earlier. Thus, we get a universal container that can run &lt;code&gt;rails&lt;/code&gt;, &lt;code&gt;sidekiq&lt;/code&gt;, &amp;amp; &lt;code&gt;anycable-rails&lt;/code&gt;.&lt;br&gt;
To be able to quickly roll back to the previous version of the application without doing an additional build, we use tags with a hash of the commit and the current environment (for example &lt;code&gt;production-2a4521a1&lt;/code&gt;). Each docker image has a tag with the current version marked as &lt;code&gt;:latest&lt;/code&gt;. The same tag is used to run in hypervisors (we’ll talk about them below). Accordingly, a rollback to any version of the system that is stored in the registry takes no more than a minute.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pushing configuration files to AWS EB
&lt;/h2&gt;

&lt;p&gt;So, we have collected the images and now should only deliver them to the Elastic Beanstalk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-deploy:
  image: $BASE_IMAGES_URL:$CI_PROJECT_NAMESPACE-aws-utils
  stage: deploy
  environment:
    name: $CI_COMMIT_BRANCH
  only:
    - master
    - integration
  script:
    - ./bin/ci-deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For convenience, we wrote a small script that runs in the &lt;code&gt;aws-utils&lt;/code&gt; image and therefore can work with the AWS API. So, all that is left to do is run &lt;code&gt;eb deploy&lt;/code&gt; command with the corresponding parameters in the directory, where the &lt;code&gt;.elasticbeanstalk&lt;/code&gt; configuration file is located. &lt;br&gt;
At this stage we have already received an image with a  tag in the format  &lt;code&gt;RAILS_ENV-SHORT_SHA_COMMIT&lt;/code&gt;; it remains to specify ElasticBeanstalk in the configuration file, so that it downloads this particular version. To do that, let’s use &lt;code&gt;sed&lt;/code&gt; and replace the tag with &lt;code&gt;RAILS_ENV-SHORT_SHA_COMMIT&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# bin/ci-deploy.sh

#!/bin/sh
xargs -P 3 -I {} sh -c 'eval "$1"' - {} &amp;lt;&amp;lt; "EOF"
cd eb/$RAILS_ENV/rails; sed -i 's/tag/'$RAILS_ENV-$CI_COMMIT_SHORT_SHA'/' Dockerrun.aws.json; eb deploy --profile default --label $LABEL-rails --timeout 30

cd eb/$RAILS_ENV/sidekiq; sed -i 's/tag/'$RAILS_ENV-$CI_COMMIT_SHORT_SHA'/' Dockerrun.aws.json; eb deploy --profile default --label $LABEL-sidekiq --timeout 30

cd eb/$RAILS_ENV/anycable-rails;  sed -i 's/tag/'$RAILS_ENV-$CI_COMMIT_SHORT_SHA'/' Dockerrun.aws.json; eb deploy --profile default --label $LABEL-anycable-rails --timeout 30
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After successfully uploading the image to Elastic Beanstalk each container starts according to the script from &lt;code&gt;Dockerrun.aws.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Balancers and Autoscaling
&lt;/h2&gt;

&lt;p&gt;Now that we have prepared the environments and configured the deploy process, we are fully ready for production. Running a final performance check, we are seeing WebSocket connection errors.&lt;/p&gt;

&lt;p&gt;A couple of hours of checking EB components and we discover that the AWS Application Load Balancer doesn’t want to route traffic to the anycable-rails port. This is due to a health check failure: incorrect response from the port (204 or 200). Dealing with anycable-rails, it’s clear that it runs with 50051 port and responds via HTTP2/gRPC by default, while ALB is waiting for a reply via HTTP. HTTP health check can be configured in anycable-rails with the &lt;code&gt;--http-health-port&lt;/code&gt; option. However, ALB is not able to do a health check for one port and proxy traffic to another. At this point we realize that we could try to proxy traffic to anycable-rails through Nginx as well as parse the requests for health checks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xrxN_S2F--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/20/bb947b89-1f62-40b1-8ec9-54ce60b65338.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xrxN_S2F--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/20/bb947b89-1f62-40b1-8ec9-54ce60b65338.jpeg" width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After reconfiguring Nginx, and re-checking, the ALB accepts our response for a health check, and traffic successfully goes to the container with anycable-rails. However, right away, we are running into a new error at anycable-go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context=ws Websocket session initialization failed: rpc error: code = Unavailable
desc = all SubConns are in TransientFailure, latest connection error: connection
closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we go back to &lt;code&gt;anycable-rails&lt;/code&gt; to carefully check the logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"PRI * HTTP/2.0" 400 173 "-" "-"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An idea occurs that there might be a problem with ALB, which disassembles HTTPS on its side but sends requests to the host via HTTP.  After creating a self-signed certificate and configuring Nginx to listen to the port in order to forward gRPC to HTTP/2 with the certificate, we get another error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"PRI * HTTP/2.0" 400 173 "-" "-”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We continued EB configuration, made full-fledged end-2-end encryption, but unfortunately this didn’t help. Finally, we tried to work with anycable-rails directly without ALB, and it didn’t work out either. It became clear that the problem was in ALB, though according to the documentation ALB supports HTTP/2. While ALB parses HTTP/2 traffic on its side, it sends requests to the host via HTTP/1.1, and gRPC uses HTTP/2.&lt;br&gt;
Then we remembered that apart from ALB, Elastic Beanstalk has its own second balancer – NLB, which does nothing with traffic, unlike ALB. We decided to try it out. Besides dividing application and anycable we can be more accurate with autoscaling. As a result, we arrived at the following scheme for our service. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FDDsT_Y4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/21/acb53e14-4c6d-4acd-8ddd-0bd2e9f3f59b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FDDsT_Y4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://jetrockets.pro/uploads/posts/uploads/21/acb53e14-4c6d-4acd-8ddd-0bd2e9f3f59b.png" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Hypervisors
&lt;/h2&gt;

&lt;p&gt;Elastic Beanstalk does not imply the use of an EC2 instance as we are accustomed to: you can, of course, connect via ssh with a key, but setting up a system of rights through IAM will be problematic. Only administrators should have access to such instances in order to resolve extraordinary cases.&lt;br&gt;
And the first question is how to give developers access to the console rails.&lt;/p&gt;

&lt;p&gt;We decided to make two micro-instances (t2.micro) on EC2, one for each environment, and called them hypervisors. Developers can use this environment to perform various tasks: launching rails console, connecting to RDS or ElasticCache, viewing logs, and so on.&lt;/p&gt;

&lt;p&gt;We deployed Docker to both instances, so AWS IAM created the user (registry-user) with two policies: &lt;code&gt;AmazonEC2ContainerRegistryReadOnly&lt;/code&gt; &amp;amp; &lt;code&gt;CloudWatchReadOnlyAccess&lt;/code&gt;. The selected policies allow us to download the Docker image from the registry and view CloudWatch logs. To make these operations easier we wrote two tooling bash scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tooling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first script accesses the rails console. The integration and production options differ only in  ECR addresses. It is important in CI to make &lt;code&gt;integration-latest&lt;/code&gt; &amp;amp; &lt;code&gt;production-latest&lt;/code&gt; tags, to download the current image in the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rails c"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="si"&gt;$(&lt;/span&gt;aws ecr get-login &lt;span class="nt"&gt;--no-include-email&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; default&lt;span class="si"&gt;)&lt;/span&gt;
docker pull some-address.dkr.ecr.us-east-1.amazonaws.com/x-project-server:integration-latest
docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; some-address.dkr.ecr.us-east-1.amazonaws.com/x-project-server:integration-latest &lt;span class="nv"&gt;$COMMAND&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second script allows viewing logs from various instances in the console. Docker sends all logs to stdout by default, no one can read them there, so we add them to the file, and stream the file to CloudWatch. The problem is Docker doesn’t send multi-container logs to CloudWatch. To solve this you need to add &lt;a href="https://github.com/awsdocs/elastic-beanstalk-samples/blob/master/configuration-files/aws-provided/instance-configuration/logs-streamtocloudwatch-linux.config"&gt;.ebextensions settings for the CW agent&lt;/a&gt; and configure &lt;a href="https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_docker_v2config.html"&gt;Dockerrun.aws.json for logging&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now when all the logs are in CloudWatch, tooling can be built. To get the logs we use &lt;a href="https://github.com/jorgebastida/awslogs"&gt;awslogs&lt;/a&gt;. Below is a sample script for integration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$minutes&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;span class="k"&gt;fi

case&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;rails&lt;span class="p"&gt;)&lt;/span&gt;
    awslogs get /aws/elasticbeanstalk/Application-integration/var/log/containers/app/integration.log &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'m ago'&lt;/span&gt;
  &lt;span class="p"&gt;;;&lt;/span&gt;
  anycable&lt;span class="p"&gt;)&lt;/span&gt;
    awslogs get /aws/elasticbeanstalk/Anycable-rails-integration/var/log/containers/RPC/integration.log &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'m ago'&lt;/span&gt;
  &lt;span class="p"&gt;;;&lt;/span&gt;
  nginx&lt;span class="p"&gt;)&lt;/span&gt;
    awslogs get /aws/elasticbeanstalk/Application-integration/var/log/containers/nginx/access.log &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'m ago'&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"↓--- NGNIX ERROR LOGS ---↓"&lt;/span&gt;
    awslogs get /aws/elasticbeanstalk/Application-integration/var/log/containers/nginx/error.log &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'m ago'&lt;/span&gt;
  &lt;span class="p"&gt;;;&lt;/span&gt;
  sidekiq&lt;span class="p"&gt;)&lt;/span&gt;
    awslogs get /aws/elasticbeanstalk/Sidekiq-integration/var/log/eb-docker/containers/eb-current-app/stdouterr.log &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;minutes&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'m ago'&lt;/span&gt;
  &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Supported serices rails, anycable, nginx, sidekiq."&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage ./logs ralis 30"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Output will be logs from rails env from the last 30 minutes"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"If output is emply, it seems last N (default 20) minuters no logs from app"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;AWS Elastic Beanstalk seems to be the best solution for scalable Rails hosting. After complex first time configuration every new project can be deployed to it within one hour.&lt;/p&gt;

&lt;p&gt;The most valuable thing is that developers don’t need any additional help from dev-ops team to scale their apps. All needed information about current application performance can be found in AWS dashboards and scaling configuration can be edited directly in web-interface. Another good point is that if your project has peak load only a few days within a month (e.g. report generation by the end of month) you will not pay for not needed instances during the whole month.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>aws</category>
      <category>scaling</category>
      <category>elasticbeanstalk</category>
    </item>
    <item>
      <title>Migrating user passwords from Django to Ruby</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Wed, 19 Feb 2020 06:40:34 +0000</pubDate>
      <link>https://dev.to/jetrockets/migrating-user-passwords-from-django-to-ruby-4339</link>
      <guid>https://dev.to/jetrockets/migrating-user-passwords-from-django-to-ruby-4339</guid>
      <description>&lt;p&gt;One of our clients asked us to migrate his existing Django application to Ruby. A part of this process was a migration of an existing users database.&lt;br&gt;
Of course we had to find a way to use existing crypto passwords from Django and not asking our users to reset them&lt;/p&gt;

&lt;p&gt;Passwords in Django are stored as a string that consists of four parts splitted with a dollar sign.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;algorithm&amp;gt;$&amp;lt;iterations&amp;gt;$&amp;lt;salt&amp;gt;$&amp;lt;hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Django uses the &lt;a href="https://en.wikipedia.org/wiki/PBKDF2"&gt;PBKDF2&lt;/a&gt; algorithm with a SHA256 hash. I found a rather outdated &lt;a href="https://github.com/emerose/pbkdf2-ruby"&gt;Ruby gem&lt;/a&gt; that implements a Password-Based Key-Derivation Function and gave it a try.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migrate_django_password_seamlessly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iteration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;django_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crypted_django_password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;PBKDF2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;password: &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;salt: &lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;iterations: &lt;/span&gt;&lt;span class="mi"&gt;36000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bin_string&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;

  &lt;span class="c1"&gt;# check if hash of user provided password equals to the password in a database&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;django_password&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;password: &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;password_confirmation: &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method above is a part of user sign in service and called only if &lt;code&gt;crypted_django_password&lt;/code&gt;column from &lt;code&gt;users&lt;/code&gt; table is not null. &lt;/p&gt;

</description>
      <category>django</category>
      <category>ruby</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Two edge cases in PostgreSQL full-text search</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Thu, 09 Jan 2020 17:30:38 +0000</pubDate>
      <link>https://dev.to/jetrockets/two-edge-cases-in-postgresql-full-text-search-498e</link>
      <guid>https://dev.to/jetrockets/two-edge-cases-in-postgresql-full-text-search-498e</guid>
      <description>&lt;p&gt;We widely use PostgreSQL full-text search in our projects. It is fast, reliable, and doesn't add any additional technical complexity. But sometimes it may not work as you expect it too.&lt;/p&gt;

&lt;p&gt;Usually we add a &lt;code&gt;ts_vector&lt;/code&gt; column right in our data tables. Let's see how it has it is done.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;tsv&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Handyman&lt;/td&gt;
&lt;td&gt;‘handyman’:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heating &amp;amp; Cooling&lt;/td&gt;
&lt;td&gt;‘cool’:2 ‘heat’:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Painting&lt;/td&gt;
&lt;td&gt;‘paint’:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roofing&lt;/td&gt;
&lt;td&gt;‘roof’:1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Column &lt;code&gt;tsv&lt;/code&gt; is generated with &lt;code&gt;to_tsvector&lt;/code&gt; function from a &lt;code&gt;title&lt;/code&gt; column with a trigger function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;skills_tsv_update_trigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can query this table using &lt;code&gt;to_tsquery&lt;/code&gt; fuction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;skills&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'roof'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 1 record found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A record with &lt;code&gt;Roofing&lt;/code&gt; has been found, and this makes sense. But what should be found for &lt;code&gt;handy&lt;/code&gt; query? You can imagine that it will be a &lt;code&gt;Handyman&lt;/code&gt; skill and you are wrong.  Before explaining why this happens, let's try another query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;skills&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'roofing'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- no records found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No records were found, even if we have a record with &lt;code&gt;Roofing&lt;/code&gt; title in our table.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;to_tsvector&lt;/code&gt; function internally calls a parser which breaks the document text into tokens and assigns a type to each token. For each token, a list of dictionaries is consulted, where the list can vary depending on the token type. The first dictionary that recognizes the token emits one or more normalized lexemes to represent the token. For example, &lt;code&gt;roofing&lt;/code&gt; became &lt;code&gt;roof&lt;/code&gt;. Some words are recognized as stop words, like &lt;code&gt;&amp;amp;&lt;/code&gt; in our example. It is possible to have many different configurations in the same database, and predefined configurations are available for various languages. In our example we used the default configuration english for the English language.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;to_tsquery&lt;/code&gt; function parses user input and converts it to &lt;code&gt;tsquery&lt;/code&gt; data type. It uses the same dictionaries as &lt;code&gt;to_tsvector&lt;/code&gt; function. Let's try to understand what happens in two queries that were listed above.&lt;/p&gt;

&lt;p&gt;The result of &lt;code&gt;to_tsquery('pg_catalog.english', 'handy')&lt;/code&gt; will be &lt;code&gt;handi&lt;/code&gt; because of pluralization rules for words ending on &lt;code&gt;y&lt;/code&gt;. The query listed below doesn't return any records because &lt;code&gt;'handyman':1&lt;/code&gt; vector does not overlap with &lt;code&gt;handi&lt;/code&gt; query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;skills&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'handy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- no records found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How we can cover this situation? As you remember you can change a configuration that is used to parse user input. Let's try a &lt;code&gt;simple&lt;/code&gt; configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'handy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 'handy'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like this is what we need, let's add this to our query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;skills&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'handy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt;
    &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'handy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    

&lt;span class="c1"&gt;-- 1 record found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A problem with &lt;code&gt;Roofing&lt;/code&gt; and &lt;code&gt;roof&lt;/code&gt; can be solved in the same way. Let's change the function that is used to populate &lt;code&gt;tsv&lt;/code&gt; column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;skills_tsv_update_trigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;tsv&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Handyman&lt;/td&gt;
&lt;td&gt;‘handyman’:1,2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heating &amp;amp; Cooling&lt;/td&gt;
&lt;td&gt;‘cool’:2 ‘cooling’:4 ‘heat’:1 ‘heating’:3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Painting&lt;/td&gt;
&lt;td&gt;‘paint’:1 ‘painting’:2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roofing&lt;/td&gt;
&lt;td&gt;‘roof’:1 ‘roofing’:2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now vector has more data and query below works as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;skills&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog.english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'roofing'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 1 record found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>postgres</category>
    </item>
    <item>
      <title>How to add HTTP Basic auth to Amber application</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Mon, 25 Nov 2019 21:44:17 +0000</pubDate>
      <link>https://dev.to/jetrockets/how-to-add-http-basic-auth-to-amber-application-4pc8</link>
      <guid>https://dev.to/jetrockets/how-to-add-http-basic-auth-to-amber-application-4pc8</guid>
      <description>&lt;p&gt;As you already &lt;a href="https://twitter.com/igor_alexandrov/status/1166754279631327232"&gt;may know&lt;/a&gt;, one of our projects have a &lt;a href="https://crystal-lang.org/"&gt;Crystal&lt;/a&gt; application in production. It is created with &lt;a href="https://amberframework.org/"&gt;Amber framework&lt;/a&gt; and works just perfect.&lt;/p&gt;

&lt;p&gt;The only thing that I don't personally like in Amber is not clear and sometimes outdated &lt;a href="https://docs.amberframework.org/amber/"&gt;documentation&lt;/a&gt;, after about 11 years with Rails I still think that Rails Guides are the number one developer documentation in the world.&lt;/p&gt;

&lt;p&gt;I had a task to add HTTP Basic auth to a couple of URLs in Amber application, and after studying documentation found that Amber doesn't provide necessary &lt;a href="https://docs.amberframework.org/amber/guides/routing/pipelines"&gt;Pipe&lt;/a&gt; out of the box. Ok, next place to search for the answer was &lt;a href="https://gitter.im/amberframework/amber"&gt;Gitter&lt;/a&gt; and after about an hour &lt;a href="https://github.com/drujensen"&gt;Dru Jensen&lt;/a&gt; helped me with a code example.&lt;/p&gt;

&lt;p&gt;Amber uses internally &lt;a href="https://crystal-lang.org/api/0.31.1/HTTP/Handler.html"&gt;HTTP::Handler&lt;/a&gt; for Pipes as well as Kemal does for &lt;a href="https://kemalcr.com/guide/#middleware"&gt;Middlewares&lt;/a&gt;, so we can easily use code from &lt;a href="https://github.com/kemalcr/kemal-basic-auth"&gt;Basic Auth for Kemal&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/pipes/http_basic_auth_pipe.cr&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"crypto/subtle"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HTTPBasicAuthPipe&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Handler&lt;/span&gt;
  &lt;span class="no"&gt;BASIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Basic"&lt;/span&gt;
  &lt;span class="no"&gt;AUTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Authorization"&lt;/span&gt;
  &lt;span class="no"&gt;AUTH_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Could not verify your access level for that URL.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;You have to login with proper credentials"&lt;/span&gt;
  &lt;span class="no"&gt;HEADER_LOGIN_REQUIRED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Basic realm=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Login Required&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@credentials&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_BASIC_USERNAME"&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_BASIC_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
      &lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_BASIC_USERNAME"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_BASIC_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;AUTH&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;AUTH&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starts_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BASIC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;authorized?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"WWW-Authenticate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HEADER_LOGIN_REQUIRED&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt; &lt;span class="no"&gt;AUTH_MESSAGE&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorized?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;BASIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;not_nil!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Credentials&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@entries&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;given_password&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
      &lt;span class="n"&gt;test_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;given_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Crypto&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constant_time_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;given_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;username&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="kp"&gt;nil&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;given_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# return a password that cannot possibly be correct if the username is wrong&lt;/span&gt;
      &lt;span class="n"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"not &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;given_password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

      &lt;span class="c1"&gt;# iterate through each possibility to not leak info about valid usernames&lt;/span&gt;
      &lt;span class="vi"&gt;@entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Crypto&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constant_time_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;pw&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;routes.cr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="ss"&gt;:api&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;plug&lt;/span&gt; &lt;span class="no"&gt;HTTPBasicAuthPipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>crystal</category>
      <category>amberframework</category>
      <category>amber</category>
    </item>
    <item>
      <title>Double splat arguments in Crystal</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Thu, 10 Oct 2019 08:48:27 +0000</pubDate>
      <link>https://dev.to/jetrockets/double-splat-arguments-in-crystal-5cd4</link>
      <guid>https://dev.to/jetrockets/double-splat-arguments-in-crystal-5cd4</guid>
      <description>&lt;p&gt;In Crystal, as well as in Ruby you can use double splat arguments. Unfortunately they behave a bit different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# {:b=&amp;gt;2, :a=&amp;gt;1} &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code in Ruby works as it should. If we try the same in Crystal (&lt;a href="https://play.crystal-lang.org/#/r/7r0l"&gt;https://play.crystal-lang.org/#/r/7r0l&lt;/a&gt;), we got an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error in line 2
Error: duplicate key: a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens because &lt;code&gt;**options&lt;/code&gt; is a &lt;code&gt;NamedTuple&lt;/code&gt; and it cannot have duplicate keys. I found that using &lt;code&gt;NamedTuple#merge&lt;/code&gt; can be a workaround (&lt;a href="https://play.crystal-lang.org/#/r/7s1c"&gt;https://play.crystal-lang.org/#/r/7s1c&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;b: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;a: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# {:b=&amp;gt;2, :a=&amp;gt;1} &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hack!&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>ruby</category>
    </item>
    <item>
      <title>How to parse CSV with double quote (") character in Crystal</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Mon, 16 Sep 2019 18:19:17 +0000</pubDate>
      <link>https://dev.to/jetrockets/how-to-parse-csv-with-double-quote-character-in-crystal-2a6c</link>
      <guid>https://dev.to/jetrockets/how-to-parse-csv-with-double-quote-character-in-crystal-2a6c</guid>
      <description>&lt;p&gt;We use microservice written in Crystal to parse large CSV files (about 1.5Gb). Some rows in these files may contain no closed " characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;,Y,FEDERAL NATIONAL MORTGAGE ASSOCIATION "F,,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Crystal default CSV parse settings this row and everything after it won't be parsed correctly because &lt;code&gt;DEFAULT_QUOTE_CHAR&lt;/code&gt; constant is equal to &lt;code&gt;"&lt;/code&gt;. Of course you can override &lt;code&gt;quote_char&lt;/code&gt; param in &lt;code&gt;CSV&lt;/code&gt; constructor with something that cannot be found in your document.&lt;/p&gt;

&lt;p&gt;From my point of view the best is to use zero byte which is &lt;code&gt;'\u0000'&lt;/code&gt; in Crystal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;headers: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;strip: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quote_char: &lt;/span&gt;&lt;span class="s1"&gt;'\u0000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;
  &lt;span class="c1"&gt;# ... &lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hack!&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Metka Gem</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Wed, 04 Sep 2019 19:21:30 +0000</pubDate>
      <link>https://dev.to/jetrockets/metka-gem-2765</link>
      <guid>https://dev.to/jetrockets/metka-gem-2765</guid>
      <description>&lt;p&gt;Based on work that I've done in this post &lt;a href="https://jetrockets.pro/blog/migrate-tags-in-rails-to-postgresql-array-from-actsastaggableon"&gt;https://jetrockets.pro/blog/migrate-tags-in-rails-to-postgresql-array-from-actsastaggableon&lt;/a&gt; I decided to create a gem.&lt;/p&gt;

&lt;p&gt;Today I made the first release and prepared a lot of work for future.&lt;/p&gt;

&lt;p&gt;Here is a link to repository: &lt;a href="https://github.com/jetrockets/metka"&gt;https://github.com/jetrockets/metka&lt;/a&gt;&lt;/p&gt;

</description>
      <category>railstagging</category>
      <category>tagging</category>
      <category>ormextension</category>
      <category>rails</category>
    </item>
    <item>
      <title>Request Api Adapter</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Fri, 09 Aug 2019 18:31:03 +0000</pubDate>
      <link>https://dev.to/jetrockets/request-api-adapter-3ln2</link>
      <guid>https://dev.to/jetrockets/request-api-adapter-3ln2</guid>
      <description>&lt;p&gt;When developing client applications, it is often necessary to send requests to the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can make our lives a little easier. A convenient abstraction is apiAdapter&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apiAdapter.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getUsers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;getUsers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By defining a request in one place, you can now simply call the adapter method you want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;apiAdapter&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./apiAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;apiAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUsers&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is also a useful option to specify basic settings for all requests, as well as handling errors and successful requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;withCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;getUsers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;successHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;errorHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/atamurana/request-api-adapter"&gt;Github&lt;/a&gt;&lt;br&gt;
&lt;a href="https://codesandbox.io/embed/new-ywzqb"&gt;Live example&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>Migrate tags in Rails to PostgreSQL array from ActsAsTaggableOn</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Wed, 31 Jul 2019 17:24:30 +0000</pubDate>
      <link>https://dev.to/jetrockets/migrate-tags-in-rails-to-postgresql-array-from-actsastaggableon-1mf8</link>
      <guid>https://dev.to/jetrockets/migrate-tags-in-rails-to-postgresql-array-from-actsastaggableon-1mf8</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/mbleigh/acts-as-taggable-on"&gt;ActsAsTaggableOn&lt;/a&gt; is a swiss army knife solution if you need to add tags to your ActiveRecord model.&lt;br&gt;
Just by adding one gem to your Gemfile and &lt;code&gt;acts_as_taggable&lt;/code&gt; to the model you get everything you need: adding tags, searching for a model by tag, getting top tags, etc. However, sometimes you don't need all these.&lt;/p&gt;

&lt;p&gt;In our project, we used &lt;code&gt;acts_as_taggable&lt;/code&gt; to store tags for &lt;code&gt;Note&lt;/code&gt; model. Then we displayed a list of notes on several pages with assigned tags and had autocompleted input for tags on &lt;code&gt;Note&lt;/code&gt; form. Everything worked well, but since we use PostgreSQL, I decided to store tags as an array in Note model.&lt;/p&gt;

&lt;p&gt;First of all, I added &lt;code&gt;tags Array&amp;lt;String&amp;gt;&lt;/code&gt; column to &lt;code&gt;Note&lt;/code&gt;, after this migrated acts_as_taggable tags to &lt;code&gt;notes&lt;/code&gt; table with migration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MigrateNoteTags&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
    UPDATE notes 
    SET tags = grouped_taggings.tags_array 
    FROM
      (
      SELECT
        taggings.taggable_id,
        ARRAY_AGG ( tags.NAME ) tags_array 
      FROM
        taggings
        LEFT JOIN tags ON taggings.tag_id = tags.ID 
      WHERE
        taggable_type = 'Note' 
      GROUP BY
        taggings.taggable_id 
      ) AS grouped_taggings 
    WHERE
      notes.ID = grouped_taggings.taggable_id
&lt;/span&gt;&lt;span class="no"&gt;    SQL&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To have backward compatibility, I added &lt;code&gt;Note#tag_list&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tag_list&lt;/span&gt;
  &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last thing is to add the ability to search for tags. Since there about 500k records in the Notes table, I decided to create an SQL view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;note_tags&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;UNNEST&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;taggings_count&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;notes&lt;/span&gt; 
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! It takes from 100ms to 150ms to search for tags in this view, which is fine for me.&lt;/p&gt;

&lt;p&gt;If you have more significant data sets, then the best would be to create &lt;code&gt;tags&lt;/code&gt; table and add triggers to &lt;code&gt;notes&lt;/code&gt; table that will update &lt;code&gt;tags&lt;/code&gt; on INSERT/UPDATE/DELETE.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>postgres</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Mistakes to Avoid When Outsourcing Web and Mobile Development</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Wed, 31 Jul 2019 07:15:21 +0000</pubDate>
      <link>https://dev.to/jetrockets/mistakes-to-avoid-when-outsourcing-web-and-mobile-development-1ikm</link>
      <guid>https://dev.to/jetrockets/mistakes-to-avoid-when-outsourcing-web-and-mobile-development-1ikm</guid>
      <description>&lt;p&gt;&lt;strong&gt;The pursuit of a low price&lt;/strong&gt;. Developing and launching a successful web or mobile application requires the participation of many professionals: business analysts, designers, programmers, QA testers and managers, among others. Software development is a complex and knowledge-intensive process, which makes the work of dependable programmers extremely valuable. Because of this, hiring a company that offers you a low hourly rate means that their employees are not being well compensated in turn and thus are less likely to be highly skilled.&lt;/p&gt;

&lt;p&gt;The best plan is to look for a company headquartered in the United States that has an offshore development center. That way, you will be able to take advantage of lower prices, at least  compared to U.S.-based development teams, while still enjoying superior quality and the legal protection offered by in-country corporations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring the language barrier.&lt;/strong&gt; Choosing a team with poor English abilities may seem like a mere inconvenience, but this supposedly minor obstacle can actually endanger a project's success. Misunderstanding certain nuances of a desired product can cause architectural mistakes that could lead to significant issues later on, resulting in rework, delays and further additional expenses. When interviewing your potential product team, make sure to speak with the actual developers that will be assigned to your project, not just the sales person or project manager. Use a video conference to conduct an in-person interview with the team and confirm that you can understand each other clearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lack of technical oversight.&lt;/strong&gt; If you do not have a technical leader on your internal team, you may run into significant problems. This is a familiar scenario: during the sales process, the team you are thinking about hiring demonstrates great technical knowledge and sounds very convincing, but as you begin the work process, you realize that the team may not have the necessary technical skills after all. To avoid a situation like this, you should hire a freelance technical advisor, who will be able to interview the team beforehand and ensure that they have the necessary technical skills to deliver a successful solution. Online services such as Codementor, Upwork, or Toptal are a few options that  help you hire such an individual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hiring developers vs hiring consultants.&lt;/strong&gt; Unless you have a strong internal technical leadership, it is advisable to outsource your product development to a company that provides a full range of technical consulting services as opposed to a company that offers developers for-hire. When working with a consulting firm, you get access to a dedicated team of professionals who can help you design, architect and implement your product while taking into account both your short-term and long-term business goals. Developers for-hire simply execute the tasks you present to them, often ignoring best practices and your long-term vision at once.&lt;/p&gt;

&lt;p&gt;As an example, let’s say that you want to build a mobile application with a minimal budget and you read online that it can be done quickly as a container app. You then hire a team of programmers to execute this specific task. In the beginning everything looks great—the project has been developed to your specification and it functions just as planned. After a month or two, you decide to add animations, improve the design or add a little more complexity to the logic of the application.Unfortunately, though, your hired development team is unable to accomplish any of these tasks because the technology they used for development does not support these requirements. If the changes you want to make are critical for your business, there is only one way out: rewriting your application from scratch.&lt;/p&gt;

&lt;p&gt;This situation can easily be avoided by working with a team of development consultants who know how to ask the right questions and can help you solve immediate problems while planning for the future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating estimates as definitive project budgets.&lt;/strong&gt; Consider the following scenario: you spend six months developing a detailed technical specification document and you send an RFP to a development company. After a week or so, the company responds with a proposal that includes a budget of 3,000 man-hours. I can guarantee with 100% certainty that your expenses will run over from this proposal. Why? Because it is impossible to accurately assess such a large and abstract number of work hours.&lt;/p&gt;

&lt;p&gt;Instead, ask for a rough assessment of the entire project to ensure that the overall estimated budget fits your goals. Also try to pinpoint the composition of the development team and request a clear short-term plan that covers the initial development phase, usually a window of the first four to six weeks. After this you can choose to either proceed with the team on a retainer basis, which will help you manage your costs, or ask about a quote for the next four to six weeks of work based on priorities. Make sure to attend weekly meetings with the team and ask for regular budget updates.&lt;/p&gt;

&lt;p&gt;Finally, before making a hiring decision, &lt;strong&gt;ask for three or four references&lt;/strong&gt; and be sure to contact them over the phone, not via email. Inquire about the team's management skills, their responsiveness, and their ability to handle emergency situations.&lt;/p&gt;

&lt;p&gt;Finding a reliable technology partner is a daunting task, but when approached with patience and due diligence it can quickly pay off, helping your business grow and achieve its goals.&lt;/p&gt;

&lt;p&gt;At JetRockets we specialize in designing, developing and supporting custom software solutions that help clients achieve their unique business goals. If you have any questions or are looking for a complimentary consultation, don’t hesitate to reach out.&lt;/p&gt;

</description>
      <category>management</category>
      <category>development</category>
      <category>projectmanagement</category>
      <category>remoteteam</category>
    </item>
    <item>
      <title>Networking and thermal conditions debugging in Xcode 11</title>
      <dc:creator>Igor Alexandrov</dc:creator>
      <pubDate>Wed, 24 Jul 2019 06:42:37 +0000</pubDate>
      <link>https://dev.to/jetrockets/networking-and-thermal-conditions-debugging-in-xcode-11-3a38</link>
      <guid>https://dev.to/jetrockets/networking-and-thermal-conditions-debugging-in-xcode-11-3a38</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fonswk289f5gkpi4ivv0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fonswk289f5gkpi4ivv0o.png"&gt;&lt;/a&gt;&lt;br&gt;
Xcode 11 introduces tool for networking and thermal conditions debugging. This feature requires device running iOS 13. Previously network conditioning feature was available within &lt;a href="https://developer.apple.com/download/more/?q=Additional%20Tools" rel="noopener noreferrer"&gt;Additional Tools for Xcode&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  See also
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2019/401/?time=1604" rel="noopener noreferrer"&gt;What’s New in Xcode 11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2019/412" rel="noopener noreferrer"&gt;Debugging in Xcode 11&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>debugging</category>
      <category>xcode11</category>
      <category>thermalconditions</category>
      <category>networkingconditions</category>
    </item>
  </channel>
</rss>
