<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.academic.djack.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.academic.djack.dev/" rel="alternate" type="text/html" /><updated>2026-05-14T00:31:37-03:00</updated><id>https://www.academic.djack.dev/feed.xml</id><title type="html">Djack | SRE &amp;amp; Platform Engineer</title><subtitle>Be happy</subtitle><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><entry xml:lang="en"><title type="html">Building an SFTP with S3 Files on EC2</title><link href="https://www.academic.djack.dev/posts/2026/05/building-sftp-s3-files-ec2/" rel="alternate" type="text/html" title="Building an SFTP with S3 Files on EC2" /><published>2026-05-13T00:00:00-03:00</published><updated>2026-05-13T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2026/05/building-sftp-s3-files-ec2-en</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2026/05/building-sftp-s3-files-ec2/"><![CDATA[<h1 id="building-an-sftp-with-s3-files-on-ec2">Building an SFTP with S3 Files on EC2</h1>

<p>S3 Files (launched Nov/2025) is the new AWS NFS file system backed by S3 buckets. Instead of testing with a “hello world”, I picked a useful case from the start: an <strong>SFTP server on EC2</strong> with <code class="language-plaintext highlighter-rouge">/home/sftp</code> mounted via S3 Files — every partner upload lands straight in the bucket, no agent, no worker, no cron.</p>

<p>The full setup runs at ~$14/month (t4g.small + EBS + bucket + S3 Files). As a market reference, Transfer Family has a fixed ~$216/month, but the comparison isn’t the focus here — the focus is understanding the new service by building something real on top of it.</p>

<p>Tested on <strong>AL2023 + amazon-efs-utils 3.1.0</strong>, May/2026.</p>

<p><strong>TL;DR — the four lessons:</strong></p>

<ol>
  <li>The IAM principal is <code class="language-plaintext highlighter-rouge">elasticfilesystem.amazonaws.com</code>, not <code class="language-plaintext highlighter-rouge">s3files.*</code></li>
  <li><code class="language-plaintext highlighter-rouge">mount -t efs</code> fails silently; you need <code class="language-plaintext highlighter-rouge">-t s3files</code></li>
  <li>The helper needs <code class="language-plaintext highlighter-rouge">botocore</code> AND <code class="language-plaintext highlighter-rouge">elasticfilesystem:DescribeMountTargets</code> (not in any managed policy)</li>
  <li>Versioning is mandatory → <code class="language-plaintext highlighter-rouge">rm</code> creates a delete marker; without lifecycle, cost grows forever</li>
</ol>

<p>This post is the walkthrough of what worked + the literal errors that came up. It’s not a mechanical tutorial (“run these commands”); it’s the actual sequence with the decisions I had to make.</p>

<h2 id="what-s3-files-is">What S3 Files is</h2>

<p>In one sentence: you mount a local directory on an EC2 instance and anything you write there shows up in S3 within seconds, with the bucket as source of truth (versioning, lifecycle, replication).</p>

<h2 id="architecture">Architecture</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        Internet
           │
        :22 TCP
           │
     ┌─────▼─────┐
     │ EC2 AL2023│
     │   ARM     │
     │           │
     │  sshd +   │
     │  chroot   │
     │  fail2ban │
     └──┬─────┬──┘
        │     │
   :2049│     │ EBS gp3
   NFS  │     │ (OS + host keys)
        ▼
  ┌──────────────┐
  │ S3 Files     │
  │ /home/sftp   │
  └──────┬───────┘
         │ auto sync
         ▼
   ┌─────────┐
   │ Bucket  │
   │  S3     │
   └─────────┘
</code></pre></div></div>

<p>EC2 t4g.small running Amazon Linux 2023, minimal IAM, IMDSv2 required, SG with restricted egress. Provisioned via AWS CDK (Python).</p>

<hr />

<h2 id="lesson-1-the-iam-principal-is-called-elasticfilesystem-not-s3files">Lesson 1: the IAM principal is called <code class="language-plaintext highlighter-rouge">elasticfilesystem</code>, not <code class="language-plaintext highlighter-rouge">s3files</code></h2>

<p>S3 Files needs a <strong>service role</strong> that the service assumes to read/write your bucket. The first question I had to answer was: which service principal is correct?</p>

<p>The prerequisites page (<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">s3-files-prereq-policies.html</a>) answers it in the very first trust policy block — and the answer is less obvious than the service name suggests:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">assumed_by</span><span class="o">=</span><span class="n">iam</span><span class="p">.</span><span class="n">PrincipalWithConditions</span><span class="p">(</span>
    <span class="n">iam</span><span class="p">.</span><span class="n">ServicePrincipal</span><span class="p">(</span><span class="s">"elasticfilesystem.amazonaws.com"</span><span class="p">),</span>
    <span class="n">conditions</span><span class="o">=</span><span class="p">{</span>
        <span class="s">"StringEquals"</span><span class="p">:</span> <span class="p">{</span><span class="s">"aws:SourceAccount"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">account</span><span class="p">},</span>
        <span class="s">"ArnLike"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"aws:SourceArn"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"arn:aws:s3files:</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">region</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">account</span><span class="si">}</span><span class="s">:file-system/*"</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Two details worth highlighting:</p>

<ol>
  <li>
    <p><strong>The principal is <code class="language-plaintext highlighter-rouge">elasticfilesystem.amazonaws.com</code>, not <code class="language-plaintext highlighter-rouge">s3files.*</code></strong>. S3 Files reuses the EFS control plane behind the scenes. Trusting the service name to guess the principal lands you on <code class="language-plaintext highlighter-rouge">Invalid principal in policy: "SERVICE":"s3files.amazonaws.com"</code>.</p>
  </li>
  <li>
    <p>The <code class="language-plaintext highlighter-rouge">aws:SourceAccount</code> and <code class="language-plaintext highlighter-rouge">aws:SourceArn</code> conditions are the <strong>confused deputy countermeasure</strong> — they close the gap where another file system, possibly from another account, could trick the EFS principal into assuming your role. The docs ship them ready to use; keep them in your code.</p>
  </li>
</ol>

<p>Lesson: for any new AWS service, opening the IAM/prerequisites page before writing code saves a broken deploy.</p>

<h2 id="lesson-2-the-mount-type-is-s3files-not-efs">Lesson 2: the mount type is <code class="language-plaintext highlighter-rouge">s3files</code>, not <code class="language-plaintext highlighter-rouge">efs</code></h2>

<p>The docs list <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> (≥ 3.0.0) as a prerequisite and describe a “mount helper”. The instinct, coming from someone who has used EFS, is to mount with <code class="language-plaintext highlighter-rouge">-t efs</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$FS_ID</span><span class="s2">:/ /home/sftp efs _netdev,tls,noatime 0 0"</span> <span class="o">&gt;&gt;</span> /etc/fstab
mount <span class="nt">-a</span> <span class="nt">-t</span> efs
</code></pre></div></div>

<p>And nothing happens. The log at <code class="language-plaintext highlighter-rouge">/var/log/amazon/efs/mount.log</code> shows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to resolve fs-XXX.efs.us-east-1.amazonaws.com
</code></pre></div></div>

<p>The helper, with <code class="language-plaintext highlighter-rouge">-t efs</code>, looks for an EFS endpoint — which doesn’t exist for an S3 Files file system. The correct command lives in <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-getting-started.html">Step 3 of the Getting Started tutorial</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>mount <span class="nt">-t</span> s3files &lt;file-system-id&gt;:/ /mnt/s3files
</code></pre></div></div>

<p>And in fstab:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fs-XXXXXXXXXXXXXXXXX:/  /home/sftp  s3files  _netdev,noatime  0 0
</code></pre></div></div>

<p>The maturity here is realizing <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> has become an umbrella package: the same binary responds to two configs (<code class="language-plaintext highlighter-rouge">/etc/amazon/efs/efs-utils.conf</code> and <code class="language-plaintext highlighter-rouge">/etc/amazon/efs/s3files-utils.conf</code>) depending on the mount type declared. With <code class="language-plaintext highlighter-rouge">-t s3files</code>, the helper opens a local stunnel TLS at <code class="language-plaintext highlighter-rouge">127.0.0.1:&lt;random-port&gt;</code> and tunnels NFS to that endpoint:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mount | grep sftp
127.0.0.1:/ on /home/sftp type nfs4 (rw,noatime,vers=4.2,...,port=20423,...)
</code></pre></div></div>

<p>Lesson: for services that reuse existing packages/infrastructure (<code class="language-plaintext highlighter-rouge">amazon-efs-utils</code>, <code class="language-plaintext highlighter-rouge">elasticfilesystem</code> principal), the Getting Started tutorial usually carries the detail the prerequisites page omits. Worth reading both before you implement.</p>

<h2 id="lesson-3-amazon-efs-utils-doesnt-pull-botocore-and-needs-an-extra-iam-permission">Lesson 3: <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> doesn’t pull <code class="language-plaintext highlighter-rouge">botocore</code>, and needs an extra IAM permission</h2>

<p>Even with <code class="language-plaintext highlighter-rouge">-t s3files</code> correct, two errors show up in sequence. Both share a root cause: <code class="language-plaintext highlighter-rouge">mount.s3files</code> needs to <strong>discover the mount target IP at runtime</strong>, and to do that it makes an API call with <code class="language-plaintext highlighter-rouge">boto3</code>.</p>

<h3 id="error-1-botocore-missing">Error 1: <code class="language-plaintext highlighter-rouge">botocore</code> missing</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR - Failed to import botocore, please install botocore first.
</code></pre></div></div>

<p>The docs do warn about this in a whole section (<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">Step 2: Install botocore</a>). What’s worth recording: on Amazon Linux 2023 the <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> package <strong>does not declare</strong> <code class="language-plaintext highlighter-rouge">python3-botocore</code> as a dependency — <code class="language-plaintext highlighter-rouge">dnf install amazon-efs-utils</code> alone won’t resolve it. Definitive fix in user-data:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dnf <span class="nb">install</span> <span class="nt">-y</span> amazon-efs-utils python3-botocore
</code></pre></div></div>

<h3 id="error-2-missing-iam-permission">Error 2: missing IAM permission</h3>

<p>After fixing <code class="language-plaintext highlighter-rouge">botocore</code>, the next error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User: ... is not authorized to perform:
  elasticfilesystem:DescribeMountTargets on the specified resource
</code></pre></div></div>

<p>This part is worth highlighting: efs-utils 3.1.0 <strong>calls the EFS API</strong> to discover the mount target IP, even when the file system is S3 Files. The <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">IAM role for attaching your file system</a> section recommends either the managed policy <code class="language-plaintext highlighter-rouge">AmazonS3FilesClientFullAccess</code> or granular <code class="language-plaintext highlighter-rouge">s3files:ClientMount</code> + <code class="language-plaintext highlighter-rouge">s3files:ClientWrite</code>. Neither includes <code class="language-plaintext highlighter-rouge">elasticfilesystem:*</code>. Solution:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iam</span><span class="p">.</span><span class="n">PolicyStatement</span><span class="p">(</span>
    <span class="n">sid</span><span class="o">=</span><span class="s">"EfsUtilsMountTargetLookup"</span><span class="p">,</span>
    <span class="n">actions</span><span class="o">=</span><span class="p">[</span>
        <span class="s">"elasticfilesystem:DescribeMountTargets"</span><span class="p">,</span>
        <span class="s">"elasticfilesystem:DescribeFileSystems"</span><span class="p">,</span>
    <span class="p">],</span>
    <span class="n">resources</span><span class="o">=</span><span class="p">[</span><span class="s">"*"</span><span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>I added it as an explicit statement for two reasons: it makes visible <strong>why</strong> that permission exists (efs-utils, not the service), and it survives a future package update that may not need it anymore.</p>

<blockquote>
  <p>The docs mention the <code class="language-plaintext highlighter-rouge">AmazonElasticFileSystemUtils</code> managed policy in another context (CloudWatch). I haven’t tested, but it likely covers these two actions already — possible simplification.</p>
</blockquote>

<p>Lesson: when an SDK/client is shared between services, always look at <strong>the APIs it calls internally</strong>, not just the target service’s. The mount helper’s log tells you exactly which call failed — that’s the fastest path to correct IAM.</p>

<h2 id="lesson-4-versioning-is-mandatory--and-that-changes-what-delete-means">Lesson 4: versioning is mandatory — and that changes what “delete” means</h2>

<p>S3 Files <strong>requires</strong> versioning enabled on the bucket. The docs state it plainly in the <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">AWS account and compute setup</a> section:</p>

<blockquote>
  <p><em>Your S3 bucket has versioning enabled. <strong>S3 Files requires S3 Versioning</strong> to synchronize changes between your file system and your S3 bucket.</em></p>
</blockquote>

<p>Without it, file system creation fails. The operational consequence is non-trivial:</p>

<ul>
  <li>When an SFTP user runs <code class="language-plaintext highlighter-rouge">rm file.txt</code> inside the chroot, S3 Files passes it to the bucket. Since the bucket is versioned, <strong>there is no real delete</strong>: a <em>delete marker</em> is created and the original version is preserved.</li>
  <li><code class="language-plaintext highlighter-rouge">aws s3 ls</code> (lists visible objects) shows the object gone, but <code class="language-plaintext highlighter-rouge">aws s3api list-object-versions</code> keeps showing both: the old version and the delete marker hiding it.</li>
  <li>Without a <strong>lifecycle policy</strong>, those versions stay forever in the bucket, paying storage.</li>
</ul>

<p>I lived through this when removing a test user:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>userdel testuser
<span class="nb">rm</span> <span class="nt">-rf</span> /home/sftp/testuser
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">aws s3 ls</code> showed the bucket “clean”. But <code class="language-plaintext highlighter-rouge">list-object-versions</code> revealed three versions + three delete markers for <code class="language-plaintext highlighter-rouge">testuser/upload/teste.txt</code>. For a real purge:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span><span class="p">,</span> <span class="n">subprocess</span>
<span class="n">v</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
    <span class="s">"aws"</span><span class="p">,</span> <span class="s">"s3api"</span><span class="p">,</span> <span class="s">"list-object-versions"</span><span class="p">,</span>
    <span class="s">"--bucket"</span><span class="p">,</span> <span class="n">BUCKET</span><span class="p">,</span> <span class="s">"--prefix"</span><span class="p">,</span> <span class="s">"testuser/"</span><span class="p">,</span> <span class="s">"--region"</span><span class="p">,</span> <span class="n">REGION</span><span class="p">,</span>
    <span class="s">"--output"</span><span class="p">,</span> <span class="s">"json"</span><span class="p">],</span> <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="n">stdout</span><span class="p">)</span>
<span class="n">objs</span> <span class="o">=</span> <span class="p">[{</span><span class="s">'Key'</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="s">'Key'</span><span class="p">],</span> <span class="s">'VersionId'</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="s">'VersionId'</span><span class="p">]}</span>
        <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'Versions'</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[])</span> <span class="o">+</span> <span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'DeleteMarkers'</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[])]</span>
</code></pre></div></div>

<p>Then send the batch to <code class="language-plaintext highlighter-rouge">delete-objects</code>. The structural fix is to <strong>always add a lifecycle policy</strong> when creating the bucket:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bucket</span> <span class="o">=</span> <span class="n">s3</span><span class="p">.</span><span class="n">Bucket</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s">"SftpBucket"</span><span class="p">,</span>
    <span class="n">versioned</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>  <span class="c1"># S3 Files requirement
</span>    <span class="n">lifecycle_rules</span><span class="o">=</span><span class="p">[</span>
        <span class="n">s3</span><span class="p">.</span><span class="n">LifecycleRule</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="s">"DeleteOldVersions"</span><span class="p">,</span>
            <span class="n">noncurrent_version_expiration</span><span class="o">=</span><span class="n">Duration</span><span class="p">.</span><span class="n">days</span><span class="p">(</span><span class="mi">90</span><span class="p">),</span>
        <span class="p">),</span>
        <span class="n">s3</span><span class="p">.</span><span class="n">LifecycleRule</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="s">"CleanupDeleteMarkers"</span><span class="p">,</span>
            <span class="n">expired_object_delete_marker</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="p">),</span>
    <span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Without this, cost grows linearly with file movement and it’s easy to forget.</p>

<p>Lesson: when a service <strong>requires</strong> versioning, lifecycle isn’t optimization — it’s part of the design. Treating it as a mandatory bucket feature (same construct, same PR) prevents it from becoming operational debt.</p>

<h2 id="when-s3-files-makes-sense">When S3 Files makes sense</h2>

<p>For a low-to-medium volume SFTP (a few GB/month) the full setup runs at ~$14/month: t4g.small + EBS + bucket + S3 Files cost. As a market reference, Transfer Family has a fixed ~$216/month before the first byte transferred — that’s when S3 Files starts to make a lot of sense for teams comfortable maintaining an EC2.</p>

<p>The <strong>breakeven</strong> vs Transfer Family sits around 4.3 TB/month of traffic — above that, S3 Files cache cost ($0.06/GB stored in cache + $0.03/GB sync) grows past the fixed Transfer Family cost. For most partner-SFTP cases (a few files per day), you stay far from that limit.</p>

<p>What S3 Files does <strong>not</strong> ship out of the box (that managed services do):</p>
<ul>
  <li>Centralized SSH user and key management.</li>
  <li>AS2 and FTPS support.</li>
  <li>Zero-touch operation — you still have an EC2 to patch and monitor.</li>
</ul>

<p>Where it earns a spot as a new tool in the kit:</p>
<ul>
  <li>Pipelines where the <strong>bucket must be source of truth</strong> (lifecycle, replication, Object Lock).</li>
  <li>Legacy workloads that speak NFS but the team already has S3 tooling (Glue, Athena, Lambda).</li>
  <li>Replacing EFS in write-once/read-many scenarios, where S3 storage ($0.023/GB) beats EFS ($0.30/GB).</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">Prerequisites for S3 Files</a> — IAM, SG, botocore and versioning (source of lessons #1, #3 and #4)</li>
  <li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-getting-started.html">Tutorial: Getting started with S3 Files</a> — where <code class="language-plaintext highlighter-rouge">mount -t s3files</code> finally appears</li>
  <li><a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3files-filesystem.html">AWS::S3Files::FileSystem CFN</a> — resource properties</li>
  <li><a href="https://dev.to/aws-builders/aws-s3-files-just-made-transfer-family-sftp-obsolete-for-most-use-cases-4me">AWS S3 Files just made Transfer Family SFTP obsolete</a> — original post that motivated the experiment</li>
</ul>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="aws" /><category term="s3" /><category term="s3-files" /><category term="sftp" /><category term="cdk" /><category term="infrastructure" /><summary type="html"><![CDATA[S3 Files (launched November 2025) is the new AWS NFS file system backed by S3 buckets. Instead of testing with a "hello world", I picked a useful case from the start: an SFTP on EC2 with `/home/sftp` mounted via S3 Files, files landing straight in the bucket. Along the way, four lessons the docs do not highlight: peculiar IAM principal, wrong mount type, hidden botocore dependency, and versioning that changes what delete means.]]></summary></entry><entry xml:lang="pt"><title type="html">Montando SFTP com S3 Files no EC2</title><link href="https://www.academic.djack.dev/posts/2026/05/montando-sftp-s3-files-ec2/" rel="alternate" type="text/html" title="Montando SFTP com S3 Files no EC2" /><published>2026-05-13T00:00:00-03:00</published><updated>2026-05-13T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2026/05/montando-sftp-s3-files-ec2</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2026/05/montando-sftp-s3-files-ec2/"><![CDATA[<h1 id="montando-sftp-com-s3-files-no-ec2">Montando SFTP com S3 Files no EC2</h1>

<p>S3 Files (lançado nov/2025) é o novo NFS da AWS lastreado por bucket S3. Em vez de testar com um “hello world”, escolhi um caso útil de primeira: <strong>servidor SFTP em EC2</strong> com <code class="language-plaintext highlighter-rouge">/home/sftp</code> montado via S3 Files — cada upload de parceiro cai direto no bucket, sem agente, sem worker, sem cron.</p>

<p>Saiu por ~$14/mês (t4g.small + EBS + bucket + S3 Files). Como referência de mercado, Transfer Family tem fixo de ~$216/mês, mas a comparação não é o foco aqui — o foco é entender o serviço novo construindo algo de verdade em cima dele.</p>

<p>Testado em <strong>AL2023 + amazon-efs-utils 3.1.0</strong>, maio/2026.</p>

<p><strong>TL;DR — os quatro aprendizados:</strong></p>

<ol>
  <li>O principal IAM é <code class="language-plaintext highlighter-rouge">elasticfilesystem.amazonaws.com</code>, não <code class="language-plaintext highlighter-rouge">s3files.*</code></li>
  <li><code class="language-plaintext highlighter-rouge">mount -t efs</code> falha silenciosamente; precisa ser <code class="language-plaintext highlighter-rouge">-t s3files</code></li>
  <li>O helper precisa de <code class="language-plaintext highlighter-rouge">botocore</code> E de <code class="language-plaintext highlighter-rouge">elasticfilesystem:DescribeMountTargets</code> (não está em nenhuma managed policy)</li>
  <li>Versionamento é obrigatório → <code class="language-plaintext highlighter-rouge">rm</code> cria delete marker; sem lifecycle vira custo eterno</li>
</ol>

<p>O post é o passo a passo do que funcionou + os erros literais que apareceram. Não é tutorial mecânico (“execute esses comandos”); é a sequência real, com as decisões que tive que tomar.</p>

<h2 id="o-que-é-s3-files">O que é S3 Files</h2>

<p>Em uma frase: você monta um diretório local em uma EC2 e tudo o que escrever lá aparece no S3 em segundos, mantendo o bucket como fonte da verdade (com versionamento, lifecycle e replicação).</p>

<h2 id="arquitetura-do-experimento">Arquitetura do experimento</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        Internet
           │
        :22 TCP
           │
     ┌─────▼─────┐
     │ EC2 AL2023│
     │   ARM     │
     │           │
     │  sshd +   │
     │  chroot   │
     │  fail2ban │
     └──┬─────┬──┘
        │     │
   :2049│     │ EBS gp3
   NFS  │     │ (SO + host keys)
        ▼
  ┌──────────────┐
  │ S3 Files     │
  │ /home/sftp   │
  └──────┬───────┘
         │ sync auto
         ▼
   ┌─────────┐
   │ Bucket  │
   │  S3     │
   └─────────┘
</code></pre></div></div>

<p>EC2 t4g.small com Amazon Linux 2023, IAM minimal, IMDSv2 obrigatório, SG com egress restrito. Tudo provisionado via AWS CDK (Python).</p>

<hr />

<h2 id="aprendizado-1-o-principal-iam-se-chama-elasticfilesystem-não-s3files">Aprendizado 1: o principal IAM se chama <code class="language-plaintext highlighter-rouge">elasticfilesystem</code>, não <code class="language-plaintext highlighter-rouge">s3files</code></h2>

<p>S3 Files exige uma <strong>service role</strong> que o serviço assume para ler e escrever no seu bucket. A primeira pergunta que precisei responder foi: qual é o service principal correto?</p>

