<?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
     <title>BigBinary Blog</title>
     <link href="https://www.bigbinary.com/feed.xml" rel="self"/>
     <link href="https://www.bigbinary.com/"/>
     <updated>2026-10-10T10:51:40+00:00</updated>
     <id>https://www.bigbinary.com/</id>
     <entry>
       <title><![CDATA[How we fixed app downtime issue in NeetoDeploy]]></title>
       <author><name>Abhishek T</name></author>
      <link href="https://www.bigbinary.com/blog/how-we-fixed-app-down-time-in-neeto-deploy"/>
      <updated>2024-07-09T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/how-we-fixed-app-down-time-in-neeto-deploy</id>
      <content type="html"><![CDATA[<p><em>We are building <a href="https://neeto.com/neetoDeploy">NeetoDeploy</a>, a compellingalternative to Heroku. Stay updated by following NeetoDeploy on<a href="https://twitter.com/neetodeploy">Twitter</a> and reading our<a href="https://www.bigbinary.com/blog/categories/neetodeploy">blog</a>.</em></p><p>At <a href="https://www.neeto.com/">neeto</a> we are building 20+ applications, and most ofour applications are running in NeetoDeploy. Once we migrated from Heroku toNeetoDeploy, we started getting 520 response code for our applications. Thisissue was occurring randomly and rarely.</p><h3>What is 520 response code?</h3><p>A 520 response code happens when the connection is started on the origin webserver, but the request is not completed. This could be due to server crashes orthe inability to handle the incoming requests because of insufficient resources.</p><p>When we looked at our logs closely, we found that all the 520 response codesituations occurred when we restarted or deployed the app. From this, weconcluded that the new pods are failing to handle requests from the clientinitially and working fine after some time.</p><h3>What is wrong with new pods?</h3><p>Once our investigation narrowed down to the new pods, we quickly realized thatrequests are arriving at the server even when the server is not fully ready yetto take new requests.</p><p>When we create a new pod in Kubernetes, it is marked as &quot;Ready&quot;, and requestsare sent to it as soon as its containers start. However, the servers initiatedwithin these containers may require additional time to boot up and to becomeready to accept the requests fully.</p><h4>Let's try restarting the application</h4><pre><code class="language-bash">$ kubectl rollout restart deployment bling-staging-web</code></pre><p>As we can see, a new container is getting created for the new pod. The READYstatus for the new pod is 0. It means it's not yet READY.</p><pre><code class="language-bash">NAME                               READY  STATUS             RESTARTS  AGEbling-staging-web-656f74d9d-6kpzz  1/1    Running            0         2m8sbling-staging-web-79fc6f978-cdjf5  0/1    ContainerCreating  0         5s</code></pre><p>Now we can see that the new pod is marked as READY (1 out of 1), and the old oneis terminating.</p><pre><code class="language-bash">NAME                               READY  STATUS             RESTARTS  AGEbling-staging-web-656f74d9d-6kpzz  0/1    Terminating        0         2m9sbling-staging-web-79fc6f978-cdjf5  1/1    Running            0         6s</code></pre><p>The new pod is shown as <code>READY</code> as soon as the container was created. But onchecking the logs, we could see that the server was still starting up and notready yet.</p><pre><code>[1] Puma starting in cluster mode...[1] Installing dependencies...</code></pre><p>From the above observation, we understood that the pod is marked as &quot;READY&quot;right after the container is created. Consequently, requests are received evenbefore the server is fully prepared to serve them, and they get a 520 responsecode.</p><h2>Solution</h2><p>To fix this issue, we must ensure that pods are marked as &quot;Ready&quot; only after theserver is up and ready to accept the requests. We can do this by usingKubernetes<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">health probes</a>.More than six years ago we wrote<a href="https://www.bigbinary.com/blog/deploying-rails-applications-using-kubernetes-with-zero-downtime">a blog</a>on how we can leverage the readiness and liveness probes of Kubernetes.</p><h3>Adding Startup probe</h3><p>Initially, we only added<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes">Startup probe</a>since we had a problem with the boot-up phase. You can read more about theconfiguration settings<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes">here</a>.</p><p>The following configuration will add the Startup probe for the deployments:</p><pre><code class="language-yaml">startupProbe:  failureThreshold: 10  httpGet:    path: /health_check    port: 3000    scheme: HTTP  periodSeconds: 5  successThreshold: 1  timeoutSeconds: 60  initialDelaySeconds: 10</code></pre><p><code>/health_check</code> is a route in the application that is expected to return a 200response code if all is going well. Now, let's restart the application againafter adding the Startup probe.</p><p>Container is created for the new pod, but the pod is still not &quot;Ready&quot;.</p><pre><code class="language-bash">NAME                               READY  STATUS            RESTARTS  AGEbling-staging-web-656f74d9d-6kpzz  1/1    Running           0         2m8sbling-staging-web-79fc6f978-cdjf5  0/1    Running           0         5s</code></pre><p>The new pod is marked as &quot;Ready&quot;, and the old one is &quot;Terminating&quot;.</p><pre><code class="language-bash">NAME                               READY  STATUS            RESTARTS  AGEbling-staging-web-656f74d9d-6kpzz  0/1    Terminating       0         2m38sbling-staging-web-79fc6f978-cdjf5  1/1    Running           0         35s</code></pre><p>If we check the logs, we can see the health check request:</p><pre><code>  [1] Puma starting in cluster mode...  [1] Installing dependencies...  [1] * Puma version: 6.3.1 (ruby 3.2.2-p53) (&quot;Mugi No Toki Itaru&quot;)  [1] *  Min threads: 5  [1] *  Max threads: 5  [1] *  Environment: heroku  [1] *   Master PID: 1  [1] *      Workers: 1  [1] *     Restarts: () hot () phased [1] * Listening on http://0.0.0.0:3000 [1] Use Ctrl-C to stop [2024-02-10T02:40:48.944785 #23]  INFO -- : [bb9e756a-51cc-4d6b-9a4a-96b0464f6740] Started GET &quot;/health_check&quot; for 192.168.120.195 at 2024-02-10 02:40:48 +0000 [2024-02-10T02:40:48.946148 #23]  INFO -- : [bb9e756a-51cc-4d6b-9a4a-96b0464f6740] Processing by HealthCheckController#healthy as */* [2024-02-10T02:40:48.949292 #23]  INFO -- : [bb9e756a-51cc-4d6b-9a4a-96b0464f6740] Completed 200 OK in 3ms (Allocations: 691)</code></pre><p>Now, the pod is marked as &quot;Ready&quot; only after the health check succeeds, in otherwords, only when the server is prepared to accept the requests.</p><h3>Fixing the Startup probe for production applications</h3><p>Once we released the health check for our deployments, we found that healthchecks were failing for all production applications but working for staging andreview applications.</p><p>We were getting the following error in our production applications.</p><pre><code class="language-bash">Startup probe failed: Get &quot;https://192.168.43.231:3000/health_check&quot;: http: server gave HTTP response to HTTPS client2024-02-12 06:40:04 +0000 HTTP parse error, malformed request: #&lt;Puma::HttpParserError: Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?&gt;</code></pre><p>From the above logs, it was clear that the issue was related to SSLconfiguration. On comparing the production environment configuration with theothers, we figured out that we had enabled<a href="https://guides.rubyonrails.org/configuring.html#config-force-ssl">force_ssl</a>for production applications. The <code>force_ssl=true</code> setting ensures that allincoming requests are SSL encrypted and will automatically redirect to their SSLcounterparts.</p><p>The following diagram broadly shows the path of an incoming request.</p><p><img src="/blog/images/images_used_in_blog/2024/how-we-fixed-app-down-time-in-neeto-deploy/image4.png" alt="HTTPS request path"></p><p>From the above diagram, we can infer the following things:</p><ul><li>SSL verification is happening in the ingress controller and not in the server.</li><li>Client requests are going through the ingress controller before reaching theserver.</li><li>Request from ingress controller to the pod is an HTTP request.</li><li>The HTTP health check requests are directly sent from Kubelet to the pod anddo not go through the ingress controller.</li></ul><p>Here is how our health check request works.</p><ol><li><a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/">Kubelet</a>sends an HTTP request to the server directly.</li><li>Since <code>force_ssl</code> is enabled,<a href="https://api.rubyonrails.org/v7.1.2/classes/ActionDispatch/SSL.html">ActionDispatch::SSL</a>middleware redirects the request to HTTPS.</li><li>When the HTTPS request reaches the server, <a href="https://puma.io/">Puma</a> throws<code>Are you trying to open an SSL connection to a non-SSL Puma?</code> error since noSSL certificates are configured with the server.</li></ol><p>The solution to our problem lies in understanding why only the health checkrequest is rejected, whereas the request from the ingress controller is not,even though both are HTTP requests. This is because ingress controller sets someheaders before forwarding to the pod, and the header we are concerned about is<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto">X-FORWARDED-PROTO</a>.The <code>X-Forwarded-Proto</code> header contains the HTTP/HTTPS scheme the client used toaccess the application. When a client makes an HTTPS request, the ingresscontroller terminates the SSL/TLS connection and forwards the request to thebackend service using plain HTTP after adding the<code>X-Forwarded-Proto</code> along withthe other headers.</p><p>Everything started working after adding the <code>X-Forwarded-Proto</code> header to ourstartup probe request.</p><pre><code class="language-yaml">startupProbe:  failureThreshold: 10  httpGet:    httpHeaders:      - name: X-FORWARDED-PROTO        value: https    path: &lt;%= health_check_url %&gt;    port: &lt;%= port %&gt;    scheme: HTTP  periodSeconds: 5  successThreshold: 1  timeoutSeconds: 60  initialDelaySeconds: 10</code></pre><p>We also added<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes">Readiness</a>and<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request">Liveness</a>probes for our deployments.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email at<a href="mailto:invite@neeto.com">invite@neeto.com</a>.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">Twitter</a>. You can also join ourSlack community to chat with us about any Neeto product.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Adding sleep when idle to NeetoDeploy to cut costs]]></title>
       <author><name>Sreeram Venkitesh</name></author>
      <link href="https://www.bigbinary.com/blog/cost-reduction-in-neeto-deploy-by-turning-off-inactive-apps"/>
      <updated>2024-01-19T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/cost-reduction-in-neeto-deploy-by-turning-off-inactive-apps</id>
      <content type="html"><![CDATA[<p><em>We are building <a href="https://neeto.com/neetoDeploy">NeetoDeploy</a>, a compellingHeroku alternative. Stay updated by following NeetoDeploy on<a href="https://twitter.com/neetodeploy">Twitter</a> and reading our<a href="https://www.bigbinary.com/blog/categories/neetodeploy">blog</a>.</em></p><h2>What is sleep when idle feature</h2><p>&quot;Sleep when idle&quot; is a feature of NeetoDeploy, which puts the deployedapplication to sleep when there is no hit to the server for 5 minutes. Thishelps reduce the cost of the server.</p><p>&quot;Sleep when idle&quot; feature can be enabled not only for the pull request reviewapplications, but for staging and production applications too. Many folks buildapplications to learn and for hobby. In such cases, there is no point in runningthe server when the server is not likely to get any traffic. Since NeetoDeploybilling is based on the usage &quot;Sleep when idle&quot; feature helps keep the bill lowfor the users.</p><p>Let's say you build something and you deployed to production. You shared it withyour friends. For a day or two you got a bit of traffic, and after that youmoved on to other things. If &quot;sleep when idle&quot; is enabled then you don't need toworry about anything. If the server is not getting any traffic then you will notbe billed.</p><h2>How is Neeto using sleep when idle feature</h2><p>At <a href="https://neeto.com">neeto</a>, we are building 20+ applications at the sametime. It means lots of pull requests for all these products and thus lots of PRreview apps are created.</p><p>For a long time, we were using Heroku to build the review apps. However whenNeetoDeploy started to become stable, we movedto generating PR review apps fromHeroku to NeetoDeploy. This helped reduce cost.</p><h2>How to make deployments sleep when idle?</h2><p>This video describes how &quot;sleep when idle&quot; feature is implemented.</p><p>&lt;iframewidth=&quot;560&quot;height=&quot;315&quot;src=&quot;https://youtube.com/embed/trn2DJyTjnw&quot;frameborder=&quot;0&quot;title=&quot;How we designed NeetoDeploy's 'sleep when idle' feature&quot;allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot;allowfullscreen</p><blockquote><p>&lt;/iframe&gt;</p></blockquote><p>Keeping the apps running only when they're being used involves two steps:</p><ol><li>Scaling the deployments down and bringing them back up again</li><li>Figuring out when to do the scaling</li></ol><p>The deployments can be scaled easily using the <code>kubectl scale</code> command. Forexample, if we want to turn our deployment off, we can run the following toupdate our deployment to zero replicas, essentially destroying all the pods.</p><pre><code class="language-bash">kubectl scale deployment/nginx --replicas=0</code></pre><p>We can also delete our service, ingress or any other resource we might havecreated for our deployment. The configuration of the deployment itself wouldstill be present in the cluster even when we make it sleep, since the KubernetesDeployment is not deleted.</p><p>When we want to bring our app back up again, we can use the same command to spinup new pods:</p><pre><code class="language-bash">kubectl scale deployment/nginx --replicas=1</code></pre><p>The challenge was to figure out <em>when</em> to do this. We decided that we'd have athreshold based on the time the app is last accessed by users. If theapplication is not accessed for more than five minutes, we consider theapplication to be idle and we will scale it down. It'll be brought back up whena user tries to access it again.</p><h2>Exploring existing solutions</h2><p>There are existing CNCF projects like <a href="https://knative.dev/">Knative</a> and<a href="https://keda.sh/">Keda</a>, which can potentially be used to achieve what we wanthere. We spent some time exploring these but realized that these solutionsweren't exactly suitable for our requirements. Kubernetes also natively has a<code>HPAScaleToZero</code><a href="https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates">feature gate</a>which enables the<a href="https://www.bigbinary.com/blog/solving-scalability-in-neeto-deploy#understanding-kubernetes-autoscalers">Horizontal Pod Autoscaler</a>to scale down deployments to zero pods, but this is still in alpha and, hence isnot available in EKS yet.</p><p>Ultimately, we decided to write our own service for achieving this. The entirebackend of NeetoDeploy was designed as<a href="https://www.bigbinary.com/blog/neeto-deploy-zero-to-one">a collection of microservices</a>from day one. So it made sense to build our <em>pod idling service</em> as anothermicroservice that runs in our cluster.</p><h2>Figuring out when to make applications sleep</h2><p>To know when applications can be idled, we need to know when people areaccessing the applications from their browsers. Since all the requests toapplications deployed on NeetoDeploy would go through our load balancer, itwould contain the information of when every app was last accessed.</p><p>We use <a href="https://traefik.io/traefik/">Traefik</a> as our load balancer and we usedTraefik's <a href="https://doc.traefik.io/traefik/middlewares/overview/">middlewares</a> toretrieve and process the information of when apps are being accessed. We wrote acustom middleware to send all the request information to the pod idling service,whenever an app is being accessed. The pod idling service would store all theURLs, along with the timestamp at which they were accessed, in a Redis cache.The following graphic shows how the request information would be collected andstored by the pod idling service into its Redis cache, both of which are runningwithin the cluster.</p><p><img src="/blog/images/images_used_in_blog/2024/cost-reduction-in-neeto-deploy-by-turning-off-inactive-apps/pod-idling-new-architecture.png" alt="The architecture of the pod idling service"></p><p>The pod idling service would then filter the apps that were last accessed morethan five minutes ago. It then sends a request to the cluster to scale all theseapps down. We'd also delete any related resources like the Services and theIngressRoutes used to configure networking for the deployments.</p><p>We first tested this by running the service manually, and sure enough, all theinactive deployments are filtered and scaled properly. We then added this as acron job in the pod idling service, which would run every five minutes. Thismeans that no app would run for more than five minutes if they're not beingused.</p><p>But wait! How would we bring the app back up after scaling it down?</p><h2>Building the downtime service</h2><p>As we discussed above, we use Traefik's IngressRoutes to route traffic to theapplication being accessed. We made use of the<a href="https://doc.traefik.io/traefik/v2.10/routing/routers/#priority_1">priority parameter</a>of IngressRoutes to boot up apps that are sleeping. Essentially, we created awildcard Traefik IngressRoute that points to a &quot;downtime service&quot; deployment,which is a React app that serves a message of <code>There's nothing here, yet</code> to letusers know that the app they're trying to access doesn't exist. You can see thisin action if you visit a random URL in NeetoDeploy, say something like<a href="https://nonexistent-appname.neetodeployapp.com">nonexistent-appname.neetodeployapp.com</a></p><p><img src="/blog/images/images_used_in_blog/2024/cost-reduction-in-neeto-deploy-by-turning-off-inactive-apps/downtime-service-page.png" alt="The downtime service page"></p><p>Wildcard IngressRoutes have the least priority by default. So if we create a&quot;catch-all&quot; wildcard IngressRoute, any invalid url without an IngressRoute ofits own, can be redirected to a single Service in Kubernetes. This is how we'reredirecting non-existent apps to the page shown above. In the following graphic,we can see how a request to a random URL is routed to the downtime service withthe wildcard IngressRoute.</p><p><img src="/blog/images/images_used_in_blog/2024/cost-reduction-in-neeto-deploy-by-turning-off-inactive-apps/downtime-service-architecture.png" alt="Architecture of how the downtime service works in NeetoDeploy"></p><p>This also means that if an app is scaled down by the pod idling service and getsits IngressRoute deleted, the next time a user tries to access the app, therequest would instead be routed to the downtime service. We need to handle thescale up logic from the downtime service.</p><p>Whenever a user requests a URL that doesn't have an IngressRoute, there are twopossibilities.</p><ol><li>The app doesn't exist.</li><li>The app exists, but is currently scaled down.</li></ol><p>The downtime service would first check the cluster if the requested app ispresent in the cluster in a sleeping state. If not then the user will be servedthe &quot;There's nothing here, yet&quot; page. If there is a sleeping deployment,however, we boot it back up. The downtime service sends the scale up request tothe cluster. We keep redirecting the user back to the url till the app is up andrunning. This redirection would keep happening until the app is scaled up sincewe create the Service and IngressRoute only after the pods of the app arerunning. At this point, the request will be routed to the correct pod by theapp's IngressRoute, since it has a higher priority than the wildcardIngressRoute of the downtime service. All of these steps are illustrated in theGIF below:</p><p><img src="/blog/images/images_used_in_blog/2024/cost-reduction-in-neeto-deploy-by-turning-off-inactive-apps/downtime-service.gif" alt="Illustration of how the downtime service works"></p><p>This design worked flawlessly and we were able to bring back scaled downapplications with as low as 20-30 seconds of wait time.</p><h2>Conclusion</h2><p>We've been running this setup for almost a year now, and it has been workingsmoothly so far. Pod idling service and the downtime service started as simplermicroservices and continue to evolve, adapting to the increasing demand as wegrow.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email atinvite@neeto.com.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">X</a>. You can also join our<a href="https://launchpass.com/neetohq">community Slack</a> to chat with us about anyNeeto product.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Building the NeetoDeploy metrics dashboard with Prometheus]]></title>
       <author><name>Sreeram Venkitesh</name></author>
      <link href="https://www.bigbinary.com/blog/using-prometheus-in-neeto-deploy"/>
      <updated>2024-01-09T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/using-prometheus-in-neeto-deploy</id>
      <content type="html"><![CDATA[<p><em>We are building <a href="https://neeto.com/neetoDeploy">NeetoDeploy</a>, a compellingalternative to Heroku. Stay updated by following NeetoDeploy on<a href="https://twitter.com/neetodeploy">Twitter</a> and reading our<a href="https://www.bigbinary.com/blog/categories/neetodeploy">blog</a>.</em></p><p>One of the features that we wanted in our cloud deployment platform,<strong>NeetoDeploy</strong> was an application metrics. We decided to use<a href="https://prometheus.io/">Prometheus</a> for building this feature. Prometheus is anopen source monitoring and alerting toolkit and is a CNCF graduated project.Venturing into the Cloud Native ecosystem of projects apart from Kubernetes wassomething we had never done before. We ended up learning a lot about Prometheusand how to use it during the course of building this feature.</p><h2>Initial setup</h2><p>We installed Prometheus in our Kubernetes cluster by writing a deploymentconfiguration YAML and applying it to our cluster. We also provisioned an AWSElastic Block Store volume using a PersistentVolumeClaim to store the metricsdata collected by Prometheus. Prometheus needed a<a href="https://github.com/prometheus/prometheus/blob/main/documentation/examples/prometheus-kubernetes.yml">configuration file</a>where we defined what all targets it will be scraping metrics from. This is aYAML file which we stored in a ConfigMap in our cluster.</p><p>Targets in Prometheus can be anything that exposes metrics data in thePrometheus format at a <code>/metrics</code> endpoint. This can be your applicationservers, Kubernetes API servers or even Prometheus itself. Prometheus wouldscrape the data at the defined <code>scrape_interval</code> and store it in the volume astime series data. This can be queried and visualized in the Prometheus dashboardthat comes bundled in the Prometheus deployment.</p><p>We used <code>kubectl port-forward</code> command to test that Prometheus is workinglocally. Once everything was tested and we confirmed, we exposed Prometheus withan ingress so that we can hit its APIs with that url.</p><p>Initially we had configured the following targets:</p><ol><li><a href="https://github.com/prometheus/node_exporter">node_exporter</a> from Prometheus,which would scrape the metrics of the machine the deployment is running on.</li><li><a href="https://github.com/kubernetes/kube-state-metrics">kube-state-metrics</a> whichwould listen to the Kubernetes API and store metrics of all the objects.</li><li><a href="https://traefik.io/traefik/">Traefik</a> for all the network-related metrics(like the number of requests etc.) since we are using Traefik as our ingresscontroller.</li><li>kubernetes-nodes</li><li>kubernetes-pods</li><li>kubernetes-cadvisor</li><li>kubernetes-service-endpoints</li></ol><p>The last 4 scrape jobs would be collecting metrics from the Kubernetes REST APIrelated to nodes, pods, containers and services respectively.</p><p>For scraping metrics from all of these targets, we had set a resource request of500 MB of RAM and 0.5 vCPU to our Prometheus deployment.</p><p>After setting up all of this, the Prometheus deployment was running fine, and wewere able to see the data from the Prometheus dashboard. Seeing this, we weresatisfied and happily started hacking with PromQL, Prometheus's query language.</p><h2>The CrashLoopBackOff crime scene</h2><p><code>CrashLoopBackOff</code> is when a Kubernetes pod is going into a loop of crashing,restarting itself and then crashing again - and this was what was happening tothe Prometheus deployment we had created. From what we could see, the pod hadcrashed, and when it gets recreated, Prometheus would initialize itself and do areload of the<a href="https://prometheus.io/docs/prometheus/latest/storage/">Write Ahead Log (WAL)</a>.</p><p>The WAL is there for adding additional durability to the database. Prometheusstores the metrics it scrapes in-memory before persisting them to the databaseas chunks, and the WAL makes sure that the in-memory data will not be lost inthe case of a crash. In our case, the Prometheus deployment was crashing and itwould get recreated. It would try to load the data from WAL into memory, andthen crash again before this was completed, leading to the CrashLoopBackOffstate.</p><p>We tried deleting the WAL blocks manually from the volume, even though thiswould incur some data loss. This was able to bring the deployment back up againsince WAL replay needn't be done. The deployment went into CrashLoopBackOffagain after a while.</p><h2>Investigating the error</h2><p>The first line of approach we took was to monitor the CPU, memory, and diskusage of the deployment. The disk usage seemed to be normal. We had provisioneda 100GB volume and it wasn't anywhere near getting used up. The CPU usage alsoseemed normal. The memory usage, however, was suspicious.</p><p>After the pods had crashed initially, we recreated the deployment and monitoredit using kubectl's <code>--watch</code> flag for following all the pod updates. While doingthis we were able to see that the pods were going into CrashLoopBackOff becausethey were getting OOMKilled first. The <code>OOMKilled</code> error in Kubernetes is when apod is terminated because it tries to use more memory than it is allotted in itsresource limits. We were consistently seeing the <code>OOMKilled</code> error so memorymust be the culprit here.</p><p>We added Prometheus itself as a target in Prometheus so that we could monitorthe memory usage of the Prometheus deployment. The following was the generaltrend of how Prometheus's memory was increasing over time. This would go onuntil the memory would cross the specified limit, and then the pod would go intoCrashLoopBackOff.</p><p><img src="/blog/images/images_used_in_blog/2024/using-prometheus-in-neeto-deploy/memory_usage.png" alt="Memory usage of the Prometheus deployment"></p><p>Now that we knew that memory was the issue, we started looking into what wascausing the memory leak. After talking with some folks from the Kubernetes Slackworkspace, we were asked to look at the TSDB status of the Prometheusdeployment. We monitored the stats in real time and saw that the number of timeseries data stored in the database was growing in tens of thousands by eachsecond! This lined up with the increase in the memory usage graph from earlier.</p><p><img src="/blog/images/images_used_in_blog/2024/using-prometheus-in-neeto-deploy/tsdb.png" alt="Prometheus TSDB stats"></p><h2>How we fixed it</h2><p>We can calculate the memory requirement for Prometheus based on the number oftargets we are scraping metrics from and the frequency at which we are scrapingthe data. The memory requirement of the deployment is a function of both ofthese parameters. In our case ,this was definitely higher than what we couldafford to allocate (based on the nodegroup's machine type) since we werescraping a lot of data at a scrape interval of 15 seconds, which was set in thedefault configuration for Prometheus.</p><p>We increased the scrape interval to 60 seconds and removed all the targets fromthe Prometheus configuration whose metrics we didn't need for building thedashboard. Within the targets that we were scraping from, we used the<code>metric_relabel_configs</code> option to persist in the database only those metricswhich we needed and to drop everything else. We only needed the<code>container_cpu_usage_seconds_total</code>, <code>container_memory_usage_bytes</code> and the<code>traefik_service_requests_total</code> metrics - so we configured Prometheus so thatonly these three would be stored in our database, and by extension the WAL.</p><p>We redeployed Prometheus after making these changes and the memory showed greatstability afterwards. The following is the memory usage of Prometheus over thelast few days. It has not exceeded 1GB.</p><p><img src="/blog/images/images_used_in_blog/2024/using-prometheus-in-neeto-deploy/memory_usage_after_fix.png" alt="Memory usage of the Prometheus deployment after the fix"></p><h2>The aftermath</h2><p>Once Prometheus was stable we were able to build the metrics dashboard with thePrometheus API in a straightforward manner. The metrics dashboard came to usewithin a couple of days, when the staging deployment of<a href="https://neetocode.com/">NeetoCode</a> had faced a downtime. You can see thechanges in the metrics from the time when the outage had occurred</p><p><img src="/blog/images/images_used_in_blog/2024/using-prometheus-in-neeto-deploy/neetocode_metrics.png" alt="NeetoCode metrics showing the downtime"></p><p>The quintessential learning that we got from this experience is to always bewary of the resources that are being used up when it comes to tasks likescraping metrics over an extended period of time. We were scraping all themetrics initially in order to explore everything, even though all the metricswere not being used. But because of this, we were able to read a lot about howPrometheus works internally, and also learn some Prometheus best practices thehard way.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email at<a href="mailto:invite@neeto.com">invite@neeto.com</a>.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">Twitter</a>. You can also join our<a href="https://neetohq.slack.com/">community Slack</a> to chat with us about any Neetoproduct.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Building NeetoDeploy CLI]]></title>
       <author><name>Sreeram Venkitesh</name></author>
      <link href="https://www.bigbinary.com/blog/building-neeto-deploy-cli"/>
      <updated>2023-12-26T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/building-neeto-deploy-cli</id>
      <content type="html"><![CDATA[<p><em>We are building <a href="https://neeto.com/neetoDeploy">NeetoDeploy</a>, a compellingHeroku alternative. Stay updated by following NeetoDeploy on<a href="https://twitter.com/neetodeploy">Twitter</a> and reading our<a href="https://www.bigbinary.com/blog/categories/neetodeploy">blog</a>.</em></p><p>Building the CLI tool for <a href="https://neetodeploy.com">NeetoDeploy</a> became our toppriority after we had built all the basic features in NeetoDeploy. Once westarted migrating our apps from Heroku to NeetoDeploy, the need for a CLI toolarose, since previously developers were using Heroku CLI for their scripts etc.</p><p>We started building the CLI as a Ruby gem using <a href="http://whatisthor.com/">Thor</a>.Installing the gem and using it would be as simple as running<code>gem install neetodeploy</code>. We wanted the users to be authenticated via Neeto'sauthentication system before they could do anything else with the CLI. For this,we added a <code>neetodeploy login</code> command, which would create a session andredirect you to the browser where you can log in. The CLI would be polling thesession login status, and once you have logged in, it would store the sessiontoken and your email address inside the <code>~/.config/neetodeploy</code> directory. Thiswill be used to authenticate your request when you run everything else.</p><h3>The basic CLI functionality</h3><p>The v1 release of the NeetoDeploy CLI shipped the following commands:</p><ul><li><code>config</code> : For listing, creating, and deleting environment variables for yourapps.</li><li><code>exec</code> : For getting access to a shell running inside your deployed app.</li><li><code>logs</code> : For streaming the application logs right to your terminal.</li></ul><p>Let's look at each of them in detail.</p><h3>Setting environment vars</h3><p>Building the <code>config</code> command was the most straightforward of the three. Thecentral source or truth for all the environment variables of the apps is storedin the web dashboard of NeetoDeploy. There are APIs already in place for doingCRUD operations on these from the dashboard app. This could be expanded for theuse of the CLI as well.</p><p>We first added API endpoints in the dashboard app, under a CLI namespace, tolist, create, and delete config variables. We then added different commands inthe CLI to send HTTP requests to the respective endpoints in the dashboard app.</p><p><img src="/blog/images/images_used_in_blog/2023/building-neeto-deploy-cli/cli-demo.gif" alt="The NeetoDeploy config command in action"></p><p>You can pass the app name as an argument, and the CLI will send a request to thedashboard app along with the session token generated when you logged in. At thebackend, this session token would be used to check if you have access to the appyou're requesting, before doing anything else.</p><p>If you have used Heroku CLI, then you would be very comfortable sinceNeetoDeploy CLI follows the same style.</p><h3>Architecting the <code>neetodeploy exec</code> command</h3><p>The <code>exec</code> command is used to get access to a shell inside your deployed app.The NeetoDeploy web dashboard already has a functioning console feature withwhich you can get access to the shell inside your applications dynos. This wasbuilt using websockets. We would deploy a &quot;console&quot; pod with the same image andenvironment variables as the app, to which we'll connect with websockets. Wehave a dyno-console-manager app which spawns a new process inside the consoledyno and handles the websocket connections, etc. When we started building theexec feature for the CLI however, we explored a few options and learnt a lot ofthings before finally settling back on the websockets approach.</p><p>Weve been using the <code>kubectl exec</code> command on a daily basis to get access toshells inside pods running in the cluster. Since <code>kubectl exec</code> is cosmeticallysimilar to how we use SSH to access remote machines, the first approach we tookinvolved using SSH to expose containers outside the cluster.</p><p>We can expose our deployment outside the cluster using SSH in two ways. Thefirst approach would be running an SSH server as a<a href="https://www.google.com/search?q=kubernetes+sidecar+container+pattern">sidecar container</a>within the console pod. The second approach would be to bundle<a href="https://man7.org/linux/man-pages/man8/sshd.8.html">sshd</a> within the image forthe console pod using a custom buildpack. With either of these two methods, youwould be able to SSH into your console deployment if you have the correct SSHkeys. We ran <code>sshd</code> inside a pod using a<a href="https://github.com/jkutner/sshd-buildpack">custom sshd buildpack</a>. We thenconfigured the SSH public key inside the pod manually with <code>kubectl exec</code>. Withthis, we were able to SSH into the pod after port forwarding the sshd processsport to localhost using <code>kubectl port-forward</code>.</p><p>Great! Now that the SSH connection works, the part of the puzzle that remainednow was how we would actually expose the deployment outside the cluster inproduction. We tried doing this in a couple of different ways.</p><h4>1. LoadBalancer</h4><p>We were able to SSH into the pod after exposing it as a<a href="https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer">LoadBalancer</a>service. This was out of the question for production however, since AWS has ahard limit on the number of application load balancers (ALB) we can create perregion. Wed also have to pay for each load balancer we create. This approachwouldnt scale at all.</p><h4>2. NodePort</h4><p>The next option was to expose the pod as a <code>NodePort</code> service. A NodePortservice in Kubernetes means that the pod would be exposed through a certain portnumber through <em>every node</em> in the cluster. We could SSH into the pod with theexternal IP of any of the nodes. We tried exposing the test console pod wecreated with a NodePort service as well. We were able to SSH into the pod fromoutside the cluster without having to do the port forward. There was onelimitation for using NodePorts that we knew would render this method suboptimalfor building a Platform as a Service. The range of ports that can be allocatedfor NodePort services is from 30000 to 32768 by default. This means that wedonly be able to run 2768 instances of <code>neetodeploy exec</code> at any given time inour current cluster setup in EKS.</p><h4>3. Bastion host, the SSH proxy server</h4><p>We thought of deploying an SSH proxy server to circumvent the hard limits on theLoadBalancer and NodePort approaches. Such proxy servers are usually calledbastion hosts or jump servers. These are servers that are designed to provideaccess to a private network from a public network. The name &quot;bastion&quot; comes fromthe military structure that projects outwards from a castle or a fort. In asimilar sense, our bastion host will be a Kubernetes deployment that is exposedto the public network and acts as an interface between our private cluster andthe public internet. For our requirement, we can proxy SSH connections toconsole pods through this bastion host deployment. We only needed to expose thebastion host as a single LoadBalancer or a NodePort service, to which the CLIcan connect. This would solve the issue of having a hard limit on the number ofLoadBalancer or NodePort services we can create.</p><p><img src="/blog/images/images_used_in_blog/2023/building-neeto-deploy-cli/bastion-host.png" alt="The bastion host architecture for exposing deployments"></p><p>We quickly set up a bastion host and tested the whole idea. It was workingseamlessly! However, there were a lot of edge cases we'd have to handle if weare going to release this to the public. Ideally, we would want a new pair ofSSH keys for each console pod. We could generate this and store it as secrets inthe cluster. Apart from the hassle of handling all the SSH keys and updating thebastion host each time, we would have to make sure that users will not be ableto get SSH access to the bastion host deployment manually, outside the CLI. Thiswas difficult since the CLI would need access to the private key in order toconnect to the bastion host anyway. This would mean that users could SSH intothe bastion host after digging through the CLI gem's source, and possibly evenexec into any deployment in the cluster if they knew what they were looking for.Users with malicious intent could possibly bring down the bastion host itselfonce they're inside, rendering the exec command unusable.</p><p>We thought about installing kubectl inside the bastion host with a restrictive<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">RBAC</a> so that wecan run <code>kubectl exec</code> from inside the bastion host, also while making sure thatusers could not run anything destructive from inside the bastion host. But thisjust adds more moving parts to the system. The SSH proxy approach makes sense ifyou have to expose your cluster among developers for internal use, but it is notideal when you are building a CLI for public use.</p><p>After figuring out <em>how not to solve the problem</em>, we came to the conclusionthat using websockets was the better approach for now. We already have thedyno-console-manager app, which we are using for the shell access from the webdashboard, we updated the same to handle connections from the CLI. From theCLI's end, we wrote a simple WebSocket client and handled the parsing of userinputs and printing the responses from the shell. All of the commands you enterwould be run on the console pod through the websocket connection.</p><h3>Streaming logs to the terminal</h3><p>Live logs were streamed to the web dashboard using websockets too. This was alsohandled by the above-mentioned dyno-console-manager. Since we had written a Rubywebsocket client for the <code>exec</code> command, we decided to use the same approachwith logs too.</p><p>The CLI would send a request to the dyno-console-manager with the apps name andthe dyno-console-manager would run <code>kubectl logs</code> for your app's deployment andstream it back to the CLI via a websocket connection it creates.</p><p>We took our time with architecting the NeetoDeploy CLI, but thanks to that welearnt a lot about designing robust systems that would scale in the long term.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email at<a href="mailto:invite@neeto.com">invite@neeto.com</a>.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">X</a>. You can also join our<a href="https://launchpass.com/neetohq">community Slack</a> to chat with us about anyNeeto product.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Optimizing NeetoDeploy's Cluster Autoscaler]]></title>
       <author><name>Sreeram Venkitesh</name></author>
      <link href="https://www.bigbinary.com/blog/optimizing-neeto-deploy-autoscaler"/>
      <updated>2023-10-10T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/optimizing-neeto-deploy-autoscaler</id>
      <content type="html"><![CDATA[<p>Once we<a href="https://bigbinary.com/blog/solving-scalability-in-neeto-deploy">configured the Cluster Autoscaler</a>to scale NeetoDeploy according to the load, the next challenge was how to dothis efficiently. The primary issue we had to solve was to reduce the time wehad to wait for the nodes to scale up.</p><h2>Challenges faced</h2><p>The Cluster Autoscaler works by monitoring the pods and checking if they arewaiting to get scheduled. If no nodes are available to schedule the waitingpods, they will be in <code>Pending</code> state, until the <code>kube-scheduler</code> finds asuitable node.</p><p>With our default Cluster Autoscaler setup, if a pod is provisioned when thecluster is at maximum capacity, the pod will go into <code>Pending</code> state and remainthere until the cluster has scaled up. This entire flow consists of thefollowing steps:</p><ul><li>The Cluster Autoscaler scans the pods and identifies the un-schedulable pods.</li><li>The Cluster Autoscaler provisions a new node. We have configured theautoscaler deployment with the necessary permissions for the autoscalinggroups in EC2, with which the autoscaler can provision new EC2 machines.</li><li>Kubernetes adds the newly provisioned node to the clusters control plane, andthe cluster is scaled up.</li><li>The pending pod gets scheduled to the newly added node.</li></ul><p>From the perspective of a user who wants to get their application deployed, aconsiderable amount of time is taken to complete all these steps in addition tothe time taken to build and release their code. We can only optimize it so muchby decreasing the scan interval of the Cluster Autoscaler. We need a mechanismby which we can always have some buffer nodes ready so that the usersdeployments never go into <code>Pending</code> state and wait for the nodes to beprovisioned.</p><h2>Pod Priority, Preemption and Overprovisioning</h2><p>Kubernetes allows us to assign different priorities to pods. By default, all thepods have a priority of <code>0</code>. When a pod is scheduled, it can preempt or evictother pods with lower priority than itself and take its place. We will use thisfeature of pod preemption to reserve resources for incoming deployments.</p><p>We created a new pod priority class with a priority of <code>-1</code> and used this tooverprovision<a href="https://www.ianlewis.org/en/almighty-pause-container">pause pods</a>. We allocateda fixed amount of CPU and memory resources to these pause pods. The pausecontainer is a placeholder container used by Kubernetes internally and doesntdo anything by itself.</p><p><img src="/blog/images/images_used_in_blog/2023/optimizing-neeto-deploy-autoscaler/overprovisioned-cluster.png" alt="Illustration of overprovisioning a Kubernetes cluster"></p><p>Now if the cluster is at full capacity and a new deployment is created,technically the newly created pod doesn't have space to be scheduled in thecluster. In normal cases, this would warrant a scale-up to be triggered by theCluster Autoscaler and the pod would be <code>Pending</code> until the new node isprovisioned and attached to the cluster.</p><p><img src="/blog/images/images_used_in_blog/2023/optimizing-neeto-deploy-autoscaler/overprovisioner-stage-1.png" alt="Illustration of a new pod getting created in an overprovisioned cluster"></p><p>We can save a lot of time from this process since we have the overprovisionedpods with a lower pod priority. The newly created deployment would have a podpriority of <code>0</code> by default, and our placeholder pause pods with a priority of<code>-1</code> would be evicted in favor of this application pod. This means that new podscan be scheduled without having to wait for the Cluster Autoscaler to do itsmagic. Some space would always be reserved in our cluster.</p><p><img src="/blog/images/images_used_in_blog/2023/optimizing-neeto-deploy-autoscaler/overprovisioner-stage-2.png" alt="Illustration of a pod evicting an overprovisioning pod with lesser priority"></p><p>The pause pods would now move to <code>Pending</code> state after being evicted, which theCluster Autoscaler will pick up, and it will start provisioning a new node. Thisway, the autoscaler doesnt have to wait for a user to create a new deploymentto scale up the cluster. Instead, it would do it in advance to reserve somespace for potential deployments.</p><p><img src="/blog/images/images_used_in_blog/2023/optimizing-neeto-deploy-autoscaler/overprovisioner-stage-3.png" alt="Illustration of how an evicted overprovisioning pod triggering cluster scale up"></p><h2>Trade-offs</h2><p>Now that we have the overprovisioning deployments configured to reserve somespace in NeetoDeploy's cluster, we had to decide how much space to reserve. Ifwe increase the CPU and memory limit for the overprovisioning pods or theirnumber of replicas, we will have more space reserved in our cluster. This meansthat we can handle more user deployments concurrently, but we will incur thecost of keeping the extra buffer running. The trade-off here is between the costwe are willing to pay and the load we want to handle.</p><p>For running NeetoDeploy, we started with three copies of overprovisioning podswith 500 milli vCPUs, and later scaled it to 10 replicas after we moved thereview apps of all the <a href="https://neeto.com">Neeto products</a> to NeetoDeploy. Wehave been running all our internal staging deployments and review apps for thepast eleven months, and this configuration has been working pretty well for usso far.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email at<a href="mailto:invite@neeto.com">invite@neeto.com</a>.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">Twitter</a>. You can also join our<a href="https://launchpass.com/neetohq">community Slack</a> to chat with us about anyNeeto product.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Scaling NeetoDeploy with the Cluster Autoscaler]]></title>
       <author><name>Sreeram Venkitesh</name></author>
      <link href="https://www.bigbinary.com/blog/solving-scalability-in-neeto-deploy"/>
      <updated>2023-08-29T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/solving-scalability-in-neeto-deploy</id>
      <content type="html"><![CDATA[<p>We are building <a href="https://www.neeto.com/neetodeploy">NeetoDeploy</a>. It's a Herokualternative. Check out our previous blog to see<a href="https://www.bigbinary.com/blog/neeto-deploy-zero-to-one">how we started</a>building NeetoDeploy.</p><p>We started using NeetoDeploy internally for all our staging applications and the&quot;pull request review&quot; applications. Very soon, we ran into scaling challenges.Some days, we have too many pull requests, and some days, very few pullrequests. Some days, the staging sites have an extra load.</p><p>Scaling is a fundamental problem in container orchestration. We need to ensurethat we have enough computing resources to handle the load, and at the sametime, we need to make sure we are not spending money on resources that are notbeing utilized. If we need to run 10 applications, we should only have to payfor as much computing as is required for running 10 applications. But if thisnumber increases to 100 one day, our system should be able to provision newcomputing resources.</p><h2>Understanding Kubernetes Autoscalers</h2><p>When we need to scale, we can manually allocate resources. This istime-consuming and repetitive. Kubernetes excels in autoscaling. It providesmany different options to meet our scaling needs.</p><p>We can define autoscalers of different types to make our cluster and ourdeployments scale up or down based on various parameters and handle trafficgracefully. Kubernetes has three kinds of autoscalers operating at differentlevels.</p><h3>Horizontal Pod Autoscaler (HPA)</h3><p>The Horizontal Pod Autoscaler can scale up our Kubernetes deployments byincreasing the number of copies of our app's container, known as<a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/">replicas</a>.We can configure our Horizontal Pod Autoscaler to scale up our apps based onmetrics like increased memory usage, network load, or even the time of the daywhen we expect more traffic. Incoming traffic would be redirected to any of thereplicas and Kubernetes takes care of this.</p><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/hpa.png" alt="Illustration of how horizontal pod autoscaling works"></p><h3>Vertical Pod Autoscaler (VPA)</h3><p>The Vertical Pod Autoscaler takes a different approach to the same problemsolved by the Horizontal Pod Autoscaler. The Vertical Pod Autoscaler increasesthe CPU and memory resources allocated to our pod based on the load. The numberof containers running would remain the same, but they would have more resourcesto work with and, hence, would be able to handle increased loads.</p><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/vpa.png" alt="Illustration of how vertical pod autoscaling works"></p><h3>Cluster Autoscaler</h3><p>While horizontal and vertical pod autoscaling operate on the level of individualdeployments, the Cluster Autoscaler scales in the context of the entire cluster.It increases or decreases the number of nodes in the cluster, making space formore deployments.</p><h2>The need for Cluster Autoscaler</h2><p>Suppose our Kubernetes cluster has three nodes. When we create deployments, ourapplications will be deployed to any of these three nodes based on the resourceavailability in the nodes. Without the Cluster Autoscaler, the number ofmachines running in our cluster will be fixed at 3.</p><p>If there is a sudden surge in traffic and we have to deploy 100 apps instead of10, the resources needed would overflow the three nodes' combined CPU and memoryresources. Essentially, our cluster will not have enough resources toaccommodate all the deployments, and the pods for our deployments will be stuckin the <code>Pending</code> state.</p><h2>How Cluster Autoscaler works</h2><p>The Cluster Autoscaler increases and decreases the number of nodes in ourcluster based on the number of deployments created. So in the above example, ifwe had the Cluster Autoscaler running, it would check if any pods are stuck in a<code>Pending</code> state, and if so, it'll spawn a new node in the cluster. If threenodes are not enough to accommodate all our deployments, the Cluster Autoscalerwould detect this and scale the cluster to four nodes.</p><p><strong>Let us see step by step how cluster autoscaler would work:</strong></p><ol><li>If our Kubernetes cluster is running at full capacity, the pods of a newlycreated deployment would have no space to fit in the cluster. In this case,the pod would be stuck in the <code>Pending</code> state and wouldn't be &quot;scheduled&quot; toany node.</li></ol><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/cluster-autoscaler-1.png" alt="Illustration of pod stuck in pending state"></p><ol start="2"><li>The Cluster Autoscaler keeps checking if there are unscheduled pods. If so,the Cluster Autoscaler triggers a scale-up by provisioning a new node andattaching it to the cluster.</li></ol><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/cluster-autoscaler-2.png" alt="Illustration of cluster autoscaler provisioning a new node"></p><ol start="3"><li>Once the new node has been provisioned, the capacity of the cluster hasincreased, and the pod, which was previously in the <code>Pending</code> state, will bescheduled to the newly created node.</li></ol><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/cluster-autoscaler-3.png" alt="Illustration of the pending pod being scheduled to the newly provisioned node"></p><p>The same thing can happen in reverse as well. Consider the case wheredeployments are deleted or scaled down, and our nodes are not utilizedcompletely.</p><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/cluster-autoscaler-4.png" alt="Illustration of nodes being under-utilized"></p><p>In such a scenario, the Cluster Autoscaler would scale down the nodes so thatonly the minimum required resources are running. Existing deployments fromdifferent nodes would be rescheduled and &quot;packed&quot; into a smaller number of nodesbefore the unwanted machines are terminated. The Cluster Autoscaler marks unusednodes as <code>SchedulingDisabled</code> so that no pods are scheduled or moved into thisnode.</p><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/cluster-autoscaler-6.png" alt="Illustration of how pods are packed into minimum number required"></p><p>The Cluster Autoscaler then unprovisions the unused node.</p><p><img src="/blog/images/images_used_in_blog/2023/solving-scalability-in-neeto-deploy/cluster-autoscaler-7.png" alt="Illustration of how unused nodes are unprovisioned"></p><p>The Cluster Autoscaler ensures that the cluster is in a stable state where nopods are stuck in the <code>Pending</code> state, and no under-utilized resources are keptrunning. By default, the Cluster Autoscaler checks for pending pods and scalesdown candidates every 10 seconds, but we can configure this to increase ordecrease the speed at which the cluster autoscales.</p><h2>Deploying the Cluster Autoscaler</h2><p>The code for Cluster Autoscaler is available at<a href="https://github.com/kubernetes/autoscaler">kubernetes/autoscaler</a>, and this canbe used to set up Cluster Autoscaler with any Kubernetes cluster.</p><p>All major cloud platforms like AWS, GCP, Azure, and DigitalOcean have ClusterAutoscaler support in their managed Kubernetes services. For example, if theCluster Autoscaler is deployed on Amazons Elastic Kubernetes Service (EKS). Itwould spawn new EC2 instances as needed and attach them to the EKS clusterscontrol plane. The complete list of all cloud providers is<a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#deployment">here</a>.</p><p>Since we are running NeetoDeploy on EKS, we referred to the documentation forsetting up autoscaler with EKS and created an OIDC identity provider in IAM. Wethen created an IAM policy for using the autoscaler with the necessarypermissions, such as <code>DescribeAutoScalingGroups</code> for describing autoscalinggroups in AWS and <code>setDesiredCapacity</code> to change the desired limit of nodes inthe cluster, etc.</p><p>We then created an IAM role with the IAM policy and OIDC provider createdearlier and used this role to set up RBAC in the cluster, along with the ClusterAutoscaler deployment. This ensures that the Cluster Autoscaler deployment hasall the permissions it needs for inspecting pending pods and working with nodes.</p><h2>Seeing the autoscaler in action</h2><p>Once all of this was done, we tried creating multiple deployments with moresignificant memory and CPU limits consecutively to see if the cluster couldhandle the increased load. Even though the autoscaler took some time to react tothe increased load, the cluster was getting scaled up according to therequirement, and none of the pods were stuck in the <code>Pending</code> state for long.Once we deleted our test deployments, the cluster automatically scaled backdown.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email at<a href="mailto:invite@neeto.com">invite@neeto.com</a>.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">Twitter</a>. You can also join our<a href="https://launchpass.com/neetohq">community Slack</a> to chat with us about anyNeeto product.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Continuous backup of PostgreSQL in NeetoDeploy]]></title>
       <author><name>Abhishek T</name></author>
      <link href="https://www.bigbinary.com/blog/postgresql-continuos-rollback-feature"/>
      <updated>2023-08-01T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/postgresql-continuos-rollback-feature</id>
      <content type="html"><![CDATA[<p><a href="https://neeto.com/neetodeploy">NeetoDeploy</a> is a Heroku alternative fordeploying web applications. Currently, NeetoDeploy is being used by Neeto for allpull request review applications and the staging application. NeetoDeploy is aplatform as a service (PaaS) solution that makes it easy to manage and deployapplications with features like database backup, auto deployment from GitHub,live logs, SSL certificates management, auto scaling, and more.</p><p>NeetoDeploy's PostgreSQL add-on has a continuous backup feature. This featureallows you to roll your database back to a specific time in the past. We cansplit the continuous backup process into data backup and recovery. Let's discussthese steps in detail.</p><h2>Initial setup</h2><p>Let us create a new PostgreSQL cluster and populate it with some data.</p><h4>Creating parent database</h4><pre><code class="language-bash">initdb /usr/local/var/postgresql-parent</code></pre><h4>Creating some sample data</h4><p>Start the PostgreSQL server and log in using <code>psql</code>. Provide the role and password ifneeded.</p><pre><code class="language-bash">pg_ctl -D /usr/local/var/postgresql-parent startpsql -d postgres</code></pre><h4>Create a new table</h4><pre><code class="language-sql">postgres=#CREATE TABLE customers (  user_id SERIAL PRIMARY KEY,  username VARCHAR(50),  email VARCHAR(100),  registration_date DATE);</code></pre><h4>Seed sample data</h4><pre><code class="language-sql">postgres=#INSERT INTO customers  (username, email, registration_date)VALUES('johndoe', 'johndoe@example.com', '2021-01-01'),('janedoe', 'janedoe@example.com', '2021-02-15'),('bobsmith', 'bobsmith@example.com', '2021-03-10'),('sarahlee', 'sarahlee@example.com', '2021-04-05'),('maxwell', 'maxwell@example.com', '2021-05-20');</code></pre><h2>Data backup</h2><p>To perform recovery, we need to have access to the existing data. Hence we muststore all the database changes in a separate location, allowing us to utilizethis data for recovery.</p><p>This is where the<a href="https://www.postgresql.org/docs/current/wal-intro.html">Write Ahead Log (WAL)</a>in PostgreSQL comes into play. PostgreSQL follows a mechanism that writes alltransactions to the WAL before committing them to the table data files. Bypreserving the WAL, we can reconstruct the data by traversing the loggedtransactions. Utilizing the information stored in the WAL, we can systematicallyreapply the recorded transactions to recreate the data and achieve the desiredrecovery. Your WAL files are in your PostgreSQL data directory's <code>pg_wal</code>folder.</p><h3>WAL archiving</h3><p>As data modifications occur in the databases, new WAL files are generated whileolder WAL files are eventually discarded. Therefore, it is crucial to store theWAL files before they are deleted. Here we will use an<a href="https://aws.amazon.com/s3/">AWS S3 bucket</a> to store our WAL files. Also, wewill use <a href="https://github.com/wal-e/wal-e">wal-e</a> and<a href="https://pypi.org/project/envdir/">envdir</a> to simplify the process.</p><h4>Install helper modules</h4><pre><code class="language-bash">python3 -m pip install wal-e envdir</code></pre><p>If you face any dependency issues, please refer<a href="https://github.com/wal-e/wal-e">wal-e</a> to fix them.</p><h4>Store AWS S3 credentials</h4><p>To use &quot;envdir&quot; with your AWS credentials, store them in a folder. The followingcode snippet demonstrates how to accomplish this:</p><pre><code class="language-bash">mkdir ~/wal-e.envecho &quot;YOUR_AWS_REGION&quot; &gt; ~/wal-e.env/AWS_REGIONecho &quot;YOUR_AWS_SECRET_ACCESS_KEY&quot; &gt; ~/wal-e.env/AWS_SECRET_ACCESS_KEYecho &quot;YOUR_AWS_ACCESS_KEY_ID&quot; &gt; ~/wal-e.env/AWS_ACCESS_KEY_IDecho &quot;YOUR_AWS_STORAGE_PATH&quot; &gt; ~/wal-e.env/WALE_S3_PREFIX</code></pre><h4>Configure WAL archiving</h4><p>Now, let's configure PostgreSQL to store the WAL files in your S3 bucket beforethey are deleted. Open your PostgreSQL configuration file &quot;postgresql.conf&quot;,which is located in your data directory i.e.,&quot;/usr/local/var/postgresql-parent&quot;, and make the following changes.</p><pre><code class="language-bash"># To enable WAL archiving.archive_mode = on# Determines how much information is written to the WAL.wal_level = replica# Force PostgreSQL to switch WAL file in every 60 secondsarchive_timeout = 60# Command for pushing the wal files to the S3 bucket.archive_command = 'envdir ~/wal-e.env wal-e wal-push %p'</code></pre><p>You can find more information about the above configurations by referring tothis<a href="https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-ARCHIVE-COMMAND">documentation</a>.</p><p>Now let's restart the server.</p><pre><code class="language-bash"> data_directory=&quot;/usr/local/var/postgresql-parent&quot; pg_ctl -D $data_directory restart -l $data_directory/postgresql.log</code></pre><p>You can watch the PostgreSQL logs like this.</p><pre><code class="language-bash"> tail -f /usr/local/var/postgresql-parent/postgresql.log</code></pre><p>After starting your server, monitor the PostgreSQL logs. If you don't observeany WAL archive logs even after 60 seconds, it is because a WAL file switch willonly occur if there are any modifications. To generate the desired outcome, makeupdates in the database. Upon doing so, you will be able to see lines similar tothe following in your PostgreSQL logs:</p><pre><code>Message: 'MSG: completed archiving to a file\nDETAIL: Archiving to &quot;s3://neeto-deploy-backups/local/wal_005/000000020000000000001.lzo&quot; complete at 158.209KiB/s.\nSTRUCTURED: time=2023-05-18T06:22:27.613242-00 pid=263 action=push-wal key=s3://neeto-deploy-backups/local/wal_005/000000020000000000001.lzo prefix=local/ rate=158.209 seg=000000020000000000001 state=complete'</code></pre><h3>Base backup</h3><p>Since we have enabled WAL archiving, all new changes are stored in the bucket asWAL files. However, what about the data we created before configuring WALarchiving? To ensure a complete recovery, we also need that data. Therefore, weneed to take a base backup of our database. The following code will initiate thebase backup process and push it to the S3 bucket.</p><pre><code class="language-bash">envdir ~/wal-e.env wal-e backup-push /usr/local/var/postgresql-parent</code></pre><p>Running the above command yields the following output.</p><pre><code>wal_e.main   INFO     MSG: starting WAL-EDETAIL: The subcommand is &quot;backup-push&quot;.STRUCTURED: time=2023-05-18T06:24:27.613242-00 pid=41178wal_e.worker.upload INFO     MSG: begin uploading a base backup volumeDETAIL: Uploading to &quot;s3://neeto-deploy-backups/local/basebackups_005/base_000000010000000000005_00000040/tar_partitions/part_00000000.tar.lzo&quot;.STRUCTURED: time=2023-05-18T06:24:29.527622-00 pid=41178wal_e.worker.upload INFO     MSG: finish uploading a base backup volumeDETAIL: Uploading to &quot;s3://neeto-deploy-backups/local/basebackups_005/base_000000010000000000005_00000040/tar_partitions/part_00000000.tar.lzo&quot;complete at 1459.68KiB/s.STRUCTURED: time=2023-05-18T06:24:33.121716-00 pid=41178NOTICE:  all required WAL segments have been archived</code></pre><p>After completing the base backup, you can verify the presence of the base backupfolder in your S3 bucket. Now we have the backup and new WAL files are beingpushed to our bucket.</p><h2>Data recovery</h2><p>Let's consider a scenario where you need to perform a rollback. Suppose youaccidentally deleted a table from your database that contained multiple records,making it impractical to recall and reinsert them manually. As a first step,make a note of the time at which the table was deleted. For instance, let's sayit occurred on 2023-05-18 11:57:11 UTC. The objective is to roll back to a pointbefore that time. Let's roll back to 2023-05-18 at 11:57:00 UTC.</p><h3>Let's make a mistake</h3><p>We are going to drop a table.</p><pre><code class="language-sql">postgres=#DROP TABLE customers;</code></pre><p>Note the time the table got deleted. Here it is 2023-05-18 11:57:11 UTC. Nowlet's discuss how we can restore the deleted table.</p><h3>Creating a new data directory</h3><p>Let's create a new data directory where we can recover the data.</p><pre><code class="language-bash">initdb /usr/local/var/postgresql-child</code></pre><h3>Fetch the base backup</h3><p>The base backup serves as the starting point, while the remaining data will beregenerated by PostgreSQL using the WAL files. To fetch the base backup, you canuse the following code snippet.</p><pre><code class="language-bash">envdir ~/wal-e.env wal-e backup-fetch /usr/local/var/postgresql-child LATEST</code></pre><p>The &quot;LATEST&quot; tag will pull the latest base backup if multiple base backups arein the bucket. On running the above command you will receive an output similarto the following.</p><pre><code>wal_e.main   INFO     MSG: starting WAL-EDETAIL: The subcommand is &quot;backup-fetch&quot;.STRUCTURED: time=2023-05-18T06:29:22.826910-00 pid=41789wal_e.worker.s3.s3_worker INFO     MSG: beginning partition downloadDETAIL: The partition being downloaded is part_00000000.tar.lzo.HINT: The absolute S3 key is local/basebackups_005/base_000000010000000000000005_00000040/tar_partitions/part_00000000.tar.lzo.STRUCTURED: time=2023-05-18T06:29:24.674572-00 pid=41789</code></pre><h3>Configuring recovery</h3><p>Now, we need to regenerate the remaining data from the WAL files. To do that,make the following changes in the PostgreSQL configuration file.</p><pre><code class="language-bash">#postgresql.conf# Command to fetch each wal files from the bucket.restore_command = 'envdir ~/wal-e.env wal-e wal-fetch %f %p'# Time at which we want to stop recovery.recovery_target_time  = '2023-05-18 11:57:00'</code></pre><p>We can stop the parent server since it is no longer needed.</p><pre><code class="language-bash">pg_ctl -D /usr/local/var/postgresql-parent stop</code></pre><p>To initiate the server in recovery mode and to allow PostgreSQL to regeneratethe data using the WAL files, create a file named &quot;recovery.signal&quot; in the newdata directory.</p><pre><code class="language-bash">touch /usr/local/var/postgresql-child/recovery.signal</code></pre><p>Start the child server pointing to the new data directory by running thefollowing command.</p><pre><code class="language-bash"> data_directory=&quot;/usr/local/var/postgresql-child&quot; pg_ctl -D $data_directory start -l $data_directory/postgresql.log</code></pre><p>If you examine the PostgreSQL logs, you will notice that the recovery process ispaused at the specified time.</p><pre><code>2023-05-18 12:07:41.688 IST [43159] LOG:  restored log file &quot;000000010000000000006&quot; from archive2023-05-18 12:07:41.692 IST [43159] LOG:  recovery stopping before commit of transaction 743, time 2023-05-18 11:57:11.135927+05:302023-05-18 12:07:41.692 IST [43159] LOG:  pausing at the end of recovery2023-05-18 12:07:41.692 IST [43159] HINT:  Execute pg_wal_replay_resume() to promote.</code></pre><p>It's important to note that you won't be able to connect to the server usingpsql at this point because it is not ready to accept connections. By default,PostgreSQL pauses the recovery process after reaching the recovery target time.To change this behavior and to allow the server to accept connections afterreaching the target time, add the following configuration to your PostgreSQLconfiguration file.</p><pre><code class="language-bash">recovery_target_action  = 'promote'</code></pre><p>After making the above change, restart the server once again by running thefollowing command.</p><pre><code class="language-bash">data_directory=&quot;/usr/local/var/postgresql-child&quot;pg_ctl -D $data_directory restart -l $data_directory/postgresql.log</code></pre><p>Now, you will observe that the server is ready to accept connections aftercompleting the recovery process. Once the recovery is successfully completed,PostgreSQL automatically deletes the &quot;recovery.signal&quot; file. You can verify thesuccessful deletion of the recovery file from your data directory.</p><p>Once the server is up and ready, connect to the server and check if the deletedtable is present. You will find that the deleted table is now restored andavailable in the database.</p><pre><code class="language-sql">postgres=#select * from customers; user_id | username |        email         | registration_date---------+----------+----------------------+-------------------       1 | johndoe  | johndoe@example.com  | 2021-01-01       2 | janedoe  | janedoe@example.com  | 2021-02-15       3 | bobsmith | bobsmith@example.com | 2021-03-10       4 | sarahlee | sarahlee@example.com | 2021-04-05       5 | maxwell  | maxwell@example.com  | 2021-05-20(5 rows)</code></pre><p>To learn more about PostgreSQL continuous backup, you can refer to the<a href="https://www.PostgreSQL.org/docs/current/continuous-archiving.html">official PostgreSQL documentation</a>.</p><h2>Challenges faced</h2><h3>Restoration of PostgreSQL roles</h3><p>When we restored the PostgreSQL database, the roles and passwords were alsorestored in the database. However, we create a new database with the desireddata in the continuous rollback feature. Therefore, we should use a new role andpassword to ensure security. We resolved this issue by changing the restoredrole and password to a new one.</p><h3>Early recovery completion</h3><p>Another issue we encountered was the early recovery of the database beforereaching the specified target time. Suppose no activity occurs in the database,and we attempt to roll back to the current time. PostgreSQL initiates therecovery process and recovers all the available WAL files. However, it fails tolocate the target time since no corresponding WAL file is available for thegiven timestamp. PostgreSQL raises the following error in this scenario andshuts down the server.</p><pre><code>2023-05-18 02:12:15.846 UTC [76] LOG:  last completed transactionwas at log time 2023-05-18 02:04:02.755919+002023-05-18 02:12:15.846 UTC [76] FATAL:  recovery ended before configuredrecovery target was reached2023-05-18 02:12:15.850 UTC [75] LOG:  startup process (PID 76) exited withexit code 12023-05-18 02:12:15.850 UTC [75] LOG:  terminating any other active serverprocesses2023-05-18 02:12:15.853 UTC [75] LOG:  shutting down due to startupprocess failure2023-05-18 02:12:15.859 UTC [75] LOG:  database system is shut down</code></pre><p>In this case, we can remove the specified target time and restart the server. Ifwe do not provide any target time, PostgreSQL will recover until the lastavailable WAL file, which will contain the latest transaction of our database.</p><h3>Recovery is an asynchronous process</h3><p>As mentioned above, we must update the restored role and password to new ones.</p><pre><code class="language-bash">update-role-and-password(){# Create a new temporary user as we cannot update the current session user.psql -d &lt;DB_NAME&gt; \-c &quot;CREATE USER tempuser WITH SUPERUSER LOGIN PASSWORD 'tempuser-pwd';&quot;# Login as a temporary user and update the parent role and password.PGPASSWORD=tempuser  PGUSER=tempuser-password  psql -d &lt;DB_NAME&gt; \-c &quot;ALTER ROLE &lt;parent-role&gt; RENAME TO &lt;NEW_ROLE&gt;;ALTER ROLE &lt;NEW_ROLE&gt; WITH PASSWORD &lt;NEW_PASSWORD&gt;;&quot;# Login as the new user and remove the temporary user.PGPASSWORD=&lt;NEW_ROLE&gt; PGUSER=&lt;NEW_PASSWORD&gt; psql -d &lt;DB_NAME&gt; \-c &quot;DROP ROLE tempuser;&quot;}# Start the server in recovery mode.pg_ctl -D /usr/local/var/postgresql-child start# Call the method to update role and password.update-role-and-password</code></pre><p>If you try the above code, you will encounter the following error.</p><pre><code>ERROR:  cannot execute CREATE ROLE in a read-only transaction</code></pre><p>Since the recovery process is asynchronous, the system may attempt to update therole before completing the recovery. It is important to note that the serverwill be in read-only mode during the recovery, which can lead to the errormentioned.</p><p>To solve this issue, it is necessary to determine when the recovery process wascompleted. This can be achieved by checking for the existence of the &quot;recovery.signal&quot; file in a loop.</p><pre><code class="language-bash">wait_until_recovery_is_completed(){  recovery_signal_path=&quot;/usr/local/var/postgresql-child/recovery.signal&quot;  while [ -f &quot;$file_path&quot; ]; do    sleep 1  # Wait for 1 second before checking again.  done}</code></pre><pre><code>pg_ctl -D /usr/local/var/postgresql-child startwait_until_recovery_is_completedupdate-role-and-password</code></pre>]]></content>
    </entry><entry>
       <title><![CDATA[NeetoDeploy: Zero to One]]></title>
       <author><name>Unnikrishnan KP</name></author>
      <link href="https://www.bigbinary.com/blog/neeto-deploy-zero-to-one"/>
      <updated>2023-06-08T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/neeto-deploy-zero-to-one</id>
      <content type="html"><![CDATA[<p>This blog chronicles the exciting journey of how we built<a href="https://neeto.com/neetodeploy">NeetoDeploy</a>. It's a Heroku alternative. Westarted from absolute zero and built a fully functional PaaS product with almostall the features that Heroku offers on their review apps, with a team of 4developers in 2 months.</p><h2>How it all started</h2><p>At <a href="https://neeto.com">Neeto</a>, we are building<a href="https://blog.neeto.com/p/neeto-products-and-people">20+ products</a>simultaneously. All of these products had their production and stagingenvironments hosted on Heroku. In addition, pull requests from these productshad review apps deployed on Heroku. With an average of 5 open PRs at any timeper product, we are talking about 100+ live review apps, 20+ staging apps, and15+ production apps. Each one of these apps needed PostgreSQL, Redis, andElasticSearch instances, besides other requirements.</p><p>In August 2022, Heroku made this<a href="https://blog.heroku.com/next-chapter">announcement</a>.</p><blockquote><p>&quot;Starting November 28, 2022, we plan to stop offering free product plans andplan to start shutting down free dynos and data services.&quot;</p></blockquote><p>It meant that in 2 months, our monthly Heroku bill would skyrocket. At first, wetalked about switching to one of the Heroku alternatives.</p><blockquote><p>&quot;Every problem is an opportunity in disguise.&quot; - John Adams.</p></blockquote><p>And we thought - &quot;Why not build our own Heroku replacement?&quot;.</p><p><a href="https://bigbinary.com">BigBinary</a> has over 11 years of Ruby on Rails and Reactconsulting expertise. However, building a Heroku replacement, a complex TheDevOps-heavy project was way out of our comfort zone. With faith in ourengineering team and an exuberant spirit of youthful adventure, we embarked on ajourney in early September 2022 to build<a href="https://neeto.com/neetodeploy">NeetoDeploy</a>.</p><h2>The Journey</h2><p>We put together a team of 4 developers. We discussed technologies ranging fromCapistrano, Ansible, and AWS to Kubernetes. The more we talked, the more itbecame clear that no single technology could solve this problem. We needed acomplex system with multiple independent components that would interact togetherto handle the following core responsibilities:</p><ol><li>GitHub integration.</li><li>A build system that would identify the application's run-time dependencies,install them in the correct order, and build runnable machine images.</li><li>A scalable containerized environment that can run the pre-built machineimages.</li><li>A place to store the built machine images.</li><li>If the application needed a connection to external services (PostgreSQL,Redis, ElasticSearch, etc.), the ability to provision those on run-time.</li><li>A dashboard to view and manage the deployments.</li><li>Live logs.</li><li>Load balancing/routing.</li></ol><h2>The Architecture</h2><p>Once we understood the core functionality, we devised an architecture for thesystem. We then drew an architecture diagram depicting the different componentsand how they would interact with each other.</p><p><img src="/blog/images/images_used_in_blog/2023/neeto-deploy-zero-to-one/neeto-deploy-arch.webp" alt="NeetoDeploy - Architecture"></p><p>This diagram helped facilitate the rest of the discussions and our questionsregarding the project's feasibility. We looked at the diagram and saw for thefirst time that we could break up this complex beast into manageable pieces thatwe could build, test and deploy independently.</p><p>As per the plan, NeetoDeploy would comprise the following independentcomponents:</p><ol><li>Slug Compiler</li><li>Dyno Manager</li><li>Add-on Manager</li><li>The Compute Cluster - with the router and load balancer</li><li>Web Dashboard</li></ol><h2>The Slug Compiler</h2><p>This component would bundle the application code, its run-time dependencies(Ruby, Node, Ruby gems, NPM packages, third-party tools like ffmpeg, etc.), andthe operating system into a single image. We could then run this image on acontainer.</p><p>Using Docker to build the image was the first approach that we tried. Afteranalyzing the problem, we understood that we needed to generate a Dockerfile onthe fly. While working on this problem, we stumbled upon<a href="https://buildpacks.io/">Cloud Native Buildpacks</a>.</p><p>Their website said, &quot;transform your application source code into images that canrun on any cloud&quot;. This is precisely what we needed.</p><p>CNB(Cloud Native Buildpacks) system has three components:</p><ol><li>Stack</li><li>Builder</li><li>Buildpacks</li></ol><p>CNB provides <a href="https://github.com/buildpacks/pack">pack utility</a> that wouldaccept stack, builder, and buildpacks as parameters and build an OCI-compliantmachine image that works with existing Docker tooling and any broader containerecosystem like Kubernetes. In addition, Heroku had open-sourced their olderstacks and builders.</p><p>Now we had everything we needed to build runnable images. But we needed theability to build multiple images in parallel since NeetoDeploy would havevarious projects, and each project could have numerous deployments (appcorresponding to the main branch for staging/production and review apps for eachcommit in a pull request).</p><p>We built the initial version of Slug Compiler as a Ruby on Rails application,exposing API endpoints to start and stop a build. It would need a GitHubrepository URL to fetch the source code, a list of buildpacks, and a callbackURL as inputs. Once the image is successfully built, it will make a POST requestto the callback URL.</p><p>The slug compiler would spin up an EC2 machine for each build request. We made acustom AMI from an Ubuntu image with <code>pack</code> and other packages required for theCNB build system to run. Once the <code>pack</code> command builds the image, it will pushit to our Dockerhub account (Docker registry) and return the image URL via acallback. It would then kill the EC2 instance.</p><p>We found uploading the image to <a href="https://hub.docker.com">Dockerhub</a> very slow.So we set up a private Docker registry on<a href="https://aws.amazon.com/ecs/">AWS ECS</a>.</p><h2>The Add-on Manager</h2><p>Applications typically need access to a database, search service, cache service,etc. The Neeto products use PostgreSQL, Redis, and ElasticSearch. We decided tobuild the first version of the add-on manager to support these add-ons.</p><p>We built the add-on infrastructure using three big<a href="https://aws.amazon.com/ec2/">AWS EC2</a> boxes to keep things simple. Each machineran a PG, Redis, and ElasticSearch server, respectively. We made the add-onmanager as a Ruby on Rails API server, which would accept the add-on type asinput and return a connection URI with login credentials included. We wouldsupport only a fixed version of the add-ons in the initial version.</p><p>Upon receiving the request for a PG add-on, we will create a new database on thePG server with a unique username/password and return the connection string. ForRedis and ElasticSearch, we simulated separate connections using namespaces.</p><p>This add-on manager had some severe drawbacks since multiple applications sharedthe same add-on servers running on the same physical machine. Two of the biggestdrawbacks had to do with Security and Resource choking.</p><ol><li>Security: With some effort, it was possible to tamper with the data of otherapplications.</li><li>Resource choking: If one application uses more CPU and Memory, it blocks allother applications.</li></ol><p>Nevertheless, this throw-away version of the add-on manager was a good start.</p><h2>The Dyno manager and the compute cluster</h2><p>We needed a secure, scalable, containerized environment to run our pre-builtimages. <a href="https://kubernetes.io">Kubernetes</a> was the obvious answer. Kubernetes,also known as K8s, is an open-source system for automating deployment, scaling,and managing containerized applications. We created our cluster on<a href="https://aws.amazon.com/eks/">AWS EKS</a>.</p><p>Dyno manager was once again a Ruby on Rails API server app, which takes apre-built OCI image URL and a callback URL as inputs. It would then interfacewith the Kubernetes cluster using the<a href="https://kubernetes.io/docs/reference/kubectl/">kubectl CLI</a> to deploy the imageon the cluster.</p><p>Our first cluster was of fixed size. It had four nodes (EC2 machines).Kubernetes would deploy images to these machines and manage them. But thefixed-size cluster soon proved inadequate as we had more parallel deploymentsthan could be accommodated. So we set up cluster autoscaling.</p><h2>Load balancing and routing</h2><p>All applications hosted on NeetoDeploy are made publicly available on a_.neetoreviewapp.com subdomain. A public IP address is needed to set up a<a href="https://www.cloudflare.com/en-gb/learning/dns/dns-records/dns-cname-record/">DNS CNAME record</a>for _.neetoreviewapp.com. We provisioned an<a href="https://aws.amazon.com/elasticloadbalancing/">Elastic Load Balancer</a>, an AWScomponent with a public IP address. ELB is the component of our system that actsas the interface to the outside world.</p><p>We now needed to route the incoming web requests based on the subdomain to oneof the deployed applications, which required a programmable router. An<a href="https://docs.nginx.com/nginx-ingress-controller/">Nginx ingress controller</a>would meet this requirement. The Nginx ingress controller is essentially anNginx server set up as a reverse proxy with the redirect rules necessary.</p><h2>The Web Dashboard</h2><p>NeetoDeploy needed an admin user-facing web application that would perform thefollowing functions:</p><ol><li>Admin user login.</li><li>Initiate Github account integration.</li><li>Select a repo and create a project.</li><li>View and manage the deployments for a project.</li><li>Interact with other components (Slug compiler, dyno manager, and add-onmanager) to coordinate and orchestrate the functioning of the NeetoDeploysystem.</li><li>Maintain state like store project-specific environment variables, add-onconnection URLs, pre-built image URLs, etc.</li></ol><p>We built the Dashboard app using Ruby on Rails, React.js,<a href="https://blog.neeto.com/p/neetoui-is-the-ui-library-of-neeto">neetoUI</a>, andother integrations typical of any Neeto product.</p><h2>Live logging</h2><p>We needed to fetch and display two types of logs at run-time:</p><ul><li>Build and release logs.</li><li>Application logs.</li></ul><p>Fetching live logs from a remote machine and displaying them in the browser is acomplicated problem. We went with<a href="https://api.rubyonrails.org/classes/ActionController/Live/SSE.html">Server Side Events</a>(SSE) for the first version and streamed the logs directly to the browser.</p><ol><li>The dyno manager would tail <code>kubectl logs</code>.</li><li>STDOUT was continuously read and transmitted as server-side events.</li><li>The dyno manager sent the <code>app-logs-url</code> to the dashboard app via a callback.</li><li>Dashboard app had a log-display React component that streamed the SSE eventsusing the <code>app-logs-url</code> and displayed the live logs.</li></ol><h2>How it all came together</h2><ol><li>The Admin user signed up for a new account.</li><li>The admin logged in to the Dashboard app.</li><li>The GitHub account was connected by the admin.</li><li>The admin created a new project after selecting a GitHub repository.</li><li>The admin made a new Pull Request in the selected GitHub repository.</li><li>The dashboard app received the GitHub event for the new pull requestcreation.</li><li>The dashboard app requested the slug compiler to build an image for therepository using code corresponding to the pull request's latest commit. Italso sent the list of buildpacks to use.</li><li>The dashboard app also requested the add-on manager to create the necessaryadd-ons.</li><li>The dashboard app received the add-on URLs and saved them.</li><li>Slug compiler built the image, stored it on ECS and made a callback to thedashboard app with the image URL, and the dashboard app saved the image URL.</li><li>The dashboard app requested the dyno manager to deploy and start thenecessary web and worker processes after passing the image URL and add-onURLs as environment variables to set on the containers.</li><li>The Dyno manager talked to Kubernetes, completed the specified clusterdeployments and set up the necessary ingress rules.</li><li>The application is available for public access athttps://xyz.neetoreviewapp.com.</li></ol><h2>Roadmap</h2><p>We improved each component to make them scalable, secure and robust. We addedfeatures like CLI, performance metrics, continuous database backup, enabledcaching, reduced build time, horizontal scaling to support the increased load,platform extensibility with custom build packs, stabilized live logging with<a href="https://fluentbit.io/">Fluentbit</a> and more.</p><p>We migrated the review apps and staging environment of all<a href="https://neeto.com">neeto</a> products to NeetoDeploy. We also migrated productioninstances of most of our static websites. We are now gearing up to migrate theproduction instances of all the Neeto products. Exciting features like customplans for dynos, support for more add-on types, autoscaling to supportunexpected surges in traffic and more, are in the pipeline.</p><p>In the coming weeks and months, we will be writing more about the challenges wefaced and how we are building NeetoDeploy. Stay tuned for more.</p><p>If your application runs on Heroku, you can deploy it on NeetoDeploy without anychange. If you want to give NeetoDeploy a try, then please send us an email at<a href="mailto:invite@neeto.com">invite@neeto.com</a>.</p><p>If you have questions about NeetoDeploy or want to see the journey, followNeetoDeploy on <a href="https://twitter.com/neetodeploy">Twitter</a>. You can also join our<a href="https://launchpass.com/neetohq">community Slack</a> to chat with us about anyNeeto product.</p>]]></content>
    </entry>
     </feed>