<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>크로스컴파일 on Chanyeol Dev</title>
    <link>https://chanyeols.com/tags/%ED%81%AC%EB%A1%9C%EC%8A%A4%EC%BB%B4%ED%8C%8C%EC%9D%BC/</link>
    <description>Recent content in 크로스컴파일 on Chanyeol Dev</description>
    <generator>Hugo</generator>
    <language>ko-kr</language>
    <lastBuildDate>Mon, 13 Apr 2026 11:00:00 +0900</lastBuildDate>
    <atom:link href="https://chanyeols.com/tags/%ED%81%AC%EB%A1%9C%EC%8A%A4%EC%BB%B4%ED%8C%8C%EC%9D%BC/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>사내 VM IP 관리 시스템 만들기 - Node.js 에이전트 → Go 에이전트 전환 (2편)</title>
      <link>https://chanyeols.com/posts/ipam-02-agent-go/</link>
      <pubDate>Mon, 13 Apr 2026 11:00:00 +0900</pubDate>
      <guid>https://chanyeols.com/posts/ipam-02-agent-go/</guid>
      <description>pkg로 번들한 Node.js 에이전트가 너무 무거워서 Go 단일 바이너리로 전환한 과정을 정리합니다. 크기 비교, 크로스 컴파일, NIC 정보 수집 구현을 다룹니다.</description>
      <content:encoded><![CDATA[<h2 id="처음엔-nodejs로-만들었다">처음엔 Node.js로 만들었다</h2>
<p>에이전트를 처음 만들 때 익숙한 Node.js를 썼다. 로직 자체는 단순하다.</p>
<ul>
<li>30초마다 <code>POST /api/heartbeat</code> 호출</li>
<li>호스트명, MAC 주소, IP 주소를 payload에 담아 전송</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> os <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;os&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> SERVER_URL <span style="color:#ff7b72;font-weight:bold">=</span> process.env.IPAM_SERVER_URL;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> INTERVAL_MS <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">30_000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> getNics() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> interfaces <span style="color:#ff7b72;font-weight:bold">=</span> os.networkInterfaces();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> result <span style="color:#ff7b72;font-weight:bold">=</span> [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">const</span> [name, addrs] <span style="color:#ff7b72">of</span> Object.entries(interfaces)) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">const</span> addr <span style="color:#ff7b72">of</span> addrs) {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">if</span> (addr.family <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#39;IPv4&#39;</span> <span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span> <span style="color:#ff7b72;font-weight:bold">!</span>addr.internal) {
</span></span><span style="display:flex;"><span>        result.push({
</span></span><span style="display:flex;"><span>          interfaceName<span style="color:#ff7b72;font-weight:bold">:</span> name,
</span></span><span style="display:flex;"><span>          macAddress<span style="color:#ff7b72;font-weight:bold">:</span> addr.mac,
</span></span><span style="display:flex;"><span>          ipAddress<span style="color:#ff7b72;font-weight:bold">:</span> addr.address,
</span></span><span style="display:flex;"><span>        });
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> result;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> sendHeartbeat() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> axios.post(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>SERVER_URL<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">/api/heartbeat`</span>, {
</span></span><span style="display:flex;"><span>      hostname<span style="color:#ff7b72;font-weight:bold">:</span> os.hostname(),
</span></span><span style="display:flex;"><span>      networkInterfaces<span style="color:#ff7b72;font-weight:bold">:</span> getNics(),
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    console.error(<span style="color:#a5d6ff">&#39;[heartbeat] 실패:&#39;</span>, e.message);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sendHeartbeat();
</span></span><span style="display:flex;"><span>setInterval(sendHeartbeat, INTERVAL_MS);
</span></span></code></pre></div><p>동작은 잘 했다. 문제는 배포였다.</p>
<hr>
<h2 id="nodejs-에이전트의-문제-배포-크기">Node.js 에이전트의 문제: 배포 크기</h2>
<p>VM마다 Node.js 런타임을 설치하거나, <code>pkg</code>로 번들해서 배포해야 한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;scripts&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;build&#34;</span>: <span style="color:#a5d6ff">&#34;pkg src/index.js --target node18-win-x64 --output dist/ipam-agent.exe&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>pkg</code>를 쓰면 Node.js 런타임을 exe 안에 포함시켜서 단일 실행 파일로 만들 수 있다. 근데 크기가 문제였다.</p>
<pre tabindex="0"><code>ipam-agent.exe — 45 MB
</code></pre><p>Node.js 18 런타임 전체가 포함되니 이 크기가 나올 수밖에 없다. 에이전트를 30개 VM에 배포하면 총 1.3 GB를 올려야 한다. 업데이트할 때마다 다시 올려야 하고, 느린 내부 네트워크 환경에서는 부담이 된다.</p>
<hr>
<h2 id="go로-전환">Go로 전환</h2>
<p>Go는 런타임을 따로 설치하지 않아도 된다. 컴파일하면 의존성이 모두 포함된 단일 바이너리가 나온다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#ff7b72">package</span><span style="color:#6e7681"> </span>main<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span><span style="color:#6e7681"> </span>(<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;bytes&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;encoding/json&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;fmt&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;net&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;net/http&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;os&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">&#34;time&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span><span style="color:#6e7681"> </span>(<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>agentVersion<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;1.0.0&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>intervalSec<span style="color:#6e7681">  </span>=<span style="color:#6e7681"> </span><span style="color:#a5d6ff">30</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">type</span><span style="color:#6e7681"> </span>NicInfo<span style="color:#6e7681"> </span><span style="color:#ff7b72">struct</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>InterfaceName<span style="color:#6e7681"> </span><span style="color:#ff7b72">string</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">`json:&#34;interfaceName&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>MacAddress<span style="color:#6e7681">    </span><span style="color:#ff7b72">string</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">`json:&#34;macAddress&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>IpAddress<span style="color:#6e7681">     </span><span style="color:#ff7b72">string</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">`json:&#34;ipAddress&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">type</span><span style="color:#6e7681"> </span>HeartbeatPayload<span style="color:#6e7681"> </span><span style="color:#ff7b72">struct</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>MacAddress<span style="color:#6e7681">        </span><span style="color:#ff7b72">string</span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">`json:&#34;macAddress&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>Hostname<span style="color:#6e7681">          </span><span style="color:#ff7b72">string</span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">`json:&#34;hostname&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>AgentVersion<span style="color:#6e7681">      </span><span style="color:#ff7b72">string</span><span style="color:#6e7681">    </span><span style="color:#a5d6ff">`json:&#34;agentVersion&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>NetworkInterfaces<span style="color:#6e7681"> </span>[]NicInfo<span style="color:#6e7681"> </span><span style="color:#a5d6ff">`json:&#34;networkInterfaces&#34;`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></div><h3 id="nic-정보-수집">NIC 정보 수집</h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">getNics</span>()<span style="color:#6e7681"> </span>[]NicInfo<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>ifaces,<span style="color:#6e7681"> </span>_<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>net.<span style="color:#d2a8ff;font-weight:bold">Interfaces</span>()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">var</span><span style="color:#6e7681"> </span>result<span style="color:#6e7681"> </span>[]NicInfo<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">for</span><span style="color:#6e7681"> </span>_,<span style="color:#6e7681"> </span>iface<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">range</span><span style="color:#6e7681"> </span>ifaces<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#8b949e;font-style:italic">// 루프백 제외</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>iface.Flags<span style="color:#ff7b72;font-weight:bold">&amp;</span>net.FlagLoopback<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">!=</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">0</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">continue</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>addrs,<span style="color:#6e7681"> </span>_<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>iface.<span style="color:#d2a8ff;font-weight:bold">Addrs</span>()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">for</span><span style="color:#6e7681"> </span>_,<span style="color:#6e7681"> </span>addr<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">range</span><span style="color:#6e7681"> </span>addrs<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">var</span><span style="color:#6e7681"> </span>ip<span style="color:#6e7681"> </span>net.IP<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">switch</span><span style="color:#6e7681"> </span>v<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>addr.(<span style="color:#ff7b72">type</span>)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">case</span><span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span>net.IPNet:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>ip<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>v.IP<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">case</span><span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span>net.IPAddr:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>ip<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>v.IP<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#8b949e;font-style:italic">// IPv4만, 링크로컬 제외</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>ip<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">==</span><span style="color:#6e7681"> </span><span style="color:#79c0ff">nil</span><span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">||</span><span style="color:#6e7681"> </span>ip.<span style="color:#d2a8ff;font-weight:bold">IsLinkLocalUnicast</span>()<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">||</span><span style="color:#6e7681"> </span>ip.<span style="color:#d2a8ff;font-weight:bold">To4</span>()<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">==</span><span style="color:#6e7681"> </span><span style="color:#79c0ff">nil</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span><span style="color:#ff7b72">continue</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>result<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>append(result,<span style="color:#6e7681"> </span>NicInfo{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>InterfaceName:<span style="color:#6e7681"> </span>iface.Name,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>MacAddress:<span style="color:#6e7681">    </span>iface.HardwareAddr.<span style="color:#d2a8ff;font-weight:bold">String</span>(),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>IpAddress:<span style="color:#6e7681">     </span>ip.<span style="color:#d2a8ff;font-weight:bold">String</span>(),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>})<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span>result<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></div><h3 id="heartbeat-전송">heartbeat 전송</h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">sendHeartbeat</span>(serverURL<span style="color:#6e7681"> </span><span style="color:#ff7b72">string</span>)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>nics<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">getNics</span>()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>len(nics)<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">==</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">0</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>fmt.<span style="color:#d2a8ff;font-weight:bold">Println</span>(<span style="color:#a5d6ff">&#34;[heartbeat] NIC 없음, 스킵&#34;</span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">return</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>payload<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>HeartbeatPayload{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>MacAddress:<span style="color:#6e7681">        </span>nics[<span style="color:#a5d6ff">0</span>].MacAddress,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>Hostname:<span style="color:#6e7681">          </span><span style="color:#d2a8ff;font-weight:bold">getHostname</span>(),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>AgentVersion:<span style="color:#6e7681">      </span>agentVersion,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>NetworkInterfaces:<span style="color:#6e7681"> </span>nics,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>body,<span style="color:#6e7681"> </span>_<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>json.<span style="color:#d2a8ff;font-weight:bold">Marshal</span>(payload)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>resp,<span style="color:#6e7681"> </span>err<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>http.<span style="color:#d2a8ff;font-weight:bold">Post</span>(serverURL<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">&#34;/api/heartbeat&#34;</span>,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;application/json&#34;</span>,<span style="color:#6e7681"> </span>bytes.<span style="color:#d2a8ff;font-weight:bold">NewReader</span>(body))<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>err<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">!=</span><span style="color:#6e7681"> </span><span style="color:#79c0ff">nil</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>fmt.<span style="color:#d2a8ff;font-weight:bold">Printf</span>(<span style="color:#a5d6ff">&#34;[heartbeat] 실패: %v\n&#34;</span>,<span style="color:#6e7681"> </span>err)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">return</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">defer</span><span style="color:#6e7681"> </span>resp.Body.<span style="color:#d2a8ff;font-weight:bold">Close</span>()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>fmt.<span style="color:#d2a8ff;font-weight:bold">Printf</span>(<span style="color:#a5d6ff">&#34;[heartbeat] %s → %d\n&#34;</span>,<span style="color:#6e7681"> </span>time.<span style="color:#d2a8ff;font-weight:bold">Now</span>().<span style="color:#d2a8ff;font-weight:bold">Format</span>(<span style="color:#a5d6ff">&#34;15:04:05&#34;</span>),<span style="color:#6e7681"> </span>resp.StatusCode)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">getHostname</span>()<span style="color:#6e7681"> </span><span style="color:#ff7b72">string</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>h,<span style="color:#6e7681"> </span>err<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>os.<span style="color:#d2a8ff;font-weight:bold">Hostname</span>()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>err<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">!=</span><span style="color:#6e7681"> </span><span style="color:#79c0ff">nil</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;unknown&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span>h<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">func</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">main</span>()<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>serverURL<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>os.<span style="color:#d2a8ff;font-weight:bold">Getenv</span>(<span style="color:#a5d6ff">&#34;IPAM_SERVER_URL&#34;</span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>serverURL<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">==</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;&#34;</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>fmt.<span style="color:#d2a8ff;font-weight:bold">Println</span>(<span style="color:#a5d6ff">&#34;[에이전트] IPAM_SERVER_URL 환경변수가 설정되지 않았습니다.&#34;</span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>os.<span style="color:#d2a8ff;font-weight:bold">Exit</span>(<span style="color:#a5d6ff">1</span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>fmt.<span style="color:#d2a8ff;font-weight:bold">Printf</span>(<span style="color:#a5d6ff">&#34;[에이전트] 시작 — 서버: %s\n&#34;</span>,<span style="color:#6e7681"> </span>serverURL)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#d2a8ff;font-weight:bold">sendHeartbeat</span>(serverURL)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>ticker<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>time.<span style="color:#d2a8ff;font-weight:bold">NewTicker</span>(intervalSec<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">*</span><span style="color:#6e7681"> </span>time.Second)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">for</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">range</span><span style="color:#6e7681"> </span>ticker.C<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#d2a8ff;font-weight:bold">sendHeartbeat</span>(serverURL)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></div><hr>
<h2 id="빌드">빌드</h2>
<h3 id="windows-대상">Windows 대상</h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go build -ldflags<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;-s -w&#34;</span> -o ipam-agent.exe .
</span></span></code></pre></div><ul>
<li><code>-s</code> — 심볼 테이블 제거</li>
<li><code>-w</code> — DWARF 디버그 정보 제거</li>
<li>결과 크기: <strong>약 6 MB</strong></li>
</ul>
<p>Node.js 버전(45 MB) 대비 <strong>87% 감소</strong>다.</p>
<h3 id="크로스-컴파일">크로스 컴파일</h3>
<p>Go는 크로스 컴파일이 빌드 플래그 하나다. macOS나 Linux에서 Windows용 바이너리를 만들 수 있다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Linux에서 Windows 64비트 빌드</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">GOOS</span><span style="color:#ff7b72;font-weight:bold">=</span>windows <span style="color:#79c0ff">GOARCH</span><span style="color:#ff7b72;font-weight:bold">=</span>amd64 go build -ldflags<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;-s -w&#34;</span> -o ipam-agent.exe .
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Linux용 빌드</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">GOOS</span><span style="color:#ff7b72;font-weight:bold">=</span>linux <span style="color:#79c0ff">GOARCH</span><span style="color:#ff7b72;font-weight:bold">=</span>amd64 go build -ldflags<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;-s -w&#34;</span> -o ipam-agent .
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># ARM (Raspberry Pi 등)</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">GOOS</span><span style="color:#ff7b72;font-weight:bold">=</span>linux <span style="color:#79c0ff">GOARCH</span><span style="color:#ff7b72;font-weight:bold">=</span>arm64 go build -ldflags<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;-s -w&#34;</span> -o ipam-agent-arm64 .
</span></span></code></pre></div><p>Node.js <code>pkg</code>는 타겟별로 다른 빌드 프로세스가 필요했는데, Go는 환경변수 두 개로 끝난다.</p>
<hr>
<h2 id="에이전트-배포-방법">에이전트 배포 방법</h2>
<h3 id="windows-vm">Windows VM</h3>
<ol>
<li><code>ipam-agent.exe</code>를 VM에 복사</li>
<li>환경변수 설정</li>
<li>윈도우 서비스로 등록 (NSSM 사용)</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-batch" data-lang="batch"><span style="display:flex;"><span>nssm install IpamAgent <span style="color:#a5d6ff">&#34;C:\ipam-agent.exe&#34;</span>
</span></span><span style="display:flex;"><span>nssm set IpamAgent AppEnvironmentExtra <span style="color:#a5d6ff">&#34;IPAM_SERVER_URL=http://ipam-server:8080&#34;</span>
</span></span><span style="display:flex;"><span>nssm start IpamAgent
</span></span></code></pre></div><h3 id="linux-vm-systemd">Linux VM (systemd)</h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># /etc/systemd/system/ipam-agent.service</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">[Unit]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">IPAM Agent</span>
</span></span><span style="display:flex;"><span>After<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">network.target</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">[Service]</span>
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">/usr/local/bin/ipam-agent</span>
</span></span><span style="display:flex;"><span>Environment<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">IPAM_SERVER_URL=http://ipam-server:8080</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">always</span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">5</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">[Install]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">multi-user.target</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl enable --now ipam-agent
</span></span></code></pre></div><h3 id="systemd-재시작-정책">systemd 재시작 정책</h3>
<p><code>Restart=always</code>를 쓰면 에이전트가 예외로 종료돼도 자동 재시작된다. <code>RestartSec=5</code>는 재시작 전 대기 시간이다. 네트워크가 잠깐 끊겼다가 복구될 때 바로 heartbeat를 재개한다.</p>
<hr>
<h2 id="gomod">go.mod</h2>
<p>외부 의존성이 없다. 표준 라이브러리만 쓴다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>module<span style="color:#6e7681"> </span>ipam<span style="color:#ff7b72;font-weight:bold">-</span>agent<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">go</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">1.22</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p><code>net/http</code>, <code>encoding/json</code>, <code>net</code>, <code>os</code>, <code>time</code> — 전부 표준 라이브러리다. 의존성 관리가 필요 없어서 배포가 간단하다.</p>
<hr>
<h2 id="nodejs-vs-go-비교">Node.js vs Go 비교</h2>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>Node.js + pkg</th>
          <th>Go</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>바이너리 크기</td>
          <td>~45 MB</td>
          <td>~6 MB</td>
      </tr>
      <tr>
          <td>런타임 필요</td>
          <td>없음 (번들)</td>
          <td>없음</td>
      </tr>
      <tr>
          <td>크로스 컴파일</td>
          <td>타겟별 다른 빌드</td>
          <td>환경변수 2개</td>
      </tr>
      <tr>
          <td>외부 의존성</td>
          <td>axios</td>
          <td>없음</td>
      </tr>
      <tr>
          <td>빌드 시간</td>
          <td>1~2분 (pkg 번들링)</td>
          <td>수초</td>
      </tr>
      <tr>
          <td>코드 복잡도</td>
          <td>낮음</td>
          <td>낮음</td>
      </tr>
  </tbody>
</table>
<p>에이전트처럼 단순한 로직을 반복 실행하는 프로그램에는 Go가 훨씬 적합하다.</p>
<hr>
<p>다음 편에서는 Spring Boot 백엔드에서 heartbeat를 처리하고, 이상을 감지해서 Slack으로 알림을 보내는 로직을 다룬다.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