<p>A doc de pré-requisitos (<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">s3-files-prereq-policies.html</a>) responde já no primeiro bloco de trust policy — e a resposta é menos óbvia do que parece pelo nome do serviço:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">assumed_by</span><span class="o">=</span><span class="n">iam</span><span class="p">.</span><span class="n">PrincipalWithConditions</span><span class="p">(</span>
    <span class="n">iam</span><span class="p">.</span><span class="n">ServicePrincipal</span><span class="p">(</span><span class="s">"elasticfilesystem.amazonaws.com"</span><span class="p">),</span>
    <span class="n">conditions</span><span class="o">=</span><span class="p">{</span>
        <span class="s">"StringEquals"</span><span class="p">:</span> <span class="p">{</span><span class="s">"aws:SourceAccount"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">account</span><span class="p">},</span>
        <span class="s">"ArnLike"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"aws:SourceArn"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"arn:aws:s3files:</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">region</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">account</span><span class="si">}</span><span class="s">:file-system/*"</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Dois detalhes valem destaque:</p>

<ol>
  <li>
    <p><strong>O principal é <code class="language-plaintext highlighter-rouge">elasticfilesystem.amazonaws.com</code>, não <code class="language-plaintext highlighter-rouge">s3files.*</code></strong>. S3 Files reaproveita a infra de controle do EFS por trás dos panos. Confiar no nome do serviço pra adivinhar o principal te leva pro erro <code class="language-plaintext highlighter-rouge">Invalid principal in policy: "SERVICE":"s3files.amazonaws.com"</code>.</p>
  </li>
  <li>
    <p>As condições <code class="language-plaintext highlighter-rouge">aws:SourceAccount</code> e <code class="language-plaintext highlighter-rouge">aws:SourceArn</code> são <strong>contra-medida ao confused deputy</strong> — fecham a brecha em que outro file system, possivelmente de outra conta, poderia induzir o principal EFS a assumir sua role. A doc traz isso pronto; manter no código.</p>
  </li>
</ol>

<p>Lição: pra serviço novo da AWS, abrir a página de IAM/pré-requisitos antes de escrever código economiza um deploy quebrado.</p>

<h2 id="aprendizado-2-o-tipo-do-mount-é-s3files-não-efs">Aprendizado 2: o tipo do mount é <code class="language-plaintext highlighter-rouge">s3files</code>, não <code class="language-plaintext highlighter-rouge">efs</code></h2>

<p>A doc lista <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> (≥ 3.0.0) como pré-requisito e descreve um “mount helper”. O instinto, vindo de quem já usou EFS, é montar com <code class="language-plaintext highlighter-rouge">-t efs</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$FS_ID</span><span class="s2">:/ /home/sftp efs _netdev,tls,noatime 0 0"</span> <span class="o">&gt;&gt;</span> /etc/fstab
mount <span class="nt">-a</span> <span class="nt">-t</span> efs
</code></pre></div></div>

<p>E nada acontece. O log em <code class="language-plaintext highlighter-rouge">/var/log/amazon/efs/mount.log</code> mostra:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to resolve fs-XXX.efs.us-east-1.amazonaws.com
</code></pre></div></div>

<p>O helper, com <code class="language-plaintext highlighter-rouge">-t efs</code>, busca um endpoint EFS — que não existe para um file system S3 Files. O comando correto está no <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-getting-started.html">Step 3 do tutorial Getting Started</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>mount <span class="nt">-t</span> s3files &lt;file-system-id&gt;:/ /mnt/s3files
</code></pre></div></div>

<p>E no fstab:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fs-XXXXXXXXXXXXXXXXX:/  /home/sftp  s3files  _netdev,noatime  0 0
</code></pre></div></div>

<p>A maturidade aqui é entender que <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> virou um pacote guarda-chuva: o mesmo binário responde a duas configs (<code class="language-plaintext highlighter-rouge">/etc/amazon/efs/efs-utils.conf</code> e <code class="language-plaintext highlighter-rouge">/etc/amazon/efs/s3files-utils.conf</code>) dependendo do tipo declarado no mount. Com <code class="language-plaintext highlighter-rouge">-t s3files</code>, o helper abre um stunnel TLS local em <code class="language-plaintext highlighter-rouge">127.0.0.1:&lt;porta-aleatória&gt;</code> e tunela NFS pra esse endpoint:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mount | grep sftp
127.0.0.1:/ on /home/sftp type nfs4 (rw,noatime,vers=4.2,...,port=20423,...)
</code></pre></div></div>

<p>Lição: para serviços que reusam pacotes/infraestrutura existentes (<code class="language-plaintext highlighter-rouge">amazon-efs-utils</code>, principal <code class="language-plaintext highlighter-rouge">elasticfilesystem</code>), o tutorial Getting Started costuma trazer o detalhe que o pré-requisitos omite. Vale ler os dois antes de implementar.</p>

<h2 id="aprendizado-3-o-pacote-amazon-efs-utils-não-puxa-botocore-e-exige-uma-permissão-iam-extra">Aprendizado 3: o pacote <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> não puxa <code class="language-plaintext highlighter-rouge">botocore</code> e exige uma permissão IAM extra</h2>

<p>Mesmo com <code class="language-plaintext highlighter-rouge">-t s3files</code> correto, dois erros aparecem em sequência. Os dois têm raiz comum: o <code class="language-plaintext highlighter-rouge">mount.s3files</code> precisa <strong>descobrir o IP do mount target em runtime</strong>, e pra isso ele faz uma chamada de API com <code class="language-plaintext highlighter-rouge">boto3</code>.</p>

<h3 id="erro-1-botocore-ausente">Erro 1: <code class="language-plaintext highlighter-rouge">botocore</code> ausente</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR - Failed to import botocore, please install botocore first.
</code></pre></div></div>

<p>A doc avisa numa seção inteira (<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">Step 2: Install botocore</a>). O detalhe que vale registrar é que no Amazon Linux 2023 o pacote <code class="language-plaintext highlighter-rouge">amazon-efs-utils</code> <strong>não declara</strong> <code class="language-plaintext highlighter-rouge">python3-botocore</code> como dependência — <code class="language-plaintext highlighter-rouge">dnf install amazon-efs-utils</code> sozinho não resolve. Fix definitivo no user-data:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dnf <span class="nb">install</span> <span class="nt">-y</span> amazon-efs-utils python3-botocore
</code></pre></div></div>

<h3 id="erro-2-permissão-iam-ausente">Erro 2: permissão IAM ausente</h3>

<p>Resolvido o <code class="language-plaintext highlighter-rouge">botocore</code>, o próximo erro:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User: ... is not authorized to perform:
  elasticfilesystem:DescribeMountTargets on the specified resource
</code></pre></div></div>

<p>Essa parte vale o destaque: o efs-utils 3.1.0 <strong>chama a API do EFS</strong> para descobrir o IP do mount target, mesmo quando o file system é S3 Files. A doc de <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">IAM role for attaching your file system</a> recomenda a managed policy <code class="language-plaintext highlighter-rouge">AmazonS3FilesClientFullAccess</code> ou as ações granulares <code class="language-plaintext highlighter-rouge">s3files:ClientMount</code> + <code class="language-plaintext highlighter-rouge">s3files:ClientWrite</code>. Nenhuma delas inclui <code class="language-plaintext highlighter-rouge">elasticfilesystem:*</code>. Solução:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iam</span><span class="p">.</span><span class="n">PolicyStatement</span><span class="p">(</span>
    <span class="n">sid</span><span class="o">=</span><span class="s">"EfsUtilsMountTargetLookup"</span><span class="p">,</span>
    <span class="n">actions</span><span class="o">=</span><span class="p">[</span>
        <span class="s">"elasticfilesystem:DescribeMountTargets"</span><span class="p">,</span>
        <span class="s">"elasticfilesystem:DescribeFileSystems"</span><span class="p">,</span>
    <span class="p">],</span>
    <span class="n">resources</span><span class="o">=</span><span class="p">[</span><span class="s">"*"</span><span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Adicionei como statement explícito por dois motivos: deixa visível <strong>por que</strong> essa permissão existe (referência ao efs-utils, não ao serviço), e sobrevive a uma futura atualização do pacote que talvez nem precise mais dela.</p>

<blockquote>
  <p>A doc menciona a managed policy <code class="language-plaintext highlighter-rouge">AmazonElasticFileSystemUtils</code> em outro contexto (CloudWatch). Não testei, mas é provável que ela já cubra essas duas ações — fica como possível simplificação.</p>
</blockquote>

<p>Lição: quando um SDK/cliente é compartilhado entre serviços, sempre olhe <strong>as APIs que ele chama internamente</strong>, não só as do serviço-alvo. O log do mount helper diz exatamente qual chamada falhou — esse é o caminho mais rápido pra IAM correto.</p>

<h2 id="aprendizado-4-bucket-versionado-é-obrigatório--e-isso-muda-o-significado-de-deletar">Aprendizado 4: bucket versionado é obrigatório — e isso muda o significado de “deletar”</h2>

<p>O S3 Files <strong>exige</strong> versionamento ligado no bucket. Está explícito na seção <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">AWS account and compute setup</a> da doc:</p>

<blockquote>
  <p><em>Your S3 bucket has versioning enabled. <strong>S3 Files requires S3 Versioning</strong> to synchronize changes between your file system and your S3 bucket.</em></p>
</blockquote>

<p>Sem isso, a criação do file system falha. A consequência operacional não é trivial:</p>

<ul>
  <li>Quando um usuário SFTP roda <code class="language-plaintext highlighter-rouge">rm arquivo.txt</code> no chroot, o S3 Files repassa para o bucket. Como o bucket é versionado, <strong>não há delete de fato</strong>: cria-se um <em>delete marker</em> e a versão original fica preservada.</li>
  <li><code class="language-plaintext highlighter-rouge">aws s3 ls</code> (lista as visíveis) mostra o objeto sumido, mas <code class="language-plaintext highlighter-rouge">aws s3api list-object-versions</code> continua mostrando ambos: a versão antiga e o delete marker que a esconde.</li>
  <li>Sem <strong>lifecycle policy</strong>, essas versões ficam indefinidamente no bucket pagando armazenamento.</li>
</ul>

<p>Eu vivenciei isso ao apagar um usuário de teste:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>userdel testuser
<span class="nb">rm</span> <span class="nt">-rf</span> /home/sftp/testuser
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">aws s3 ls</code> apresentou o bucket “limpo”. Mas <code class="language-plaintext highlighter-rouge">list-object-versions</code> revelou três versões + três delete markers do <code class="language-plaintext highlighter-rouge">testuser/upload/teste.txt</code>. Para purge real:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span><span class="p">,</span> <span class="n">subprocess</span>
<span class="n">v</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
    <span class="s">"aws"</span><span class="p">,</span> <span class="s">"s3api"</span><span class="p">,</span> <span class="s">"list-object-versions"</span><span class="p">,</span>
    <span class="s">"--bucket"</span><span class="p">,</span> <span class="n">BUCKET</span><span class="p">,</span> <span class="s">"--prefix"</span><span class="p">,</span> <span class="s">"testuser/"</span><span class="p">,</span> <span class="s">"--region"</span><span class="p">,</span> <span class="n">REGION</span><span class="p">,</span>
    <span class="s">"--output"</span><span class="p">,</span> <span class="s">"json"</span><span class="p">],</span> <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="n">stdout</span><span class="p">)</span>
<span class="n">objs</span> <span class="o">=</span> <span class="p">[{</span><span class="s">'Key'</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="s">'Key'</span><span class="p">],</span> <span class="s">'VersionId'</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="s">'VersionId'</span><span class="p">]}</span>
        <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'Versions'</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[])</span> <span class="o">+</span> <span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'DeleteMarkers'</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[])]</span>
</code></pre></div></div>

<p>E enviar o lote ao <code class="language-plaintext highlighter-rouge">delete-objects</code>. A solução estrutural é <strong>sempre adicionar uma lifecycle policy</strong> ao criar o bucket:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bucket</span> <span class="o">=</span> <span class="n">s3</span><span class="p">.</span><span class="n">Bucket</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s">"SftpBucket"</span><span class="p">,</span>
    <span class="n">versioned</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>  <span class="c1"># exigência do S3 Files
</span>    <span class="n">lifecycle_rules</span><span class="o">=</span><span class="p">[</span>
        <span class="n">s3</span><span class="p">.</span><span class="n">LifecycleRule</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="s">"DeleteOldVersions"</span><span class="p">,</span>
            <span class="n">noncurrent_version_expiration</span><span class="o">=</span><span class="n">Duration</span><span class="p">.</span><span class="n">days</span><span class="p">(</span><span class="mi">90</span><span class="p">),</span>
        <span class="p">),</span>
        <span class="n">s3</span><span class="p">.</span><span class="n">LifecycleRule</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="s">"CleanupDeleteMarkers"</span><span class="p">,</span>
            <span class="n">expired_object_delete_marker</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="p">),</span>
    <span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Sem isso, o custo cresce linearmente com a movimentação de arquivos e fica fácil esquecer.</p>

<p>Lição: quando o serviço <strong>exige</strong> versionamento, lifecycle não é otimização — é parte do desenho. Tratar como recurso obrigatório do bucket (no mesmo construct, no mesmo PR) evita que vire dívida operacional.</p>

<h2 id="quando-faz-sentido-usar-s3-files">Quando faz sentido usar S3 Files</h2>

<p>Para um SFTP de volume baixo a médio (alguns GB/mês) o setup completo sai por ~$14/mês: t4g.small + EBS + bucket + custo do S3 Files. Como referência de mercado, o Transfer Family tem fixo de ~$216/mês antes do primeiro byte transferido — aí o S3 Files começa a fazer muito sentido para times que topam manter uma EC2.</p>

<p>O <strong>breakeven</strong> com Transfer Family sai por volta de 4,3 TB/mês de tráfego — acima disso o custo de cache do S3 Files ($0,06/GB armazenado em cache + $0,03/GB sync) cresce além do fixo do Transfer Family. Para a maioria dos casos de SFTP de parceiros (alguns arquivos por dia), você fica muito longe desse limite.</p>

<p>O que o S3 Files <strong>não</strong> entrega de fábrica (e que serviços managed entregam):</p>
<ul>
  <li>Gerência centralizada de usuários e chaves SSH.</li>
  <li>Suporte a AS2 e FTPS.</li>
  <li>Operação zero-touch — você ainda tem uma EC2 pra patchar e monitorar.</li>
</ul>

<p>E onde ele entra como ferramenta nova no toolkit:</p>
<ul>
  <li>Pipelines em que o <strong>bucket precisa ser fonte da verdade</strong> (lifecycle, replicação, Object Lock).</li>
  <li>Workloads legadas que falam NFS mas o time já tem todo o ferramental S3 (Glue, Athena, Lambda).</li>
  <li>Substituir EFS em cenários de mais escrita-uma-vez/leitura-muitas, onde o custo do S3 ($0,023/GB) supera o do EFS ($0,30/GB).</li>
</ul>

<h2 id="referências">Referências</h2>

<ul>
  <li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html">Prerequisites for S3 Files</a> — IAM, SG, botocore e versionamento (fonte dos aprendizados #1, #3 e #4)</li>
  <li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-getting-started.html">Tutorial: Getting started with S3 Files</a> — onde finalmente aparece <code class="language-plaintext highlighter-rouge">mount -t s3files</code></li>
  <li><a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3files-filesystem.html">AWS::S3Files::FileSystem CFN</a> — propriedades do recurso</li>
  <li><a href="https://dev.to/aws-builders/aws-s3-files-just-made-transfer-family-sftp-obsolete-for-most-use-cases-4me">AWS S3 Files just made Transfer Family SFTP obsolete</a> — post original que motivou o experimento</li>
</ul>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="aws" /><category term="s3" /><category term="s3-files" /><category term="sftp" /><category term="cdk" /><category term="infrastructure" /><summary type="html"><![CDATA[O S3 Files (lançado em novembro/2025) é o novo NFS da AWS lastreado por bucket S3. Em vez de testar com um "hello world", escolhi um caso útil de primeira: SFTP em EC2 com `/home/sftp` montado via S3 Files, com dados caindo direto no bucket. No caminho, quatro aprendizados que a documentação não destaca: principal IAM peculiar, mount com tipo errado, dependência implícita do botocore e versionamento que muda o significado de delete.]]></summary></entry><entry xml:lang="pt"><title type="html">Gerenciando Secrets com SOPS: KMS, GCP e GPG</title><link href="https://www.academic.djack.dev/posts/2026/05/gerenciando-secrets-sops/" rel="alternate" type="text/html" title="Gerenciando Secrets com SOPS: KMS, GCP e GPG" /><published>2026-05-09T00:00:00-03:00</published><updated>2026-05-09T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2026/05/gerenciando-secrets-com-sops-kms</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2026/05/gerenciando-secrets-sops/"><![CDATA[<h1 id="gerenciando-secrets-com-sops-kms-gcp-e-gpg">Gerenciando Secrets com SOPS: KMS, GCP e GPG</h1>

<p>Sexta-feira, 17h. Você termina aquela feature, roda um <code class="language-plaintext highlighter-rouge">git add .</code> satisfeito, commit, push. Vai pegar um cafe. No caminho de volta pro computador, aquele frio na barriga: “espera… o <code class="language-plaintext highlighter-rouge">.env</code> estava no staged?”. Abre o GitHub, confere o commit e la esta — <code class="language-plaintext highlighter-rouge">AWS_SECRET_ACCESS_KEY</code> em texto plano, publicado para o mundo. O resto da sexta vira um incidente de seguranca: revogar chaves, rotacionar credenciais, avisar o time, rezar para ninguem ter visto.</p>

<p>Se voce ja viveu isso (ou vive com medo de viver), o <strong>SOPS</strong> e para voce. Ele permite commitar seus arquivos de secrets <strong>criptografados</strong> direto no repositorio. Sem medo. Sem incidente. Sem sexta-feira arruinada.</p>

<hr />

<h2 id="o-problema">O Problema</h2>

<p>Toda aplicacao tem secrets: tokens de API, credenciais de banco, chaves privadas. O desafio e como compartilha-las com o time sem:</p>

<ul>
  <li>Commitar <code class="language-plaintext highlighter-rouge">.env</code> em texto plano no repositorio</li>
  <li>Depender de alguem mandar credenciais por Slack/email</li>
  <li>Perder acesso quando alguem sai do time</li>
  <li>Manter um vault complexo para projetos pequenos/medios</li>
</ul>

<p>O <strong>SOPS</strong> (Secrets OPerationS) resolve isso: ele criptografa apenas os <strong>valores</strong> dos seus arquivos de secrets, mantendo as chaves legiveis. Combinado com um KMS (AWS, GCP, Azure) ou GPG, qualquer pessoa autorizada pode decriptar — sem compartilhar senha.</p>

<h2 id="tldr">TL;DR</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Instalar SOPS</span>
<span class="c"># https://github.com/getsops/sops/releases</span>

<span class="c"># Configurar a chave KMS</span>
<span class="nb">export </span><span class="nv">SOPS_KMS_ARN</span><span class="o">=</span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sua-chave"</span>

<span class="c"># Criptografar um arquivo existente</span>
sops encrypt .env <span class="o">&gt;</span> .env.enc

<span class="c"># Editar secrets (decripta, abre editor, re-criptografa ao salvar)</span>
sops edit .env

<span class="c"># Usar secrets como variáveis de ambiente (sem arquivo decriptado em disco)</span>
sops exec-env .env <span class="s2">"helm install ..."</span>
</code></pre></div></div>

<hr />

<h2 id="indice">Indice</h2>

<ul>
  <li><a href="#o-que-é-sops">O que e SOPS</a></li>
  <li><a href="#instalação">Instalacao</a></li>
  <li><a href="#configuração-inicial">Configuracao Inicial</a></li>
  <li><a href="#workflow-no-dia-a-dia">Workflow no Dia a Dia</a></li>
  <li><a href="#formatos-suportados">Formatos Suportados</a></li>
  <li><a href="#backends-de-criptografia">Backends de Criptografia</a></li>
  <li><a href="#usando-com-helm-e-kubernetes">Usando com Helm e Kubernetes</a></li>
  <li><a href="#boas-práticas">Boas Praticas</a></li>
  <li><a href="#conclusão">Conclusao</a></li>
</ul>

<hr />

<h2 id="o-que-é-sops">O que é SOPS</h2>

<p><a href="https://github.com/getsops/sops">SOPS</a> é uma ferramenta da Mozilla (agora mantida pela CNCF) que criptografa arquivos de configuração. O diferencial:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Arquivo original (.env)</span>
<span class="nv">GITHUB_PAT</span><span class="o">=</span>ghp_abc123xyz789
<span class="nv">AWS_SECRET_KEY</span><span class="o">=</span>wJalrXUtnFEMI/K7MDENG/bPxRfiCY
<span class="nv">DB_PASSWORD</span><span class="o">=</span>super-secret-123
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Após criptografar com SOPS</span>
<span class="nv">GITHUB_PAT</span><span class="o">=</span>ENC[AES256_GCM,data:k8vM2n...,type:str]
<span class="nv">AWS_SECRET_KEY</span><span class="o">=</span>ENC[AES256_GCM,data:pQ7xR...,type:str]
<span class="nv">DB_PASSWORD</span><span class="o">=</span>ENC[AES256_GCM,data:mN3kL...,type:str]
</code></pre></div></div>

<p>As <strong>chaves</strong> (<code class="language-plaintext highlighter-rouge">GITHUB_PAT</code>, <code class="language-plaintext highlighter-rouge">AWS_SECRET_KEY</code>) continuam legíveis — você sabe o que o arquivo contém sem decriptar. Apenas os <strong>valores</strong> são criptografados.</p>

<p>Isso significa que:</p>
<ul>
  <li>O arquivo pode ser commitado no repositório com segurança</li>
  <li>Code review consegue ver quais secrets foram adicionadas/removidas</li>
  <li><code class="language-plaintext highlighter-rouge">git diff</code> mostra mudanças estruturais sem expor valores</li>
</ul>

<hr />

<h2 id="por-que-usar-um-kms-key-management-service">Por que usar um KMS (Key Management Service)</h2>

<p>O SOPS suporta vários backends. Os cloud KMS (AWS, GCP, Azure) compartilham vantagens sobre GPG/age:</p>

<ul>
  <li><strong>Controle via IAM</strong> — quem pode decriptar é controlado por políticas do cloud provider</li>
  <li><strong>Auditoria</strong> — cada uso da chave é registrado (CloudTrail, Cloud Audit Logs)</li>
  <li><strong>Rotação automática</strong> — o provider rotaciona a chave sem quebrar arquivos existentes</li>
  <li><strong>Sem segredo compartilhado</strong> — não precisa distribuir uma chave privada entre o time</li>
</ul>

<p>Se você não usa nenhum cloud provider, GPG e age funcionam perfeitamente — só exigem mais gestão manual de chaves.</p>

<hr />

<h2 id="instalação">Instalação</h2>

<h3 id="sops">SOPS</h3>

<p>Baixe o binário da <a href="https://github.com/getsops/sops/releases">página de releases</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Linux (amd64)</span>
curl <span class="nt">-LO</span> https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
<span class="nb">mv </span>sops-v3.9.4.linux.amd64 /usr/local/bin/sops
<span class="nb">chmod</span> +x /usr/local/bin/sops

<span class="c"># macOS (via Homebrew)</span>
brew <span class="nb">install </span>sops

<span class="c"># Verificar</span>
sops <span class="nt">--version</span>
</code></pre></div></div>

<h3 id="aws-cli">AWS CLI</h3>

<p>Você precisa do AWS CLI configurado com credenciais que tenham acesso à chave KMS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws sts get-caller-identity  <span class="c"># Verifica se está autenticado</span>
</code></pre></div></div>

<hr />

<h2 id="configuração-inicial">Configuração Inicial</h2>

<h3 id="1-criar-ou-identificar-a-chave-kms">1. Criar (ou identificar) a chave KMS</h3>

<p>Se você já tem uma chave KMS, pegue o ARN ou alias. Se não:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws kms create-key <span class="nt">--description</span> <span class="s2">"SOPS encryption key"</span>

<span class="c"># Criar um alias para facilitar</span>
aws kms create-alias <span class="se">\</span>
    <span class="nt">--alias-name</span> <span class="nb">alias</span>/sops-key <span class="se">\</span>
    <span class="nt">--target-key-id</span> &lt;key-id-retornado&gt;
</code></pre></div></div>

<h3 id="2-exportar-o-arn">2. Exportar o ARN</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SOPS_KMS_ARN</span><span class="o">=</span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
</code></pre></div></div>

<h3 id="3-criar-o-arquivo-sopsyaml-opcional-recomendado">3. Criar o arquivo <code class="language-plaintext highlighter-rouge">.sops.yaml</code> (opcional, recomendado)</h3>

<p>Na raiz do projeto, crie um <code class="language-plaintext highlighter-rouge">.sops.yaml</code> para não precisar passar o ARN toda vez:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">secrets\.ya?ml$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
</code></pre></div></div>

<p>Com isso, qualquer arquivo <code class="language-plaintext highlighter-rouge">.env</code> ou <code class="language-plaintext highlighter-rouge">secrets.yml</code> será automaticamente criptografado com a chave correta.</p>

<hr />

<h2 id="workflow-no-dia-a-dia">Workflow no Dia a Dia</h2>

<h3 id="criptografar-um-arquivo-pela-primeira-vez">Criptografar um arquivo pela primeira vez</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Se tem .sops.yaml configurado:</span>
sops encrypt .env <span class="o">&gt;</span> .env.enc
<span class="nb">mv</span> .env.enc .env

<span class="c"># Ou especificando a chave manualmente:</span>
sops encrypt <span class="nt">--kms</span> <span class="s2">"arn:aws:kms:..."</span> .env <span class="o">&gt;</span> .env.enc
</code></pre></div></div>

<h3 id="editar-secrets">Editar secrets</h3>

<p>O comando mais usado. Decripta, abre no editor, re-criptografa ao salvar:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops edit .env
</code></pre></div></div>

<p>Usa o editor definido em <code class="language-plaintext highlighter-rouge">$EDITOR</code> (vim, nano, code, etc).</p>

<h3 id="usar-secrets-sem-arquivo-em-disco">Usar secrets sem arquivo em disco</h3>

<p>O <code class="language-plaintext highlighter-rouge">exec-env</code> injeta as secrets como variáveis de ambiente em um subprocesso — nada fica em texto plano no disco:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Abre um shell com todas as variáveis disponíveis</span>
sops exec-env .env <span class="s2">"bash"</span>

<span class="c"># Executa um comando específico</span>
sops exec-env .env <span class="s2">"helm upgrade --set token=</span><span class="se">\$</span><span class="s2">GITHUB_PAT ..."</span>
</code></pre></div></div>

<h3 id="decriptar-para-stdout-debug">Decriptar para stdout (debug)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops decrypt .env
</code></pre></div></div>

<h3 id="ver-diff-entre-versões">Ver diff entre versões</h3>

<p>Como as chaves são legíveis, <code class="language-plaintext highlighter-rouge">git diff</code> funciona normalmente para mostrar quais secrets foram adicionadas ou removidas.</p>

<hr />

<h2 id="formatos-suportados">Formatos Suportados</h2>

<p>O SOPS funciona com múltiplos formatos:</p>

<table>
  <thead>
    <tr>
      <th>Formato</th>
      <th>Extensão</th>
      <th>Uso</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>dotenv</td>
      <td><code class="language-plaintext highlighter-rouge">.env</code></td>
      <td>Variáveis de ambiente</td>
    </tr>
    <tr>
      <td>YAML</td>
      <td><code class="language-plaintext highlighter-rouge">.yml</code>, <code class="language-plaintext highlighter-rouge">.yaml</code></td>
      <td>Helm values, configs K8s</td>
    </tr>
    <tr>
      <td>JSON</td>
      <td><code class="language-plaintext highlighter-rouge">.json</code></td>
      <td>Configs de app</td>
    </tr>
    <tr>
      <td>INI</td>
      <td><code class="language-plaintext highlighter-rouge">.ini</code></td>
      <td>Configs legadas</td>
    </tr>
    <tr>
      <td>Binary</td>
      <td>qualquer</td>
      <td>Arquivos inteiros (criptografa tudo)</td>
    </tr>
  </tbody>
</table>

<h3 id="exemplo-com-yaml">Exemplo com YAML</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># secrets.yml (após SOPS encrypt)</span>
<span class="na">database</span><span class="pi">:</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">ENC[AES256_GCM,data:mQ2x...,type:str]</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">ENC[AES256_GCM,data:k9Lp...,type:str]</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">5432</span>  <span class="c1"># valores não-sensíveis podem ser excluídos da criptografia</span>
<span class="na">sops</span><span class="pi">:</span>
    <span class="na">kms</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">arn</span><span class="pi">:</span> <span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">3.9.4</span>
</code></pre></div></div>

<h3 id="criptografar-apenas-alguns-campos">Criptografar apenas alguns campos</h3>

<p>Use <code class="language-plaintext highlighter-rouge">--encrypted-regex</code> para criptografar apenas campos que contenham “password”, “secret”, “token”, etc:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops encrypt <span class="nt">--encrypted-regex</span> <span class="s1">'^(password|secret|token)$'</span> config.yml
</code></pre></div></div>

