<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Igor Kuznetsov on Medium]]></title>
        <description><![CDATA[Stories by Igor Kuznetsov on Medium]]></description>
        <link>https://medium.com/@igkuz?source=rss-80d3d7acb66b------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*9Dmt2Uco1sIApsHn4nSExQ.jpeg</url>
            <title>Stories by Igor Kuznetsov on Medium</title>
            <link>https://medium.com/@igkuz?source=rss-80d3d7acb66b------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 11 May 2026 09:42:54 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@igkuz/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Building a dev setup for the AI era]]></title>
            <link>https://medium.com/@igkuz/building-a-dev-setup-for-the-ai-era-cb9a8dd6943e?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb9a8dd6943e</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[containers]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Sat, 09 May 2026 09:48:28 GMT</pubDate>
            <atom:updated>2026-05-09T10:18:18.478Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8FO1lV9kX_hh1ZkE.jpg" /></figure><p>We already had a local development setup.</p><p>It was not beautiful, but it worked. There was a docker-compose.yml file. I could start the services I needed and do my job. Some engineers used Docker Compose too. Others ran parts of the system directly on their machines. People used different editors as well: VS Code, Cursor, Zed, or just a terminal. For a backend project, this was normal.</p><p>Then Claude Code entered the picture.</p><p>That changed how I looked at the whole setup. The question was no longer just:</p><blockquote><em>Can a developer run the project locally?</em></blockquote><p>It became:</p><blockquote><em>Can a developer and an AI agent work in this project without giving the agent my whole laptop?</em></blockquote><p>That second question is harder.</p><p>When I use an AI coding agent, I want it to do actual work. I want it to read files, run tests, inspect errors, make changes, and try again. If I have to approve every small command, the flow breaks. At some point the tool starts feeling like a very slow autocomplete.</p><p>But the other extreme is also uncomfortable. Running an agent in a permissive mode directly on the host gives it access to much more than the project. My laptop has SSH keys, shell history, browser data, personal config, other repositories, and a lot of accidental state. Even if I trust the tool, that is too much surface area.</p><p>So this work was not about using devcontainers because they are fashionable. I was trying to build a better local development setup for a world where AI agents are part of the workflow.</p><p>The setup needed to stay familiar for people who already used Docker Compose. I wanted it to feel native for people working in IDEs. Another requirement: give Claude Code enough room to be useful without exposing the whole host.</p><p>In this article, I will walk through how I took an existing Docker Compose setup and added service-specific devcontainers, ran Claude Code inside them, kept secrets and VPN access scoped to the project, and looked at what still needs stronger guardrails.</p><h3>The setup was fine until it was not</h3><p>Before this work, the local workflow was straightforward.</p><p>Clone the repo. Start Docker Compose, or run the services locally. Turn on the corporate VPN when staging services were needed. Run Claude Code on the host, or run it somewhere else if you had your own setup.</p><p>Nothing was obviously broken. That is probably why this kind of work is easy to postpone. The pain is spread out across small things.</p><p>One person runs everything through Docker Compose. Another person runs one service locally and the rest in containers. Debugging works in one editor but not in another. None of these problems is dramatic, but they add up.</p><p>VPN access had the same shape. Some workflows needed staging services behind the corporate VPN. Turning the VPN on globally worked, but it affected the whole machine. Routing changed. DNS changed. Other unrelated work could be affected. The project needed VPN access sometimes; my whole laptop did not.</p><p>Claude Code made these tradeoffs more visible.</p><p>An AI agent is useful when it can move with fewer interruptions. But fewer interruptions usually mean broader permissions. If those permissions are on the host, I do not like the risk. If they are inside a project container, I can live with the tradeoff.</p><p>A container is not a perfect security boundary. I would not describe this as “secure” in an absolute sense. But it is still a smaller box than my laptop, and that matters.</p><p>The rough principle became:</p><blockquote><em>Give the agent room to work, but make the room smaller.</em></blockquote><h3>What changed with AI agents</h3><p>I was thinking a lot about <a href="https://www.youtube.com/watch?v=kgwvAyF7qsA&amp;ref=igkuz.ru">Dan Guido’s talk</a> on how Trail of Bits rebuilt their company around AI. The detail that stayed with me was not a specific repository or tool. It was the idea that adopting AI is not just installing a CLI.</p><p>If you want people to use these tools seriously, you need a workflow around them. You need defaults. You need a good first run. You need some policy. You need a place where the tool can act without forcing the developer to make a security decision every two minutes.</p><p>AI adoption needs the same standards as any other tool. If every developer has to decide how to install the agent, where to run it, what permissions are OK, and where config belongs, they spend energy before doing real work. A standard dev environment removes one of those decisions: the project opens the same way, the agent runs in the same kind of container, shared settings live in the repo, and personal auth stays personal.</p><p>That matched my experience.</p><p>If Claude Code asks for approval all the time, people will use it less. Or they will give it broad permissions in the easiest place: directly on the host. Nobody wants to fight their tools all day.</p><p>I wanted a middle ground:</p><ul><li>Claude Code runs inside the devcontainer</li><li>the main filesystem it sees is the project</li><li>secrets are mounted only when needed</li><li>project guidance lives in the repo</li><li>personal auth stays outside the repo</li><li>later, we can add stricter network and command guardrails</li></ul><p>This does not solve every problem. It just changes the default from “agent on my laptop” to “agent in this project environment”.</p><p>For me, that is already a meaningful improvement.</p><h3>Two services, one Compose stack</h3><p>The project was a monorepo with a few connected services. I will call them an API Gateway and a backend service.</p><p>The API Gateway proxies requests to the backend service. They also communicate through queue-like flows. In production, that is closer to AWS SQS. Locally, Redis stands in for that piece. The backend service also needs PostgreSQL.</p><p>So the local setup had the API Gateway, the backend service, Redis, and PostgreSQL.</p><p>The design question was simple to ask and annoying to answer:</p><blockquote><em>How should devcontainers work in a monorepo with multiple connected services?</em></blockquote><p>One answer is to create one big devcontainer for everything. I did not like that. It makes the environment heavier, and it does not match how I usually work. Most of the time I am inside one service, even if I need the rest of the stack running nearby.</p><p>Another answer is to give each service its own full Docker Compose setup. That also felt wrong. It duplicates configuration and makes cross-service work harder.</p><p>The approach that fit this repo was somewhere in the middle.</p><p>We kept one root Docker Compose stack. Then each service got its own devcontainer config. The API Gateway devcontainer could start the pieces it needed. The backend devcontainer could start its own dependencies. When I needed to work across the boundary, both devcontainers could exist at the same time.</p><p>In practice, the structure looked roughly like this:</p><pre>repo/<br>  docker-compose.yml<br>  api-gateway/<br>    .devcontainer/<br>      devcontainer.json<br>  backend/<br>    .devcontainer/<br>      devcontainer.json</pre><p>The names do not matter much. The important part is that docker-compose.yml stays at the root, while each service owns its own devcontainer entry point.</p><p>This is not a universal monorepo pattern. If you have dozens of services, you may need something more deliberate. But for a small set of connected services, this worked well.</p><p>The devcontainer did not replace Docker Compose. It reused it.</p><p>That distinction made the design much easier to explain. Docker Compose remained the shared runtime model. Devcontainers became the IDE-friendly entry point into that model.</p><h3>Reusing Docker Compose</h3><p>One thing I liked about this setup is that it did not create a second stack.</p><p>The project already had a working docker-compose.yml, so the devcontainer config pointed at it:</p><pre>{<br>  &quot;dockerComposeFile&quot;: [&quot;../../docker-compose.yml&quot;],<br>  &quot;service&quot;: &quot;api-gateway&quot;,<br>  &quot;workspaceFolder&quot;: &quot;/workspace&quot;,<br>  &quot;shutdownAction&quot;: &quot;none&quot;<br>}</pre><p>This is simplified, but the shape is the point.</p><p>The IDE opens inside the selected Compose service. The other services still come from the same Compose file. People who prefer terminal workflows can continue using Compose directly.</p><p>One setting was more important than I expected:</p><pre>{ &quot;shutdownAction&quot;: &quot;none&quot; }</pre><p>This matters when multiple devcontainers share the same Compose stack.</p><p>Imagine the API Gateway is open in one IDE window and the backend service is open in another. Closing one window should not stop services that the other window still needs. Without thinking about shutdown behavior, it is easy to make one editor window accidentally own the whole stack.</p><p>With shutdownAction: &quot;none&quot;, the devcontainer behaves more like an entry point. It does not try to be the lifecycle manager for everything.</p><p>That is a small config detail, but it changes how the setup feels.</p><h3>Keeping the container idle</h3><p>Another mistake I wanted to avoid was auto-starting the app as soon as the devcontainer opened.</p><p>At first that sounds convenient. Open the container, app starts, done.</p><p>In practice, it gets in the way.</p><p>I wanted VS Code tasks and launch configs to control the workflow. Sometimes I want watch mode. Sometimes I want debug mode. Sometimes I just want a terminal with dependencies installed. If the container starts the app by default, then a task can start a second copy and hit a port conflict.</p><p>So the devcontainer should prepare the environment, then wait.</p><p>The setting for that is:</p><pre>{ &quot;overrideCommand&quot;: true }</pre><p>With this enabled, the container does not run the normal service command from Docker Compose. It stays alive, and the editor tasks decide what happens next.</p><p>For debugging Node inside the container, the inspector needs to listen on an address reachable from outside the process namespace:</p><pre>node --inspect=0.0.0.0:9229 ...</pre><p>Binding to localhost inside the container can be confusing because it is localhost from the container&#39;s point of view, not necessarily from the host or debugger&#39;s point of view.</p><p>I also learned to be explicit with ports. Docker Compose can publish ports. VS Code can auto-forward ports. Both features are useful, but if they both try to be clever at the same time, debugging becomes harder to reason about.</p><p>The pattern I settled on was:</p><ul><li>Compose defines the important service ports</li><li>editor tasks start the app</li><li>launch configs attach the debugger</li><li>the devcontainer provides the environment</li></ul><p>That separation made the workflow easier to debug.</p><h3>Moving readiness into Compose</h3><p>One cleanup came from a CodeRabbit review.</p><p>There was a startup script that waited for PostgreSQL with a shell loop. You have probably seen this pattern: try to connect, sleep, try again, repeat until the database accepts connections.</p><p>It worked. I have written this kind of script many times.</p><p>But in this setup, it was in the wrong layer. Docker Compose was already managing PostgreSQL, so Compose should also know when PostgreSQL is ready.</p><p>The cleaner version was to use a PostgreSQL healthcheck with pg_isready, then make the backend service wait for it:</p><pre>services:<br>  postgres:<br>    healthcheck:<br>      test: pg_isready -U backend -h 127.0.0.1<br>      interval: 5s<br><br>  backend:<br>    depends_on:<br>      redis:<br>        condition: service_started<br>      postgres:<br>        condition: service_healthy</pre><p>The actual username and service names depend on the project, but the idea is the same: let PostgreSQL report when it can accept connections, and let Compose use that signal.</p><p>The important part for the dependent service is this:</p><pre>depends_on:<br>  postgres:<br>    condition: service_healthy</pre><p>This moved the readiness concern closer to the service that owns it.</p><p>A post-create script should prepare the workspace. It can install dependencies or create local files. But waiting for infrastructure belongs in the infrastructure config when Compose is already responsible for those services.</p><p>CodeRabbit was useful here because it pointed at a rough edge. I still had to decide whether the suggestion fit the architecture. That is the right relationship with AI review tools, at least for me. They are good at making me look twice. They do not get to make the design decision.</p><h3>Running Claude Code inside the devcontainer</h3><p>This was the section I cared about most.</p><p>Installing Claude Code was only half the work. The real decision was where it should run.</p><p>That meant running it inside the devcontainer as the existing non-root node user, with the terminal starting at the repo root. This mattered because the Claude Code devcontainer docs recommend a non-root user, especially with --dangerously-skip-permissions: if I reduce approval prompts for the agent, I do not also want it running as root, and I do not want to mount more of the host than the project needs.</p><p>The repo root part sounds minor, but it matters. Claude Code needs to find project-level guidance and settings. If the terminal opens in the wrong directory, the tool may miss files like CLAUDE.md, or the developer has to remember extra setup steps.</p><p>Persistence also needed a bit of care.</p><p>A named volume for ~/.claude can preserve Claude Code&#39;s directory across container rebuilds. But it does not automatically preserve sibling files such as ~/.claude.json.</p><p>That distinction matters because tool state and auth can live in different places.</p><p>The pattern I liked was:</p><ul><li>project-owned Claude guidance stays in the repo</li><li>developer-owned auth stays outside the repo</li><li>auth is mounted through docker-compose.override.yml</li><li>the mount is read-only where possible</li></ul><p>For example:</p><pre>services:<br>  api-gateway:<br>    volumes:<br>      - ~/.claude.json:/home/node/.claude.json:ro</pre><p>The /home/node path in this example is not random. It matches the non-root user inside the container. If the container uses a different non-root user, the mount path should match that user&#39;s home directory instead.</p><p>This gives the team a shared way to configure Claude Code for the project without putting personal credentials in Git.</p><p>I like this boundary. The repository can describe how the agent should behave in this codebase. The developer can bring their own auth. Those are different concerns, and they should stay separate.</p><p>Reference: <a href="https://code.claude.com/docs/en/devcontainer?ref=igkuz.ru">Claude Code devcontainer docs</a>.</p><h3>Putting VPN access in the container</h3><p>The VPN work followed the same logic.</p><p>Some workflows need staging services behind a corporate VPN. That does not mean my whole laptop should be on that VPN all day.</p><p>So I moved VPN access into the devcontainer path.</p><p>The image can include OpenVPN tooling. A local Compose override can add the device, capability, and personal .ovpn file:</p><pre>services:<br>  api-gateway:<br>    cap_add:<br>      - NET_ADMIN<br>    devices:<br>      - /dev/net/tun:/dev/net/tun<br>    volumes:<br>      - ~/vpn/company.ovpn:/etc/openvpn/company.ovpn:ro</pre><p>The real .ovpn file is personal and should not be committed. The repo can include a docker-compose.override.yml.example to show the shape of the setup, and each developer can create their own local override.</p><p>I also added an editor task to connect the VPN. That made it part of the normal development workflow instead of a separate thing I had to remember.</p><p>The rule here is simple:</p><blockquote><em>Put access where the project needs it.</em></blockquote><p>The app sometimes needs the VPN. My whole machine does not.</p><h3>What I would add next</h3><p>The setup is still not as strict as I would like.</p><p>The next thing I would look at is network control. Running Claude Code inside a container is better than running it on the host, but an open container network still allows a lot.</p><p>A stricter version could allow access to Claude Code, package registries, required internal services, and selected staging endpoints. Everything else could be blocked by default.</p><p>That sounds clean on paper. In real projects it can get annoying quickly. Package managers download from more places than you expect. Company networks have weird dependencies. Internal tools call other internal tools. A strict allowlist needs maintenance, or people will route around it.</p><p>I would also like better guardrails around destructive actions:</p><ul><li>deleting large directory trees</li><li>force-pushing branches</li><li>changing sensitive config</li><li>installing unexpected packages</li></ul><p>Some of this could be handled with hooks. Some of it needs team policy. Some of it may be too fragile to treat as security.</p><p>I am fine with that. Guardrails do not have to be perfect to be useful. They just need to catch enough mistakes to be worth the friction.</p><h3>The result</h3><p>This work started as devcontainer setup, but that is not really what it was about.</p><p>It was about making the development environment fit the way we now work.</p><p>After the changes, IDE users get a native devcontainer workflow. Terminal users can still use Docker Compose. The two connected services can run together. Claude Code can work with fewer approval prompts, but inside a project environment instead of directly on the host. VPN access can be scoped to the container. The setup is also documented in the project instead of living only in someone’s head.</p><p>That feels like progress.</p><p>It does not remove all risk. It does not mean this exact design fits every repo. And it definitely does not make AI agents magically safe.</p><p>But I am much more comfortable giving Claude Code room to work inside a constrained project environment than on my laptop.</p><p>That is the practical tradeoff I wanted: fewer interruptions, more useful autonomy, and a smaller blast radius when something goes wrong.</p><h3>References</h3><p>These resources might be inspirational for you as they were for me. Specifically I want to mention the video by Dan Guido about his approach to transform the company to AI native. It’s a top to bottom action, but you can learn a lot on hands on actions.</p><ul><li><a href="https://containers.dev/">Dev Container Specification</a></li><li><a href="https://containers.dev/implementors/json_reference/">Dev Container JSON reference</a></li><li><a href="https://code.visualstudio.com/docs/devcontainers/create-dev-container">VS Code Dev Containers documentation</a></li><li><a href="https://code.visualstudio.com/remote/advancedcontainers/connect-multiple-containers">VS Code documentation on connecting to multiple containers</a></li><li><a href="https://code.claude.com/docs/en/devcontainer">Claude Code devcontainer documentation</a></li><li><a href="https://github.com/trailofbits/claude-code-config">Trail of Bits Claude Code config</a></li><li><a href="https://github.com/trailofbits/claude-code-devcontainer">Trail of Bits Claude Code devcontainer</a></li><li><a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development">Evil Martians: Ruby on Whales: Docker for Ruby &amp; Rails development</a></li></ul><p>Dan Guido’s talk on rebuilding Trail of Bits around AI</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FkgwvAyF7qsA%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DkgwvAyF7qsA&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FkgwvAyF7qsA%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/c28836c183d39de194cb919a45626ce4/href">https://medium.com/media/c28836c183d39de194cb919a45626ce4/href</a></iframe><p><em>Originally published at </em><a href="https://igkuz.ru/building-a-dev-setup-for-the-ai-era/"><em>https://igkuz.ru</em></a><em> on May 9, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb9a8dd6943e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Managing Unicorn & Puma web servers with systemd]]></title>
            <link>https://medium.com/@igkuz/managing-unicorn-puma-with-systemd-93e95f75d1ae?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/93e95f75d1ae</guid>
            <category><![CDATA[puma]]></category>
            <category><![CDATA[systemd]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[web-server]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Mon, 16 Sep 2019 10:21:01 GMT</pubDate>
            <atom:updated>2019-09-16T10:21:01.236Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*OdNnJUONIPG0loiDLt9BEg.png" /><figcaption>© image from <a href="https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units">DO post</a>.</figcaption></figure><p>In production environment, you have to deal with service crashes and auto restarts, and there are plenty of tools to that end— supervisord, monit etc. Having lots of projects, we try to use standard utilities that come with distros, Ubuntu in our case.</p><p>Standard Ubuntu init system is <a href="https://www.freedesktop.org/software/systemd/man/systemd.service.html">Systemd</a>, definitely one of the most famous and widely used tools. It’s a security standard to run apps under non-privileged users, and systemd offers a solution in this case.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*DUaAH_yQIZ0ZHgn6.png" /></figure><p>Our initial setup is Ruby via RVM and Unicorn/Puma running simple Rack application.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/0*_6koVm1d4Eq15DYz.png" /></figure><p>Important notice, I don’t provide Docker files with examples as systemd won’t succeed in a non-privileged mode in Docker, so I advise you to test this in a separate virtual machine. You can find more on this case on a<a href="https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities">skubuntu</a> and <a href="https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities">Docker docs</a>. For macOS, you can use <a href="https://linuxcontainers.org/">LXD</a> or <a href="https://multipass.run/">multipass</a> to spin up Ubuntu VM. LXD setup can be tricky, so for this project, I’ve prepared a cloud-init file that can be used as a multipass entrypoint.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5824086ef6dc8367cbf94dd4cb5542ff/href">https://medium.com/media/5824086ef6dc8367cbf94dd4cb5542ff/href</a></iframe><pre>multipass launch -n sysd -c 1 -m 2g --cloud-init init.yml xenial</pre><p>Based on Xenial, because we made an infrastructure upgrade from 12.04 to 16.04 LTS in 2017, so it’s our standard for this &amp; next year.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*2E1-sB2zARv_-kn_" /></figure><p>Assume we have a non-privileged user awesomeapp with home directory /apps/awesomeapp and shell /bin/bash. App root /apps/awesomeapp/awesomeapp-git.</p><p>Let’s prepare the directory structure for systemd files:</p><pre>mkdir -p /apps/awesomeapp/.config/systemd/user</pre><p>Create unicorn.service . All files for the application can be found at <a href="https://github.com/igkuz/unicorn-puma-systemd">sample repo on my Github</a>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d89516b1ebade7e149e331de9fd4faed/href">https://medium.com/media/d89516b1ebade7e149e331de9fd4faed/href</a></iframe><p>Reload systemd daemon:</p><pre>awesomeapp@sysd:~$ systemctl --user daemon-reload<br>Failed to connect to bus: No such file or directory</pre><p>There is a discussion about this error on <a href="https://answers.launchpad.net/ubuntu/+source/systemd/+question/287454">launchpad</a>. To fix it, just add to /apps/awesomeapp/.profile.</p><pre>export XDG_RUNTIME_DIR=/run/user/`id -u`</pre><p>After that, from root or your sudo user enable <a href="https://www.freedesktop.org/software/systemd/man/loginctl.html#enable-linger%20USER%E2%80%A6">user lingering</a>:</p><pre>sudo loginctl enable-linger awesomeapp</pre><p>check that it works:</p><pre>loginctl user-status awesomeapp</pre><p>You should see smth like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YZ8sc0_EORBqGM-xDLQ4Wg.png" /><figcaption>User status after lingering is enabled.</figcaption></figure><p>Login back to the user and reload the daemon:</p><pre>sudo -iu awesomeapp<br>systemctl --user daemon-reload</pre><p>systemctl command under app user without sudo, it’s not a mistake. App user can use it freely, so, dev team can maintain service files easily without operations team involved.</p><p>Check the status:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JuX604GjH1zM97s-Oh_5-A.png" /><figcaption>Checking Unicorn systemd service status</figcaption></figure><p>It’s inactive, not running and disabled. It means that it won’t start up after boot. For example, you have 1..N app machines, and some of them can go down for system upgrades or just because of a crash, so we need the app running after reboot.</p><pre>systemctl --user enable unicorn</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0ewjFasd16uTWQ5HRJM8AQ.png" /><figcaption>Enabling Unicorn systemd service</figcaption></figure><p>Let’s give it a try:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oXmOvdCT56DtqkcQEQQ2fQ.png" /><figcaption>Starting and checking status for Unicorn systemd service</figcaption></figure><p>Unicorn master process and 2 worker process, as we configured.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/878/1*sRqmSpqNlrfO3MDWtPfHcw.png" /><figcaption>Checking app response for Unicorn systemd service</figcaption></figure><p>Let’s do the same for Puma, standard Rails web server. Puma.service file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dd3caa2cafaae40f5f7f531dc4358dcf/href">https://medium.com/media/dd3caa2cafaae40f5f7f531dc4358dcf/href</a></iframe><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JlZou4xrG7HtZq1I08RLgQ.png" /><figcaption>Puma systemd service run and check</figcaption></figure><p>Now you can reboot VM and check that everything up and running.</p><p>All commands with comments.</p><p>Unicorn:</p><pre># check unicorn.service status<br>systemctl --user status unicorn</pre><pre># start unicorn.service<br>systemctl --user start unicorn</pre><pre># start unicorn.service on VM boot<br>systemctl --user enable unicorn</pre><pre># disable starting unicorn.service on VM boot<br>systemctl --user disable unicorn</pre><pre># restart unicorn.service<br>systemctl --user restart unicorn</pre><pre># graceful restart unicorn.service<br>systemctl --user reload unicorn</pre><pre># stop unicorn.service<br>systemctl --user stop unicorn</pre><p>Puma:</p><pre># check puma.service status<br>systemctl --user status puma</pre><pre># start puma.service<br>systemctl --user start puma</pre><pre># start puma.service on VM boot<br>systemctl --user enable puma</pre><pre># disable starting puma.service on VM boot<br>systemctl --user disable puma</pre><pre># restart puma.service<br>systemctl --user restart puma</pre><pre># graceful restart puma.service<br>systemctl --user reload puma</pre><pre># stop puma.service<br>systemctl --user stop puma</pre><p>That’s it for today. Puma and Unicorn configs are for the sample app, for production purposes, you definitely must adopt to needs and load.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=93e95f75d1ae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ruby retry/scheduled tasks with Dead Letter Exchange in RabbitMQ]]></title>
            <link>https://medium.com/@igkuz/ruby-retry-scheduled-tasks-with-dead-letter-exchange-in-rabbitmq-9e38aa39089b?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/9e38aa39089b</guid>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[scheduled-tasks]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Wed, 13 Mar 2019 07:01:02 GMT</pubDate>
            <atom:updated>2019-03-13T07:01:02.377Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*c3Mi-AblbQuRBTrM" /><figcaption>©<a href="https://soulstudiostore.com/design/paula-studio-ill-do-it-later-mood-clock">https://soulstudiostore.com/design/paula-studio-ill-do-it-later-mood-clock</a></figcaption></figure><p>There is a project where I need outgoing requests rate limiting. This is the opposite case for the more common situation when you develop API and rate limit clients incoming requests.</p><p>With outgoing requests, you need to queue them and give a slot only for some. We’re collecting analytics for web pages. As we collect data only from public sources, we try not to be banned by firewalls or DDoS protection services. Making 1 request per second for content sites is more than enough, as there is no need for speed.</p><p>RabbitMQ Cluster is part of our infrastructure and the default queuing solution. Unfortunately, RabbitMQ doesn’t come with native support for delayed or scheduled messages. Fortunately, RabbitMQ has Dead Letter Exchanges (DLX), which allows us to simulate message scheduling.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/889/1*N4YDDxr5MvhQbiYsWmfbNA.png" /></figure><p>I’m going to explain how it works and what do we need to build.</p><p>When you create a queue (Q1) bound to exchange (X), you can also specify the Dead Letter Exchange (DLX) to route rejected messages. This process is automatically handled by RMQ. Another queue (Q2) bound to DLX to consume the messages after they’ve been rejected from original (Q1).<br>And to deliver messages automatically back we can set original exchange as dead letter exchange to the (Q2).<br>Look at the image.</p><p>Sounds a little bit complex, but it’s cool that we need only 1 consumer, all other stuff will be handled by RabbitMQ.</p><h4>How to?</h4><ol><li>Create TargetQueue bound to TargetExchange:<br>– set Dead Letter Exchange to RetryExchange</li><li>Create RetryQueue bound to RetryExchange:<br>– set Dead Letter Exchange to TargetExchange<br>– set Message Time To Live (TTL) to the desired time (1 minute for example)</li></ol><p>Steps to test the solution:</p><ol><li>Publish message to TargetQueue</li><li>Consumer gets the message and tries to process it</li><li>Process fails, consumer rejects the message</li><li>Rabbit routes the message with the same routing key to RetryExchange</li><li>Message moves to RetryQueue, sits for 90 seconds</li><li>When message expires, it is resent to TargetExchange and routed to TargetQueue</li></ol><h4>Let’s proceed to code</h4><p>In Ruby, there are two popular solutions for RabbitMQ. <a href="https://github.com/ruby-amqp/bunny">Bunny</a> and <a href="https://github.com/jondot/sneakers">Sneakers</a>, which is a really nice abstraction for Bunny. For this post, I choose Bunny as it can show some low-level operations. For my next post, where the whole project is going to be described, I’ll show the “Sneakers” way.</p><p>As always, the final solution can be found in <a href="https://github.com/igkuz/rate-limit-with-rmq-sample">my repository on GitHub</a>.</p><p>Create a Gemfile and install all necessary stuff.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6c2339366ba946682ab494dbb9b17ede/href">https://medium.com/media/6c2339366ba946682ab494dbb9b17ede/href</a></iframe><p>Create publisher, it only publishes a message with routing key to an exchange.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/85fd9d95fef1edd457c46f19e1ab2b1b/href">https://medium.com/media/85fd9d95fef1edd457c46f19e1ab2b1b/href</a></iframe><p>Create consumer, it will get messages, process them, try to get the slot for the request and whet it fails, reject them.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1d1687c2b7773d19452a96ff82f7fd57/href">https://medium.com/media/1d1687c2b7773d19452a96ff82f7fd57/href</a></iframe><p>Create the starting point for our applications. We create exchanges and queues in it.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/62bc29306014c75165248de1d72b2482/href">https://medium.com/media/62bc29306014c75165248de1d72b2482/href</a></iframe><p>For rate-limiting, I prefer <a href="https://redislabs.com/redis-best-practices/basic-rate-limiting/">Redis way</a>. It is one of the most popular solutions for in-memory databases. I think a lot of ruby projects use Sidekiq, so you definitely have Redis at your infrastructure. It can be tricky to develop a rate-limiting without Redis or Memcached, but still, there is a way. If you feel strong and enjoy inventing the bicycles, code it by yourself. For example — <a href="https://en.wikipedia.org/wiki/Token_bucket">Token buckets</a>.</p><h4>App in action</h4><p>docker-compose run app ./start</p><p>Take a look at RabbitMQ management page. It is available at <a href="http://localhost:8080.">http://localhost:8080.</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QPJwZ3CkxSDyjZPMAh2CKQ.png" /><figcaption>Created RabbitMQ exchanges</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2jLy6UTJrSGpO497oTWBXQ.png" /><figcaption>Created RabbitMQ queues</figcaption></figure><p>docker-compose run app ./publisher</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W9uxR2Vr0z_emHBXlPhlZw.png" /><figcaption>10 messages published</figcaption></figure><p>As we can see all messages are published, but as there are no consumers, all they sit in a queue.</p><p>docker-compose run app ./consumer</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*A13sUbBr7_1qnPJ-_MSUOQ.png" /><figcaption>Messages processed by consumer</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aBVP6jQKd0YntZjvur3pZQ.png" /><figcaption>Messages reside in retry.queue</figcaption></figure><p>Wait for 60 seconds and look at the terminal.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3m1L0bHfHqQVaFIDn8048Q.png" /><figcaption>Messages processed after 1 minute waiting in retry.queue</figcaption></figure><p>The messages are returned back to the work.queue , once again, only 1 message is processed as there are no slot for others, and they are pushed back to retry.queue by Rabbit.</p><p>If you grabbed the code from the repo, read the README and started Docker containers, then you can visit <a href="http://localhost:8080">http://localhost:8080</a> and enjoy watching how messages are processed and requeued by yourself.</p><h4>Notes on DLX &amp; TTL</h4><p>There are several ways how you can set a DLX and TTL.</p><p><strong>DLX:</strong></p><ol><li>When declaring a queue.</li><li>With RabbitMQ policies.</li></ol><p><strong>TTL</strong>:</p><ol><li>When declaring a queue</li><li>With RabbitMQ policies</li><li>On a per-message basis</li></ol><h4>Pros &amp; cons</h4><p>When you declare a queue, the only way to change options is to stop all the consumers and publishers, drop the queue and redefine it from the ground. It’s not that good, as you don’t know how can publish to it. But all configuration can be done via code.</p><p>When you create policies, you can change the policy at any time and no app redeployment will be needed. On the other hand, you must grant people permission to create policies, which is dangerous. Another option is to ask system administrators or operations team to change the policy, which can be a little bit annoying for them.</p><p>TTL can be added with an expiration header to message. When a message expires and gets to the head of the queue, it is automatically routed to DLX by RabbitMQ.</p><p>Feel free to ask questions in comments or connect directly on <a href="https://twitter.com/igkuz">twitter</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9e38aa39089b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to setup Ruby Object Mapper (ROM) for standalone project]]></title>
            <link>https://medium.com/@igkuz/how-to-setup-ruby-object-mapper-rom-for-standalone-project-15472fcf31e1?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/15472fcf31e1</guid>
            <category><![CDATA[data-mapper]]></category>
            <category><![CDATA[mysql]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[ruby-object-mapper]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Mon, 11 Mar 2019 07:01:00 GMT</pubDate>
            <atom:updated>2019-03-11T07:01:00.845Z</atom:updated>
            <content:encoded><![CDATA[<p>I’ve been looking at Data Mapper project for a long time. It transformed into Ruby Object Mapper and when I came up with a simple standalone project for collection post analytics, I decided to use it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M5oYJ-sluW2Nh2SPBANeIA.png" /></figure><p><a href="https://rom-rb.org/4.0/learn/">Official ROM documentation</a> is a great place to start, but there is a lack of information on some basic setup. That’s why I thought that my experience will be useful for those who just came from Active Record and looking for first steps.</p><p>We’re going to build a simple application that starts Pry as a console. There will be 2 entities — Company and Post. You can check the <a href="https://github.com/igkuz/rom-sample-app"><strong>rom-sample-app</strong></a><strong> </strong>for the full code example on GitHub.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1002/1*MeyTXJDgm69HG2UJcaAkXg.png" /><figcaption>Company &amp; Post relations</figcaption></figure><h4>Project setup</h4><pre>$ mkdir rom-sample-app &amp;&amp; cd rom-sample-app<br>$ touch Gemfile</pre><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ebb933a6ea24186ae00881fb3006ba85/href">https://medium.com/media/ebb933a6ea24186ae00881fb3006ba85/href</a></iframe><pre>$ bundle install</pre><p>Create boot.rb and require project dependencies.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1799a960801c7bdf93ef5a005fcf1362/href">https://medium.com/media/1799a960801c7bdf93ef5a005fcf1362/href</a></iframe><p>Create console.</p><pre>$ touch console<br>$ chmod +x console</pre><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/47a69d6a796f16a44915cd38aba7409b/href">https://medium.com/media/47a69d6a796f16a44915cd38aba7409b/href</a></iframe><p>Now we can get Pry console by calling $ ./console . Next we must connect to DB and create our entities.</p><h4>Working with MySQL from ROM</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/595/1*MW1Ik8U21awUO-_sDhCsrw.png" /><figcaption>Connecting ROM &amp; MySQL</figcaption></figure><p>Prepare ROM::Configuration to connect app to MySQL database.</p><p>ROM uses Sequel API and we have to count with it. Rails provide default Rake tasks for creating and deleting the DB, ROM doesn’t. I won’t describe the process of creating and deleting the database as it is necessary only 1 time in production environment. Most of the time this process involve DevOps engineers or system administrators and developers only grab the config. I suppose you can create the database for development and test environments on your own.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/874014c7b3eb3d4c768fff42001cb046/href">https://medium.com/media/874014c7b3eb3d4c768fff42001cb046/href</a></iframe><p>You shouldn’t work with config like this in production environment. Read from separate file or env variables instead. But for the prototyping purposes it’s more than enough.</p><p>Now we have MySQL server, created database and in console we can work with MAIN_CONTAINER object. Calling ROM.container will finalize the process of configuring ROM and all the hooks and callbacks will be invoked. So if you need something to register or configure please do it before calling container method.</p><h4>Working with ROM migrations</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/684/0*1ARbzxsMdFExwA6M.jpg" /><figcaption>ROM migrations</figcaption></figure><p>The SQL adapter uses Sequel migration API exposed by SQL gateways. You can either use the built-in rake tasks, or handle migrations manually. To load migration tasks simply require them and provide db:setup task which sets up ROM.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3ecf12728a138d191109ec2864b9b272/href">https://medium.com/media/3ecf12728a138d191109ec2864b9b272/href</a></iframe><p>Create migrations for Company and Post.</p><pre>$ bundle exec rake db:create_migration[create_companies]<br>$ bundle exec rake db:create_migration[create_posts]</pre><p>Now there are 2 files in db/migrate directory. Migration names are prepended by timestamps. Timestamp migrations are created by default, but there is other setup where you can use just integers. <a href="https://rom-rb.org/4.0/learn/sql/migrations/">More on that in official docs.</a></p><p>I used ROM in real project, so I’m going to put here the schema and explain why I needed these fields in other publication.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/414b2d9cdbd0eefcdfec0e7519a89865/href">https://medium.com/media/414b2d9cdbd0eefcdfec0e7519a89865/href</a></iframe><p>Run migrations and check that schema exists. On bundle exec rake db:migrate there will be a check for pending migrations. We don’t have any abstractions to work with data, just gateway connection for now.</p><pre>$ bundle exec rake db:migrate<br>  &lt;= db:migrate executed</pre><pre>$ ./console<br>pry(main)&gt; MAIN_CONTAINER.<br>  gateways[:default].connection.schema(:companies)<br>=&gt; [[:id,<br>  {:primary_key=&gt;true,<br>   :auto_increment=&gt;true,<br>   :generated=&gt;false,<br>...<br> [:updated_at, {:primary_key=&gt;false, :generated=&gt;false, :allow_null=&gt;true, :default=&gt;nil, :db_type=&gt;&quot;datetime&quot;, :type=&gt;:datetime, :ruby_default=&gt;nil}]]</pre><h4>ROM Relations</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/1*F3g4KS1ZM3lag9Kp3tmvMw.png" /><figcaption>ROM Relations for Company and Post</figcaption></figure><p>Users of ROM implement Relations, which give access to data. A relation is defined as a set of tuples identified by unique pairs of attributes and their values. An example of relations is tables in a SQL server. Relations are really the heart of ROM. They provide APIs for reading the data from various databases, and low-level interfaces for making changes in the databases.</p><p>Let’s create Post &amp; Company relations in lib/relations/ directory.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f53d9799b955c95bd21cba4c56389510/href">https://medium.com/media/f53d9799b955c95bd21cba4c56389510/href</a></iframe><p>As ROM uses dependency injection throughout the lib, we must register our components. <a href="https://rom-rb.org/4.0/learn/advanced/explicit-setup/">More on that in official docs</a>.</p><pre>configuration.register_relation(Companies, Posts)</pre><p>We put this code into console file. When your app grows, you would definitely move it to a special place like initializer or smth similar. But for demonstration purposes it’s more than enough to put it near the app running code.</p><p>Now we can access relations from MAIN_CONTAINER, like:</p><pre>pry(main)&gt; MAIN_CONTAINER.relations[:companies].count<br>=&gt; 0<br>pry(main)&gt; MAIN_CONTAINER.relations[:posts].count<br>=&gt; 0</pre><p>Let’s go further with ROM Commands. We want not only querying the data, but add or update some.</p><h4>ROM Commands</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*3BO6gL38HcwzJHilXS19XQ.png" /></figure><p>Commands are used to make changes in your data. Every adapter provides its own command specializations, that can use database-specific features.</p><p>Core commands include following types:</p><ul><li>:create - a command which inserts new tuples</li><li>:update - a command which updates existing tuples</li><li>:delete - a command which deletes existing tuples</li></ul><p>We are going to create commands for Posts and Companies and put them into lib/commands/.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d3cc6c55362046c644aea6c95307430c/href">https://medium.com/media/d3cc6c55362046c644aea6c95307430c/href</a></iframe><p>Note the lines</p><pre>use timestamps<br>timestamp :created_at, :updated_at</pre><p>This how we use Timestamps plugin to automatically set the dates like Active Record does.</p><p>Now we must register our commands:</p><pre>configuration.register_command(CreateCompany, DeleteCompany)<br>configuration.register_command(CreatePost, UpdatePost, DeletePost)</pre><p>Let’s create the 1st company:</p><pre>[1] pry(main)&gt; companies = MAIN_CONTAINER.relations[:companies]<br>[2] pry(main)&gt; companies.command(:create).call(<br>       name: &#39;My 1st Company&#39;, domain: &#39;<a href="http://example.com&#39;">http://example.com&#39;</a>)<br>=&gt; {:id=&gt;1,<br> :name=&gt;&quot;My 1st Company&quot;,<br> :domain=&gt;&quot;<a href="http://example.com">http://example.com</a>&quot;,<br> :state=&gt;&quot;running&quot;,<br> :created_at=&gt;2019-03-09 14:24:23 +0000,<br> :updated_at=&gt;2019-03-09 14:24:23 +0000}</pre><p>That’s it. id and state were assigned automatically, timestamps creation was also handled by ROM.</p><h4>Testing with ROM</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*VUsd3eXCQ9HhKa3Y.gif" /><figcaption>©<a href="https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters">https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters</a></figcaption></figure><p>The question then arises, “How should I test this stuff?..”. We’re going to use RSpec with <a href="https://github.com/rom-rb/rom-factory">ROM factory</a>. It’s kind of replacement for <a href="https://github.com/thoughtbot/factory_bot">factory_bot</a> gem by Thoughbot.</p><p>It’s time to add settings.yml to project and put config there. Also we need to add rspec dependency to Gemfile and create test database. View the commits to the repo: <a href="https://github.com/igkuz/rom-sample-app/commit/1dfcc4d3cd609a36f0996718728f71d25449fd03">1</a>, <a href="https://github.com/igkuz/rom-sample-app/commit/b4dabfa4130d91466018b76f7b323246fead91ee">2</a>.</p><p>Define factories</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8b7857424d5aaf13c5e6a35570151da8/href">https://medium.com/media/8b7857424d5aaf13c5e6a35570151da8/href</a></iframe><p>Check company_spec.rb in <a href="https://github.com/igkuz/rom-sample-app">repository</a> for simple test of creating company with 1 post.</p><p>This is it. We created a standalone console application with Ruby Object Mapper. Additional info on running app in docker container is in README.</p><p>If you have any questions, write a comment or connect directly on <a href="https://twitter.com/igkuz">twitter</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=15472fcf31e1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Socket Activated Containers (Unicorn + Systemd)]]></title>
            <link>https://medium.com/@igkuz/socket-activated-containers-unicorn-systemd-bce541b48628?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/bce541b48628</guid>
            <category><![CDATA[systemd]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[containers]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Tue, 17 Jul 2018 08:34:00 GMT</pubDate>
            <atom:updated>2018-07-17T09:09:06.193Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZtB3bSD2qOkRT3xJ_5gDQg.jpeg" /></figure><p>У клиента есть большое количество медийных спец проектов (~250). Это виджеты, лендинги, апишки и т.д. С 2012 года это все живет на 1 машине со связкой Nginx + Passenger + Ruby.</p><p>Все хорошо, за исключением момента обновления ОС, когда все проблемы со старыми/новыми версиями пакетов вылезают и заявляют о себе в полный голос.</p><p>Казалось бы, идеальная история для контейнеров, но есть одно но. Passenger или PHP-FPM умеют то, что из коробки <a href="https://github.com/kubernetes/kubernetes/issues/484">нет даже у Kubernetes</a> — это старт по входящему трафику.</p><p>На просторах сети это называется (гуглится) — Socket Activation. На Network или Unix сокет приходит пакет, сервис запускается. Покопавшись на Stack Overflow и некоторых админских форумах, понял, что мысль о том, что контейнер, можно только по необходимости поднимать интересует многих. Такие запросы есть у больших ребят — привет велосипеды (читай свои решения).</p><p>Оказывается то, что нужно написали уже лет 7 назад. <a href="http://0pointer.de/blog/projects/socket-activation.html">Пост</a> про реализацию функционала в systemd датирован 2011 годом. Ну и в 2013 уже вышел <a href="http://0pointer.de/blog/projects/socket-activated-containers.html">пост</a> про старт контейнера с помощью той же функциональности. Есть одно но, там используются стандартные средства systemd для виртуализации, нас же интересуют более популярные на данный момент Docker.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/819/1*PITVMrEg3tFTHU50hKStcQ.gif" /><figcaption>Схему забрал из <a href="https://developer.atlassian.com/blog/2015/03/docker-systemd-socket-activation/">блога Atlassian</a>.</figcaption></figure><p>Во всех статьях говорили о proxy сервисе, который принимает трафик и отвечает за запуск контейнера. Gочему нельзя сокет использовать сразу в контейнере? Вопрос этот возникает, потому что на каждый сервис придется делать 3 конфига systemd и минимум 2 сокета, что как мне казалось много и не слишком красиво. Посмотрел потом на конфиги kubernetes и 3 конфига systemd показались маленькими, простыми и понятными.</p><p>Но так что с сокетом то? А вот я не нашел способа как его пробросить в контейнер. Если кто знает, с радостью послушаю. Проблема не в софте (unicorn и puma), который может переиспользовать проброшенный сокет. Есть стандартные процедуры для этого. Через ENV переменные передаются ID процесса (<em>LISTEN_PID) </em>и номер слушающего сокета (<em>LISTEN_FDS</em>), после чего софт не должен пытаться открыть новый, а переиспользовать (подключиться) к сокету с соответствующими координатами.</p><p>Проблема заключается в том, что ID процесса и ссылку на сокет будут переданы с host машины, контейнер естественно ничего о них не знает.</p><p>Шарить данные с хост машиной конечно можно, но безопасно ли?</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7d81a48401be2437677883a7e900125e/href">https://medium.com/media/7d81a48401be2437677883a7e900125e/href</a></iframe><p>Сделал <a href="https://gist.github.com/igkuz/87d2087ccf333870c50cbc04ebfedf04">gist</a> с файлами для тестов. Простейшее Rack приложение и набор сервисов для systemd. Чтобы это все завелось надо завести отдельного пользователя, назвал его sact, и сложить Gemfile, Gemfile.lock, unicorn.rb, config.ru файлы в /apps/sact/sact-git . Установить docker, сложить systemd сервисный файлы в /etc/systemd/system и перезагрузить демона через systemctl daemon-reload . Также понадобиться nginx. Но думаю для тех кто это читает не составит большого труда разобраться.</p><p>Если обратиться на 80 порт localhost</p><p>curl localhost, то получим Simple app for test . Небольшая задержка на старте нас не пугает, потому что подразумевается что к сервисы обращаются не часто.</p><p>Что я не успел сделать — отслеживание трафика за определенный момент времени с целью остановить сервис. По идее это просто парсинг логов, если за последние N минут ничего не было, то останавливаем сервис с контейнером. Если мы соберемся делать такое решение на наших 250+ проектов, то обязательно напишу.</p><p>Stay tuned…</p><p>P.S. Где что подсматривал:</p><ol><li><a href="http://0pointer.de/blog/projects/socket-activated-containers.html">От разработчика systemd про активацию контейнеров.</a></li><li><a href="http://ahmet2mir.eu/blog/2015/python_ssl_sockets_and_systemd_activation/">Это</a> на Python, но можно посмотреть как пробрасывается Socket из Systemd</li><li><a href="http://ku1ik.com/2012/01/21/systemd-socket-activation-and-ruby.html">А это пример на Ruby как работать с сокетом Sytemd</a></li><li><a href="https://developer.atlassian.com/blog/2015/03/docker-systemd-socket-activation/">Откуда взял гифку</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bce541b48628" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Переезд]]></title>
            <link>https://medium.com/@igkuz/moving-out-42290a7cb7e0?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/42290a7cb7e0</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[it]]></category>
            <category><![CDATA[system-administration]]></category>
            <category><![CDATA[data-center]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Tue, 06 Feb 2018 07:01:02 GMT</pubDate>
            <atom:updated>2018-02-06T08:05:50.536Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*5rfEfJW8kuO-q3AL0B6_YA.gif" /></figure><p>В декабре 2017 мы решили сменить датацентр (ДЦ). Решение серьезное и принималось из-за 2х причин. Отсутствие защиты от DDoS атак и ценник. Про то как классно ДЦ нам помог(нет) во время атаки я уже <a href="https://medium.com/@igkuz/отражая-ddos-f80c1737c19b">рассказывал,</a> ну и снизить затраты на инфраструктуру не потеряв в производительности это challenge.</p><p>Важные решения в компании не принимаются одним человеком и должны быть хорошо аргументированы.</p><h4>Как защищал решение перед руководством</h4><p>1. Провел исследование нескольких вендоров<br>2. Составил таблицу +/- нового вендора<br>3. Посчитал новую стоимость владения (TCO — Total Cost of Ownership)<br>4. Посчитал стоимость переезда. Это человеко-часы, возможный простой по другим задачам, риски на частичную потерю работоспособности системы или полный выход из строя системы<br>5. Составил план переезда<br>5. Оформил вышеперечисленные пункты в набор слов и таблиц Excel<br>6. Написал письмо на всех заинтересованных</p><p>На что обращают внимание:</p><ul><li>CFO — профит/потерю в деньгах</li><li>CEO — репутационные риски из-за простоя системы, как простой по другим задачам затронет бизнес в целом, целесообразность переезда именно сейчас</li></ul><h4>Как выбрать ДЦ</h4><p>В моем случае всё было достаточно просто, потому что у нас уже была часть инфраструктуры в этом ДЦ. Но сравнения с другими хостингам мы с системным администратором тем не менее проводили.</p><p>Приоритеты в списке расставьте сами:</p><ol><li>Географическое расположение. Чем ближе, тем лучше, но не Россия 😏</li><li>Стоимость оборудования. Дешево, не всегда хорошо, нужно смотреть на характеристики.</li><li>Характеристики оборудования. Дата выпуска процессоров, их частота. Объем оперативной памяти и возможность для расширения. SSD/HDD, возможность RAID с батарейкой, сетевые карты и входящий канал (10Gb/s — отлично, 1Gb/s — пойдет).</li><li>Функционал по настройке сети. В Softlayer можно сделать хороший <a href="http://www.linuxvirtualserver.org">LVS</a> и утилизировать по факту канал каждого сервера, получая 1xN Gb/s (где N — кол-во серверов). В Hetzner IP прибит к серверу и с другой машины отвечать нельзя. Failover IP перемещается только через запрос в API.</li><li>Защита от DDoS. Да, в 2018 без неё никуда, особенно если вы интернет издание. Есть конечно бесплатные CloudFlare и Google Shield, но вы теряете в контроле. Если ДЦ может защитить вас на сетевых уровнях, то лучше выбрать такой ДЦ.</li><li>Включенный в пакет сетевой трафик. Оборудование быстро устаревает, а за трафик в последняя время берут дорого. Чем больше в базовом пакете, тем лучше.</li><li>Наличие русскоговорящей поддержки. Да, на англ можно спокойно всё узнать, но налаженный контакт с менеджером в ДЦ намного полезнее. Можно узнать много подробностей и получить хорошую консультацию о том как на их мощностях построить нужную инфраструктуру.</li><li>Облачные сервисы. У нас есть <a href="https://ceph.com/">Ceph</a> для хранения изображений, если бы похожее решение as a service было у ДЦ, то я думаю взял бы. Но пока поддерживаем сами.</li><li>Выделенный канал с другими ДЦ. Как правило большие ребята прокидывают свои кабели или выкупают мощность, чтобы дать крупным клиентам возможность перекидывать большие объемы данных между разными провайдерам.</li></ol><p>После этапа согласований можно приступить к переезду. Описываю личный опыт:<br>1. Заказали машины нужной конфигурации. Обычно 3 рабочих дня, но в этот раз делали неделю, списываю на нестандартный запрос (RAID с батарейкой, дополнительные сетевые, failover IP, дополнительная подсеть).<br>2. Засетапили инфраструктуру. Образы виртуальных машин, настройка сетевых интерфейсов, балансировка, небольшие тесты железа. (1,5–2 недели)<br>3. Подготовка проектов к переезду. К этому моменту должны быть известны подсети, отказоустойчивые адреса для сервисов и оттестированы failover. (2 дня)<br>4. Зафиксировали план переезда. Это просто последовательность действий, чтобы во время работ не задумываться о следующем шаге и ничего не забыть.<br>5. Согласовали дату и временное окно. Сотрудники не ходят в приложения и ничего не создают, чтобы избежать лишних проблем. Мы пока не Google и даже не VK, поэтому можем себе позволить.</p><h4>Как выглядит сам переезд</h4><p>Что у вас должно быть на старте:</p><ul><li>Набор серых IP всех виртуальных машин</li><li>Набор highly available IP для инфраструктурных сервисов (Memcached, DB, Elastic, LoadBalancer, etc)</li><li>VPN туннели между ДЦ</li><li>Набор конфигов приложений с новыми адресами</li><li>Список команд для старта/стопа приложений/очередей/сервисов под рукой</li><li>Холодная голова и запас времени</li></ul><p><strong>Cache</strong>. У нас свой небольшой CDN и чтобы вал запросов не положил хранилище статики, добавили новый IP на балансер с меньшим чем у основных машин весом и дали кэшу прогреться. После стабильных 90% Cache hit, переключаем весь трафик на новые машины.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/556/1*FfqlMadbKjVCKp9WD3IPPQ.png" /></figure><p><strong>DB</strong>. Master-Master репликация с одни активным мастером. Исходная позиция old-db0&lt;-&gt;old-db1. Приложения смотрят HA IP –&gt; old-db1.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/673/1*WbQcmPxSgIfBiKcy0LTaLg.png" /></figure><p>Делаем read replica new-db1 и new-db0. Приходим к виду newdb0&lt;-new-db1&lt;-old-db1&lt;-&gt;old-db0. Это схему надо настроить заранее, чтобы реплики успели подтянуть данные.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-243frRm_Tqg2sRENnVruQ.png" /></figure><p>Рвем мастер-мастер old-db0&lt;-&gt;old-db1, настраиваем новый master-master old-db1&lt;-&gt;new-db1. Переходим к приложениям.</p><p>Поднимаем приложения на новых машинах. Новые инстансы смотрят в HA IP –&gt; new-db1. Очереди не поднимаем, записи в базу пока нет. Проверяем работоспособность. Прогреваем кэш несколькими запросами.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zoqmmAPMgd2Lb6B4RyGkwg.png" /></figure><p>На старых машинах выключаем очереди и всё что может писать в базу, кроме пользовательских запросов. Делаем проксирование запросов в новый DC на nginx. Переключаем DNS. Ускоряем обновление публичных DNS через волшебную <a href="https://developers.google.com/speed/public-dns/cache">кнопку Google</a>.</p><p>Выключаем приложения на старых машинах, проверяем наличие коннектов на old-db1. Когда кроме процессов репликации ничего нет, идем рвать мастер-мастер. Делаем новый мастер-мастер new-db1&lt;–&gt;new-db0. Рвем связь со старой базой в DC1.</p><p>Включаем очереди и дополнительные сервисы. Запускаем переиндексацию документов в Elastic. Можно было бы перетащить базу и доиндексировать, но данных немного, поэтому решили сделать переиндекс для профилактики. Elastic только для поиска, а он исторически делает &lt;1% трафика, поэтому не страшно выдать пользоветлям пустой ответ.</p><p>Делаем ручные проверки критичных мест приложений. Если что-то идет не так, то чиним, но у нас было ок.</p><p>У изданий была куча дополнительных доменов, нужно переключить все. Что именно можно найти в конфигах nginx в разделе server –&gt; server_name.</p><p>Проверяем все правила алертов мониторинга и идем отдыхать.</p><p>Спустя 2 дня, когда DNS обновились, начинаем тушить машины в старом ДЦ и запрашиваем их выключение.</p><p>В итоге мы получили снижение стоимости владения в 4 раза, но потеряли в кол-ве процессоров.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=42290a7cb7e0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Отражая DDoS]]></title>
            <link>https://medium.com/@igkuz/%D0%BE%D1%82%D1%80%D0%B0%D0%B6%D0%B0%D1%8F-ddos-f80c1737c19b?source=rss-80d3d7acb66b------2</link>
            <guid isPermaLink="false">https://medium.com/p/f80c1737c19b</guid>
            <category><![CDATA[it]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[ddos-protection]]></category>
            <category><![CDATA[system-administration]]></category>
            <dc:creator><![CDATA[Igor Kuznetsov]]></dc:creator>
            <pubDate>Thu, 04 Jan 2018 15:54:34 GMT</pubDate>
            <atom:updated>2018-01-04T15:54:34.292Z</atom:updated>
            <content:encoded><![CDATA[<p>Представьте ситуацию — вы идете домой с работы по знакомой улице, слушаете музыку и готовитесь достать ключи, но из-за угла выходит пара здоровых ребят, которые выше, сильнее, быстрее (на первый взгляд) и бьют вас прямым в челюсть. Вот так выглядит DDoS атака для 99% компаний. Это неожиданно и неприятно для владельца бизнеса, но очень интересно для разработчиков и системных администраторов.</p><p>Отличная возможность проверить реакцию: хваленые системы мониторинга, обещанные плюшки от софта и провайдеров, и, самое главное, собственные решения по резервированию.</p><p>За последние 4 месяца напали на 2 наших издания. В первый раз вымогали 1 BTC, второй раз ничего не просили, просто били.</p><p>DDoS атака это внештатная ситуация и закладываться под 2000% нагрузки при построении инфраструктуры нет смысла.</p><p>Почему? — Потому что атака может быть такой, что Akamai и Yandex не отобьются. И возможность держать 2000% будет каплей в море, но за время простоя оборудования компания сожжет вагон денег, и все равно ресурс ляжет.</p><p>Нужно ли готовиться, если все так плохо? — Однозначно. Но как обычно с умом.</p><h4>Как это выглядит в кино?</h4><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F8FdvWuSl7eY%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D8FdvWuSl7eY&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F8FdvWuSl7eY%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/a62cc8a64e46f2571572fd4a54d40edc/href">https://medium.com/media/a62cc8a64e46f2571572fd4a54d40edc/href</a></iframe><p>Куча открытых терминалов, что-то куда-то летит, шифруется-криптуется-пингуется.</p><p>[Загорается красная лампочка].</p><p>— Сэр, на нас напали</p><p>–– Покажи им, Джон.</p><p>[Тут обязательно ломается спутник, детектится плохой парень и вот уже бравые ребята выносят дверь какого-нибудь бункера и всех ластают].</p><h4>Как это выглядит в реальности?</h4><p>Едешь ночью из бара на такси. Звонит мониторинг и электронный голос надиктовывает сообщение — &lt;sitename&gt; unreachable. С телефона 504 и спиннер браузера нарезает бесконечные круги пока ты судорожно пытаешься вспомнить пароль от Zabbix… А там лавина 500-х и в 1000 раз больше запросов. И тебе надо что-то делать. Все спутники до тебя уже сломали при запуске или при сборке, так что выбор невелик, надо как-то отбиваться. Как в кино бывает только в кино.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YM0FVJFChSlwCAtm_VJnfQ.png" /><figcaption>Скрин Zabbix с одной из app машин. 500 — черный, 400 — красный, total — зеленый.</figcaption></figure><p>Нормальная нагрузка в это время 600–800rpm на 1 app машину.</p><h4>Как идет атака?</h4><p>Нас атаковали двумя самыми простыми и действенными способами:</p><ol><li>Атака на уровне <a href="https://en.wikipedia.org/wiki/Application_layer">приложения</a> (L7). GET/POST запросы в разные location (/, /login, /wp-login) с рандомизацией User-Agent и cookie.</li><li>Атака на <a href="https://en.wikipedia.org/wiki/Transport_layer">транспортном</a> уровне (L4). <a href="https://en.wikipedia.org/wiki/SYN_flood">SYN flood</a>, <a href="https://en.wikipedia.org/wiki/UDP_flood_attack">UDP flood</a>.</li></ol><p>Если от первого реально отбиться, настроив фильтры или WAF, то от второго не всегда. UDP flood крайне эффективен. Запретить UDP пакеты можно только с помощью хостера.</p><p>Флоу обычно такой — бьют в приложение (L7), если отбились, то бьют в (L4), если и тут отбились, то увеличивают размер. Дальше бьют и в L7 и в L4.</p><h4>Как отбивались?</h4><ol><li>Нашли в логах сервера паттерны в запросах к приложению. Вытащили IP адреса и зафильтровали трафик на балансере.</li><li>Запретили POST там, где он не нужен.</li><li>Начали снимать дамп TCP и UDP пакетов. Обнаружили UDP flood.</li><li>Созвонились с хостером и попросили отключить UDP трафик к нашему IP. Но наш прекрасный хостер не сделал этого, а отправил в Null route. Проще говоря, заблокировал весь трафик к нашему IP, чтобы защитить свою инфраструктуру, и выключил нас.</li><li>Переключили на новый IP и подали заявки в <a href="https://www.cloudflare.com/galileo/">CloudFlare Project Galileo</a> и <a href="https://projectshield.withgoogle.com/public/">Google Shield</a>.</li><li>Завели аккаунт и оплатили тариф professional в CloudFlare.</li><li>Перенесли зону в CloudFlare. Это была самая длительная операция. Зона переносилась 1,5 часа, это время сайт работал с сильными перебоями. (Вот <a href="https://developers.google.com/speed/public-dns/cache?hl=ru">тут</a> можно ускорить обновление публичных DNS).</li><li>Включили магическую кнопку Under Attack. Это javascript challenge и агрессивная фильтрация запросов. До входа на сайт показывается страница, на которой должен отработать js скрипт и средиректить пользователя через 5 секунд.</li><li>Включили rate limit на количество запросов с одного IP и отключили такую же настройку на nginx.</li><li>Включили WAF (Web Application Firewall) от CloudFlare. Последующая настройка WAF для издания заняла примерно 3 дня. Это отслеживание запросов и работы редакции, отключение части фильтров и адаптация под нашу систему.</li><li>Настроили nginx, чтобы не блокировать запросы от подсети CloudFlare и прокидывать реальные IP пользователей в систему.</li></ol><p>Как итог, издание было недоступно в сумме около 2х часов — что укладывается в SLA.</p><h4>Тайминги</h4><p>В среднем ботнет живет 1,5–2 недели. Хостеры заблокируют взломанные машины, с которых идет подозрительный трафик, или атакующим просто надоест.</p><p>Вымогатели атаковали неделю, били по 2–3 часа в пиковое по трафику время, а также ночью и рано утром. На другом издании включили в 12:00 и держали до 16:30 следующего дня. Причины и цели остались неизвестными.</p><p>CloudFlare включил в программу Galileo в течение двух дней. Защита на тарифе professional была сразу, но бесплатной её сделали спустя два дня.</p><p>Google Shield дал доступ к своему сервису спустя три дня. К тому времени мы уже уехали за щит CloudFlare.</p><h4>Что сделать сразу после прочтения этого текста?</h4><ol><li>Проверить TTL в критичных DNS записях (сделайте хотя бы 900-1800). Это 15–30 мин. Сторонний DNS хостинг не умрет от такого TTL, self-hosted надо смотреть и помнить о резервировании сервиса. Низкий TTL позволит быстро менять IP и быстрее встать после переноса зоны.</li><li>Купите IP подсеть. Стоит копейки, если уходить за внешний щит, то нужно менять IP и выключать старый. Будете отбиваться сами, возможность менять IP даст время для маневров.</li><li>Подайте заявки в <a href="https://www.cloudflare.com/">CloudFlare</a> и <a href="https://projectshield.withgoogle.com">GoogleShield</a>. Это сервисы по защите медиа. Плюсы и минусы каждого распишу в отдельной статье. В кратце, у Shield быстрее сетап, но хуже защита.</li></ol><p>Не нужно бросаться переписывать приложение или наращивать мощность машин. Приложение быстро не перепишите, а машины не помогут.</p><h4>Какие сервисы можно использовать?</h4><p><em>Бесплатные</em></p><ol><li>CloudFlare Project Gallileo</li><li>Google Shield</li></ol><p><em>Платные</em></p><ol><li><a href="https://qrator.net/en/">Qrator</a></li><li><a href="https://www.akamai.com/uk/en/products/cloud-security/kona-site-defender.jsp">Kona Site Defender</a> от Akamai</li></ol><p>Сервисов по защите от DDoS атак намного больше, я лишь перечислил от известных компаний и добавил российского вендора.</p><h4>Хостинг</h4><p>Одна из самых важных составляющих инфраструктуры. Подходить к выбору надо очень аккуратно, особенно если вы медиа и на вас есть законодательные ограничения. Внимательно изучайте документацию на сайте хостинга и смотрите есть ли защита от DDoS атак. Платная или бесплатная и что именно хостер под этим понимает. Советую не полениться и позвонить. Консультанты на линии могут переключить на технарей, которые смогут более точно рассказать какой уровень атаки хостер выдержит и как сможет вам помочь.</p><p>Мы использовали Softlayer (IBM), всё что сделали эти ребята, так это отправили в null route без разговоров. Даже простая просьба отключить весь UDP трафик до наших машин была проигнорирована. Хоть Softlayer предоставляет самый большой функционал по работе с сетью, все же отсутствие защиты от DDoS атак и нежелания помогать заставили нас искать другие варианты.</p><p>Советую посмотреть в сторону Hetzner, Online.net и OVH. Это крупные компании, которые предоставляют защиту от DDoS. Российских хостеров не смотрел, но думаю там тоже есть игроки с нужными опциями. Hetzner предоставляет условно бесплатно. Качество и границы не проверял.</p><h4>Что делать если вы сейчас под атакой?</h4><p>Во-первых, выдохнуть. На вас уже напали и если подготовки было 0, то рвать волосы на голове или жопе уже бесполезно.</p><p>Во-вторых, понять проблему:</p><ol><li>Вектор атаки.</li><li>Масштаб трагедии.</li></ol><p>L7 — смотрите логи, вытаскивайте IP и фильтруйте.</p><p>L4 — tcpdump на балансировщике (машине где у вас висит основной IP) и составляйте список IP и подсетей. Можно фильтровать или отправить хостеру/сервис по защите.</p><p>Если вам забили канал мусорным трафиком, то вам поможет или внешний щит или хостер, других вариантов просто нет.</p><p>Можете написать в <a href="https://www.facebook.com/igkuznetsov">FB</a>, чем смогу помогу.</p><h4>Если вы смелый, ловкий, умелый…</h4><p>Ботнет это куча зараженных машин (камеры, роутеры, виртуальные машины с WP, etc). У него есть 1-N управляющих центров. За центрами могут быть еще центры и уже за ними человек (группа людей).</p><p>Чтобы отбить ботнет нужно сломать одну из зараженных машин, определить откуда идут команды, сломать одну из машин управляющего центра, посмотреть нет ли там еще одного слоя и так вглубь. Контрол центры можно пролечить и остановить DDoS. TL; DR; статья от Wired о том, как отбивали один из самых больших <a href="https://www.wired.com/2017/03/russian-hacker-spy-botnet/">ботнетов</a>.</p><p>Весь предыдущий абзац это нарушение законодательства, как минимум в РФ, поэтому прежде чем так делать, даже если вы сможете, подумайте.</p><p>Многие издания подвергаются DDoS атакам. Например, в апреле нападали на <a href="https://medium.com/meduza-how-it-works/ddos-ab63424e595e">Медузу</a>. Это всегда неприятно, затрагивает работу всей команды и бизнеса в целом и вам нужно быть готовым к этому.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f80c1737c19b" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>