<hr />

<h2 id="usando-com-helm-e-kubernetes">Usando com Helm e Kubernetes</h2>

<p>O caso de uso mais comum: passar secrets para <code class="language-plaintext highlighter-rouge">helm install</code> sem expô-las.</p>

<h3 id="padrão-com-exec-env">Padrão com <code class="language-plaintext highlighter-rouge">exec-env</code></h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .env criptografado contém GITHUB_PAT=ghp_xxx</span>
sops exec-env .env <span class="s2">"helm install runner </span><span class="se">\</span><span class="s2">
    --set githubConfigSecret.github_token=</span><span class="se">\$</span><span class="s2">GITHUB_PAT </span><span class="se">\</span><span class="s2">
    --namespace arc-runners </span><span class="se">\</span><span class="s2">
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set"</span>
</code></pre></div></div>

<h3 id="padrão-com-script">Padrão com script</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># update-runners.sh — executar via: sops exec-env .env "./update-runners.sh"</span>

helm upgrade <span class="nt">--install</span> runner <span class="se">\</span>
    <span class="nt">--namespace</span> arc-runners <span class="se">\</span>
    <span class="nt">--set</span> githubConfigSecret.github_token<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_PAT</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops exec-env .env <span class="s2">"./update-runners.sh"</span>
</code></pre></div></div>

<h3 id="valores-sensíveis-em-um-values-file">Valores sensíveis em um values file</h3>

<p>Se preferir manter tudo em YAML:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Criar values-secrets.yml com tokens</span>
sops edit values-secrets.yml

<span class="c"># Usar decriptado inline com helm</span>
sops decrypt values-secrets.yml | helm <span class="nb">install </span>runner <span class="se">\</span>
    <span class="nt">--namespace</span> arc-runners <span class="se">\</span>
    <span class="nt">-f</span> - <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
</code></pre></div></div>

<hr />

<h2 id="boas-práticas">Boas Práticas</h2>

<h3 id="1-commite-o-arquivo-criptografado">1. Commite o arquivo criptografado</h3>

<pre><code class="language-gitignore"># .gitignore
# NÃO ignore o .env se ele está criptografado com SOPS
# Ignore apenas se for texto plano:
# .env
</code></pre>

<p>O ponto central do SOPS é poder versionar secrets com segurança. Se o arquivo está criptografado, ele <strong>deve</strong> estar no repositório.</p>

<h3 id="2-use-sopsyaml-no-projeto">2. Use <code class="language-plaintext highlighter-rouge">.sops.yaml</code> no projeto</h3>

<p>Evita que alguém esqueça de especificar a chave e criptografe com o backend errado.</p>

<h3 id="3-controle-acesso-via-iam">3. Controle acesso via IAM</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"kms:Decrypt"</span><span class="p">,</span><span class="w"> </span><span class="s2">"kms:DescribeKey"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:key/key-id"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Apenas quem precisa decriptar recebe <code class="language-plaintext highlighter-rouge">kms:Decrypt</code>. Outros podem ver a estrutura do arquivo sem acessar os valores.</p>

<h3 id="4-nunca-decripte-para-um-arquivo">4. Nunca decripte para um arquivo</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ruim — cria arquivo em texto plano no disco</span>
sops decrypt .env <span class="o">&gt;</span> .env.plain

<span class="c"># Bom — usa em memória apenas</span>
sops exec-env .env <span class="s2">"comando"</span>
</code></pre></div></div>

<h3 id="5-adicione-múltiplas-chaves-kms">5. Adicione múltiplas chaves KMS</h3>

<p>Para redundância ou acesso cross-account:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key,arn:aws:kms:us-east-1:yyyyyyyyyyyy:alias/sops-backup"</span>
</code></pre></div></div>

<p>Qualquer uma das chaves pode decriptar o arquivo.</p>

<hr />

<h2 id="backends-de-criptografia">Backends de Criptografia</h2>

<p>O SOPS não é exclusivo da AWS. Ele suporta múltiplos backends — escolha o que faz sentido para sua infraestrutura:</p>

<h3 id="aws-kms">AWS KMS</h3>

<p>Ideal se seu time já está na AWS. Controle de acesso via IAM, auditoria via CloudTrail.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SOPS_KMS_ARN</span><span class="o">=</span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
sops encrypt .env
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
</code></pre></div></div>

<h3 id="gcp-cloud-kms">GCP Cloud KMS</h3>

<p>Mesmo conceito, para times no Google Cloud. Controle via IAM do GCP.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Criar keyring e chave (uma vez)</span>
gcloud kms keyrings create sops <span class="nt">--location</span> global
gcloud kms keys create sops-key <span class="se">\</span>
    <span class="nt">--location</span> global <span class="se">\</span>
    <span class="nt">--keyring</span> sops <span class="se">\</span>
    <span class="nt">--purpose</span> encryption

<span class="c"># Criptografar</span>
sops encrypt <span class="se">\</span>
    <span class="nt">--gcp-kms</span> <span class="s2">"projects/meu-projeto/locations/global/keyRings/sops/cryptoKeys/sops-key"</span> <span class="se">\</span>
    .env
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">gcp_kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">projects/meu-projeto/locations/global/keyRings/sops/cryptoKeys/sops-key"</span>
</code></pre></div></div>

<p>Para decriptar, basta ter <code class="language-plaintext highlighter-rouge">gcloud auth application-default login</code> configurado com permissão <code class="language-plaintext highlighter-rouge">cloudkms.cryptoKeyVersions.useToDecrypt</code>.</p>

<h3 id="azure-key-vault">Azure Key Vault</h3>

<p>Para times no Azure:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">azure_keyvault</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://meu-vault.vault.azure.net/keys/sops-key/version-id"</span>
</code></pre></div></div>

<h3 id="gpgpgp">GPG/PGP</h3>

<p>Funciona sem nenhum cloud provider. Cada membro do time tem sua chave GPG, e você criptografa para múltiplos destinatários:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Listar chaves disponíveis</span>
gpg <span class="nt">--list-keys</span>

<span class="c"># Criptografar para um ou mais fingerprints</span>
sops encrypt <span class="se">\</span>
    <span class="nt">--pgp</span> <span class="s2">"FP_PESSOA_1,FP_PESSOA_2,FP_PESSOA_3"</span> <span class="se">\</span>
    .env
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">pgp</span><span class="pi">:</span> <span class="s2">"</span><span class="s">FP_PESSOA_1,FP_PESSOA_2,FP_PESSOA_3"</span>
</code></pre></div></div>

<p>Quando alguém entra ou sai do time, você adiciona/remove o fingerprint e re-criptografa:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Adicionar novo membro</span>
sops updatekeys .env
</code></pre></div></div>

<p>Vantagem: zero dependência de cloud. Desvantagem: gestão manual de chaves e distribuição.</p>

<h3 id="age-moderno-simples">age (moderno, simples)</h3>

<p>Substituto moderno do GPG, mais simples de usar:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Gerar chave</span>
age-keygen <span class="nt">-o</span> key.txt
<span class="c"># Output: public key: age1xxxxxxx...</span>

<span class="c"># Criptografar</span>
sops encrypt <span class="nt">--age</span> <span class="s2">"age1xxxxxxx..."</span> .env

<span class="c"># Decriptar</span>
<span class="nb">export </span><span class="nv">SOPS_AGE_KEY_FILE</span><span class="o">=</span>key.txt
sops decrypt .env
</code></pre></div></div>

<p>Recomendado para projetos pessoais ou times pequenos que não querem a complexidade do GPG.</p>

<h3 id="múltiplos-backends-simultaneamente">Múltiplos backends simultaneamente</h3>

<p>Você pode combinar backends — útil para times distribuídos entre clouds ou para redundância:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml — qualquer uma das chaves pode decriptar</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
    <span class="na">gcp_kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">projects/meu-projeto/locations/global/keyRings/sops/cryptoKeys/sops-key"</span>
    <span class="na">pgp</span><span class="pi">:</span> <span class="s2">"</span><span class="s">FP_DO_ADMIN"</span>
</code></pre></div></div>

<p>Isso garante que se um provider ficar indisponível, você ainda tem acesso via outro backend.</p>

<h3 id="qual-escolher">Qual escolher?</h3>

<table>
  <thead>
    <tr>
      <th>Backend</th>
      <th>Quando usar</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AWS KMS</td>
      <td>Time na AWS, controle de acesso via IAM</td>
    </tr>
    <tr>
      <td>GCP Cloud KMS</td>
      <td>Time no GCP, mesmo modelo de IAM</td>
    </tr>
    <tr>
      <td>Azure Key Vault</td>
      <td>Time no Azure</td>
    </tr>
    <tr>
      <td>GPG/PGP</td>
      <td>Sem cloud, time com chaves GPG existentes</td>
    </tr>
    <tr>
      <td>age</td>
      <td>Projetos pessoais, simplicidade máxima</td>
    </tr>
    <tr>
      <td>Múltiplos</td>
      <td>Times multi-cloud ou para redundância</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="conclusão">Conclusão</h2>

<p>O SOPS com KMS resolve o problema de secrets com um workflow simples:</p>

<ol>
  <li><strong>Criptografe</strong> — <code class="language-plaintext highlighter-rouge">sops encrypt .env</code></li>
  <li><strong>Commite</strong> — o arquivo criptografado vai para o repositório</li>
  <li><strong>Edite</strong> — <code class="language-plaintext highlighter-rouge">sops edit .env</code> quando precisar alterar</li>
  <li><strong>Use</strong> — <code class="language-plaintext highlighter-rouge">sops exec-env .env "comando"</code> para injetar sem expor</li>
</ol>

<p>Sem servidores extras, sem vault para manter, sem senhas compartilhadas. Quem tem acesso IAM à chave KMS consegue trabalhar. Quem não tem, vê apenas valores criptografados.</p>

<p>Para projetos que já estão na AWS, é a forma mais simples de sair do <code class="language-plaintext highlighter-rouge">.env</code> em texto plano para algo seguro e auditável.</p>

<hr />

<h2 id="referências">Referências</h2>

<ul>
  <li><a href="https://github.com/getsops/sops">SOPS - GitHub</a></li>
  <li><a href="https://getsops.io/">SOPS Documentation</a></li>
  <li><a href="https://docs.aws.amazon.com/kms/latest/developerguide/">AWS KMS Developer Guide</a></li>
  <li><a href="https://github.com/FiloSottile/age">age encryption</a></li>
</ul>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="devops" /><category term="seguranca" /><category term="aws" /><category term="gcp" /><category term="sops" /><category term="secrets" /><category term="gpg" /><summary type="html"><![CDATA[Aprenda a criptografar e gerenciar secrets de projetos usando SOPS. Suporta AWS KMS, GCP Cloud KMS, Azure Key Vault, age e GPG — escolha o backend que faz sentido para seu time.]]></summary></entry><entry xml:lang="en"><title type="html">GitHub Actions Self-Hosted Runners on EKS with ARC</title><link href="https://www.academic.djack.dev/en/posts/2026/05/github-actions-self-hosted-runners-eks/" rel="alternate" type="text/html" title="GitHub Actions Self-Hosted Runners on EKS with ARC" /><published>2026-05-09T00:00:00-03:00</published><updated>2026-05-09T00:00:00-03:00</updated><id>https://www.academic.djack.dev/en/posts/2026/05/github-actions-self-hosted-runners-eks-en</id><content type="html" xml:base="https://www.academic.djack.dev/en/posts/2026/05/github-actions-self-hosted-runners-eks/"><![CDATA[<h1 id="github-actions-self-hosted-runners-on-eks-with-arc">GitHub Actions Self-Hosted Runners on EKS with ARC</h1>

<h2 id="the-problem">The Problem</h2>

<p>GitHub Actions managed runners work fine for small projects, but in production scenarios you hit limitations:</p>

<ul>
  <li><strong>Fixed hardware</strong> — standard Linux runners offer only 4 vCPUs and 16GB RAM. If your workflow builds heavy images, runs integration tests in parallel or needs more memory for compilation, you’re stuck with what GitHub offers</li>
  <li><strong>High cost</strong> with execution minutes on private repositories</li>
  <li><strong>Lack of control</strong> over the environment (tool versions, internal dependencies)</li>
  <li><strong>Latency</strong> when pulling heavy images from private registries</li>
  <li><strong>Security</strong> — jobs running on shared infrastructure without access to your VPC</li>
</ul>

<p>With self-hosted runners on EKS, <strong>you choose the machine</strong>. Need CPU for compilation? Use <code class="language-plaintext highlighter-rouge">c6i.2xlarge</code>. Memory-heavy workflow? Use <code class="language-plaintext highlighter-rouge">r6i.xlarge</code>. And with node groups separated by workload type, each pipeline runs on ideal hardware without paying for idle resources.</p>

<p>The solution: run your own runners inside the Kubernetes cluster on AWS, with autoscaling based on real job demand.</p>

<h2 id="tldr-architecture-summary">TL;DR (Architecture Summary)</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GitHub Actions (webhook) → ARC Controller → Scale Set → Runner Pods (EKS)
                                                              ↓
                                                    Custom image (ECR)
                                                              ↓
                                                    CronJob renews ECR credentials every 5h
</code></pre></div></div>

<p><strong>Stack:</strong></p>
<ul>
  <li><strong>EKS</strong> — Kubernetes cluster on AWS</li>
  <li><strong>ARC</strong> — Actions Runner Controller (native autoscaling)</li>
  <li><strong>ECR</strong> — Private registry for custom runner image</li>
  <li><strong>Helm</strong> — release management</li>
  <li><strong>SOPS</strong> — secrets encryption with KMS</li>
</ul>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#prerequisites">Prerequisites</a></li>
  <li><a href="#architecture">Architecture</a></li>
  <li><a href="#step-1-custom-runner-image">Step 1: Custom Runner Image</a></li>
  <li><a href="#step-2-push-to-ecr">Step 2: Push to ECR</a></li>
  <li><a href="#step-3-install-the-arc-controller">Step 3: Install the ARC Controller</a></li>
  <li><a href="#step-4-configure-the-runner-scale-set">Step 4: Configure the Runner Scale Set</a></li>
  <li><a href="#step-5-automatic-ecr-credential-renewal">Step 5: Automatic ECR Credential Renewal</a></li>
  <li><a href="#step-6-automation-with-script">Step 6: Automation with Script</a></li>
  <li><a href="#using-the-runners-in-workflows">Using the Runners in Workflows</a></li>
  <li><a href="#troubleshooting">Troubleshooting</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<p>Before starting, you need:</p>

<ul>
  <li><strong>EKS</strong> cluster running with <code class="language-plaintext highlighter-rouge">kubectl</code> configured</li>
  <li><strong>Helm 3</strong> installed</li>
  <li><strong>AWS CLI</strong> authenticated with ECR permissions</li>
  <li><strong>GitHub PAT</strong> (Personal Access Token) with <code class="language-plaintext highlighter-rouge">admin:org</code> or <code class="language-plaintext highlighter-rouge">repo</code> scope</li>
  <li><strong>SOPS</strong> configured with KMS to manage secrets (optional, but recommended)</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verify cluster connection</span>
kubectl get nodes

<span class="c"># If connection error, update kubeconfig</span>
aws eks update-kubeconfig <span class="nt">--region</span> us-east-1 <span class="nt">--name</span> your-eks-cluster
</code></pre></div></div>

<hr />

<h2 id="architecture">Architecture</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────┐
│                        AWS (EKS)                        │
│                                                         │
│  ┌──────────────────┐    ┌───────────────────────────┐  │
│  │  arc-systems ns  │    │     arc-runners ns        │  │
│  │                  │    │                           │  │
│  │  ARC Controller  │───▶│  Runner Scale Set        │  │
│  │  (manages pods)  │    │  ├─ runner-repo-1        │  │
│  │                  │    │  ├─ runner-repo-2        │  │
│  └──────────────────┘    │  └─ runner-org           │  │
│                          │                           │  │
│                          │  CronJob ECR (5h)        │  │
│                          │  (renews docker secret)   │  │
│                          └───────────────────────────┘  │
│                                                         │
│  ┌──────────────────┐                                   │
│  │       ECR        │                                   │
│  │ my-app-github-     │◀── Custom image                   │
│  │ action:latest    │    (tools + dependencies)         │
│  └──────────────────┘                                   │
└─────────────────────────────────────────────────────────┘
         ▲
         │ webhooks (job queued/completed)
         │
┌────────┴────────┐
│  GitHub Actions  │
│  (your repos)    │
└─────────────────┘
</code></pre></div></div>

<p>The flow works like this:</p>

<ol>
  <li>A workflow is triggered on GitHub</li>
  <li>GitHub sends a webhook to the ARC Controller</li>
  <li>The Controller scales the Runner Scale Set (creates pods)</li>
  <li>The pod runs the job using the custom ECR image</li>
  <li>When finished, the pod is destroyed (scale to zero)</li>
</ol>

<hr />

<h2 id="step-1-custom-runner-image">Step 1: Custom Runner Image</h2>

<p>The base GitHub Actions Runner image is minimal. To run your pipelines, you probably need additional tools.</p>

<h3 id="dockerfile">Dockerfile</h3>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ghcr.io/actions/actions-runner:latest</span>
<span class="k">USER</span><span class="s"> root</span>

<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\
</span>    git gcc make wget curl jq netcat-openbsd

<span class="k">RUN </span><span class="nb">chown </span>root:runner <span class="nt">-R</span> /opt/ <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+w /opt

<span class="c"># Install the same toolset used in GitHub hosted runners (Ubuntu 24.04)</span>
<span class="k">RUN </span>wget https://raw.githubusercontent.com/actions/runner-images/main/images/ubuntu/toolsets/toolset-2404.json
<span class="k">RUN </span><span class="nv">APT_PACKAGES</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat </span>toolset-2404.json | jq <span class="nt">-r</span> <span class="se">\
</span>    <span class="s1">'.apt | [.vital_packages[], .common_packages[], .cmd_packages[]] | del(.[] | select(. == "lib32z1" or . == "netcat")) | join(" ")'</span><span class="si">)</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="nt">--no-install-recommends</span> <span class="k">${</span><span class="nv">APT_PACKAGES</span><span class="k">}</span>

<span class="k">USER</span><span class="s"> runner</span>
</code></pre></div></div>

<p>The strategy here is to reuse the <strong>official toolset</strong> from GitHub for Ubuntu 24.04 runners. This ensures compatibility with most Actions that expect pre-installed tools (like <code class="language-plaintext highlighter-rouge">zip</code>, <code class="language-plaintext highlighter-rouge">unzip</code>, <code class="language-plaintext highlighter-rouge">python3</code>, etc).</p>

<h3 id="local-build-to-test">Local build to test</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> my-github-runner <span class="nt">--pull</span> <span class="nt">--no-cache</span> <span class="nb">.</span>
</code></pre></div></div>

<hr />

<h2 id="step-2-push-to-ecr">Step 2: Push to ECR</h2>

<p>Publish the image to your private registry:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Variables</span>
<span class="nv">ECR_REGISTRY</span><span class="o">=</span><span class="s2">"xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com"</span>
<span class="nv">ECR_REPOSITORY</span><span class="o">=</span><span class="s2">"my-github-runner"</span>

<span class="c"># Authenticate with ECR</span>
aws ecr get-login-password <span class="nt">--region</span> us-east-1 | <span class="se">\</span>
    docker login <span class="nt">--username</span> AWS <span class="nt">--password-stdin</span> <span class="nv">$ECR_REGISTRY</span>

<span class="c"># Tag and push</span>
docker tag my-github-runner:latest <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:latest
docker push <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:latest
</code></pre></div></div>

<p>If the ECR repository doesn’t exist yet:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws ecr create-repository <span class="se">\</span>
    <span class="nt">--repository-name</span> my-github-runner <span class="se">\</span>
    <span class="nt">--region</span> us-east-1
</code></pre></div></div>

<hr />

<h2 id="step-3-install-the-arc-controller">Step 3: Install the ARC Controller</h2>

<p>The ARC Controller is the central component that receives webhooks from GitHub and manages the runner pod lifecycle.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"arc-systems"</span>
<span class="nv">INSTALLATION_NAME</span><span class="o">=</span><span class="s2">"arc"</span>

helm <span class="nb">install</span> <span class="nv">$INSTALLATION_NAME</span> <span class="se">\</span>
    <span class="nt">--namespace</span> <span class="nv">$NAMESPACE</span> <span class="se">\</span>
    <span class="nt">--create-namespace</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
</code></pre></div></div>

<p>Verify the controller is running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> arc-systems
</code></pre></div></div>

<p>Expected output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME                                     READY   STATUS    RESTARTS   AGE
arc-gha-runner-scale-set-controller-xxx  1/1     Running   0          30s
</code></pre></div></div>

<hr />

<h2 id="step-4-configure-the-runner-scale-set">Step 4: Configure the Runner Scale Set</h2>

<p>This is where we configure the runners that will execute jobs. Each Scale Set can be associated with a repository or organization.</p>

<h3 id="values-file-valuesyml--relevant-parts">Values file (<code class="language-plaintext highlighter-rouge">values.yml</code>) — relevant parts</h3>

<p>The complete file with all available options is in the <a href="https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml">official chart documentation</a>. Here I highlight the essentials:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">githubConfigUrl</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://github.com/your-org/your-repo"</span>
<span class="na">githubConfigSecret</span><span class="pi">:</span>
  <span class="na">github_token</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>

<span class="na">maxRunners</span><span class="pi">:</span> <span class="m">10</span>
<span class="na">minRunners</span><span class="pi">:</span> <span class="m">0</span>

<span class="na">containerMode</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">dind"</span>
</code></pre></div></div>

<p>The most important part is the pod <code class="language-plaintext highlighter-rouge">template</code>, where you define image, node placement and ECR access:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">template</span><span class="pi">:</span>
  <span class="na">spec</span><span class="pi">:</span>
    <span class="na">activeDeadlineSeconds</span><span class="pi">:</span> <span class="m">3000</span>
    <span class="na">nodeSelector</span><span class="pi">:</span>
      <span class="na">intent</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-jobs"</span>
    <span class="na">tolerations</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-xlarge"</span>
        <span class="na">operator</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Equal"</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">effect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">NoSchedule"</span>
    <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">runner</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/my-github-runner:latest</span>
        <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">/home/runner/run.sh"</span><span class="pi">]</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">DOCKER_HOST</span>
            <span class="na">value</span><span class="pi">:</span> <span class="s">unix:///var/run/docker.sock</span>
    <span class="na">imagePullSecrets</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-credentials</span>
</code></pre></div></div>

<h3 id="key-configuration-points">Key configuration points</h3>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">containerMode: dind</code></td>
      <td>Docker-in-Docker — allows jobs to run <code class="language-plaintext highlighter-rouge">docker build</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">activeDeadlineSeconds: 3000</code></td>
      <td>Auto-kill stuck pods (50 min)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nodeSelector: ci-jobs</code></td>
      <td>Runs only on dedicated CI nodes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tolerations: ci-xlarge</code></td>
      <td>Allows using nodes with specific taint</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imagePullSecrets</code></td>
      <td>Uses ECR secret for image pull</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">minRunners: 0</code></td>
      <td>Scale to zero when there are no jobs</td>
    </tr>
  </tbody>
</table>

<h3 id="install-the-runner-scale-set">Install the Runner Scale Set</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">INSTALLATION_NAME</span><span class="o">=</span><span class="s2">"runner-your-repo"</span>
<span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"arc-runners"</span>
<span class="nv">GITHUB_CONFIG_URL</span><span class="o">=</span><span class="s2">"https://github.com/your-org/your-repo"</span>

helm <span class="nb">install</span> <span class="s2">"</span><span class="nv">$INSTALLATION_NAME</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--namespace</span> <span class="s2">"</span><span class="nv">$NAMESPACE</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--create-namespace</span> <span class="se">\</span>
    <span class="nt">--values</span> values.yml <span class="se">\</span>
    <span class="nt">--set</span> githubConfigSecret.github_token<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_PAT</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--set</span> <span class="nv">githubConfigUrl</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_CONFIG_URL</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
</code></pre></div></div>

<blockquote>
  <p><strong>Tip:</strong> To avoid leaving <code class="language-plaintext highlighter-rouge">GITHUB_PAT</code> in plain text, use <a href="/en/posts/2026/05/managing-secrets-sops/">SOPS</a> with AWS KMS to encrypt your secret files.</p>
</blockquote>

<hr />

<h2 id="step-5-automatic-ecr-credential-renewal">Step 5: Automatic ECR Credential Renewal</h2>

<p>ECR tokens expire every <strong>12 hours</strong>. Without automatic renewal, your runners will fail when trying to pull the image.</p>

<p>The solution is a CronJob that runs every 5 hours and recreates the <code class="language-plaintext highlighter-rouge">docker-registry</code> secret:</p>

<h3 id="the-cronjob--the-central-piece">The CronJob — the central piece</h3>

<p>The job uses <code class="language-plaintext highlighter-rouge">alpine/k8s</code> (which already has <code class="language-plaintext highlighter-rouge">aws</code> CLI and <code class="language-plaintext highlighter-rouge">kubectl</code>) to obtain a new token and recreate the secret:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">batch/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">CronJob</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">arc-runners</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">*/5</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span>
  <span class="na">jobTemplate</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">template</span><span class="pi">:</span>
        <span class="na">spec</span><span class="pi">:</span>
          <span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">sa-health-check</span>
          <span class="na">containers</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper</span>
            <span class="na">image</span><span class="pi">:</span> <span class="s">alpine/k8s:1.27.15</span>
            <span class="na">envFrom</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="na">secretRef</span><span class="pi">:</span>
                  <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper-secrets</span>
              <span class="pi">-</span> <span class="na">configMapRef</span><span class="pi">:</span>
                  <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper-cm</span>
            <span class="na">command</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="s">/bin/bash</span>
              <span class="pi">-</span> <span class="s">-c</span>
              <span class="pi">-</span> <span class="pi">|-</span>
                <span class="s">ECR_TOKEN=$(aws ecr get-login-password --region ${AWS_REGION})</span>
                <span class="s">kubectl delete secret --ignore-not-found $DOCKER_SECRET_NAME -n arc-runners</span>
                <span class="s">kubectl create secret docker-registry $DOCKER_SECRET_NAME \</span>
                  <span class="s">--docker-server=https://${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com \</span>
                  <span class="s">--docker-username=AWS \</span>
                  <span class="s">--docker-password="${ECR_TOKEN}" \</span>
                  <span class="s">--namespace=arc-runners</span>
          <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Never</span>
</code></pre></div></div>

<p>The CronJob needs a <code class="language-plaintext highlighter-rouge">ServiceAccount</code> with minimal permission to delete and create the specific secret:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Role</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">arc-runners</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">role-ecr-secret-renewal</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
  <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">secrets"</span><span class="pi">]</span>
  <span class="na">resourceNames</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">ecr-registry-credentials"</span><span class="pi">]</span>
  <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">delete"</span><span class="pi">]</span>
<span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
  <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">secrets"</span><span class="pi">]</span>
  <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">create"</span><span class="pi">]</span>
</code></pre></div></div>

<p>AWS credentials are stored in a separate <code class="language-plaintext highlighter-rouge">Secret</code> (<code class="language-plaintext highlighter-rouge">ecr-registry-helper-secrets</code>) with <code class="language-plaintext highlighter-rouge">AWS_ACCESS_KEY_ID</code>, <code class="language-plaintext highlighter-rouge">AWS_SECRET_ACCESS_KEY</code> and <code class="language-plaintext highlighter-rouge">AWS_ACCOUNT</code>.</p>

<h3 id="apply-and-verify">Apply and verify</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Apply all resources</span>
kubectl apply <span class="nt">-f</span> cronjob.yaml

<span class="c"># Verify the CronJob</span>
kubectl get cronjob <span class="nt">-n</span> arc-runners

<span class="c"># Test manually (without waiting for the schedule)</span>
kubectl create job <span class="nt">--from</span><span class="o">=</span>cronjob/ecr-registry-helper ecr-test <span class="nt">-n</span> arc-runners

<span class="c"># View logs</span>
kubectl logs <span class="nt">-n</span> arc-runners <span class="nt">-l</span> job-name<span class="o">=</span>ecr-test <span class="nt">-f</span>
</code></pre></div></div>

<h3 id="why-the-rbac-is-minimal">Why the RBAC is minimal</h3>

<p>The Role grants only <code class="language-plaintext highlighter-rouge">delete</code> on the specific secret <code class="language-plaintext highlighter-rouge">ecr-registry-credentials</code> and generic <code class="language-plaintext highlighter-rouge">create</code> — the bare minimum needed for the delete/create cycle. No extra permissions.</p>

<hr />

<h2 id="step-6-automation-with-script">Step 6: Automation with Script</h2>

<p>When you have multiple runners (one per repository), manually updating each one is impractical. This script automates the entire flow:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">set</span> <span class="nt">-e</span>

<span class="nv">ECR_REGISTRY</span><span class="o">=</span><span class="s2">"xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com"</span>
<span class="nv">ECR_REPOSITORY</span><span class="o">=</span><span class="s2">"my-github-runner"</span>
<span class="nv">IMAGE_TAG</span><span class="o">=</span><span class="s2">"latest"</span>
<span class="nv">AWS_REGION</span><span class="o">=</span><span class="s2">"us-east-1"</span>
<span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"arc-runners"</span>

<span class="c"># 1. Authenticate with ECR</span>
<span class="nb">echo</span> <span class="s2">"Authenticating with ECR..."</span>
aws ecr get-login-password <span class="nt">--region</span> <span class="nv">$AWS_REGION</span> | <span class="se">\</span>
    docker login <span class="nt">--username</span> AWS <span class="nt">--password-stdin</span> <span class="nv">$ECR_REGISTRY</span>

<span class="c"># 2. Build and push image</span>
<span class="nb">echo</span> <span class="s2">"Building Docker image..."</span>
docker build <span class="nt">-t</span> my-github-runner <span class="nt">--pull</span> <span class="nt">--no-cache</span> <span class="nb">.</span>
docker tag my-github-runner:latest <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:<span class="nv">$IMAGE_TAG</span>
docker push <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:<span class="nv">$IMAGE_TAG</span>

<span class="c"># 3. Update ARC controller</span>
<span class="nb">echo</span> <span class="s2">"Updating ARC controller..."</span>
helm upgrade <span class="nt">--install</span> arc <span class="se">\</span>
    <span class="nt">--namespace</span> arc-systems <span class="se">\</span>
    <span class="nt">--create-namespace</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

<span class="c"># 4. Update all runners</span>
<span class="nv">RELEASES</span><span class="o">=</span><span class="si">$(</span>helm list <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$NAMESPACE</span><span class="s2">"</span> <span class="nt">--short</span><span class="si">)</span>

<span class="k">for </span>RELEASE <span class="k">in</span> <span class="nv">$RELEASES</span><span class="p">;</span> <span class="k">do
    </span><span class="nb">echo</span> <span class="s2">"  → Updating release: </span><span class="nv">$RELEASE</span><span class="s2">"</span>
    helm upgrade <span class="nt">--install</span> <span class="s2">"</span><span class="nv">$RELEASE</span><span class="s2">"</span> <span class="se">\</span>
        <span class="nt">--namespace</span> <span class="s2">"</span><span class="nv">$NAMESPACE</span><span class="s2">"</span> <span class="se">\</span>
        <span class="nt">--reuse-values</span> <span class="se">\</span>
        <span class="nt">--set</span> githubConfigSecret.github_token<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_PAT</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
        oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
<span class="k">done</span>

<span class="c"># 5. Summary</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"Summary:"</span>
helm list <span class="nt">-n</span> arc-runners <span class="nt">-o</span> json | <span class="se">\</span>
    jq <span class="nt">-r</span> <span class="s1">'["NAME","REVISION","APP_VERSION"], (.[] | [.name, (.revision|tostring), .app_version]) | @tsv'</span> | <span class="se">\</span>
    column <span class="nt">-t</span>
</code></pre></div></div>

<p>Execute with secrets via SOPS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops exec-env .env <span class="s2">"./update-all-runners.sh"</span>
</code></pre></div></div>

<hr />

<h2 id="using-the-runners-in-workflows">Using the Runners in Workflows</h2>

<p>After everything is configured, usage is simple. In your workflow, reference the runner scale set name:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/ci.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CI</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">runner-your-repo</span>  <span class="c1"># helm release name</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">docker build -t app .</span>
          <span class="s">docker run app npm test</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">runs-on</code> must match the Helm installation name (the <code class="language-plaintext highlighter-rouge">INSTALLATION_NAME</code> used in <code class="language-plaintext highlighter-rouge">helm install</code>).</p>

<h3 id="runner-per-organization-vs-per-repository">Runner per organization vs per repository</h3>

<table>
  <thead>
    <tr>
      <th>Scope</th>
      <th><code class="language-plaintext highlighter-rouge">githubConfigUrl</code></th>
      <th>Use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Repository</td>
      <td><code class="language-plaintext highlighter-rouge">https://github.com/org/repo</code></td>
      <td>Jobs only from this repo</td>
    </tr>
    <tr>
      <td>Organization</td>
      <td><code class="language-plaintext highlighter-rouge">https://github.com/org</code></td>
      <td>Any repo in the org can use it</td>
    </tr>
  </tbody>
</table>

<p>For organizations, the PAT needs the <code class="language-plaintext highlighter-rouge">admin:org</code> scope.</p>

<hr />

<h2 id="troubleshooting">Troubleshooting</h2>

<h3 id="error-kubernetes-cluster-unreachable">Error: <code class="language-plaintext highlighter-rouge">kubernetes cluster unreachable</code></h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error: kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp 127.0.0.1:8080: connect: connection refused
</code></pre></div></div>

<p><strong>Solution:</strong> Update kubeconfig:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws eks update-kubeconfig <span class="nt">--region</span> us-east-1 <span class="nt">--name</span> your-eks-cluster
</code></pre></div></div>

<h3 id="runners-not-showing-up-on-github">Runners not showing up on GitHub</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verify controller is running</span>
kubectl get pods <span class="nt">-n</span> arc-systems

<span class="c"># Check controller logs</span>
kubectl logs <span class="nt">-n</span> arc-systems <span class="nt">-l</span> app.kubernetes.io/name<span class="o">=</span>gha-runner-scale-set-controller

<span class="c"># Verify listener is active</span>
kubectl get pods <span class="nt">-n</span> arc-runners
</code></pre></div></div>

<h3 id="pods-with-imagepullbackoff">Pods with <code class="language-plaintext highlighter-rouge">ImagePullBackOff</code></h3>

<p>The ECR secret probably expired:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Force manual renewal</span>
kubectl create job <span class="nt">--from</span><span class="o">=</span>cronjob/ecr-registry-helper ecr-renew-now <span class="nt">-n</span> arc-runners

<span class="c"># Verify secret exists</span>
kubectl get secret ecr-registry-credentials <span class="nt">-n</span> arc-runners
</code></pre></div></div>

<h3 id="stuck-jobs">Stuck jobs</h3>

<p>The <code class="language-plaintext highlighter-rouge">activeDeadlineSeconds: 3000</code> in the template kills pods after 50 minutes. To clean up manually:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List old pods</span>
kubectl get pods <span class="nt">-n</span> arc-runners <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp

<span class="c"># Delete stuck pods</span>
kubectl delete pod &lt;pod-name&gt; <span class="nt">-n</span> arc-runners
</code></pre></div></div>

<h3 id="check-helm-releases">Check Helm releases</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm list <span class="nt">-n</span> arc-systems   <span class="c"># controller</span>
helm list <span class="nt">-n</span> arc-runners   <span class="c"># runners</span>
</code></pre></div></div>

<hr />

<h2 id="costs-self-hosted-vs-managed">Costs: Self-Hosted vs Managed</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>GitHub Hosted</th>
      <th>Self-Hosted (EKS)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hardware</td>
      <td>Fixed: 4 vCPU / 16GB RAM</td>
      <td>You choose (c6i, r6i, m6i…)</td>
    </tr>
    <tr>
      <td>Cost per minute</td>
      <td>$0.008 (Linux)</td>
      <td>EC2 instance cost</td>
    </tr>
    <tr>
      <td>Free minutes</td>
      <td>2000/month (private)</td>
      <td>Unlimited</td>
    </tr>
    <tr>
      <td>Scale to zero</td>
      <td>N/A</td>
      <td>Yes (pay only when running)</td>
    </tr>
    <tr>
      <td>VPC access</td>
      <td>No</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>Custom image</td>
      <td>Limited</td>
      <td>Full control</td>
    </tr>
    <tr>
      <td>Pull latency</td>
      <td>High (public registry)</td>
      <td>Low (ECR in same region)</td>
    </tr>
    <tr>
      <td>GPU available</td>
      <td>No</td>
      <td>Yes (p3, g5, etc)</td>
    </tr>
  </tbody>
</table>

<p>For teams with high CI/CD volume (&gt;5000 min/month) or workflows requiring specific hardware, self-hosted on EKS is generally cheaper and faster.</p>

<h3 id="choosing-instance-type-by-workload">Choosing instance type by workload</h3>

<p>The big advantage is being able to direct each job type to appropriate hardware using <code class="language-plaintext highlighter-rouge">nodeSelector</code> and <code class="language-plaintext highlighter-rouge">tolerations</code>:</p>

<table>
  <thead>
    <tr>
      <th>Workload</th>
      <th>Recommended instance</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Docker build / compilation</td>
      <td><code class="language-plaintext highlighter-rouge">c6i.2xlarge</code> (8 vCPU)</td>
      <td>CPU-intensive, parallel build</td>
    </tr>
    <tr>
      <td>Integration tests</td>
      <td><code class="language-plaintext highlighter-rouge">m6i.xlarge</code> (4 vCPU / 16GB)</td>
      <td>Balanced</td>
    </tr>
    <tr>
      <td>Heavy database tests</td>
      <td><code class="language-plaintext highlighter-rouge">r6i.xlarge</code> (4 vCPU / 32GB)</td>
      <td>Memory-intensive</td>
    </tr>
    <tr>
      <td>ML / image processing</td>
      <td><code class="language-plaintext highlighter-rouge">g5.xlarge</code> (GPU)</td>
      <td>GPU workloads</td>
    </tr>
  </tbody>
</table>

<p>On EKS, you create <strong>separate node groups</strong> with labels and taints, and each runner scale set points to the ideal node group:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Runner for heavy builds (CPU)</span>
<span class="na">template</span><span class="pi">:</span>
  <span class="na">spec</span><span class="pi">:</span>
    <span class="na">nodeSelector</span><span class="pi">:</span>
      <span class="na">intent</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-cpu-heavy"</span>
    <span class="na">tolerations</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-cpu-heavy"</span>
        <span class="na">operator</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Equal"</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">effect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">NoSchedule"</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Runner for database tests (memory)</span>
<span class="na">template</span><span class="pi">:</span>
  <span class="na">spec</span><span class="pi">:</span>
    <span class="na">nodeSelector</span><span class="pi">:</span>
      <span class="na">intent</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-memory"</span>
    <span class="na">tolerations</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-memory"</span>
        <span class="na">operator</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Equal"</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">effect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">NoSchedule"</span>
</code></pre></div></div>

<p>This way, a heavy <code class="language-plaintext highlighter-rouge">docker build</code> doesn’t compete for resources with integration tests, and you don’t pay for 32GB of RAM on jobs that only need CPU.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>With this architecture you have:</p>

<ul>
  <li><strong>Scale to zero</strong> — no costs when no jobs are running</li>
  <li><strong>Autoscaling</strong> — ARC creates pods on demand based on job queue</li>
  <li><strong>Custom image</strong> — all tools your pipelines need, pre-installed</li>
  <li><strong>Security</strong> — runners inside the VPC, with access to internal resources</li>
  <li><strong>Automation</strong> — ECR credentials automatically renewed, batch updates via script</li>
</ul>

<p>The initial setup has moderate complexity, but once running, maintenance is minimal. The update script and credentials CronJob cover the two points that cause the most day-to-day issues.</p>

<p><strong>Next step:</strong> Clone this setup, adapt the variables for your environment and start with a runner for a test repository. Then just replicate for the rest.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://github.com/actions/actions-runner-controller">Actions Runner Controller (ARC)</a></li>
  <li><a href="https://github.com/actions/actions-runner-controller/pkgs/container/actions-runner-controller-charts%2Fgha-runner-scale-set">ARC Helm Charts</a></li>
  <li><a href="https://docs.github.com/en/actions/hosting-your-own-runners">GitHub Actions Self-Hosted Runners</a></li>
  <li><a href="https://docs.aws.amazon.com/eks/latest/userguide/">Amazon EKS Documentation</a></li>
  <li><a href="https://github.com/getsops/sops">SOPS - Secrets OPerationS</a></li>
</ul>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="kubernetes" /><category term="github-actions" /><category term="devops" /><category term="aws" /><category term="helm" /><category term="ci-cd" /><summary type="html"><![CDATA[Complete guide to set up GitHub Actions self-hosted runners on Amazon EKS using Actions Runner Controller (ARC), with custom ECR image, autoscaling and automatic credential renewal.]]></summary></entry><entry xml:lang="pt"><title type="html">GitHub Actions Self-Hosted Runners no EKS com ARC</title><link href="https://www.academic.djack.dev/posts/2026/05/github-actions-self-hosted-runners-eks/" rel="alternate" type="text/html" title="GitHub Actions Self-Hosted Runners no EKS com ARC" /><published>2026-05-09T00:00:00-03:00</published><updated>2026-05-09T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2026/05/github-actions-self-hosted-runners-eks</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2026/05/github-actions-self-hosted-runners-eks/"><![CDATA[<h1 id="github-actions-self-hosted-runners-no-eks-com-arc">GitHub Actions Self-Hosted Runners no EKS com ARC</h1>

<h2 id="o-problema">O Problema</h2>

<p>Os runners managed do GitHub Actions funcionam bem para projetos pequenos, mas em cenários de produção você esbarra em limitações:</p>

<ul>
  <li><strong>Hardware fixo</strong> — runners Linux padrão oferecem apenas 4 vCPUs e 16GB RAM. Se seu workflow faz build de imagens pesadas, roda testes de integração em paralelo ou precisa de mais memória para compilação, você fica refém do que o GitHub oferece</li>
  <li><strong>Custo elevado</strong> com minutos de execução em repositórios privados</li>
  <li><strong>Falta de controle</strong> sobre o ambiente (versões de ferramentas, dependências internas)</li>
  <li><strong>Latência</strong> ao baixar imagens pesadas de registries privados</li>
  <li><strong>Segurança</strong> — jobs rodando em infraestrutura compartilhada sem acesso à sua VPC</li>
</ul>

<p>Com self-hosted runners no EKS, <strong>você escolhe a máquina</strong>. Precisa de CPU para compilação? Use <code class="language-plaintext highlighter-rouge">c6i.2xlarge</code>. Workflow pesado em memória? Use <code class="language-plaintext highlighter-rouge">r6i.xlarge</code>. E com node groups separados por tipo de workload, cada pipeline roda no hardware ideal sem pagar por recursos ociosos.</p>

<p>A solução: rodar seus próprios runners dentro do cluster Kubernetes na AWS, com autoscaling baseado na demanda real de jobs.</p>

<h2 id="tldr-arquitetura-resumida">TL;DR (Arquitetura Resumida)</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GitHub Actions (webhook) → ARC Controller → Scale Set → Runner Pods (EKS)
                                                              ↓
                                                    Imagem customizada (ECR)
                                                              ↓
                                                    CronJob renova credenciais ECR a cada 5h
</code></pre></div></div>

<p><strong>Stack:</strong></p>
<ul>
  <li><strong>EKS</strong> — cluster Kubernetes na AWS</li>
  <li><strong>ARC</strong> — Actions Runner Controller (autoscaling nativo)</li>
  <li><strong>ECR</strong> — Registry privado para imagem customizada dos runners</li>
  <li><strong>Helm</strong> — gerenciamento de releases</li>
  <li><strong>SOPS</strong> — criptografia de secrets com KMS</li>
</ul>

<hr />

<h2 id="índice">Índice</h2>

<ul>
  <li><a href="#pré-requisitos">Pré-requisitos</a></li>
  <li><a href="#arquitetura">Arquitetura</a></li>
  <li><a href="#passo-1-imagem-customizada-do-runner">Passo 1: Imagem Customizada do Runner</a></li>
  <li><a href="#passo-2-push-para-o-ecr">Passo 2: Push para o ECR</a></li>
  <li><a href="#passo-3-instalar-o-arc-controller">Passo 3: Instalar o ARC Controller</a></li>
  <li><a href="#passo-4-configurar-o-runner-scale-set">Passo 4: Configurar o Runner Scale Set</a></li>
  <li><a href="#passo-5-renovação-automática-de-credenciais-ecr">Passo 5: Renovação Automática de Credenciais ECR</a></li>
  <li><a href="#passo-6-automação-com-script">Passo 6: Automação com Script</a></li>
  <li><a href="#usando-os-runners-nos-workflows">Usando os Runners nos Workflows</a></li>
  <li><a href="#troubleshooting">Troubleshooting</a></li>
  <li><a href="#conclusão">Conclusão</a></li>
</ul>

<hr />

<h2 id="pré-requisitos">Pré-requisitos</h2>

<p>Antes de começar, você precisa ter:</p>

<ul>
  <li>Cluster <strong>EKS</strong> rodando com <code class="language-plaintext highlighter-rouge">kubectl</code> configurado</li>
  <li><strong>Helm 3</strong> instalado</li>
  <li><strong>AWS CLI</strong> autenticada com permissões para ECR</li>
  <li><strong>GitHub PAT</strong> (Personal Access Token) com scope <code class="language-plaintext highlighter-rouge">admin:org</code> ou <code class="language-plaintext highlighter-rouge">repo</code></li>
  <li><strong>SOPS</strong> configurado com KMS para gerenciar secrets (opcional, mas recomendado)</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verificar conexão com o cluster</span>
kubectl get nodes

<span class="c"># Se der erro de conexão, atualize o kubeconfig</span>
aws eks update-kubeconfig <span class="nt">--region</span> us-east-1 <span class="nt">--name</span> seu-cluster-eks
</code></pre></div></div>

<hr />

<h2 id="arquitetura">Arquitetura</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────┐
│                        AWS (EKS)                        │
│                                                         │
│  ┌──────────────────┐    ┌───────────────────────────┐  │
│  │  arc-systems ns  │    │     arc-runners ns        │  │
│  │                  │    │                           │  │
│  │  ARC Controller  │───▶│  Runner Scale Set        │  │
│  │  (gerencia pods) │    │  ├─ runner-repo-1        │  │
│  │                  │    │  ├─ runner-repo-2        │  │
│  └──────────────────┘    │  └─ runner-org           │  │
│                          │                           │  │
│                          │  CronJob ECR (5h)        │  │
│                          │  (renova docker secret)   │  │
│                          └───────────────────────────┘  │
│                                                         │
│  ┌──────────────────┐                                   │
│  │       ECR        │                                   │
│  │ my-app-github-     │◀── Imagem customizada             │
│  │ action:latest    │    (tools + dependências)         │
│  └──────────────────┘                                   │
└─────────────────────────────────────────────────────────┘
         ▲
         │ webhooks (job queued/completed)
         │
┌────────┴────────┐
│  GitHub Actions  │
│  (seus repos)    │
└─────────────────┘
</code></pre></div></div>

<p>O fluxo funciona assim:</p>

<ol>
  <li>Um workflow é disparado no GitHub</li>
  <li>O GitHub envia um webhook para o ARC Controller</li>
  <li>O Controller escala o Runner Scale Set (cria pods)</li>
  <li>O pod roda o job usando a imagem customizada do ECR</li>
  <li>Ao terminar, o pod é destruído (scale to zero)</li>
</ol>

<hr />

<h2 id="passo-1-imagem-customizada-do-runner">Passo 1: Imagem Customizada do Runner</h2>

<p>A imagem base do GitHub Actions Runner é mínima. Para rodar seus pipelines, você provavelmente precisa de ferramentas adicionais.</p>

<h3 id="dockerfile">Dockerfile</h3>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ghcr.io/actions/actions-runner:latest</span>
<span class="k">USER</span><span class="s"> root</span>

<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\
</span>    git gcc make wget curl jq netcat-openbsd

<span class="k">RUN </span><span class="nb">chown </span>root:runner <span class="nt">-R</span> /opt/ <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>g+w /opt

<span class="c"># Instala o mesmo toolset usado nos runners hosted do GitHub (Ubuntu 24.04)</span>
<span class="k">RUN </span>wget https://raw.githubusercontent.com/actions/runner-images/main/images/ubuntu/toolsets/toolset-2404.json
<span class="k">RUN </span><span class="nv">APT_PACKAGES</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat </span>toolset-2404.json | jq <span class="nt">-r</span> <span class="se">\
</span>    <span class="s1">'.apt | [.vital_packages[], .common_packages[], .cmd_packages[]] | del(.[] | select(. == "lib32z1" or . == "netcat")) | join(" ")'</span><span class="si">)</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="nt">--no-install-recommends</span> <span class="k">${</span><span class="nv">APT_PACKAGES</span><span class="k">}</span>

<span class="k">USER</span><span class="s"> runner</span>
</code></pre></div></div>

<p>A estratégia aqui é reutilizar o <strong>toolset oficial</strong> do GitHub para runners Ubuntu 24.04. Isso garante compatibilidade com a maioria dos Actions que esperam ferramentas pré-instaladas (como <code class="language-plaintext highlighter-rouge">zip</code>, <code class="language-plaintext highlighter-rouge">unzip</code>, <code class="language-plaintext highlighter-rouge">python3</code>, etc).</p>

<h3 id="build-local-para-testar">Build local para testar</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> my-github-runner <span class="nt">--pull</span> <span class="nt">--no-cache</span> <span class="nb">.</span>
</code></pre></div></div>

<hr />

<h2 id="passo-2-push-para-o-ecr">Passo 2: Push para o ECR</h2>

<p>Publique a imagem no seu registry privado:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Variáveis</span>
<span class="nv">ECR_REGISTRY</span><span class="o">=</span><span class="s2">"xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com"</span>
<span class="nv">ECR_REPOSITORY</span><span class="o">=</span><span class="s2">"my-github-runner"</span>

<span class="c"># Autenticar no ECR</span>
aws ecr get-login-password <span class="nt">--region</span> us-east-1 | <span class="se">\</span>
    docker login <span class="nt">--username</span> AWS <span class="nt">--password-stdin</span> <span class="nv">$ECR_REGISTRY</span>

<span class="c"># Tag e push</span>
docker tag my-github-runner:latest <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:latest
docker push <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:latest
</code></pre></div></div>

<p>Se o repositório ECR ainda não existir:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws ecr create-repository <span class="se">\</span>
    <span class="nt">--repository-name</span> my-github-runner <span class="se">\</span>
    <span class="nt">--region</span> us-east-1
</code></pre></div></div>

<hr />

<h2 id="passo-3-instalar-o-arc-controller">Passo 3: Instalar o ARC Controller</h2>

<p>O ARC Controller é o componente central que recebe webhooks do GitHub e gerencia o ciclo de vida dos runner pods.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"arc-systems"</span>
<span class="nv">INSTALLATION_NAME</span><span class="o">=</span><span class="s2">"arc"</span>

helm <span class="nb">install</span> <span class="nv">$INSTALLATION_NAME</span> <span class="se">\</span>
    <span class="nt">--namespace</span> <span class="nv">$NAMESPACE</span> <span class="se">\</span>
    <span class="nt">--create-namespace</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
</code></pre></div></div>

<p>Verifique se o controller está rodando:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> arc-systems
</code></pre></div></div>

<p>Saída esperada:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME                                     READY   STATUS    RESTARTS   AGE
arc-gha-runner-scale-set-controller-xxx  1/1     Running   0          30s
</code></pre></div></div>

<hr />

<h2 id="passo-4-configurar-o-runner-scale-set">Passo 4: Configurar o Runner Scale Set</h2>

<p>Aqui é onde configuramos os runners que vão executar os jobs. Cada Scale Set pode ser associado a um repositório ou organização.</p>

<h3 id="arquivo-de-valores-valuesyml--partes-relevantes">Arquivo de valores (<code class="language-plaintext highlighter-rouge">values.yml</code>) — partes relevantes</h3>

<p>O arquivo completo com todas as opções disponíveis está na <a href="https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml">documentação oficial do chart</a>. Aqui destaco o essencial:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">githubConfigUrl</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://github.com/sua-org/seu-repo"</span>
<span class="na">githubConfigSecret</span><span class="pi">:</span>
  <span class="na">github_token</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>

<span class="na">maxRunners</span><span class="pi">:</span> <span class="m">10</span>
<span class="na">minRunners</span><span class="pi">:</span> <span class="m">0</span>

<span class="na">containerMode</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">dind"</span>
</code></pre></div></div>

<p>A parte mais importante é o <code class="language-plaintext highlighter-rouge">template</code> do pod, onde você define imagem, node placement e acesso ao ECR:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">template</span><span class="pi">:</span>
  <span class="na">spec</span><span class="pi">:</span>
    <span class="na">activeDeadlineSeconds</span><span class="pi">:</span> <span class="m">3000</span>
    <span class="na">nodeSelector</span><span class="pi">:</span>
      <span class="na">intent</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-jobs"</span>
    <span class="na">tolerations</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-xlarge"</span>
        <span class="na">operator</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Equal"</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">effect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">NoSchedule"</span>
    <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">runner</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/my-github-runner:latest</span>
        <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">/home/runner/run.sh"</span><span class="pi">]</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">DOCKER_HOST</span>
            <span class="na">value</span><span class="pi">:</span> <span class="s">unix:///var/run/docker.sock</span>
    <span class="na">imagePullSecrets</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-credentials</span>
</code></pre></div></div>

<h3 id="pontos-importantes-da-configuração">Pontos importantes da configuração</h3>

<table>
  <thead>
    <tr>
      <th>Campo</th>
      <th>Descrição</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">containerMode: dind</code></td>
      <td>Docker-in-Docker — permite que jobs façam <code class="language-plaintext highlighter-rouge">docker build</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">activeDeadlineSeconds: 3000</code></td>
      <td>Kill automático de pods travados (50 min)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nodeSelector: ci-jobs</code></td>
      <td>Roda apenas em nodes dedicados para CI</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tolerations: ci-xlarge</code></td>
      <td>Permite usar nodes com taint específico</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imagePullSecrets</code></td>
      <td>Usa secret do ECR para pull da imagem</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">minRunners: 0</code></td>
      <td>Scale to zero quando não há jobs</td>
    </tr>
  </tbody>
</table>

<h3 id="instalar-o-runner-scale-set">Instalar o Runner Scale Set</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">INSTALLATION_NAME</span><span class="o">=</span><span class="s2">"runner-seu-repo"</span>
<span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"arc-runners"</span>
<span class="nv">GITHUB_CONFIG_URL</span><span class="o">=</span><span class="s2">"https://github.com/sua-org/seu-repo"</span>

helm <span class="nb">install</span> <span class="s2">"</span><span class="nv">$INSTALLATION_NAME</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--namespace</span> <span class="s2">"</span><span class="nv">$NAMESPACE</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--create-namespace</span> <span class="se">\</span>
    <span class="nt">--values</span> values.yml <span class="se">\</span>
    <span class="nt">--set</span> githubConfigSecret.github_token<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_PAT</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--set</span> <span class="nv">githubConfigUrl</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_CONFIG_URL</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
</code></pre></div></div>

<blockquote>
  <p><strong>Dica:</strong> Para não deixar o <code class="language-plaintext highlighter-rouge">GITHUB_PAT</code> em texto plano, use <a href="/posts/2026/05/gerenciando-secrets-sops/">SOPS</a> com AWS KMS para criptografar seus arquivos de secrets.</p>
</blockquote>

<hr />

<h2 id="passo-5-renovação-automática-de-credenciais-ecr">Passo 5: Renovação Automática de Credenciais ECR</h2>

<p>Os tokens do ECR expiram a cada <strong>12 horas</strong>. Sem renovação automática, seus runners vão falhar ao tentar fazer pull da imagem.</p>

<p>A solução é um CronJob que roda a cada 5 horas e recria o secret <code class="language-plaintext highlighter-rouge">docker-registry</code>:</p>

<h3 id="o-cronjob--a-parte-central">O CronJob — a parte central</h3>

<p>O job usa <code class="language-plaintext highlighter-rouge">alpine/k8s</code> (que já tem <code class="language-plaintext highlighter-rouge">aws</code> CLI e <code class="language-plaintext highlighter-rouge">kubectl</code>) para obter um novo token e recriar o secret:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">batch/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">CronJob</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">arc-runners</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">*/5</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span>
  <span class="na">jobTemplate</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">template</span><span class="pi">:</span>
        <span class="na">spec</span><span class="pi">:</span>
          <span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">sa-health-check</span>
          <span class="na">containers</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper</span>
            <span class="na">image</span><span class="pi">:</span> <span class="s">alpine/k8s:1.27.15</span>
            <span class="na">envFrom</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="na">secretRef</span><span class="pi">:</span>
                  <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper-secrets</span>
              <span class="pi">-</span> <span class="na">configMapRef</span><span class="pi">:</span>
                  <span class="na">name</span><span class="pi">:</span> <span class="s">ecr-registry-helper-cm</span>
            <span class="na">command</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="s">/bin/bash</span>
              <span class="pi">-</span> <span class="s">-c</span>
              <span class="pi">-</span> <span class="pi">|-</span>
                <span class="s">ECR_TOKEN=$(aws ecr get-login-password --region ${AWS_REGION})</span>
                <span class="s">kubectl delete secret --ignore-not-found $DOCKER_SECRET_NAME -n arc-runners</span>
                <span class="s">kubectl create secret docker-registry $DOCKER_SECRET_NAME \</span>
                  <span class="s">--docker-server=https://${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com \</span>
                  <span class="s">--docker-username=AWS \</span>
                  <span class="s">--docker-password="${ECR_TOKEN}" \</span>
                  <span class="s">--namespace=arc-runners</span>
          <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Never</span>
</code></pre></div></div>

<p>O CronJob precisa de um <code class="language-plaintext highlighter-rouge">ServiceAccount</code> com permissão mínima para deletar e criar o secret específico:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Role</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">arc-runners</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">role-ecr-secret-renewal</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
  <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">secrets"</span><span class="pi">]</span>
  <span class="na">resourceNames</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">ecr-registry-credentials"</span><span class="pi">]</span>
  <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">delete"</span><span class="pi">]</span>
<span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
  <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">secrets"</span><span class="pi">]</span>
  <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">create"</span><span class="pi">]</span>
</code></pre></div></div>

<p>As credenciais AWS ficam em um <code class="language-plaintext highlighter-rouge">Secret</code> separado (<code class="language-plaintext highlighter-rouge">ecr-registry-helper-secrets</code>) com <code class="language-plaintext highlighter-rouge">AWS_ACCESS_KEY_ID</code>, <code class="language-plaintext highlighter-rouge">AWS_SECRET_ACCESS_KEY</code> e <code class="language-plaintext highlighter-rouge">AWS_ACCOUNT</code>.</p>

<h3 id="aplicar-e-verificar">Aplicar e verificar</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Aplicar todos os recursos</span>
kubectl apply <span class="nt">-f</span> cronjob.yaml

<span class="c"># Verificar o CronJob</span>
kubectl get cronjob <span class="nt">-n</span> arc-runners

<span class="c"># Testar manualmente (sem esperar o schedule)</span>
kubectl create job <span class="nt">--from</span><span class="o">=</span>cronjob/ecr-registry-helper ecr-test <span class="nt">-n</span> arc-runners

<span class="c"># Ver logs</span>
kubectl logs <span class="nt">-n</span> arc-runners <span class="nt">-l</span> job-name<span class="o">=</span>ecr-test <span class="nt">-f</span>
</code></pre></div></div>

<h3 id="por-que-o-rbac-é-mínimo">Por que o RBAC é mínimo</h3>

<p>O Role concede apenas <code class="language-plaintext highlighter-rouge">delete</code> no secret específico <code class="language-plaintext highlighter-rouge">ecr-registry-credentials</code> e <code class="language-plaintext highlighter-rouge">create</code> genérico — o mínimo necessário para o ciclo delete/create. Nenhuma permissão extra.</p>

<hr />

<h2 id="passo-6-automação-com-script">Passo 6: Automação com Script</h2>

<p>Quando você tem múltiplos runners (um por repositório), atualizar manualmente cada um é inviável. Este script automatiza todo o fluxo:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">set</span> <span class="nt">-e</span>

<span class="nv">ECR_REGISTRY</span><span class="o">=</span><span class="s2">"xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com"</span>
<span class="nv">ECR_REPOSITORY</span><span class="o">=</span><span class="s2">"my-github-runner"</span>
<span class="nv">IMAGE_TAG</span><span class="o">=</span><span class="s2">"latest"</span>
<span class="nv">AWS_REGION</span><span class="o">=</span><span class="s2">"us-east-1"</span>
<span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"arc-runners"</span>

<span class="c"># 1. Autenticar no ECR</span>
<span class="nb">echo</span> <span class="s2">"Authenticating with ECR..."</span>
aws ecr get-login-password <span class="nt">--region</span> <span class="nv">$AWS_REGION</span> | <span class="se">\</span>
    docker login <span class="nt">--username</span> AWS <span class="nt">--password-stdin</span> <span class="nv">$ECR_REGISTRY</span>

<span class="c"># 2. Build e push da imagem</span>
<span class="nb">echo</span> <span class="s2">"Building Docker image..."</span>
docker build <span class="nt">-t</span> my-github-runner <span class="nt">--pull</span> <span class="nt">--no-cache</span> <span class="nb">.</span>
docker tag my-github-runner:latest <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:<span class="nv">$IMAGE_TAG</span>
docker push <span class="nv">$ECR_REGISTRY</span>/<span class="nv">$ECR_REPOSITORY</span>:<span class="nv">$IMAGE_TAG</span>

<span class="c"># 3. Atualizar ARC controller</span>
<span class="nb">echo</span> <span class="s2">"Updating ARC controller..."</span>
helm upgrade <span class="nt">--install</span> arc <span class="se">\</span>
    <span class="nt">--namespace</span> arc-systems <span class="se">\</span>
    <span class="nt">--create-namespace</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

<span class="c"># 4. Atualizar todos os runners</span>
<span class="nv">RELEASES</span><span class="o">=</span><span class="si">$(</span>helm list <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$NAMESPACE</span><span class="s2">"</span> <span class="nt">--short</span><span class="si">)</span>

<span class="k">for </span>RELEASE <span class="k">in</span> <span class="nv">$RELEASES</span><span class="p">;</span> <span class="k">do
    </span><span class="nb">echo</span> <span class="s2">"  → Updating release: </span><span class="nv">$RELEASE</span><span class="s2">"</span>
    helm upgrade <span class="nt">--install</span> <span class="s2">"</span><span class="nv">$RELEASE</span><span class="s2">"</span> <span class="se">\</span>
        <span class="nt">--namespace</span> <span class="s2">"</span><span class="nv">$NAMESPACE</span><span class="s2">"</span> <span class="se">\</span>
        <span class="nt">--reuse-values</span> <span class="se">\</span>
        <span class="nt">--set</span> githubConfigSecret.github_token<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_PAT</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
        oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
<span class="k">done</span>

<span class="c"># 5. Resumo</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"Summary:"</span>
helm list <span class="nt">-n</span> arc-runners <span class="nt">-o</span> json | <span class="se">\</span>
    jq <span class="nt">-r</span> <span class="s1">'["NAME","REVISION","APP_VERSION"], (.[] | [.name, (.revision|tostring), .app_version]) | @tsv'</span> | <span class="se">\</span>
    column <span class="nt">-t</span>
</code></pre></div></div>

<p>Execute com secrets via SOPS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops exec-env .env <span class="s2">"./update-all-runners.sh"</span>
</code></pre></div></div>

<hr />

<h2 id="usando-os-runners-nos-workflows">Usando os Runners nos Workflows</h2>

<p>Depois de tudo configurado, usar é simples. No seu workflow, referencie o nome do runner scale set:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/ci.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">CI</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">runner-seu-repo</span>  <span class="c1"># nome do helm release</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">docker build -t app .</span>
          <span class="s">docker run app npm test</span>
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">runs-on</code> deve corresponder ao nome da instalação Helm (o <code class="language-plaintext highlighter-rouge">INSTALLATION_NAME</code> usado no <code class="language-plaintext highlighter-rouge">helm install</code>).</p>

<h3 id="runner-por-organização-vs-por-repositório">Runner por organização vs por repositório</h3>

<table>
  <thead>
    <tr>
      <th>Scope</th>
      <th><code class="language-plaintext highlighter-rouge">githubConfigUrl</code></th>
      <th>Uso</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Repositório</td>
      <td><code class="language-plaintext highlighter-rouge">https://github.com/org/repo</code></td>
      <td>Jobs apenas desse repo</td>
    </tr>
    <tr>
      <td>Organização</td>
      <td><code class="language-plaintext highlighter-rouge">https://github.com/org</code></td>
      <td>Qualquer repo da org pode usar</td>
    </tr>
  </tbody>
</table>

<p>Para organizações, o PAT precisa do scope <code class="language-plaintext highlighter-rouge">admin:org</code>.</p>

<hr />

<h2 id="troubleshooting">Troubleshooting</h2>

<h3 id="erro-kubernetes-cluster-unreachable">Erro: <code class="language-plaintext highlighter-rouge">kubernetes cluster unreachable</code></h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error: kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp 127.0.0.1:8080: connect: connection refused
</code></pre></div></div>

<p><strong>Solução:</strong> Atualize o kubeconfig:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws eks update-kubeconfig <span class="nt">--region</span> us-east-1 <span class="nt">--name</span> seu-cluster-eks
</code></pre></div></div>

<h3 id="runners-não-aparecem-no-github">Runners não aparecem no GitHub</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verificar se o controller está rodando</span>
kubectl get pods <span class="nt">-n</span> arc-systems

<span class="c"># Verificar logs do controller</span>
kubectl logs <span class="nt">-n</span> arc-systems <span class="nt">-l</span> app.kubernetes.io/name<span class="o">=</span>gha-runner-scale-set-controller

<span class="c"># Verificar se o listener está ativo</span>
kubectl get pods <span class="nt">-n</span> arc-runners
</code></pre></div></div>

<h3 id="pods-com-imagepullbackoff">Pods com <code class="language-plaintext highlighter-rouge">ImagePullBackOff</code></h3>

<p>O secret do ECR provavelmente expirou:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Forçar renovação manual</span>
kubectl create job <span class="nt">--from</span><span class="o">=</span>cronjob/ecr-registry-helper ecr-renew-now <span class="nt">-n</span> arc-runners

<span class="c"># Verificar se o secret existe</span>
kubectl get secret ecr-registry-credentials <span class="nt">-n</span> arc-runners
</code></pre></div></div>

<h3 id="jobs-travados-stuck">Jobs travados (stuck)</h3>

<p>O <code class="language-plaintext highlighter-rouge">activeDeadlineSeconds: 3000</code> no template mata pods após 50 minutos. Para limpar manualmente:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Listar pods antigos</span>
kubectl get pods <span class="nt">-n</span> arc-runners <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp

<span class="c"># Deletar pods travados</span>
kubectl delete pod &lt;pod-name&gt; <span class="nt">-n</span> arc-runners
</code></pre></div></div>

<h3 id="verificar-releases-helm">Verificar releases Helm</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm list <span class="nt">-n</span> arc-systems   <span class="c"># controller</span>
helm list <span class="nt">-n</span> arc-runners   <span class="c"># runners</span>
</code></pre></div></div>

<hr />

<h2 id="custos-self-hosted-vs-managed">Custos: Self-Hosted vs Managed</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>GitHub Hosted</th>
      <th>Self-Hosted (EKS)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hardware</td>
      <td>Fixo: 4 vCPU / 16GB RAM</td>
      <td>Você escolhe (c6i, r6i, m6i…)</td>
    </tr>
    <tr>
      <td>Custo por minuto</td>
      <td>$0.008 (Linux)</td>
      <td>Custo da instância EC2</td>
    </tr>
    <tr>
      <td>Minutos gratuitos</td>
      <td>2000/mês (private)</td>
      <td>Ilimitado</td>
    </tr>
    <tr>
      <td>Scale to zero</td>
      <td>N/A</td>
      <td>Sim (paga só quando roda)</td>
    </tr>
    <tr>
      <td>Acesso à VPC</td>
      <td>Não</td>
      <td>Sim</td>
    </tr>
    <tr>
      <td>Imagem customizada</td>
      <td>Limitado</td>
      <td>Total controle</td>
    </tr>
    <tr>
      <td>Latência de pull</td>
      <td>Alta (registry público)</td>
      <td>Baixa (ECR na mesma região)</td>
    </tr>
    <tr>
      <td>GPU disponível</td>
      <td>Não</td>
      <td>Sim (p3, g5, etc)</td>
    </tr>
  </tbody>
</table>

<p>Para times com alto volume de CI/CD (&gt;5000 min/mês) ou workflows que exigem hardware específico, self-hosted no EKS geralmente sai mais barato e mais rápido.</p>

<h3 id="escolhendo-o-tipo-de-instância-por-workload">Escolhendo o tipo de instância por workload</h3>

<p>A grande vantagem é poder direcionar cada tipo de job para o hardware adequado usando <code class="language-plaintext highlighter-rouge">nodeSelector</code> e <code class="language-plaintext highlighter-rouge">tolerations</code>:</p>

<table>
  <thead>
    <tr>
      <th>Workload</th>
      <th>Instância recomendada</th>
      <th>Por que</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Build Docker / compilação</td>
      <td><code class="language-plaintext highlighter-rouge">c6i.2xlarge</code> (8 vCPU)</td>
      <td>CPU-intensive, build paralelo</td>
    </tr>
    <tr>
      <td>Testes de integração</td>
      <td><code class="language-plaintext highlighter-rouge">m6i.xlarge</code> (4 vCPU / 16GB)</td>
      <td>Balanceado</td>
    </tr>
    <tr>
      <td>Testes com banco pesado</td>
      <td><code class="language-plaintext highlighter-rouge">r6i.xlarge</code> (4 vCPU / 32GB)</td>
      <td>Memory-intensive</td>
    </tr>
    <tr>
      <td>ML / processamento de imagem</td>
      <td><code class="language-plaintext highlighter-rouge">g5.xlarge</code> (GPU)</td>
      <td>Workloads com GPU</td>
    </tr>
  </tbody>
</table>

<p>No EKS, você cria <strong>node groups separados</strong> com labels e taints, e cada runner scale set aponta para o node group ideal:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Runner para builds pesados (CPU)</span>
<span class="na">template</span><span class="pi">:</span>
  <span class="na">spec</span><span class="pi">:</span>
    <span class="na">nodeSelector</span><span class="pi">:</span>
      <span class="na">intent</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-cpu-heavy"</span>
    <span class="na">tolerations</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-cpu-heavy"</span>
        <span class="na">operator</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Equal"</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">effect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">NoSchedule"</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Runner para testes com banco (memória)</span>
<span class="na">template</span><span class="pi">:</span>
  <span class="na">spec</span><span class="pi">:</span>
    <span class="na">nodeSelector</span><span class="pi">:</span>
      <span class="na">intent</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-memory"</span>
    <span class="na">tolerations</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ci-memory"</span>
        <span class="na">operator</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Equal"</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">effect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">NoSchedule"</span>
</code></pre></div></div>

<p>Assim, um <code class="language-plaintext highlighter-rouge">docker build</code> pesado não compete por recursos com testes de integração, e você não paga por 32GB de RAM em jobs que só precisam de CPU.</p>

<hr />

<h2 id="conclusão">Conclusão</h2>

<p>Com essa arquitetura você tem:</p>

<ul>
  <li><strong>Scale to zero</strong> — sem custos quando não há jobs rodando</li>
  <li><strong>Autoscaling</strong> — o ARC cria pods sob demanda baseado na fila de jobs</li>
  <li><strong>Imagem customizada</strong> — todas as ferramentas que seus pipelines precisam, pré-instaladas</li>
  <li><strong>Segurança</strong> — runners dentro da VPC, com acesso a recursos internos</li>
  <li><strong>Automação</strong> — credenciais ECR renovadas automaticamente, updates em batch via script</li>
</ul>

<p>O setup inicial tem complexidade moderada, mas uma vez rodando, a manutenção é mínima. O script de update e o CronJob de credenciais cobrem os dois pontos que mais causam problemas no dia a dia.</p>

<p><strong>Próximo passo:</strong> Clone este setup, adapte as variáveis para seu ambiente e comece com um runner para um repositório de teste. Depois é só replicar para os demais.</p>

<hr />

<h2 id="referências">Referências</h2>

<ul>
  <li><a href="https://github.com/actions/actions-runner-controller">Actions Runner Controller (ARC)</a></li>
  <li><a href="https://github.com/actions/actions-runner-controller/pkgs/container/actions-runner-controller-charts%2Fgha-runner-scale-set">ARC Helm Charts</a></li>
  <li><a href="https://docs.github.com/en/actions/hosting-your-own-runners">GitHub Actions Self-Hosted Runners</a></li>
  <li><a href="https://docs.aws.amazon.com/eks/latest/userguide/">Amazon EKS Documentation</a></li>
  <li><a href="https://github.com/getsops/sops">SOPS - Secrets OPerationS</a></li>
</ul>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="kubernetes" /><category term="github-actions" /><category term="devops" /><category term="aws" /><category term="helm" /><category term="ci-cd" /><summary type="html"><![CDATA[Guia completo para configurar GitHub Actions self-hosted runners no Amazon EKS usando Actions Runner Controller (ARC), com imagem customizada no ECR, autoscaling e renovação automática de credenciais.]]></summary></entry><entry xml:lang="en"><title type="html">Managing Secrets with SOPS: KMS, GCP and GPG</title><link href="https://www.academic.djack.dev/en/posts/2026/05/managing-secrets-sops/" rel="alternate" type="text/html" title="Managing Secrets with SOPS: KMS, GCP and GPG" /><published>2026-05-09T00:00:00-03:00</published><updated>2026-05-09T00:00:00-03:00</updated><id>https://www.academic.djack.dev/en/posts/2026/05/managing-secrets-with-sops-kms</id><content type="html" xml:base="https://www.academic.djack.dev/en/posts/2026/05/managing-secrets-sops/"><![CDATA[<h1 id="managing-secrets-with-sops-kms-gcp-and-gpg">Managing Secrets with SOPS: KMS, GCP and GPG</h1>

<p>Friday, 5 PM. You finish that feature, run a satisfying <code class="language-plaintext highlighter-rouge">git add .</code>, commit, push. Go grab a coffee. On the way back to your computer, that sinking feeling: “wait… was the <code class="language-plaintext highlighter-rouge">.env</code> in staged?”. You open GitHub, check the commit and there it is — <code class="language-plaintext highlighter-rouge">AWS_SECRET_ACCESS_KEY</code> in plain text, published to the world. The rest of Friday becomes a security incident: revoke keys, rotate credentials, notify the team, pray no one saw it.</p>

<p>If you’ve lived through this (or live in fear of it), <strong>SOPS</strong> is for you. It lets you commit your secret files <strong>encrypted</strong> directly into the repository. No fear. No incident. No ruined Friday.</p>

<hr />

<h2 id="the-problem">The Problem</h2>

<p>Every application has secrets: API tokens, database credentials, private keys. The challenge is how to share them with the team without:</p>

<ul>
  <li>Committing <code class="language-plaintext highlighter-rouge">.env</code> in plain text to the repository</li>
  <li>Relying on someone sending credentials via Slack/email</li>
  <li>Losing access when someone leaves the team</li>
  <li>Maintaining a complex vault for small/medium projects</li>
</ul>

<p><strong>SOPS</strong> (Secrets OPerationS) solves this: it encrypts only the <strong>values</strong> in your secret files, keeping the keys readable. Combined with a KMS (AWS, GCP, Azure) or GPG, anyone authorized can decrypt — without sharing passwords.</p>

<h2 id="tldr">TL;DR</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install SOPS</span>
<span class="c"># https://github.com/getsops/sops/releases</span>

<span class="c"># Configure the KMS key</span>
<span class="nb">export </span><span class="nv">SOPS_KMS_ARN</span><span class="o">=</span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/your-key"</span>

<span class="c"># Encrypt an existing file</span>
sops encrypt .env <span class="o">&gt;</span> .env.enc

<span class="c"># Edit secrets (decrypts, opens editor, re-encrypts on save)</span>
sops edit .env

<span class="c"># Use secrets as environment variables (no decrypted file on disk)</span>
sops exec-env .env <span class="s2">"helm install ..."</span>
</code></pre></div></div>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#what-is-sops">What is SOPS</a></li>
  <li><a href="#installation">Installation</a></li>
  <li><a href="#initial-configuration">Initial Configuration</a></li>
  <li><a href="#daily-workflow">Daily Workflow</a></li>
  <li><a href="#supported-formats">Supported Formats</a></li>
  <li><a href="#encryption-backends">Encryption Backends</a></li>
  <li><a href="#using-with-helm-and-kubernetes">Using with Helm and Kubernetes</a></li>
  <li><a href="#best-practices">Best Practices</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="what-is-sops">What is SOPS</h2>

<p><a href="https://github.com/getsops/sops">SOPS</a> is a tool originally from Mozilla (now maintained by the CNCF) that encrypts configuration files. The differentiator:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Original file (.env)</span>
<span class="nv">GITHUB_PAT</span><span class="o">=</span>ghp_abc123xyz789
<span class="nv">AWS_SECRET_KEY</span><span class="o">=</span>wJalrXUtnFEMI/K7MDENG/bPxRfiCY
<span class="nv">DB_PASSWORD</span><span class="o">=</span>super-secret-123
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># After encrypting with SOPS</span>
<span class="nv">GITHUB_PAT</span><span class="o">=</span>ENC[AES256_GCM,data:k8vM2n...,type:str]
<span class="nv">AWS_SECRET_KEY</span><span class="o">=</span>ENC[AES256_GCM,data:pQ7xR...,type:str]
<span class="nv">DB_PASSWORD</span><span class="o">=</span>ENC[AES256_GCM,data:mN3kL...,type:str]
</code></pre></div></div>

<p>The <strong>keys</strong> (<code class="language-plaintext highlighter-rouge">GITHUB_PAT</code>, <code class="language-plaintext highlighter-rouge">AWS_SECRET_KEY</code>) remain readable — you know what the file contains without decrypting. Only the <strong>values</strong> are encrypted.</p>

<p>This means:</p>
<ul>
  <li>The file can be safely committed to the repository</li>
  <li>Code review can see which secrets were added/removed</li>
  <li><code class="language-plaintext highlighter-rouge">git diff</code> shows structural changes without exposing values</li>
</ul>

<hr />

<h2 id="why-use-a-kms-key-management-service">Why Use a KMS (Key Management Service)</h2>

<p>SOPS supports several backends. Cloud KMS services (AWS, GCP, Azure) share advantages over GPG/age:</p>

<ul>
  <li><strong>IAM-based access control</strong> — who can decrypt is controlled by cloud provider policies</li>
  <li><strong>Auditing</strong> — every key usage is logged (CloudTrail, Cloud Audit Logs)</li>
  <li><strong>Automatic rotation</strong> — the provider rotates the key without breaking existing files</li>
  <li><strong>No shared secret</strong> — no need to distribute a private key across the team</li>
</ul>

<p>If you don’t use any cloud provider, GPG and age work perfectly — they just require more manual key management.</p>

<hr />

<h2 id="installation">Installation</h2>

<h3 id="sops">SOPS</h3>

<p>Download the binary from the <a href="https://github.com/getsops/sops/releases">releases page</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Linux (amd64)</span>
curl <span class="nt">-LO</span> https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
<span class="nb">mv </span>sops-v3.9.4.linux.amd64 /usr/local/bin/sops
<span class="nb">chmod</span> +x /usr/local/bin/sops

<span class="c"># macOS (via Homebrew)</span>
brew <span class="nb">install </span>sops

<span class="c"># Verify</span>
sops <span class="nt">--version</span>
</code></pre></div></div>

<h3 id="aws-cli">AWS CLI</h3>

<p>You need the AWS CLI configured with credentials that have access to the KMS key:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws sts get-caller-identity  <span class="c"># Verify authentication</span>
</code></pre></div></div>

<hr />

<h2 id="initial-configuration">Initial Configuration</h2>

<h3 id="1-create-or-identify-the-kms-key">1. Create (or identify) the KMS key</h3>

<p>If you already have a KMS key, get the ARN or alias. If not:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws kms create-key <span class="nt">--description</span> <span class="s2">"SOPS encryption key"</span>

<span class="c"># Create an alias for convenience</span>
aws kms create-alias <span class="se">\</span>
    <span class="nt">--alias-name</span> <span class="nb">alias</span>/sops-key <span class="se">\</span>
    <span class="nt">--target-key-id</span> &lt;returned-key-id&gt;
</code></pre></div></div>

<h3 id="2-export-the-arn">2. Export the ARN</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SOPS_KMS_ARN</span><span class="o">=</span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
</code></pre></div></div>

<h3 id="3-create-the-sopsyaml-file-optional-recommended">3. Create the <code class="language-plaintext highlighter-rouge">.sops.yaml</code> file (optional, recommended)</h3>

<p>At the project root, create a <code class="language-plaintext highlighter-rouge">.sops.yaml</code> so you don’t need to pass the ARN every time:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">secrets\.ya?ml$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
</code></pre></div></div>

<p>With this, any <code class="language-plaintext highlighter-rouge">.env</code> or <code class="language-plaintext highlighter-rouge">secrets.yml</code> file will automatically be encrypted with the correct key.</p>

<hr />

<h2 id="daily-workflow">Daily Workflow</h2>

<h3 id="encrypt-a-file-for-the-first-time">Encrypt a file for the first time</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># If .sops.yaml is configured:</span>
sops encrypt .env <span class="o">&gt;</span> .env.enc
<span class="nb">mv</span> .env.enc .env

<span class="c"># Or specifying the key manually:</span>
sops encrypt <span class="nt">--kms</span> <span class="s2">"arn:aws:kms:..."</span> .env <span class="o">&gt;</span> .env.enc
</code></pre></div></div>

<h3 id="edit-secrets">Edit secrets</h3>

<p>The most used command. Decrypts, opens in editor, re-encrypts on save:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops edit .env
</code></pre></div></div>

<p>Uses the editor defined in <code class="language-plaintext highlighter-rouge">$EDITOR</code> (vim, nano, code, etc).</p>

<h3 id="use-secrets-without-a-file-on-disk">Use secrets without a file on disk</h3>

<p><code class="language-plaintext highlighter-rouge">exec-env</code> injects secrets as environment variables in a subprocess — nothing stays in plain text on disk:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Open a shell with all variables available</span>
sops exec-env .env <span class="s2">"bash"</span>

<span class="c"># Execute a specific command</span>
sops exec-env .env <span class="s2">"helm upgrade --set token=</span><span class="se">\$</span><span class="s2">GITHUB_PAT ..."</span>
</code></pre></div></div>

<h3 id="decrypt-to-stdout-debug">Decrypt to stdout (debug)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops decrypt .env
</code></pre></div></div>

<h3 id="view-diff-between-versions">View diff between versions</h3>

<p>Since keys are readable, <code class="language-plaintext highlighter-rouge">git diff</code> works normally to show which secrets were added or removed.</p>

<hr />

<h2 id="supported-formats">Supported Formats</h2>

<p>SOPS works with multiple formats:</p>

<table>
  <thead>
    <tr>
      <th>Format</th>
      <th>Extension</th>
      <th>Use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>dotenv</td>
      <td><code class="language-plaintext highlighter-rouge">.env</code></td>
      <td>Environment variables</td>
    </tr>
    <tr>
      <td>YAML</td>
      <td><code class="language-plaintext highlighter-rouge">.yml</code>, <code class="language-plaintext highlighter-rouge">.yaml</code></td>
      <td>Helm values, K8s configs</td>
    </tr>
    <tr>
      <td>JSON</td>
      <td><code class="language-plaintext highlighter-rouge">.json</code></td>
      <td>App configs</td>
    </tr>
    <tr>
      <td>INI</td>
      <td><code class="language-plaintext highlighter-rouge">.ini</code></td>
      <td>Legacy configs</td>
    </tr>
    <tr>
      <td>Binary</td>
      <td>any</td>
      <td>Entire files (encrypts everything)</td>
    </tr>
  </tbody>
</table>

<h3 id="yaml-example">YAML Example</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># secrets.yml (after SOPS encrypt)</span>
<span class="na">database</span><span class="pi">:</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">ENC[AES256_GCM,data:mQ2x...,type:str]</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">ENC[AES256_GCM,data:k9Lp...,type:str]</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">5432</span>  <span class="c1"># non-sensitive values can be excluded from encryption</span>
<span class="na">sops</span><span class="pi">:</span>
    <span class="na">kms</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">arn</span><span class="pi">:</span> <span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">3.9.4</span>
</code></pre></div></div>

<h3 id="encrypt-only-specific-fields">Encrypt only specific fields</h3>

<p>Use <code class="language-plaintext highlighter-rouge">--encrypted-regex</code> to encrypt only fields containing “password”, “secret”, “token”, etc:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops encrypt <span class="nt">--encrypted-regex</span> <span class="s1">'^(password|secret|token)$'</span> config.yml
</code></pre></div></div>

<hr />

<h2 id="using-with-helm-and-kubernetes">Using with Helm and Kubernetes</h2>

<p>The most common use case: pass secrets to <code class="language-plaintext highlighter-rouge">helm install</code> without exposing them.</p>

<h3 id="pattern-with-exec-env">Pattern with <code class="language-plaintext highlighter-rouge">exec-env</code></h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Encrypted .env contains GITHUB_PAT=ghp_xxx</span>
sops exec-env .env <span class="s2">"helm install runner </span><span class="se">\</span><span class="s2">
    --set githubConfigSecret.github_token=</span><span class="se">\$</span><span class="s2">GITHUB_PAT </span><span class="se">\</span><span class="s2">
    --namespace arc-runners </span><span class="se">\</span><span class="s2">
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set"</span>
</code></pre></div></div>

<h3 id="pattern-with-script">Pattern with script</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># update-runners.sh — run via: sops exec-env .env "./update-runners.sh"</span>

helm upgrade <span class="nt">--install</span> runner <span class="se">\</span>
    <span class="nt">--namespace</span> arc-runners <span class="se">\</span>
    <span class="nt">--set</span> githubConfigSecret.github_token<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">GITHUB_PAT</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sops exec-env .env <span class="s2">"./update-runners.sh"</span>
</code></pre></div></div>

<h3 id="sensitive-values-in-a-values-file">Sensitive values in a values file</h3>

<p>If you prefer keeping everything in YAML:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create values-secrets.yml with tokens</span>
sops edit values-secrets.yml

<span class="c"># Use decrypted inline with helm</span>
sops decrypt values-secrets.yml | helm <span class="nb">install </span>runner <span class="se">\</span>
    <span class="nt">--namespace</span> arc-runners <span class="se">\</span>
    <span class="nt">-f</span> - <span class="se">\</span>
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
</code></pre></div></div>

<hr />

<h2 id="best-practices">Best Practices</h2>

<h3 id="1-commit-the-encrypted-file">1. Commit the encrypted file</h3>

<pre><code class="language-gitignore"># .gitignore
# DON'T ignore .env if it's encrypted with SOPS
# Only ignore if it's plain text:
# .env
</code></pre>

<p>The whole point of SOPS is being able to version secrets safely. If the file is encrypted, it <strong>should</strong> be in the repository.</p>

<h3 id="2-use-sopsyaml-in-the-project">2. Use <code class="language-plaintext highlighter-rouge">.sops.yaml</code> in the project</h3>

<p>Prevents someone from forgetting to specify the key and encrypting with the wrong backend.</p>

<h3 id="3-control-access-via-iam">3. Control access via IAM</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"kms:Decrypt"</span><span class="p">,</span><span class="w"> </span><span class="s2">"kms:DescribeKey"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:key/key-id"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Only those who need to decrypt receive <code class="language-plaintext highlighter-rouge">kms:Decrypt</code>. Others can see the file structure without accessing the values.</p>

<h3 id="4-never-decrypt-to-a-file">4. Never decrypt to a file</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Bad — creates plain text file on disk</span>
sops decrypt .env <span class="o">&gt;</span> .env.plain

<span class="c"># Good — uses in memory only</span>
sops exec-env .env <span class="s2">"command"</span>
</code></pre></div></div>

<h3 id="5-add-multiple-kms-keys">5. Add multiple KMS keys</h3>

<p>For redundancy or cross-account access:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key,arn:aws:kms:us-east-1:yyyyyyyyyyyy:alias/sops-backup"</span>
</code></pre></div></div>

<p>Any of the keys can decrypt the file.</p>

<hr />

<h2 id="encryption-backends">Encryption Backends</h2>

<p>SOPS isn’t exclusive to AWS. It supports multiple backends — choose what makes sense for your infrastructure:</p>

<h3 id="aws-kms">AWS KMS</h3>

<p>Ideal if your team is already on AWS. Access control via IAM, auditing via CloudTrail.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SOPS_KMS_ARN</span><span class="o">=</span><span class="s2">"arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
sops encrypt .env
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
</code></pre></div></div>

<h3 id="gcp-cloud-kms">GCP Cloud KMS</h3>

<p>Same concept, for teams on Google Cloud. Control via GCP IAM.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create keyring and key (once)</span>
gcloud kms keyrings create sops <span class="nt">--location</span> global
gcloud kms keys create sops-key <span class="se">\</span>
    <span class="nt">--location</span> global <span class="se">\</span>
    <span class="nt">--keyring</span> sops <span class="se">\</span>
    <span class="nt">--purpose</span> encryption

<span class="c"># Encrypt</span>
sops encrypt <span class="se">\</span>
    <span class="nt">--gcp-kms</span> <span class="s2">"projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key"</span> <span class="se">\</span>
    .env
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">gcp_kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key"</span>
</code></pre></div></div>

<p>To decrypt, just have <code class="language-plaintext highlighter-rouge">gcloud auth application-default login</code> configured with <code class="language-plaintext highlighter-rouge">cloudkms.cryptoKeyVersions.useToDecrypt</code> permission.</p>

<h3 id="azure-key-vault">Azure Key Vault</h3>

<p>For teams on Azure:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">azure_keyvault</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://my-vault.vault.azure.net/keys/sops-key/version-id"</span>
</code></pre></div></div>

<h3 id="gpgpgp">GPG/PGP</h3>

<p>Works without any cloud provider. Each team member has their GPG key, and you encrypt for multiple recipients:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List available keys</span>
gpg <span class="nt">--list-keys</span>

<span class="c"># Encrypt for one or more fingerprints</span>
sops encrypt <span class="se">\</span>
    <span class="nt">--pgp</span> <span class="s2">"FP_PERSON_1,FP_PERSON_2,FP_PERSON_3"</span> <span class="se">\</span>
    .env
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">pgp</span><span class="pi">:</span> <span class="s2">"</span><span class="s">FP_PERSON_1,FP_PERSON_2,FP_PERSON_3"</span>
</code></pre></div></div>

<p>When someone joins or leaves the team, you add/remove the fingerprint and re-encrypt:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Add new member</span>
sops updatekeys .env
</code></pre></div></div>

<p>Advantage: zero cloud dependency. Disadvantage: manual key management and distribution.</p>

<h3 id="age-modern-simple">age (modern, simple)</h3>

<p>Modern GPG replacement, simpler to use:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate key</span>
age-keygen <span class="nt">-o</span> key.txt
<span class="c"># Output: public key: age1xxxxxxx...</span>

<span class="c"># Encrypt</span>
sops encrypt <span class="nt">--age</span> <span class="s2">"age1xxxxxxx..."</span> .env

<span class="c"># Decrypt</span>
<span class="nb">export </span><span class="nv">SOPS_AGE_KEY_FILE</span><span class="o">=</span>key.txt
sops decrypt .env
</code></pre></div></div>

<p>Recommended for personal projects or small teams that don’t want GPG complexity.</p>

<h3 id="multiple-backends-simultaneously">Multiple backends simultaneously</h3>

<p>You can combine backends — useful for teams distributed across clouds or for redundancy:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .sops.yaml — any of the keys can decrypt</span>
<span class="na">creation_rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path_regex</span><span class="pi">:</span> <span class="s">\.env$</span>
    <span class="na">kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">arn:aws:kms:us-east-1:xxxxxxxxxxxx:alias/sops-key"</span>
    <span class="na">gcp_kms</span><span class="pi">:</span> <span class="s2">"</span><span class="s">projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key"</span>
    <span class="na">pgp</span><span class="pi">:</span> <span class="s2">"</span><span class="s">FP_OF_ADMIN"</span>
</code></pre></div></div>

<p>This ensures that if one provider becomes unavailable, you still have access via another backend.</p>

<h3 id="which-one-to-choose">Which one to choose?</h3>

<table>
  <thead>
    <tr>
      <th>Backend</th>
      <th>When to use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AWS KMS</td>
      <td>Team on AWS, IAM-based access control</td>
    </tr>
    <tr>
      <td>GCP Cloud KMS</td>
      <td>Team on GCP, same IAM model</td>
    </tr>
    <tr>
      <td>Azure Key Vault</td>
      <td>Team on Azure</td>
    </tr>
    <tr>
      <td>GPG/PGP</td>
      <td>No cloud, team with existing GPG keys</td>
    </tr>
    <tr>
      <td>age</td>
      <td>Personal projects, maximum simplicity</td>
    </tr>
    <tr>
      <td>Multiple</td>
      <td>Multi-cloud teams or for redundancy</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>SOPS with KMS solves the secrets problem with a simple workflow:</p>

<ol>
  <li><strong>Encrypt</strong> — <code class="language-plaintext highlighter-rouge">sops encrypt .env</code></li>
  <li><strong>Commit</strong> — the encrypted file goes to the repository</li>
  <li><strong>Edit</strong> — <code class="language-plaintext highlighter-rouge">sops edit .env</code> when you need to change</li>
  <li><strong>Use</strong> — <code class="language-plaintext highlighter-rouge">sops exec-env .env "command"</code> to inject without exposing</li>
</ol>

<p>No extra servers, no vault to maintain, no shared passwords. Whoever has IAM access to the KMS key can work. Whoever doesn’t, sees only encrypted values.</p>

<p>For projects already on AWS, it’s the simplest way to move from plain text <code class="language-plaintext highlighter-rouge">.env</code> to something secure and auditable.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://github.com/getsops/sops">SOPS - GitHub</a></li>
  <li><a href="https://getsops.io/">SOPS Documentation</a></li>
  <li><a href="https://docs.aws.amazon.com/kms/latest/developerguide/">AWS KMS Developer Guide</a></li>
  <li><a href="https://github.com/FiloSottile/age">age encryption</a></li>
</ul>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="devops" /><category term="security" /><category term="aws" /><category term="gcp" /><category term="sops" /><category term="secrets" /><category term="gpg" /><summary type="html"><![CDATA[Learn how to encrypt and manage project secrets using SOPS. Supports AWS KMS, GCP Cloud KMS, Azure Key Vault, age and GPG — choose the backend that makes sense for your team.]]></summary></entry><entry xml:lang="pt"><title type="html">Rastreando Bugs no Git: Encontre o PR</title><link href="https://www.academic.djack.dev/posts/2026/01/rastrear-bugs-git-pr/" rel="alternate" type="text/html" title="Rastreando Bugs no Git: Encontre o PR" /><published>2026-01-23T00:00:00-03:00</published><updated>2026-01-23T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2026/01/blog-post-1</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2026/01/rastrear-bugs-git-pr/"><![CDATA[<h1 id="rastreando-bugs-no-git-encontre-o-pr">Rastreando Bugs no Git: Encontre o PR</h1>

<h2 id="o-problema">O Problema</h2>

<p>Você está debugando código e encontra um bug na <strong>linha 42</strong> de um arquivo. As perguntas surgem imediatamente:</p>

<ul>
  <li>🤔 <strong>Quando</strong> essa linha foi modificada pela última vez?</li>
  <li>👤 <strong>Quem</strong> fez a mudança?</li>
  <li>📝 <strong>Por que</strong> foi alterada (contexto da mudança)?</li>
  <li>🔍 <strong>Qual Pull Request</strong> introduziu o problema?</li>
</ul>

<p>Este guia mostra o <strong>workflow simples de investigação</strong>, combinando Git e GitHub CLI para rastrear bugs desde a linha problemática até o PR responsável.</p>

<h2 id="tldr-workflow-em-3-passos">TL;DR (Workflow em 3 Passos)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># PASSO 1: Encontrar quando a linha foi modificada</span>
git log <span class="nt">-L</span> 42,42:app/models/user.rb

<span class="c"># PASSO 2: Ver detalhes completos do commit suspeito</span>
git show abc1234 <span class="nt">--stat</span>

<span class="c"># PASSO 3: Encontrar o PR que contém esse commit</span>
gh <span class="nb">pr </span>list <span class="nt">--search</span> <span class="s2">"abc1234"</span> <span class="nt">--state</span> all
<span class="c"># ou se souber que está merged:</span>
gh <span class="nb">pr </span>list <span class="nt">--search</span> <span class="s2">"abc1234"</span> <span class="nt">--state</span> merged
</code></pre></div></div>

<p>💡 Esse workflow permite identificar a origem de bugs em aplicações e entender o contexto completo da mudança.</p>

<hr />

<h2 id="índice">Índice</h2>

<ul>
  <li><a href="#workflow-completo-da-linha-ao-pr">Workflow: Da Linha ao PR</a></li>
  <li><a href="#passo-1-rastrear-histórico-da-linha">Passo 1: Rastrear Histórico da Linha</a></li>
  <li><a href="#passo-2-analisar-o-commit">Passo 2: Analisar o Commit</a></li>
  <li><a href="#passo-3-encontrar-o-pull-request">Passo 3: Encontrar o Pull Request</a></li>
  <li><a href="#comandos-complementares-de-investigação">Comandos Complementares de Investigação</a></li>
  <li><a href="#busca-avançada-com-git-grep">Busca Avançada com git grep</a></li>
  <li><a href="#boas-práticas">Boas Práticas</a></li>
  <li><a href="#aliases-úteis">Aliases Úteis</a></li>
  <li><a href="#conclusão">Conclusão</a></li>
</ul>

<hr />

<h2 id="workflow-completo-da-linha-ao-pr">Workflow Completo: Da Linha ao PR</h2>

<p>Vamos seguir um <strong>caso</strong> em uma aplicação Rails. Imagine que você encontrou um bug nesta linha do modelo User:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/user.rb:42</span>
<span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># ...</span>

  <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">format: </span><span class="p">{</span> <span class="ss">with: </span><span class="sr">/@/</span> <span class="p">}</span>  <span class="c1"># Bug: validação muito simplista!</span>

  <span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Essa validação de email só verifica se tem o símbolo <code class="language-plaintext highlighter-rouge">@</code>, permitindo emails inválidos como <code class="language-plaintext highlighter-rouge">"teste@"</code> ou <code class="language-plaintext highlighter-rouge">"@exemplo"</code>. Vamos investigar quando e por que foi introduzido.</p>

<h3 id="cenário">Cenário</h3>

<ul>
  <li><strong>Arquivo:</strong> <code class="language-plaintext highlighter-rouge">app/models/user.rb</code></li>
  <li><strong>Linha:</strong> 42</li>
  <li><strong>Problema:</strong> Validação de email aceita formatos inválidos</li>
  <li><strong>Impacto:</strong> Usuários podem cadastrar emails inválidos no sistema</li>
  <li><strong>Objetivo:</strong> Encontrar o PR que introduziu essa validação fraca</li>
</ul>

<hr />

<h2 id="passo-1-rastrear-histórico-da-linha">Passo 1: Rastrear Histórico da Linha</h2>

<p>O comando mais poderoso para investigar uma linha específica é o <code class="language-plaintext highlighter-rouge">git log -L</code>. Ele mostra <strong>todo o histórico</strong> de uma linha, incluindo o código antes e depois de cada mudança.</p>

<h3 id="comando-principal">Comando Principal</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">-L</span> 42,42:app/models/user.rb
</code></pre></div></div>

<p><strong>O que esse comando faz:</strong></p>
<ul>
  <li>Mostra todos os commits que modificaram a linha 42 do modelo User</li>
  <li>Exibe o diff (antes/depois) de cada mudança</li>
  <li>Lista autor, data e mensagem do commit</li>
</ul>

<h3 id="saída-exemplo">Saída Exemplo</h3>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">commit abc1234567890
Author: João Silva &lt;joao@example.com&gt;
Date:   Mon Jan 20 14:30:00 2026
</span>
    feat: simplifica validação de email no User

diff --git a/app/models/user.rb b/app/models/user.rb
<span class="gd">--- a/app/models/user.rb
</span><span class="gi">+++ b/app/models/user.rb
</span><span class="p">@@ -42,1 +42,1 @@</span>
<span class="gd">-  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
</span><span class="gi">+  validates :email, format: { with: /@/ }  # Simplificado para melhorar performance
</span></code></pre></div></div>

<p>🎯 <strong>Achamos o culpado!</strong> O commit <code class="language-plaintext highlighter-rouge">abc1234</code> substituiu a validação robusta do Rails (<code class="language-plaintext highlighter-rouge">URI::MailTo::EMAIL_REGEXP</code>) por uma regex simples que só verifica <code class="language-plaintext highlighter-rouge">@</code>.</p>

<h3 id="variação-com-formato-compacto">Variação com Formato Compacto</h3>

<p>Para ver apenas os hashes e mensagens:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">-L</span> 42,42:app/models/user.rb <span class="nt">--oneline</span>
</code></pre></div></div>

<p><strong>Saída:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>abc1234 feat: simplifica validação de email no User
def5678 refactor: adiciona validações customizadas
</code></pre></div></div>

<h3 id="rastrear-range-de-linhas">Rastrear Range de Linhas</h3>

<p>Se o bug afeta múltiplas linhas próximas (por exemplo, validações relacionadas):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">-L</span> 40,50:app/models/user.rb
</code></pre></div></div>

<p>Isso mostra mudanças nas linhas 40 a 50, útil para ver todas as validações do modelo.</p>

<hr />

<h2 id="passo-2-analisar-o-commit">Passo 2: Analisar o Commit</h2>

<p>Agora que identificamos o commit <code class="language-plaintext highlighter-rouge">abc1234</code>, vamos analisar <strong>todo o contexto</strong> dessa mudança para entender o que foi modificado.</p>

<h3 id="ver-resumo-das-mudanças">Ver Resumo das Mudanças</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show <span class="nt">--stat</span> abc1234
</code></pre></div></div>

<p><strong>Saída exemplo:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>commit abc1234567890
Author: João Silva &lt;joao@example.com&gt;
Date:   Mon Jan 20 14:30:00 2026

    feat: simplifica validação de email no User

 app/models/user.rb           |  2 +-
 app/controllers/users_controller.rb  |  3 +--
 spec/models/user_spec.rb     | 18 ------------------
 3 files changed, 2 insertions(+), 21 deletions(-)
</code></pre></div></div>

<p>⚠️ <strong>Red flags detectados:</strong></p>
<ul>
  <li>Removeu 18 linhas de specs (<code class="language-plaintext highlighter-rouge">spec/models/user_spec.rb</code>)</li>
  <li>Modificou controller também (escopo maior que esperado)</li>
  <li>Mensagem diz “simplifica” (pode ter removido validações importantes)</li>
</ul>

<h3 id="ver-diff-completo">Ver Diff Completo</h3>

<p>Para ver todas as mudanças linha por linha:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show abc1234
</code></pre></div></div>

<p>Isso mostra o diff completo de todos os arquivos modificados.</p>

<h3 id="ver-apenas-arquivos-modificados">Ver Apenas Arquivos Modificados</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show <span class="nt">--name-only</span> abc1234
</code></pre></div></div>

<p><strong>Saída:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>app/models/user.rb
app/controllers/users_controller.rb
spec/models/user_spec.rb
</code></pre></div></div>

<h3 id="ver-informações-do-autor">Ver Informações do Autor</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show <span class="nt">--format</span><span class="o">=</span><span class="s2">"%an (%ae)%nData: %ad%nMensagem: %s%n%b"</span> <span class="nt">--no-patch</span> abc1234
</code></pre></div></div>

<p><strong>Saída:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>João Silva (joao@example.com)
Data: Mon Jan 20 14:30:00 2026
Mensagem: feat: simplifica validação de email no User

Substitui URI::MailTo::EMAIL_REGEXP por validação simples
para melhorar performance do save. Issue #123 reportou
lentidão ao criar usuários em batch.

Benchmarks mostram melhoria de 40% no throughput.
</code></pre></div></div>

<p>💡 <strong>Contexto importante:</strong> A mudança foi feita por motivo de performance em operações batch, mas sacrificou a validação correta de emails individuais.</p>

<hr />

<h2 id="passo-3-encontrar-o-pull-request">Passo 3: Encontrar o Pull Request</h2>

<p>Agora vem a parte crucial: descobrir <strong>qual PR</strong> contém esse commit. Isso nos dá:</p>
<ul>
  <li>Discussões da code review</li>
  <li>Contexto completo da mudança</li>
  <li>Outros commits relacionados</li>
  <li>Quem aprovou o PR</li>
</ul>

<h3 id="método-1-buscar-pr-por-hash-do-commit">Método 1: Buscar PR por Hash do Commit</h3>

<p>O GitHub CLI permite buscar PRs que contêm um commit específico:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh <span class="nb">pr </span>list <span class="nt">--search</span> <span class="s2">"abc1234"</span> <span class="nt">--state</span> all
</code></pre></div></div>

<p><strong>Saída exemplo:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#127  feat: otimiza validação de email no User  feature/faster-validations  MERGED
</code></pre></div></div>

<p>🎯 <strong>Encontramos!</strong> O commit está no PR #127.</p>

<h3 id="método-2-buscar-apenas-prs-mergeados">Método 2: Buscar Apenas PRs Mergeados</h3>

<p>Se você sabe que o código já está em produção:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh <span class="nb">pr </span>list <span class="nt">--search</span> <span class="s2">"abc1234"</span> <span class="nt">--state</span> merged
</code></pre></div></div>

<h3 id="ver-detalhes-completos-do-pr">Ver Detalhes Completos do PR</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh <span class="nb">pr </span>view 127
</code></pre></div></div>

<p><strong>Saída exemplo:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>feat: otimiza validação de email no User #127
Merged • feature/faster-validations merged into main

  Melhora performance na criação de usuários em batch

  Mudanças:
  - Substitui URI::MailTo::EMAIL_REGEXP por validação simples
  - Remove specs redundantes de formato de email
  - Atualiza UsersController para usar validação client-side

  Performance:
  - Batch create de 1000 users: 45s → 27s (40% mais rápido)
  - Benchmarks no PR comment

  Fixes #123

───────────────────────────────────────
View this pull request on GitHub: https://github.com/empresa/projeto/pull/127
</code></pre></div></div>

<h3 id="ver-informações-específicas-em-json">Ver Informações Específicas em JSON</h3>

<p>Para análise programática:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh <span class="nb">pr </span>view 127 <span class="nt">--json</span> number,title,author,createdAt,mergedAt,reviews,url
</code></pre></div></div>

<p><strong>Saída JSON:</strong></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"number"</span><span class="p">:</span><span class="w"> </span><span class="mi">127</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"feat: otimiza validação de email no User"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"joaosilva"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"createdAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-01-20T14:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"mergedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-01-20T16:30:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reviews"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mariasilva"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"APPROVED"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://github.com/empresa/projeto/pull/127"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="ver-comentários-do-pr-no-terminal">Ver Comentários do PR no Terminal</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh <span class="nb">pr </span>view 127 <span class="nt">--comments</span>
</code></pre></div></div>

<p>Isso mostra toda a discussão da code review, onde você pode encontrar:</p>
<ul>
  <li>Questionamentos sobre a mudança</li>
  <li>Decisões tomadas</li>
  <li>Possíveis avisos ignorados</li>
</ul>

<h3 id="método-alternativo-verificar-branches">Método Alternativo: Verificar Branches</h3>

<p>Se <code class="language-plaintext highlighter-rouge">gh</code> não estiver disponível, use Git puro:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ver em quais branches o commit está</span>
git branch <span class="nt">-a</span> <span class="nt">--contains</span> abc1234

<span class="c"># Ver se está na main/master</span>
git branch <span class="nt">--contains</span> abc1234 | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"main|master"</span>
</code></pre></div></div>

<p><strong>Saída:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  main
  remotes/origin/main
</code></pre></div></div>

<p>Depois busque manualmente no GitHub por commits recentes merged na main.</p>

<hr />

<h2 id="resumo-do-workflow-completo">Resumo do Workflow Completo</h2>

<p>Juntando tudo, aqui está o fluxo de investigação de ponta a ponta:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1️⃣ DESCOBRIR: Encontrar o commit que modificou a linha</span>
git log <span class="nt">-L</span> 42,42:app/models/user.rb <span class="nt">--oneline</span>

<span class="c"># Saída: abc1234 feat: otimiza validação de email no User</span>

<span class="c"># 2️⃣ ANALISAR: Ver o que foi alterado no commit</span>
git show <span class="nt">--stat</span> abc1234

<span class="c"># Revisar: models, specs, controllers modificados</span>

<span class="c"># 3️⃣ INVESTIGAR: Encontrar o PR que contém o commit</span>
gh <span class="nb">pr </span>list <span class="nt">--search</span> <span class="s2">"abc1234"</span> <span class="nt">--state</span> merged

<span class="c"># Saída: #127 feat: otimiza validação de email no User</span>

<span class="c"># 4️⃣ CONTEXTO: Ver detalhes completos do PR</span>
gh <span class="nb">pr </span>view 127 <span class="nt">--comments</span>

<span class="c"># Ler discussões da code review, benchmarks, trade-offs</span>
</code></pre></div></div>

<h3 id="o-que-você-ganhou">O que Você Ganhou</h3>

<p>✅ <strong>Hash do commit:</strong> <code class="language-plaintext highlighter-rouge">abc1234</code>
✅ <strong>Autor:</strong> João Silva
✅ <strong>Data:</strong> 20 de janeiro de 2026
✅ <strong>Motivo:</strong> Otimizar performance em batch creates (Issue #123)
✅ <strong>PR:</strong> #127
✅ <strong>Reviewer:</strong> Maria Silva (aprovou)
✅ <strong>Trade-off:</strong> Sacrificou validação robusta por performance
✅ <strong>Impacto:</strong> Batch de 1000 users 40% mais rápido, mas emails inválidos aceitos</p>

<p>Com essas informações, você pode:</p>
<ul>
  <li>Reverter o commit se necessário</li>
  <li>Discutir com o autor sobre a decisão</li>
  <li>Criar um PR de fix referenciando o problema original</li>
  <li>Aprender com o erro para futuras code reviews</li>
</ul>

<hr />

<h2 id="comandos-complementares-de-investigação">Comandos Complementares de Investigação</h2>

<p>Além do workflow principal, estes comandos são úteis para investigações mais complexas.</p>

<h3 id="buscar-commits-por-mensagem">Buscar Commits por Mensagem</h3>

<p>Encontre commits por palavras-chave:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--all</span> <span class="nt">--oneline</span> <span class="nt">--grep</span><span class="o">=</span><span class="s2">"validation"</span>

<span class="c"># Case-insensitive</span>
git log <span class="nt">--all</span> <span class="nt">--oneline</span> <span class="nt">--grep</span><span class="o">=</span><span class="s2">"validation"</span> <span class="nt">-i</span>

<span class="c"># Com regex</span>
git log <span class="nt">--all</span> <span class="nt">--oneline</span> <span class="nt">--grep</span><span class="o">=</span><span class="s2">"^feat:"</span>
</code></pre></div></div>

<h3 id="ver-histórico-completo-de-um-arquivo">Ver Histórico Completo de um Arquivo</h3>

<p>Útil para entender a evolução de um model ou controller:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--format</span><span class="o">=</span><span class="s2">"%h - %an - %ar: %s"</span> <span class="nt">--</span> app/models/user.rb
</code></pre></div></div>

<h3 id="buscar-por-código-adicionadoremovido">Buscar por Código Adicionado/Removido</h3>

<p>Encontre quando um método específico foi adicionado ou removido:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">-S</span> <span class="s2">"def email_valid?"</span> <span class="nt">--oneline</span> <span class="nt">--</span> app/models/
</code></pre></div></div>

<p>Isso mostra todos os commits que adicionaram ou removeram o método <code class="language-plaintext highlighter-rouge">email_valid?</code> nos models.</p>

<h3 id="buscar-mudanças-em-associations">Buscar Mudanças em Associations</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">-S</span> <span class="s2">"has_many :posts"</span> <span class="nt">--oneline</span> <span class="nt">--</span> app/models/user.rb
</code></pre></div></div>

<h3 id="rastrear-mudanças-em-migrations">Rastrear Mudanças em Migrations</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span> <span class="nt">--</span> db/migrate/<span class="k">*</span><span class="nb">users</span><span class="k">*</span>.rb
</code></pre></div></div>

<hr />

<h2 id="instalando-o-github-cli">Instalando o GitHub CLI</h2>

<p>Para usar o workflow completo, você precisa ter o <a href="https://cli.github.com/">GitHub CLI</a> instalado.</p>

<h3 id="instalação">Instalação</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ubuntu/Debian</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>gh

<span class="c"># macOS</span>
brew <span class="nb">install </span>gh

<span class="c"># Arch Linux</span>
<span class="nb">sudo </span>pacman <span class="nt">-S</span> github-cli

<span class="c"># Windows (com Chocolatey)</span>
choco <span class="nb">install </span>gh
</code></pre></div></div>

<h3 id="primeira-configuração">Primeira Configuração</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Fazer login</span>
gh auth login

<span class="c"># Verificar se está autenticado</span>
gh auth status
</code></pre></div></div>

<h3 id="comandos-adicionais-úteis-do-github-cli">Comandos Adicionais Úteis do GitHub CLI</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Listar PRs abertos</span>
gh <span class="nb">pr </span>list

<span class="c"># Ver status de checks/CI do PR</span>
gh <span class="nb">pr </span>checks 127

<span class="c"># Fazer checkout de um PR localmente</span>
gh <span class="nb">pr </span>checkout 127

<span class="c"># Ver diff do PR</span>
gh <span class="nb">pr </span>diff 127

<span class="c"># Abrir PR no navegador</span>
gh <span class="nb">pr </span>view 127 <span class="nt">--web</span>

<span class="c"># Criar novo PR</span>
gh <span class="nb">pr </span>create <span class="nt">--title</span> <span class="s2">"fix: corrige validação"</span> <span class="nt">--body</span> <span class="s2">"Descrição"</span>

<span class="c"># Mergear PR</span>
gh <span class="nb">pr </span>merge 127 <span class="nt">--squash</span>
</code></pre></div></div>

<hr />

<h2 id="busca-avançada-com-git-grep">Busca Avançada com git grep</h2>

<p>O <code class="language-plaintext highlighter-rouge">git grep</code> é mais rápido que <code class="language-plaintext highlighter-rouge">grep</code> normal pois busca apenas em arquivos rastreados pelo Git. Muito útil para encontrar padrões em aplicações Rails.</p>

<h3 id="buscar-validações-em-models">Buscar Validações em Models</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="s2">"validates"</span> <span class="nt">--</span> <span class="s2">"app/models/*.rb"</span>
</code></pre></div></div>

<h3 id="buscar-uso-de-uma-gem-específica">Buscar Uso de uma Gem Específica</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="s2">"require.*sidekiq"</span> <span class="nt">--</span> <span class="s2">"*.rb"</span>
</code></pre></div></div>

<h3 id="buscar-com-contexto">Buscar com Contexto</h3>

<p>Para ver linhas ao redor do resultado (útil para ver toda a validação ou método):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="nt">-C</span> 5 <span class="s2">"validates :email"</span> <span class="nt">--</span> app/models/user.rb
</code></pre></div></div>

<p><strong>Opções de contexto:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">-A 5</code>: Mostra 5 linhas <strong>depois</strong> (útil para ver o corpo de um método)</li>
  <li><code class="language-plaintext highlighter-rouge">-B 5</code>: Mostra 5 linhas <strong>antes</strong></li>
  <li><code class="language-plaintext highlighter-rouge">-C 5</code>: Mostra 5 linhas antes <strong>e</strong> depois</li>
</ul>

<h3 id="buscar-callbacks-em-models">Buscar Callbacks em Models</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="nt">-n</span> <span class="s2">"before_save</span><span class="se">\|</span><span class="s2">after_save"</span> <span class="nt">--</span> <span class="s2">"app/models/*.rb"</span>
</code></pre></div></div>

<h3 id="buscar-todos-no-código">Buscar TODOs no Código</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="nt">-i</span> <span class="s2">"TODO</span><span class="se">\|</span><span class="s2">FIXME"</span> <span class="nt">--</span> <span class="s2">"*.rb"</span>
</code></pre></div></div>

<h3 id="buscar-configurações-sensíveis">Buscar Configurações Sensíveis</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="nt">-i</span> <span class="s2">"password</span><span class="se">\|</span><span class="s2">secret</span><span class="se">\|</span><span class="s2">key"</span> <span class="nt">--</span> <span class="s2">"config/*.yml"</span>
</code></pre></div></div>

<hr />

<p>Agora que você conhece os principais comandos de investigação e busca, vamos ver como aplicá-los seguindo boas práticas que manterão seu repositório organizado e profissional.</p>

<h2 id="boas-práticas">Boas Práticas</h2>

<h3 id="-commits-atômicos">✅ Commits Atômicos</h3>

<p>Cada commit deve representar <strong>uma mudança lógica</strong>:</p>

<p><strong>❌ Ruim:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>feat: adiciona login, corrige bug no header e atualiza README
</code></pre></div></div>

<p><strong>✅ Bom:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>feat(auth): adiciona formulário de login
fix(header): corrige alinhamento do logo
docs: atualiza instruções de instalação
</code></pre></div></div>

<h3 id="-mensagens-descritivas">✅ Mensagens Descritivas</h3>

<p><strong>❌ Ruim:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fix bug
update code
changes
WIP
</code></pre></div></div>

<p><strong>✅ Bom:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fix(auth): previne login com campos vazios
refactor(api): extrai lógica de validação para função separada
test(checkout): adiciona testes para fluxo de pagamento
</code></pre></div></div>

<h3 id="-commits-frequentes">✅ Commits Frequentes</h3>

<p>Faça commits pequenos e frequentes em vez de grandes commits:</p>
<ul>
  <li>Facilita code review</li>
  <li>Mais fácil de fazer revert</li>
  <li>Histórico mais claro</li>
  <li>Reduz conflitos de merge</li>
</ul>

<h3 id="-use-branches-descritivas">✅ Use Branches Descritivas</h3>

<p><strong>❌ Ruim:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>feature1
new-stuff
test
</code></pre></div></div>

<p><strong>✅ Bom:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>feature/user-authentication
fix/header-alignment
refactor/api-validation
</code></pre></div></div>

<hr />

<p>Para tornar seu trabalho ainda mais eficiente, você pode criar aliases que transformam comandos longos em atalhos rápidos.</p>

<h2 id="aliases-úteis">Aliases Úteis</h2>

<p>Adicione ao seu <code class="language-plaintext highlighter-rouge">~/.gitconfig</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[alias]</span>
    <span class="c"># Status resumido
</span>    <span class="py">st</span> <span class="p">=</span> <span class="s">status --short</span>

    <span class="c"># Log formatado
</span>    <span class="py">lg</span> <span class="p">=</span> <span class="s">log --graph --oneline --decorate --all</span>

    <span class="c"># Ver últimos commits
</span>    <span class="py">last</span> <span class="p">=</span> <span class="s">log -1 HEAD --stat</span>

    <span class="c"># Desfazer último commit mantendo mudanças
</span>    <span class="py">undo</span> <span class="p">=</span> <span class="s">reset HEAD~1 --soft</span>

    <span class="c"># Branches recentes
</span>    <span class="py">recent</span> <span class="p">=</span> <span class="s">branch --sort=-committerdate</span>

    <span class="c"># Amend sem editar mensagem
</span>    <span class="py">amend</span> <span class="p">=</span> <span class="s">commit --amend --no-edit</span>

    <span class="c"># Ver branches que contêm um commit
</span>    <span class="py">contains</span> <span class="p">=</span> <span class="s">branch -a --contains</span>

    <span class="c"># Limpar branches já mergeadas
</span>    <span class="py">cleanup</span> <span class="p">=</span> <span class="s">"!git branch --merged | grep -v '</span><span class="se">\\</span><span class="s">*</span><span class="se">\\</span><span class="s">|main</span><span class="se">\\</span><span class="s">|master</span><span class="se">\\</span><span class="s">|develop' | xargs -n 1 git branch -d"</span>

    <span class="c"># Aliases específicos para Rails
</span>    <span class="c"># Ver histórico de models
</span>    <span class="py">models</span> <span class="p">=</span> <span class="s">log --oneline -- app/models/</span>

    <span class="c"># Ver histórico de migrations
</span>    <span class="py">migrations</span> <span class="p">=</span> <span class="s">log --oneline -- db/migrate/</span>

    <span class="c"># Buscar validações
</span>    <span class="py">validations</span> <span class="p">=</span> <span class="s">grep "validates" -- "app/models/*.rb"</span>

    <span class="c"># Ver mudanças em routes
</span>    <span class="py">routes</span> <span class="p">=</span> <span class="s">log -p -- config/routes.rb</span>
</code></pre></div></div>

<p><strong>Uso:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Aliases gerais</span>
git st              <span class="c"># em vez de git status --short</span>
git lg              <span class="c"># log visual bonito</span>
git last            <span class="c"># ver último commit</span>
git undo            <span class="c"># desfazer último commit</span>
git recent          <span class="c"># ver branches recentes</span>
git amend           <span class="c"># amend rápido</span>
git contains abc123 <span class="c"># ver branches com commit</span>
git cleanup         <span class="c"># limpar branches mergeadas</span>

<span class="c"># Aliases específicos Rails</span>
git models          <span class="c"># ver histórico de mudanças em models</span>
git migrations      <span class="c"># ver histórico de migrations</span>
git validations     <span class="c"># listar todas as validações em models</span>
git routes          <span class="c"># ver mudanças em routes com diff</span>
</code></pre></div></div>

<hr />

<p>Para facilitar a consulta rápida, aqui está uma tabela resumindo os comandos mais importantes abordados neste guia:</p>

<h2 id="resumo-dos-comandos-mais-úteis">Resumo dos Comandos Mais Úteis</h2>

<table>
  <thead>
    <tr>
      <th>Comando</th>
      <th>Uso</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git log -L 15,15:arquivo</code></td>
      <td>Rastrear histórico de linha específica</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git log -S "texto"</code></td>
      <td>Buscar mudanças em código</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git log --grep="palavra"</code></td>
      <td>Buscar em mensagens de commit</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git branch --contains hash</code></td>
      <td>Ver branches com commit</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git show --stat &lt;hash&gt;</code></td>
      <td>Ver detalhes de um commit</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git log --format="%h - %an"</code></td>
      <td>Log formatado customizado</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gh pr view &lt;número&gt;</code></td>
      <td>Ver PR no terminal</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gh pr checkout &lt;número&gt;</code></td>
      <td>Fazer checkout de PR</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git grep -C 3 "texto"</code></td>
      <td>Buscar com contexto</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">git grep -n "padrão"</code></td>
      <td>Buscar com número de linha</td>
    </tr>
  </tbody>
</table>

<h2 id="conclusão">Conclusão</h2>

<p>Rastrear bugs de forma eficiente é uma habilidade essencial para todo desenvolvedor. Com o workflow apresentado neste guia, você pode:</p>

<p>🔍 <strong>Identificar rapidamente</strong> quando e onde um problema foi introduzido
🧠 <strong>Entender o contexto</strong> completo por trás de uma mudança
💬 <strong>Acessar discussões</strong> que levaram à decisão
⚡ <strong>Agir com conhecimento</strong> para corrigir ou reverter mudanças problemáticas</p>

<h3 id="o-workflow-em-3-passos">O Workflow em 3 Passos</h3>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">git log -L</code></strong> → Encontrar o commit que modificou a linha</li>
  <li><strong><code class="language-plaintext highlighter-rouge">git show</code></strong> → Analisar o que foi alterado</li>
  <li><strong><code class="language-plaintext highlighter-rouge">gh pr list</code></strong> → Descobrir o PR responsável</li>
</ol>

<h3 id="dica-final">Dica Final</h3>

<p>Não tente decorar todos os comandos. Salve este guia e use como referência. Com o tempo, o workflow se tornará automático:</p>

<ol>
  <li>Bug em um model? → <code class="language-plaintext highlighter-rouge">git log -L &lt;linha&gt;,&lt;linha&gt;:app/models/&lt;model&gt;.rb</code></li>
  <li>Pegou o hash? → <code class="language-plaintext highlighter-rouge">git show &lt;hash&gt; --stat</code></li>
  <li>Quer o contexto? → <code class="language-plaintext highlighter-rouge">gh pr list --search "&lt;hash&gt;"</code></li>
</ol>

<p>Dominar esse workflow vai economizar <strong>horas de investigação</strong> e tornar você muito mais eficiente em debugging e code review!</p>

<p><strong>Próximo passo:</strong> Comece usando o workflow hoje mesmo no próximo bug que você encontrar no seu projeto. A prática leva à perfeição!</p>

<hr />

<h2 id="referências">Referências</h2>

<ul>
  <li><a href="https://git-scm.com/doc">Git Documentation</a></li>
  <li><a href="https://cli.github.com/manual/">GitHub CLI Manual</a></li>
  <li><a href="https://www.conventionalcommits.org/pt-br/">Conventional Commits</a></li>
  <li><a href="https://git-scm.com/book/pt-br/v2">Pro Git Book (gratuito)</a></li>
  <li><a href="https://github.com/k88hudson/git-flight-rules">Git Flight Rules</a> - Guia prático para situações específicas</li>
</ul>

<hr />

<h2 id="sobre-este-guia">Sobre Este Guia</h2>

<p>Este workflow foi desenvolvido com base em <strong>experiência real de debugging em projetos Rails de produção</strong>. Todos os comandos foram testados e validados em situações reais de investigação.</p>

<p>Tem alguma dúvida ou sugestão de melhoria para o workflow? Compartilhe nos comentários!</p>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="git" /><category term="github" /><category term="debugging" /><category term="cli" /><category term="tutorial" /><summary type="html"><![CDATA[Aprenda o workflow completo de investigação de bugs: descubra quando uma linha foi modificada, identifique o commit responsável e encontre o Pull Request que introduziu o problema.]]></summary></entry><entry xml:lang="pt"><title type="html">Removendo Arquivos Indesejados de um Commit Git</title><link href="https://www.academic.djack.dev/posts/2026/01/remover-arquivos-commit-git/" rel="alternate" type="text/html" title="Removendo Arquivos Indesejados de um Commit Git" /><published>2026-01-03T00:00:00-03:00</published><updated>2026-01-03T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2026/01/blog-post-0</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2026/01/remover-arquivos-commit-git/"><![CDATA[<h1 id="removendo-arquivos-indesejados-de-um-commit-git">Removendo Arquivos Indesejados de um Commit Git</h1>

<h2 id="tldr-resumo-rápido">TL;DR (Resumo Rápido)</h2>

<p>Commitou arquivos errados? Use este processo:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Identifique o commit anterior ao problemático</span>
git log <span class="nt">--oneline</span> <span class="nt">-5</span>

<span class="c"># 2. Inicie rebase interativo marcando para edição</span>
<span class="nv">GIT_SEQUENCE_EDITOR</span><span class="o">=</span><span class="s2">"sed -i 's/^pick &lt;hash&gt;/edit &lt;hash&gt;/'"</span> git rebase <span class="nt">-i</span> &lt;hash-anterior&gt;

<span class="c"># 3. Remova os arquivos indesejados</span>
git reset HEAD^ <span class="nt">--</span> arquivo1.txt arquivo2.txt

<span class="c"># 4. Refaça o commit sem os arquivos</span>
git commit <span class="nt">--amend</span> <span class="nt">--no-edit</span>

<span class="c"># 5. Finalize o rebase</span>
git rebase <span class="nt">--continue</span>
</code></pre></div></div>

<p>⚠️ <strong>Atenção:</strong> Só faça isso se ainda não deu push, ou esteja preparado para usar <code class="language-plaintext highlighter-rouge">git push --force</code>.</p>

<hr />

<h2 id="índice">Índice</h2>

<ul>
  <li><a href="#o-problema">O Problema</a></li>
  <li><a href="#como-identificar-o-problema">Como Identificar o Problema</a></li>
  <li><a href="#a-solução-git-rebase-interativo">A Solução: Git Rebase Interativo</a></li>
  <li><a href="#️-cuidados-importantes">Cuidados Importantes</a></li>
  <li><a href="#alternativa-mais-simples-para-o-último-commit">Alternativa Mais Simples para o Último Commit</a></li>
  <li><a href="#comandos-úteis-relacionados">Comandos Úteis Relacionados</a></li>
  <li><a href="#resumo-do-processo-completo">Resumo do Processo Completo</a></li>
</ul>

<hr />

<h2 id="o-problema">O Problema</h2>

<p>Você já passou por aquela situação onde fez um <code class="language-plaintext highlighter-rouge">git add .</code> apressado, commitou as mudanças e depois percebeu que incluiu arquivos que não deveriam estar naquele commit? Talvez foram notas pessoais, arquivos de configuração, ou até documentação que deveria estar em um commit separado?</p>

<p>Pois é, isso acontece com frequência, especialmente quando trabalhamos em múltiplas funcionalidades simultaneamente e esquecemos de revisar o que realmente está sendo commitado.</p>

<p>Neste artigo, vou mostrar como resolvi exatamente esse problema: <strong>remover arquivos específicos de um commit mantendo as outras alterações intactas</strong>.</p>

<h2 id="um-exemplo-prático">Um Exemplo Prático</h2>

<p>Imagine que você tinha um commit chamado “fix: corrige validação de formulário” que deveria conter apenas 3 arquivos relacionados à correção:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/components/LoginForm.tsx</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/components/RegisterForm.tsx</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/utils/validation.ts</code></li>
</ul>

<p>Porém, ao verificar o commit, você descobre que incluiu acidentalmente mais 4 arquivos:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CHANGELOG.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">TODO.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">docs/README.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">scripts/test-data.js</code></li>
</ul>

<p>Esses arquivos não tinham nada a ver com a correção de validação e poluem o histórico do commit.</p>

<h2 id="como-identificar-o-problema">Como Identificar o Problema</h2>

<p>Primeiro, verifique o conteúdo do commit problemático:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show <span class="nt">--name-status</span> &lt;hash-do-commit&gt;
</code></pre></div></div>

<p>Ou veja o histórico recente:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span> <span class="nt">-10</span>
</code></pre></div></div>

<p>No nosso exemplo, vamos supor que identificamos que o commit <code class="language-plaintext highlighter-rouge">abc1234</code> tinha arquivos extras que não deveriam estar lá.</p>

<h2 id="a-solução-git-rebase-interativo">A Solução: Git Rebase Interativo</h2>

<p>A ferramenta mais poderosa para reescrever o histórico do Git é o <a href="https://git-scm.com/book/pt-br/v2/Git-Tools-Rewriting-History"><strong>rebase interativo</strong></a>. Ele permite editar, reordenar, combinar ou até excluir commits.</p>

<h3 id="passo-1-identificar-o-commit-base">Passo 1: Identificar o Commit Base</h3>

<p>Você precisa iniciar o rebase a partir do commit <strong>anterior</strong> ao que deseja editar. Se seu commit problemático é <code class="language-plaintext highlighter-rouge">abc1234</code>, você deve iniciar o rebase no commit imediatamente anterior a ele.</p>

<p>No nosso exemplo, o commit anterior era <code class="language-plaintext highlighter-rouge">def5678</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span> <span class="nt">-5</span>
<span class="c"># 9ab0def feat: adiciona modo escuro</span>
<span class="c"># 7cd8e91 docs: atualiza documentação da API</span>
<span class="c"># abc1234 fix: corrige validação de formulário  ← Este que queremos editar</span>
<span class="c"># def5678 refactor: reorganiza estrutura de pastas  ← Iniciamos o rebase aqui</span>
<span class="c"># 1234abc feat: adiciona autenticação de usuários</span>
</code></pre></div></div>

<h3 id="passo-2-iniciar-o-rebase-interativo">Passo 2: Iniciar o Rebase Interativo</h3>

<p>Execute o rebase interativo marcando o commit para edição:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GIT_SEQUENCE_EDITOR</span><span class="o">=</span><span class="s2">"sed -i 's/^pick abc1234/edit abc1234/'"</span> git rebase <span class="nt">-i</span> def5678
</code></pre></div></div>

<p><strong>Explicação do comando:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">GIT_SEQUENCE_EDITOR</code>: Define um editor automático que substitui <code class="language-plaintext highlighter-rouge">pick</code> por <code class="language-plaintext highlighter-rouge">edit</code> no commit específico</li>
  <li><code class="language-plaintext highlighter-rouge">git rebase -i</code>: Inicia o rebase interativo</li>
  <li><code class="language-plaintext highlighter-rouge">def5678</code>: O commit base (anterior ao que queremos editar)</li>
</ul>

<p>Você verá uma mensagem como:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stopped at abc1234... fix: corrige validação de formulário
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue
</code></pre></div></div>

<h3 id="passo-3-remover-os-arquivos-indesejados">Passo 3: Remover os Arquivos Indesejados</h3>

<p>Agora que o rebase parou no commit que queremos editar, podemos remover os arquivos indesejados da <a href="https://git-scm.com/book/pt-br/v2/Come%C3%A7ando-O-B%C3%A1sico-do-Git#_the_three_states">staging area</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset HEAD^ <span class="nt">--</span> CHANGELOG.md TODO.md docs/README.md scripts/test-data.js
</code></pre></div></div>

<p><strong>Explicação do comando:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">git reset HEAD^</code>: Remove arquivos do commit atual</li>
  <li><code class="language-plaintext highlighter-rouge">--</code>: Separador entre opções e nomes de arquivos</li>
  <li>Lista de arquivos: Os arquivos específicos que queremos remover</li>
</ul>

<h3 id="passo-4-refazer-o-commit">Passo 4: Refazer o Commit</h3>

<p>Agora refaça o commit sem os arquivos indesejados:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit <span class="nt">--amend</span> <span class="nt">--no-edit</span>
</code></pre></div></div>

<p><strong>Explicação:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">--amend</code>: Modifica o último commit</li>
  <li><code class="language-plaintext highlighter-rouge">--no-edit</code>: Mantém a mensagem de commit original</li>
</ul>

<p>Você verá a confirmação:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[detached HEAD 4f7a9b2] fix: corrige validação de formulário
 3 files changed, 12 insertions(+), 8 deletions(-)
</code></pre></div></div>

<p>Perfeito! Agora o commit tem apenas os 3 arquivos que deveriam estar lá.</p>

<h3 id="passo-5-continuar-o-rebase">Passo 5: Continuar o Rebase</h3>

<p>Finalize o rebase para aplicar as mudanças:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git rebase <span class="nt">--continue</span>
</code></pre></div></div>

<p>O Git automaticamente atualizará os commits subsequentes e você verá:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Successfully rebased and updated refs/heads/main.
</code></pre></div></div>

<h3 id="passo-6-verificar-o-resultado">Passo 6: Verificar o Resultado</h3>

<p>Confirme que tudo está correto:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show <span class="nt">--name-status</span> 4f7a9b2
</code></pre></div></div>

<p>E verifique o status dos arquivos removidos:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
</code></pre></div></div>

<p>Os arquivos removidos do commit agora aparecem como <strong>untracked</strong> (não rastreados):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Arquivos não monitorados:
  CHANGELOG.md
  TODO.md
  docs/README.md
  scripts/test-data.js
</code></pre></div></div>

<p>Eles continuam no seu diretório de trabalho, mas não estão mais no histórico do Git. Você pode commitá-los separadamente quando quiser ou adicioná-los ao <code class="language-plaintext highlighter-rouge">.gitignore</code>.</p>

<h2 id="forma-alternativa-sem-usar-sed">Forma Alternativa: Sem Usar sed</h2>

<p>Se você preferir uma abordagem mais manual, pode fazer o rebase interativo tradicional:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git rebase <span class="nt">-i</span> def5678
</code></pre></div></div>

<p>Isso abrirá seu editor padrão (vim, nano, etc.) mostrando:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pick abc1234 fix: corrige validação de formulário
pick 7cd8e91 docs: atualiza documentação da API
pick 9ab0def feat: adiciona modo escuro
</code></pre></div></div>

<p>Altere manualmente <code class="language-plaintext highlighter-rouge">pick</code> para <code class="language-plaintext highlighter-rouge">edit</code> na linha do commit que deseja modificar:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>edit abc1234 fix: corrige validação de formulário
pick 7cd8e91 docs: atualiza documentação da API
pick 9ab0def feat: adiciona modo escuro
</code></pre></div></div>

<p>Salve e feche o editor. O resto do processo é idêntico aos passos 3-6 acima.</p>

<h2 id="️-cuidados-importantes">⚠️ Cuidados Importantes</h2>

<h3 id="1--nunca-reescreva-histórico-público">1. 🚨 <strong>Nunca reescreva histórico público</strong></h3>

<p>Se você já fez <code class="language-plaintext highlighter-rouge">git push</code> do commit para um repositório compartilhado (GitHub, GitLab, etc.), <strong>não reescreva o histórico</strong> a menos que:</p>
<ul>
  <li>Você esteja trabalhando sozinho no branch</li>
  <li>Todos os colaboradores estejam cientes e concordem</li>
  <li>Você esteja disposto a fazer um <code class="language-plaintext highlighter-rouge">git push --force</code></li>
</ul>

<p>Reescrever histórico público pode causar problemas sérios para outros desenvolvedores que já baixaram seu código.</p>

<h3 id="2--faça-backup-antes">2. 💾 <strong>Faça backup antes</strong></h3>

<p>Antes de fazer rebase, sempre é bom criar um branch de backup:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch backup-antes-rebase
</code></pre></div></div>

<p>Assim, se algo der errado, você pode voltar:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset <span class="nt">--hard</span> backup-antes-rebase
</code></pre></div></div>

<h3 id="3--commits-posteriores-serão-reescritos">3. 🔄 <strong>Commits posteriores serão reescritos</strong></h3>

<p>Quando você edita um commit no meio do histórico, todos os commits posteriores terão seus <a href="https://git-scm.com/book/pt-br/v2/Funcionamento-Interno-do-Git-Objetos-do-Git">hashes SHA-1</a> alterados. Isso é normal e esperado, pois o hash é calculado com base no conteúdo do commit e seu histórico.</p>

<p>No nosso exemplo:</p>
<ul>
  <li><strong>Antes:</strong> <code class="language-plaintext highlighter-rouge">abc1234</code> → <code class="language-plaintext highlighter-rouge">7cd8e91</code> → <code class="language-plaintext highlighter-rouge">9ab0def</code></li>
  <li><strong>Depois:</strong> <code class="language-plaintext highlighter-rouge">4f7a9b2</code> → <code class="language-plaintext highlighter-rouge">a3b2c1d</code> → <code class="language-plaintext highlighter-rouge">e5f6g7h</code></li>
</ul>

<h3 id="4-️-conflitos-podem-acontecer">4. ⚔️ <strong>Conflitos podem acontecer</strong></h3>

<p>Se houver conflitos durante o rebase, o Git pausará e pedirá para você resolvê-los:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Resolver conflitos manualmente</span>
git add &lt;arquivos-resolvidos&gt;
git rebase <span class="nt">--continue</span>
</code></pre></div></div>

<p>Se quiser desistir do rebase:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git rebase <span class="nt">--abort</span>
</code></pre></div></div>

<h2 id="alternativa-mais-simples-para-o-último-commit">Alternativa Mais Simples para o Último Commit</h2>

<p>Se o commit que você quer editar é o <strong>último commit</strong> (HEAD), o processo é muito mais simples:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remover arquivos do último commit</span>
git reset HEAD^ <span class="nt">--</span> arquivo1.txt arquivo2.txt

<span class="c"># Refazer o commit</span>
git commit <span class="nt">--amend</span> <span class="nt">--no-edit</span>

<span class="c"># Ou com nova mensagem</span>
git commit <span class="nt">--amend</span> <span class="nt">-m</span> <span class="s2">"Nova mensagem"</span>
</code></pre></div></div>

<p>Não precisa de rebase nesse caso!</p>

<h2 id="comandos-úteis-relacionados">Comandos Úteis Relacionados</h2>

<h3 id="ver-diferenças-entre-commits">Ver diferenças entre commits</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git diff &lt;commit1&gt; &lt;commit2&gt;
</code></pre></div></div>

<h3 id="ver-apenas-nomes-dos-arquivos-modificados">Ver apenas nomes dos arquivos modificados</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git diff <span class="nt">--name-only</span> &lt;commit1&gt; &lt;commit2&gt;
</code></pre></div></div>

<h3 id="ver-histórico-de-um-arquivo-específico">Ver histórico de um arquivo específico</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--follow</span> <span class="nt">--</span> caminho/do/arquivo.rb
</code></pre></div></div>

<h3 id="desfazer-último-commit-mantendo-as-mudanças">Desfazer último commit mantendo as mudanças</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset <span class="nt">--soft</span> HEAD^
</code></pre></div></div>

<h3 id="desfazer-último-commit-descartando-as-mudanças">Desfazer último commit descartando as mudanças</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset <span class="nt">--hard</span> HEAD^
</code></pre></div></div>

<h2 id="quando-usar-cada-abordagem">Quando Usar Cada Abordagem</h2>

<table>
  <thead>
    <tr>
      <th>Cenário</th>
      <th>Solução</th>
      <th>Complexidade</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Último commit (HEAD)</td>
      <td><code class="language-plaintext highlighter-rouge">git reset HEAD^ + git commit --amend</code></td>
      <td>⭐ Fácil</td>
    </tr>
    <tr>
      <td>Commit no meio do histórico</td>
      <td><code class="language-plaintext highlighter-rouge">git rebase -i</code></td>
      <td>⭐⭐ Médio</td>
    </tr>
    <tr>
      <td>Já fez push</td>
      <td>Evite reescrever, ou use <code class="language-plaintext highlighter-rouge">--force</code> com cuidado</td>
      <td>⭐⭐⭐ Risco alto</td>
    </tr>
    <tr>
      <td>Dividir um commit em vários</td>
      <td><code class="language-plaintext highlighter-rouge">git rebase -i</code> com <code class="language-plaintext highlighter-rouge">edit</code> + múltiplos commits</td>
      <td>⭐⭐ Médio</td>
    </tr>
    <tr>
      <td>Combinar múltiplos commits</td>
      <td><code class="language-plaintext highlighter-rouge">git rebase -i</code> com <code class="language-plaintext highlighter-rouge">squash</code> ou <code class="language-plaintext highlighter-rouge">fixup</code></td>
      <td>⭐⭐ Médio</td>
    </tr>
  </tbody>
</table>

<h2 id="resumo-do-processo-completo">Resumo do Processo Completo</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Identificar o problema</span>
git log <span class="nt">--oneline</span> <span class="nt">-10</span>
git show <span class="nt">--name-status</span> &lt;hash-do-commit&gt;

<span class="c"># 2. Iniciar rebase marcando commit para edição</span>
<span class="nv">GIT_SEQUENCE_EDITOR</span><span class="o">=</span><span class="s2">"sed -i 's/^pick &lt;hash&gt;/edit &lt;hash&gt;/'"</span> git rebase <span class="nt">-i</span> &lt;commit-anterior&gt;

<span class="c"># 3. Remover arquivos indesejados</span>
git reset HEAD^ <span class="nt">--</span> arquivo1.txt arquivo2.txt arquivo3.txt

<span class="c"># 4. Refazer commit</span>
git commit <span class="nt">--amend</span> <span class="nt">--no-edit</span>

<span class="c"># 5. Continuar rebase</span>
git rebase <span class="nt">--continue</span>

<span class="c"># 6. Verificar resultado</span>
git log <span class="nt">--oneline</span> <span class="nt">-5</span>
git status
</code></pre></div></div>

<h2 id="conclusão">Conclusão</h2>

<p>O <code class="language-plaintext highlighter-rouge">git rebase -i</code> é uma ferramenta poderosa para manter seu histórico de commits limpo e organizado. Lembre-se dos pontos principais:</p>

<ul>
  <li>✅ Use <code class="language-plaintext highlighter-rouge">git rebase -i</code> para editar commits no meio do histórico</li>
  <li>✅ Combine com <code class="language-plaintext highlighter-rouge">git reset HEAD^</code> para remover arquivos específicos sem perder outras mudanças</li>
  <li>⚠️ Nunca reescreva histórico já compartilhado (a menos que todos concordem)</li>
  <li>💡 Faça backup antes com <code class="language-plaintext highlighter-rouge">git branch backup-antes-rebase</code></li>
</ul>

<p>Dominar essas técnicas permite que cada commit represente uma mudança lógica e coerente, facilitando code review, debugging e colaboração em equipe.</p>

<p><strong>Dica final:</strong> Antes de dar <code class="language-plaintext highlighter-rouge">git push</code>, sempre revise o que está sendo commitado com <code class="language-plaintext highlighter-rouge">git show</code> ou <code class="language-plaintext highlighter-rouge">git diff --staged</code>. Prevenir é melhor que remediar!</p>

<hr />

<p><strong>Referências:</strong></p>
<ul>
  <li><a href="https://git-scm.com/docs/git-rebase">Git Rebase Docs</a></li>
  <li><a href="https://git-scm.com/docs/git-reset">Git Reset Docs</a></li>
  <li><a href="https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase">Atlassian Git Rebase Tutorial</a></li>
</ul>

<hr />

<h2 id="sobre-este-tutorial">Sobre Este Tutorial</h2>

<p>Este artigo foi escrito com base em <strong>casos reais de desenvolvimento</strong>. Todos os comandos e processos foram testados e validados em projetos de produção. Os exemplos foram generalizados para aplicar a qualquer tipo de projeto.</p>

<p>Se você encontrou este artigo útil, considere compartilhá-lo com outros desenvolvedores que possam se beneficiar. Tem alguma dúvida ou sugestão? Deixe um comentário ou abra uma issue no GitHub!</p>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="git" /><category term="tutorial" /><category term="rebase" /><category term="versionamento" /><summary type="html"><![CDATA[Aprenda a usar git rebase interativo para remover arquivos específicos de um commit mantendo as outras alterações intactas. Tutorial prático com exemplo real.]]></summary></entry><entry xml:lang="pt"><title type="html">Meu TCC: Chuveiro Inteligente 🚿🧠</title><link href="https://www.academic.djack.dev/posts/2022/01/blog-post-1/" rel="alternate" type="text/html" title="Meu TCC: Chuveiro Inteligente 🚿🧠" /><published>2022-03-12T00:00:00-03:00</published><updated>2022-03-12T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2022/01/blog-post-1</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2022/01/blog-post-1/"><![CDATA[<p>Neste post vou falar um pouco sobre meu trabalho de conclusão de curso  de engenharia de Energia. Nele projetei um chuveiro “inteligente”. Bora lá conhecer um pouco desse trabalho.</p>

<h3 id="uma-breve-introdução">UMA BREVE INTRODUÇÃO</h3>

<p>O trabalho foi realizado no Brasil, onde muitas residências têm chuveiros elétricos. O chuveiro elétrico não é nada mais chuveiro à qual possui uma resistência elétrica, onde água transita para se aquecer e cair na nossa cabeça (meio doido isso de imaginar).
Durante o curso eu sempre ouvia que o chuveiro era um  vilão para sistema de energia, pois ele,  mais consumia energia numa residência. Para além disso, esse consumo ocorria no período onde a demanda da rede já era grande. Como sempre fui curioso (e teimoso), resolvi entender esse bicho e ajudar na causa.</p>

<p>Resolvi unir duas áreas que gosto, Energia e TI, trazendo o IOT (Internet das coisas) para o chuveiro e transformando o vilão num bom mocinho (não foi bem assim kkk, na realidade apenas rolou espionagem do vilão).
Antes de apresentar o dispositivo desenvolvido e o sistema de espionagem, vamos entender um pouco sobre o chuveiro elétrico. Vou trazer citação que coloquei no TCC de Pinheiro e Sangoi.</p>

<p><code class="language-plaintext highlighter-rouge"> O chuveiro elétrico é uma tecnologia brasileira, desenvolvida no fim dos anos 40, que, devido à sua difusão, modificou os hábitos de banho do povo brasileiro. Se num primeiro instante, a sua difusão permitiu uma melhoria na qualidade de vida, hoje se tornou um problema do ponto de vista energético (PINHEIRO, 2006; SANGOI, 2015).</code></p>

<p>Os primeiros chuveiros elétricos industriais desenvolvidos no Brasil eram de metal, bagulho doido véi, juntar água + Metal + Energia elétrica e tomar banho, o  inventor com certeza tinha falta parafusos (para ser um bom inventor é necessário).</p>

<p><img class="center" src="/images/post01/img1.png" /></p>

<p>Na figura, apresento o princípio de funcionamento do chuveiro elétrico, com um pouco de  conhecimento sobre elétrica é algo simples de se entender.
A corrente elétrica quando passa pela resistência, gera calor e esse calor é transferido para a água que aumentando a temperatura.</p>

<p><img class="center" src="/images/post01/img2.png" /></p>

<p>Depois dos primeiros modelos, que chamo de tradicionais no trabalho, vieram outros até chegar nos mais difundidos até o momento (2022), os eletrônicos.</p>

<p><img class="center" src="/images/post01/img3.png" /></p>

<p>O que eu fiz no trabalho, foi pegar um desses chuveiros eletrônicos e alguns sensores e mostrar para o usuário, chamei de inteligente, isso foi feito acoplando  um dispositivo extra que desenvolvi para monitorar e controlar ele remotamente ( a inteligência lklkkl ).</p>

<h3 id="sistema-proposto">SISTEMA PROPOSTO</h3>

<p>A ideia do sistema  é  apresentada na figura abaixo, onde um usuário pode controlar o chuveiro diretamente durante o banho ou remotamente pelo serviço provido na “nuvem” (um pc velho).  A “inteligência” que tem no título seria os sensores de coleta de dados que o dispositivo tem , que  trabalham em conjunto com o servidor, enviando os dados para ele e serem apresentados ao usuário.</p>

<p><img class="center" src="/images/post01/img4.png" /></p>

<p>Na figura 15 apresento o modelo de comunicação do sistema e na figura 16 o modelo do hardware do dispositivo que desenvolvi.</p>

<p><img class="center" src="/images/post01/img5.png" /></p>

<p>O  dispositivo foi desenvolvido utilizando um esp32 e e alguns sensores e o código deixei no <a href="https://github.com/juaryR/tcc_chuveiro_inteligente">github</a> (sempre precisa de melhorias, mas compriu seu propósito).</p>

<p><img class="center" src="/images/post01/img6.png" /></p>

<h3 id="materiais-utilizados">MATERIAIS UTILIZADOS</h3>

<p>Na tabela  1 apresento os componentes utilizados para montar o hardware</p>

<p><img class="center" src="/images/post01/img7.png" /></p>

<p>Na tabela 2 os softwares utilizados para criar todo sistema.</p>

<p><img class="center" src="/images/post01/img8.png" /></p>

<p>Alguns testes que realizei conforme eu ia montando o dispositivo ficam na figura abaixo.</p>

<p><img class="center" src="/images/post01/img9.png" /></p>

<p>No final saiu esse Frankenstein aí, não era bonito mas funcional.</p>

<p><img class="center" src="/images/post01/img10.png" /></p>

<h3 id="resultado">RESULTADO</h3>

<p>Na figura abaixo tem meu chuveiro e dispositivo (sim, testei no meu chuveiro, meio doido kkk) acoplados. O dispositivo foi feito de modo a ser não invasivo, podendo ser acoplado a qualquer chuveiro elétrico.
O dispositivo coleta informações de  corrente, tensão e temperatura, que são utilizados para poder saber o consumo do chuveiro em tempo real. Além disso, ele controla a potência entregue ao chuveiro e com isso a temperatura de saída.</p>

<p><img class="center" style="width: 60%;" src="/images/post01/img11.png" /></p>

<p>Essas informações coletadas são mostradas ao usuário em tempo real num dashboard, apresentado na figura abaixo.</p>

<p><img class="center" src="/images/post01/img12.png" /></p>

<p>Os dados são guardados num banco de dados de séries temporais. Os dados armazenados poderam ser utilizados em futuras análises de dados.</p>

<p><img class="center" src="/images/post01/img13.png" /></p>

<p>Em suma,o trabalho realizado foi interessante , pois consegui percorrer vários campos de conhecimento indo desde  da programação de microcontrolador, montagem de hardware, deploy de sistemas e comunicação IoT e no final entregar um produto minimamente viável.</p>

<p>Para saber mais sobre o trabalho podes ler o arquivo final <a href="https://repositorio.ufsc.br/bitstream/handle/123456789/223733/chuveiro_inteligente_Rede_IoT.pdf?sequence=1&amp;isAllowed=y">aqui</a>.</p>

<style>
.center {
  display: block;
  margin-left: auto;
  margin-right: auto;
  
}
</style>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><category term="Smart Divices." /><category term="IoT" /><category term="Heating Systems" /><summary type="html"><![CDATA[Neste post vou falar um pouco sobre meu trabalho de conclusão de curso de engenharia de Energia. Nele projetei um chuveiro “inteligente”. Bora lá conhecer um pouco desse trabalho.]]></summary></entry><entry xml:lang="pt"><title type="html">Desafios 2022</title><link href="https://www.academic.djack.dev/posts/2022/01/blog-post-0/" rel="alternate" type="text/html" title="Desafios 2022" /><published>2022-01-05T00:00:00-03:00</published><updated>2022-01-05T00:00:00-03:00</updated><id>https://www.academic.djack.dev/posts/2022/01/blog-post-0</id><content type="html" xml:base="https://www.academic.djack.dev/posts/2022/01/blog-post-0/"><![CDATA[<p>Mnr Maltas, jame soma.</p>

<p><strong>2022</strong> chegou e com ele novos desafios à vista, ainda estamos em janeiro e muitas coisas estão acontecendo (omicron está a solta). Estamos a dois anos em pandemia e cada dia surge uma nova variante do covid19, então resolvi começar o ano com  um <strong>hand on</strong> e este blog é uma das coisas que pretendo colocar em prática. Bora ver até quando dura meu entusiasmo (normalmente não dura muito kkkk).</p>

<p>Ao longo desse ano pretendo realizar vários projetos e um que já está em execução é o de programação, onde irei realizar um desafio do <a href="beecrowd.com.br">beecrowd</a> por dia. Estou colocando as resoluções num repo no  github confere <a href="https://github.com/juaryR/beecrowd_problems">aqui</a>.</p>

<p>Abraço até o próximo post.</p>]]></content><author><name>Juary Costa Rocha</name><email>contact@djack.dev</email></author><summary type="html"><![CDATA[Mnr Maltas, jame soma.]]></summary></entry></feed>